diff --git a/.deadcode-out b/.deadcode-out index 9641f272b3..61c5bcb055 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -13,6 +13,13 @@ forgejo.org/models IsErrSHANotFound IsErrMergeDivergingFastForwardOnly +forgejo.org/models/activities + GetActivityByID + NewFederatedUserActivity + CreateUserActivity + GetFollowingFeeds + FederatedUserActivity.loadActor + forgejo.org/models/auth WebAuthnCredentials @@ -54,9 +61,17 @@ forgejo.org/models/user IsErrExternalLoginUserAlreadyExist IsErrExternalLoginUserNotExist NewFederatedUser + NewFederatedUserFollower IsErrUserSettingIsNotExist GetUserAllSettings DeleteUserSetting + GetFederatedUser + GetFederatedUserByUserID + UpdateFederatedUser + GetFollowersForUser + AddFollower + RemoveFollower + IsFollowingAp forgejo.org/modules/activitypub NewContext @@ -87,12 +102,24 @@ forgejo.org/modules/eventsource Event.String forgejo.org/modules/forgefed + NewForgeFollowFromAp + NewForgeFollow + ForgeFollow.MarshalJSON + ForgeFollow.UnmarshalJSON + ForgeFollow.Validate NewForgeUndoLike ForgeUndoLike.UnmarshalJSON ForgeUndoLike.Validate + NewForgeUserActivityFromAp + NewForgeUserActivity + ForgeUserActivity.Validate + NewPersonIDFromModel GetItemByType JSONUnmarshalerFn NotEmpty + NewForgeUserActivityNoteFromAp + newNote + ForgeUserActivityNote.Validate ToRepository OnRepository @@ -204,6 +231,7 @@ forgejo.org/modules/util/filebuffer forgejo.org/modules/validation IsErrNotValid + ValidateIDExists forgejo.org/modules/web RouteMock diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9d8f54ee13..28fa9e4555 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "ghcr.io/devcontainers/features/node:1": { "version": "22" }, - "ghcr.io/devcontainers/features/git-lfs:1.2.3": {}, + "ghcr.io/devcontainers/features/git-lfs:1.2.4": {}, "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {} }, "customizations": { diff --git a/.dockerignore b/.dockerignore index 5e7a893014..807c70b000 100644 --- a/.dockerignore +++ b/.dockerignore @@ -37,13 +37,9 @@ coverage.all coverage/ cpu.out -/modules/migration/bindata.go /modules/migration/bindata.go.hash -/modules/options/bindata.go /modules/options/bindata.go.hash -/modules/public/bindata.go /modules/public/bindata.go.hash -/modules/templates/bindata.go /modules/templates/bindata.go.hash *.db diff --git a/.forgejo/issue_template/bug-report-ui.yaml b/.forgejo/issue_template/bug-report-ui.yaml index 57d578b232..8bb7bf1d49 100644 --- a/.forgejo/issue_template/bug-report-ui.yaml +++ b/.forgejo/issue_template/bug-report-ui.yaml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - **NOTE: If your issue is a security concern, please email (GPG: `A4676E79`) instead of opening a public issue.** + **NOTE: If your issue is a security concern, please email ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.** - type: markdown attributes: value: | diff --git a/.forgejo/issue_template/bug-report.yaml b/.forgejo/issue_template/bug-report.yaml index 6e9b116e60..a2b50dbca2 100644 --- a/.forgejo/issue_template/bug-report.yaml +++ b/.forgejo/issue_template/bug-report.yaml @@ -6,7 +6,7 @@ body: - type: markdown attributes: value: | - **NOTE: If your issue is a security concern, please email (GPG: `A4676E79`) instead of opening a public issue.** + **NOTE: If your issue is a security concern, please email ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.** - type: markdown attributes: value: | diff --git a/.forgejo/testdata/build-release/Dockerfile b/.forgejo/testdata/build-release/Dockerfile index d10564359e..09cce06c47 100644 --- a/.forgejo/testdata/build-release/Dockerfile +++ b/.forgejo/testdata/build-release/Dockerfile @@ -1,4 +1,4 @@ -FROM data.forgejo.org/oci/alpine:3.21 +FROM data.forgejo.org/oci/alpine:3.22 ARG RELEASE_VERSION=unkown LABEL maintainer="contact@forgejo.org" \ org.opencontainers.image.version="${RELEASE_VERSION}" diff --git a/.forgejo/workflows-composite/apt-install-from/action.yaml b/.forgejo/workflows-composite/apt-install-from/action.yaml index 615e7cb184..c1c8c950e3 100644 --- a/.forgejo/workflows-composite/apt-install-from/action.yaml +++ b/.forgejo/workflows-composite/apt-install-from/action.yaml @@ -18,7 +18,7 @@ runs: - name: install packages run: | apt-get update -qq - apt-get -q install -qq -y ${PACKAGES} + apt-get -q install --allow-downgrades -qq -y ${PACKAGES} env: PACKAGES: ${{inputs.packages}} - name: remove temporary package list to prevent using it in other steps diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml index a34f3533fd..3ab63b0589 100644 --- a/.forgejo/workflows/build-release.yml +++ b/.forgejo/workflows/build-release.yml @@ -164,7 +164,7 @@ jobs: - name: build container & release if: ${{ secrets.TOKEN != '' }} - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.5 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" @@ -183,7 +183,7 @@ jobs: - name: build rootless container if: ${{ secrets.TOKEN != '' }} - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.5 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml index 27d3b9383e..3aec46fb03 100644 --- a/.forgejo/workflows/publish-release.yml +++ b/.forgejo/workflows/publish-release.yml @@ -44,7 +44,7 @@ jobs: - uses: https://data.forgejo.org/actions/checkout@v4 - name: copy & sign - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.4 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.5 with: from-forgejo: ${{ vars.FORGEJO }} to-forgejo: ${{ vars.FORGEJO }} diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index 0035ad9b2f..5aa6c8cd98 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -28,7 +28,7 @@ jobs: runs-on: docker container: - image: data.forgejo.org/renovate/renovate:40.11.19 + image: data.forgejo.org/renovate/renovate:41.1.4 steps: - name: Load renovate repo cache diff --git a/.forgejo/workflows/testing-integration.yml b/.forgejo/workflows/testing-integration.yml new file mode 100644 index 0000000000..9e5cfb92ed --- /dev/null +++ b/.forgejo/workflows/testing-integration.yml @@ -0,0 +1,71 @@ +# +# Additional integration tests designed to run once a day when +# `mirror.yml` pushes to https://codeberg.org/forgejo-integration/forgejo +# and send a notification via email should they fail. +# +# For debug purposes: +# +# - uncomment [on].pull_request +# - swap 'forgejo-integration' and 'forgejo-coding' +# - open a pull request at https://codeberg.org/forgejo/forgejo and fix things +# - swap 'forgejo-integration' and 'forgejo-coding' +# - comment [on].pull_request +# + +name: testing-integration + +on: +# pull_request: + push: + tags: 'v[0-9]+.[0-9]+.*' + branches: + - 'forgejo' + - 'v*/forgejo' + +jobs: + test-unit: +# if: vars.ROLE == 'forgejo-coding' + if: vars.ROLE == 'forgejo-integration' + runs-on: docker + container: + image: 'data.forgejo.org/oci/node:22-bookworm' + options: --tmpfs /tmp:exec,noatime + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + - uses: ./.forgejo/workflows-composite/setup-env + - name: install git 2.30 + uses: ./.forgejo/workflows-composite/apt-install-from + with: + packages: git/bullseye git-lfs/bullseye + release: bullseye + - uses: ./.forgejo/workflows-composite/build-backend + - run: | + su forgejo -c 'make test-backend test-check' + timeout-minutes: 120 + env: + RACE_ENABLED: 'true' + TAGS: bindata + test-sqlite: +# if: vars.ROLE == 'forgejo-coding' + if: vars.ROLE == 'forgejo-integration' + runs-on: docker + container: + image: 'data.forgejo.org/oci/node:22-bookworm' + options: --tmpfs /tmp:exec,noatime + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + - uses: ./.forgejo/workflows-composite/setup-env + - name: install git 2.30 + uses: ./.forgejo/workflows-composite/apt-install-from + with: + packages: git/bullseye git-lfs/bullseye + release: bullseye + - uses: ./.forgejo/workflows-composite/build-backend + - run: | + su forgejo -c 'make test-sqlite-migration test-sqlite' + timeout-minutes: 120 + env: + TAGS: sqlite sqlite_unlock_notify + RACE_ENABLED: true + TEST_TAGS: sqlite sqlite_unlock_notify + USE_REPO_TEST_DIR: 1 diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml index 4d88d3efb0..7a93bb66a8 100644 --- a/.forgejo/workflows/testing.yml +++ b/.forgejo/workflows/testing.yml @@ -91,6 +91,7 @@ jobs: RACE_ENABLED: 'true' TAGS: bindata TEST_ELASTICSEARCH_URL: http://elasticsearch:9200 + TEST_MINIO_ENDPOINT: minio:9000 test-e2e: if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing' runs-on: docker @@ -114,6 +115,11 @@ jobs: run: | su forgejo -c 'make deps-frontend frontend' - uses: ./.forgejo/workflows-composite/build-backend + - name: Decide to run all tests + id: run-all + if: contains(github.event.pull_request.labels.*.name, 'run-all-playwright-tests') || contains(github.event.pull_request.title, 'playwright') + run: | + echo "all=1" >> "$GITHUB_OUTPUT" - name: Get changed files id: changed-files uses: https://data.forgejo.org/tj-actions/changed-files@v46 @@ -126,6 +132,7 @@ jobs: USE_REPO_TEST_DIR: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}} + RUN_ALL: ${{steps.run-all.all}} - name: Upload test artifacts on failure if: failure() uses: https://data.forgejo.org/forgejo/upload-artifact@v4 diff --git a/CODEOWNERS b/CODEOWNERS index 03b0d8753d..34cdceca09 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -10,6 +10,7 @@ # Javascript and CSS code. web_src/.* @beowulf @gusted +web_src/css/.* @0ko # HTML templates used by the backend. templates/.* @beowulf @gusted diff --git a/Dockerfile b/Dockerfile index a94f4d2b46..322e2c61a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx -FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env +FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct} @@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm COPY . ${GOPATH}/src/forgejo.org WORKDIR ${GOPATH}/src/forgejo.org -RUN make clean +RUN make clean-no-bindata RUN make frontend RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini -RUN LDFLAGS="-buildid=" make RELEASE_VERSION=$RELEASE_VERSION GOFLAGS="-trimpath" go-check generate-backend static-executable && xx-verify gitea +RUN LDFLAGS="-buildid=" make FORGEJO_GENERATE_SKIP_HASH=true RELEASE_VERSION=$RELEASE_VERSION GOFLAGS="-trimpath" go-check generate-backend static-executable && xx-verify gitea # Copy local files COPY docker/root /tmp/local @@ -51,7 +51,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /go/src/forgejo.org/environment-to-ini RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete -FROM data.forgejo.org/oci/alpine:3.21 +FROM data.forgejo.org/oci/alpine:3.22 ARG RELEASE_VERSION LABEL maintainer="contact@forgejo.org" \ org.opencontainers.image.authors="Forgejo" \ diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 36df26c042..6a3abaa4b9 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx -FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env +FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct} @@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm COPY . ${GOPATH}/src/forgejo.org WORKDIR ${GOPATH}/src/forgejo.org -RUN make clean +RUN make clean-no-bindata RUN make frontend RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini -RUN make RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea +RUN make FORGEJO_GENERATE_SKIP_HASH=true RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea # Copy local files COPY docker/rootless /tmp/local @@ -49,7 +49,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ /go/src/forgejo.org/environment-to-ini RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete -FROM data.forgejo.org/oci/alpine:3.21 +FROM data.forgejo.org/oci/alpine:3.22 ARG RELEASE_VERSION LABEL maintainer="contact@forgejo.org" \ org.opencontainers.image.authors="Forgejo" \ diff --git a/Makefile b/Makefile index 9cbf0f9e1c..e770f2a989 100644 --- a/Makefile +++ b/Makefile @@ -37,19 +37,17 @@ endif XGO_VERSION := go-1.21.x AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.1 # renovate: datasource=go +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.3.0 # renovate: datasource=go GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.8.0 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 # renovate: datasource=go -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 # renovate: datasource=go SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go -DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.32.0 # renovate: datasource=go -GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.1 # renovate: datasource=go -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@40.11.19 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go +GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go +RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... @@ -94,7 +92,7 @@ else # drop the "g" prefix prepended by git describe to the commit hash FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always 2>/dev/null | sed 's/^v//' | sed 's/\-g/-/') ifneq ($(FORGEJO_VERSION),) - ifneq ($(GITEA_COMPATIBILITY),) + ifeq ($(findstring $(GITEA_COMPATIBILITY),$(FORGEJO_VERSION)),) FORGEJO_VERSION := $(FORGEJO_VERSION)+$(GITEA_COMPATIBILITY) endif endif @@ -130,7 +128,7 @@ WEBPACK_CONFIGS := webpack.config.js tailwind.config.js WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts -BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go +BINDATA_DEST := modules/migration/bindata.go modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go @@ -223,7 +221,6 @@ help: @echo " - lint-go lint go files" @echo " - lint-go-fix lint go files and fix issues" @echo " - lint-go-vet lint go files with vet" - @echo " - lint-go-gopls lint go files with gopls" @echo " - lint-js lint js files" @echo " - lint-js-fix lint js files and fix issues" @echo " - lint-css lint css files" @@ -326,8 +323,12 @@ clean-all: clean rm -rf $(WEBPACK_DEST_ENTRIES) node_modules .PHONY: clean -clean: - rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \ +clean: clean-no-bindata + rm -rf $(BINDATA_DEST) $(BINDATA_HASH) + +.PHONY: clean-no-bindata +clean-no-bindata: + rm -rf $(EXECUTABLE) $(DIST) \ integrations*.test \ e2e*.test \ tests/integration/gitea-integration-* \ @@ -484,11 +485,6 @@ lint-go-vet: @echo "Running go vet..." @$(GO) vet ./... -.PHONY: lint-go-gopls -lint-go-gopls: - @echo "Running gopls check..." - @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA) - .PHONY: lint-editorconfig lint-editorconfig: $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .forgejo/workflows @@ -557,7 +553,7 @@ test-check: .PHONY: test\#% test\#%: - @echo "Running go test with -tags '$(TEST_TAGS)'..." + @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." @$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) .PHONY: coverage @@ -924,13 +920,11 @@ deps-tools: $(GO) install $(GOFUMPT_PACKAGE) $(GO) install $(GOLANGCI_LINT_PACKAGE) $(GO) install $(GXZ_PACKAGE) - $(GO) install $(MISSPELL_PACKAGE) $(GO) install $(SWAGGER_PACKAGE) $(GO) install $(XGO_PACKAGE) $(GO) install $(GO_LICENSES_PACKAGE) $(GO) install $(GOVULNCHECK_PACKAGE) $(GO) install $(GOMOCK_PACKAGE) - $(GO) install $(GOPLS_PACKAGE) node_modules: package-lock.json npm install --no-save diff --git a/assets/go-licenses.json b/assets/go-licenses.json index e222089dc5..c3b261320c 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -19,6 +19,11 @@ "path": "code.forgejo.org/forgejo-contrib/go-libravatar/LICENSE", "licenseText": "Copyright (c) 2016 Sandro Santilli \u003cstrk@kbt.io\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, + { + "name": "code.forgejo.org/forgejo/go-rpmutils", + "path": "code.forgejo.org/forgejo/go-rpmutils/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": "code.forgejo.org/forgejo/levelqueue", "path": "code.forgejo.org/forgejo/levelqueue/LICENSE", @@ -99,11 +104,6 @@ "path": "github.com/Azure/go-ntlmssp/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Microsoft\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, - { - "name": "github.com/DataDog/zstd", - "path": "github.com/DataDog/zstd/LICENSE", - "licenseText": "Simplified BSD License\n\nCopyright (c) 2016, Datadog \u003cinfo@datadoghq.com\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n * Neither the name of the copyright holder nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/ProtonMail/go-crypto", "path": "github.com/ProtonMail/go-crypto/LICENSE", @@ -289,11 +289,6 @@ "path": "github.com/cloudflare/circl/LICENSE", "licenseText": "Copyright (c) 2019 Cloudflare. 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 Cloudflare nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n========================================================================\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/cpuguy83/go-md2man/v2/md2man", - "path": "github.com/cpuguy83/go-md2man/v2/md2man/LICENSE.md", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Brian Goff\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/cyphar/filepath-securejoin", "path": "github.com/cyphar/filepath-securejoin/LICENSE", @@ -600,8 +595,8 @@ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, { - "name": "github.com/jaytaylor/html2text", - "path": "github.com/jaytaylor/html2text/LICENSE", + "name": "github.com/inbucket/html2text", + "path": "github.com/inbucket/html2text/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, { @@ -784,6 +779,16 @@ "path": "github.com/nwaples/rardecode/LICENSE", "licenseText": "Copyright (c) 2015, Nicholas Waples\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/olekukonko/errors", + "path": "github.com/olekukonko/errors/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2025 Oleku Konko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/olekukonko/ll", + "path": "github.com/olekukonko/ll/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2025 Oleku Konko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "github.com/olekukonko/tablewriter", "path": "github.com/olekukonko/tablewriter/LICENSE.md", @@ -809,6 +814,11 @@ "path": "github.com/opencontainers/image-spec/specs-go/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2016 The Linux Foundation.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, + { + "name": "github.com/philhofer/fwd", + "path": "github.com/philhofer/fwd/LICENSE.md", + "licenseText": "Copyright (c) 2014-2015, Philip Hofer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + }, { "name": "github.com/pierrec/lz4/v4", "path": "github.com/pierrec/lz4/v4/LICENSE", @@ -884,21 +894,11 @@ "path": "github.com/rs/xid/LICENSE", "licenseText": "Copyright (c) 2015 Olivier Poitrey \u003crs@dailymotion.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, - { - "name": "github.com/russross/blackfriday/v2", - "path": "github.com/russross/blackfriday/v2/LICENSE.txt", - "licenseText": "Blackfriday is distributed under the Simplified BSD License:\n\n\u003e Copyright © 2011 Russ Ross\n\u003e All rights reserved.\n\u003e\n\u003e Redistribution and use in source and binary forms, with or without\n\u003e modification, are permitted provided that the following conditions\n\u003e are met:\n\u003e\n\u003e 1. Redistributions of source code must retain the above copyright\n\u003e notice, this list of conditions and the following disclaimer.\n\u003e\n\u003e 2. Redistributions in binary form must reproduce the above\n\u003e copyright notice, this list of conditions and the following\n\u003e disclaimer in the documentation and/or other materials provided with\n\u003e the distribution.\n\u003e\n\u003e THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\u003e \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n\u003e LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n\u003e FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n\u003e COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n\u003e INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\n\u003e BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n\u003e LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n\u003e CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n\u003e LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n\u003e ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n\u003e POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/santhosh-tekuri/jsonschema/v6", "path": "github.com/santhosh-tekuri/jsonschema/v6/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability." }, - { - "name": "github.com/sassoftware/go-rpmutils", - "path": "github.com/sassoftware/go-rpmutils/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/sergi/go-diff/diffmatchpatch", "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE", @@ -929,15 +929,20 @@ "path": "github.com/syndtr/goleveldb/leveldb/LICENSE", "licenseText": "Copyright 2012 Suryandaru Triandana \u003csyndtr@gmail.com\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/tinylib/msgp/msgp", + "path": "github.com/tinylib/msgp/msgp/LICENSE", + "licenseText": "Copyright (c) 2014 Philip Hofer\nPortions Copyright (c) 2009 The Go Authors (license at http://golang.org) where indicated\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + }, { "name": "github.com/ulikunitz/xz", "path": "github.com/ulikunitz/xz/LICENSE", "licenseText": "Copyright (c) 2014-2022 Ulrich Kunitz\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* My name, Ulrich Kunitz, may not be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { - "name": "github.com/urfave/cli/v2", - "path": "github.com/urfave/cli/v2/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2022 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "name": "github.com/urfave/cli/v3", + "path": "github.com/urfave/cli/v3/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { "name": "github.com/valyala/fastjson", @@ -954,11 +959,6 @@ "path": "github.com/xanzy/ssh-agent/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" }, - { - "name": "github.com/xrash/smetrics", - "path": "github.com/xrash/smetrics/LICENSE", - "licenseText": "Copyright (C) 2016 Felipe da Cunha Gonçalves\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "github.com/yohcop/openid-go", "path": "github.com/yohcop/openid-go/LICENSE", diff --git a/build.go b/build.go deleted file mode 100644 index d410e171c7..0000000000 --- a/build.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build vendor - -package main - -// Libraries that are included to vendor utilities used during build. -// These libraries will not be included in a normal compilation. - -import ( - // for embed - _ "github.com/shurcooL/vfsgen" -) diff --git a/build/generate-bindata.go b/build/generate-bindata.go index 2fcb7c2f2a..67d3776847 100644 --- a/build/generate-bindata.go +++ b/build/generate-bindata.go @@ -1,5 +1,6 @@ // Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later //go:build ignore @@ -7,30 +8,40 @@ package main import ( "bytes" - "crypto/sha1" + "crypto/sha256" "fmt" + "io" + "io/fs" "log" - "net/http" "os" + "path" "path/filepath" "strconv" + "text/template" - "github.com/shurcooL/vfsgen" + "github.com/klauspost/compress/zstd" ) -func needsUpdate(dir, filename string) (bool, []byte) { - needRegen := false +func fileExists(filename string) bool { _, err := os.Stat(filename) - if err != nil { - needRegen = true + if err == nil { + return true } + if os.IsNotExist(err) { + return false + } + panic(err) +} + +func needsUpdate(dir, filename string) (bool, []byte) { + needRegen := !fileExists(filename) oldHash, err := os.ReadFile(filename + ".hash") if err != nil { oldHash = []byte{} } - hasher := sha1.New() + hasher := sha256.New() err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil { @@ -51,7 +62,7 @@ func needsUpdate(dir, filename string) (bool, []byte) { newHash := hasher.Sum([]byte{}) - if bytes.Compare(oldHash, newHash) != 0 { + if !bytes.Equal(oldHash, newHash) { return true, newHash } @@ -69,24 +80,280 @@ func main() { useGlobalModTime, _ = strconv.ParseBool(os.Args[4]) } - update, newHash := needsUpdate(dir, filename) + if os.Getenv("FORGEJO_GENERATE_SKIP_HASH") == "true" && fileExists(filename) { + fmt.Printf("bindata %s already exists and FORGEJO_GENERATE_SKIP_HASH=true\n", packageName) + return + } + update, newHash := needsUpdate(dir, filename) if !update { - fmt.Printf("bindata for %s already up-to-date\n", packageName) + fmt.Printf("bindata %s already exists and the checksum is a match\n", packageName) return } fmt.Printf("generating bindata for %s\n", packageName) - var fsTemplates http.FileSystem = http.Dir(dir) - err := vfsgen.Generate(fsTemplates, vfsgen.Options{ - PackageName: packageName, - BuildTags: "bindata", - VariableName: "Assets", - Filename: filename, - UseGlobalModTime: useGlobalModTime, - }) + + root, err := os.OpenRoot(dir) if err != nil { - log.Fatalf("%v\n", err) + log.Fatal(err) + } + + out, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) + if err != nil { + log.Fatal(err) + } + defer out.Close() + + if err := generate(root.FS(), packageName, useGlobalModTime, out); err != nil { + log.Fatal(err) } _ = os.WriteFile(filename+".hash", newHash, 0o666) } + +type file struct { + Path string + Name string + UncompressedSize int + CompressedData []byte + UncompressedData []byte +} + +type direntry struct { + Name string + IsDir bool +} + +func generate(fsRoot fs.FS, packageName string, globalTime bool, output io.Writer) error { + enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) + if err != nil { + return err + } + + files := []file{} + + dirs := map[string][]direntry{} + + if err := fs.WalkDir(fsRoot, ".", func(filePath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + entries, err := fs.ReadDir(fsRoot, filePath) + if err != nil { + return err + } + dirEntries := make([]direntry, 0, len(entries)) + for _, entry := range entries { + dirEntries = append(dirEntries, direntry{Name: entry.Name(), IsDir: entry.IsDir()}) + } + dirs[filePath] = dirEntries + return nil + } + + src, err := fs.ReadFile(fsRoot, filePath) + if err != nil { + return err + } + + dst := enc.EncodeAll(src, nil) + if len(dst) < len(src) { + files = append(files, file{ + Path: filePath, + Name: path.Base(filePath), + UncompressedSize: len(src), + CompressedData: dst, + }) + } else { + files = append(files, file{ + Path: filePath, + Name: path.Base(filePath), + UncompressedData: src, + }) + } + return nil + }); err != nil { + return err + } + + return generatedTmpl.Execute(output, map[string]any{ + "Packagename": packageName, + "GlobalTime": globalTime, + "Files": files, + "Dirs": dirs, + }) +} + +var generatedTmpl = template.Must(template.New("").Parse(`// Code generated by efs-gen. DO NOT EDIT. + +//go:build bindata + +package {{.Packagename}} + +import ( + "bytes" + "time" + "io" + "io/fs" + + "github.com/klauspost/compress/zstd" +) + +type normalFile struct { + name string + content []byte +} + +type compressedFile struct { + name string + uncompressedSize int64 + data []byte +} + +var files = map[string]any{ +{{- range .Files}} + "{{.Path}}": {{if .CompressedData}}compressedFile{"{{.Name}}", {{.UncompressedSize}}, []byte({{printf "%+q" .CompressedData}})}{{else}}normalFile{"{{.Name}}", []byte({{printf "%+q" .UncompressedData}})}{{end}}, +{{- end}} +} + +var dirs = map[string][]fs.DirEntry{ +{{- range $key, $entry := .Dirs}} + "{{$key}}": { +{{- range $entry}} + direntry{"{{.Name}}", {{.IsDir}}}, +{{- end}} + }, +{{- end}} +} + +type assets struct{} + +var Assets = assets{} + +func (a assets) Open(name string) (fs.File, error) { + f, ok := files[name] + if !ok { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + + switch f := f.(type) { + case normalFile: + return file{name: f.name, size: int64(len(f.content)), data: bytes.NewReader(f.content)}, nil + case compressedFile: + r, _ := zstd.NewReader(bytes.NewReader(f.data)) + return &compressFile{name: f.name, size: f.uncompressedSize, data: r, content: f.data}, nil + default: + panic("unknown file type") + } +} + +func (a assets) ReadDir(name string) ([]fs.DirEntry, error) { + d, ok := dirs[name] + if !ok { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + return d, nil +} + +type file struct { + name string + size int64 + data io.ReadSeeker +} + +var _ io.ReadSeeker = (*file)(nil) + +func (f file) Stat() (fs.FileInfo, error) { + return fileinfo{name: f.name, size: f.size}, nil +} + +func (f file) Read(p []byte) (int, error) { + return f.data.Read(p) +} + +func (f file) Seek(offset int64, whence int) (int64, error) { + return f.data.Seek(offset, whence) +} + +func (f file) Close() error { return nil } + +type compressFile struct { + name string + size int64 + data *zstd.Decoder + content []byte + zstdPos int64 + seekPos int64 +} + +var _ io.ReadSeeker = (*compressFile)(nil) + +func (f *compressFile) Stat() (fs.FileInfo, error) { + return fileinfo{name: f.name, size: f.size}, nil +} + +func (f *compressFile) Read(p []byte) (int, error) { + if f.zstdPos > f.seekPos { + if err := f.data.Reset(bytes.NewReader(f.content)); err != nil { + return 0, err + } + f.zstdPos = 0 + } + if f.zstdPos < f.seekPos { + if _, err := io.CopyN(io.Discard, f.data, f.seekPos - f.zstdPos); err != nil { + return 0, err + } + f.zstdPos = f.seekPos + } + n, err := f.data.Read(p) + f.zstdPos += int64(n) + f.seekPos = f.zstdPos + return n, err +} + +func (f *compressFile) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + f.seekPos = 0 + offset + case io.SeekCurrent: + f.seekPos += offset + case io.SeekEnd: + f.seekPos = f.size + offset + } + return f.seekPos, nil +} + +func (f *compressFile) Close() error { + f.data.Close() + return nil +} + +func (f *compressFile) ZstdBytes() []byte { return f.content } + +type fileinfo struct { + name string + size int64 +} + +func (f fileinfo) Name() string { return f.name } +func (f fileinfo) Size() int64 { return f.size } +func (f fileinfo) Mode() fs.FileMode { return 0o444 } +func (f fileinfo) ModTime() time.Time { return {{if .GlobalTime}}GlobalModTime(f.name){{else}}time.Unix(0, 0){{end}} } +func (f fileinfo) IsDir() bool { return false } +func (f fileinfo) Sys() any { return nil } + +type direntry struct { + name string + isDir bool +} + +func (d direntry) Name() string { return d.name } +func (d direntry) IsDir() bool { return d.isDir } +func (d direntry) Type() fs.FileMode { + if d.isDir { + return 0o755 | fs.ModeDir + } + return 0o444 +} +func (direntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist } +`)) diff --git a/build/lint-locale/lint-locale.go b/build/lint-locale/lint-locale.go index 0d80ffa4b0..dc4088c73c 100644 --- a/build/lint-locale/lint-locale.go +++ b/build/lint-locale/lint-locale.go @@ -52,7 +52,7 @@ func initBlueMondayPolicy() { policy.AllowAttrs("id").Matching(positionalPlaceholderRe).OnElements("code") // Allowed elements with no attributes. Must be a recognized tagname. - policy.AllowElements("strong", "br", "b", "strike", "code", "i") + policy.AllowElements("strong", "br", "b", "strike", "code", "i", "kbd") // TODO: Remove in `actions.workflow.dispatch.trigger_found`. policy.AllowNoAttrs().OnElements("c") diff --git a/build/lint-locale/lint-locale_test.go b/build/lint-locale/lint-locale_test.go index 9e9a931feb..dd146c0d70 100644 --- a/build/lint-locale/lint-locale_test.go +++ b/build/lint-locale/lint-locale_test.go @@ -37,6 +37,7 @@ func TestLocalizationPolicy(t *testing.T) { assert.Empty(t, checkLocaleContent([]byte("teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this will not automatically remove repositories already added with All repositories."))) assert.Empty(t, checkLocaleContent([]byte("sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if you run Forgejo as a service."))) assert.Empty(t, checkLocaleContent([]byte("hi_user_x = Hi %s,"))) + assert.Empty(t, checkLocaleContent([]byte("key = Press Shift"))) assert.Equal(t, []string{"error404: The page you are trying to reach either does not exist or you are not authorized to view it."}, checkLocaleContent([]byte("error404 = The page you are trying to reach either does not exist or you are not authorized to view it."))) }) diff --git a/cmd/actions.go b/cmd/actions.go index 54b5154cfa..12af2c8e86 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -4,25 +4,28 @@ package cmd import ( + "context" "fmt" "forgejo.org/modules/private" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - // CmdActions represents the available actions sub-commands. - CmdActions = &cli.Command{ +// CmdActions represents the available actions sub-commands. +func cmdActions() *cli.Command { + return &cli.Command{ Name: "actions", Usage: "Manage Forgejo Actions", - Subcommands: []*cli.Command{ - subcmdActionsGenRunnerToken, + Commands: []*cli.Command{ + subcmdActionsGenRunnerToken(), }, } +} - subcmdActionsGenRunnerToken = &cli.Command{ +func subcmdActionsGenRunnerToken() *cli.Command { + return &cli.Command{ Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Action: runGenerateActionsRunnerToken, @@ -36,10 +39,10 @@ var ( }, }, } -) +} -func runGenerateActionsRunnerToken(c *cli.Context) error { - ctx, cancel := installSignals() +func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setting.MustInstalled() diff --git a/cmd/admin.go b/cmd/admin.go index e04a5bc530..7e06a99cda 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -15,56 +15,64 @@ import ( "forgejo.org/modules/log" repo_module "forgejo.org/modules/repository" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - // CmdAdmin represents the available admin sub-command. - CmdAdmin = &cli.Command{ +// CmdAdmin represents the available admin sub-command. +func cmdAdmin() *cli.Command { + return &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", - Subcommands: []*cli.Command{ - subcmdUser, - subcmdRepoSyncReleases, - subcmdRegenerate, - subcmdAuth, - subcmdSendMail, + Commands: []*cli.Command{ + subcmdUser(), + subcmdRepoSyncReleases(), + subcmdRegenerate(), + subcmdAuth(), + subcmdSendMail(), }, } +} - subcmdRepoSyncReleases = &cli.Command{ +func subcmdRepoSyncReleases() *cli.Command { + return &cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } +} - subcmdRegenerate = &cli.Command{ +func subcmdRegenerate() *cli.Command { + return &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ microcmdRegenHooks, microcmdRegenKeys, }, } +} - subcmdAuth = &cli.Command{ +func subcmdAuth() *cli.Command { + return &cli.Command{ Name: "auth", Usage: "Modify external auth providers", - Subcommands: []*cli.Command{ - microcmdAuthAddOauth, - microcmdAuthUpdateOauth, - microcmdAuthAddLdapBindDn, - microcmdAuthUpdateLdapBindDn, - microcmdAuthAddLdapSimpleAuth, - microcmdAuthUpdateLdapSimpleAuth, - microcmdAuthAddSMTP, - microcmdAuthUpdateSMTP, - microcmdAuthList, - microcmdAuthDelete, + Commands: []*cli.Command{ + microcmdAuthAddOauth(), + microcmdAuthUpdateOauth(), + microcmdAuthAddLdapBindDn(), + microcmdAuthUpdateLdapBindDn(), + microcmdAuthAddLdapSimpleAuth(), + microcmdAuthUpdateLdapSimpleAuth(), + microcmdAuthAddSMTP(), + microcmdAuthUpdateSMTP(), + microcmdAuthList(), + microcmdAuthDelete(), }, } +} - subcmdSendMail = &cli.Command{ +func subcmdSendMail() *cli.Command { + return &cli.Command{ Name: "sendmail", Usage: "Send a message to all users", Action: runSendMail, @@ -86,15 +94,17 @@ var ( }, }, } +} - idFlag = &cli.Int64Flag{ +func idFlag() *cli.Int64Flag { + return &cli.Int64Flag{ Name: "id", Usage: "ID of authentication source", } -) +} -func runRepoSyncReleases(_ *cli.Context) error { - ctx, cancel := installSignals() +func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index b5e0212df7..cb95b3b3c8 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -13,17 +14,20 @@ import ( "forgejo.org/models/db" auth_service "forgejo.org/services/auth" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - microcmdAuthDelete = &cli.Command{ +func microcmdAuthDelete() *cli.Command { + return &cli.Command{ Name: "delete", Usage: "Delete specific auth source", - Flags: []cli.Flag{idFlag}, + Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } - microcmdAuthList = &cli.Command{ +} + +func microcmdAuthList() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List auth sources", Action: runListAuth, @@ -54,10 +58,10 @@ var ( }, }, } -) +} -func runListAuth(c *cli.Context) error { - ctx, cancel := installSignals() +func runListAuth(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { @@ -81,7 +85,7 @@ func runListAuth(c *cli.Context) error { // loop through each source and print w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags) - fmt.Fprintf(w, "ID\tName\tType\tEnabled\n") + fmt.Fprint(w, "ID\tName\tType\tEnabled\n") for _, source := range authSources { fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, source.Type.String(), source.IsActive) } @@ -90,12 +94,12 @@ func runListAuth(c *cli.Context) error { return nil } -func runDeleteAuth(c *cli.Context) error { +func runDeleteAuth(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index 637769b153..997d6b3a16 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -11,7 +11,7 @@ import ( "forgejo.org/models/auth" "forgejo.org/services/auth/source/ldap" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) type ( @@ -23,8 +23,8 @@ type ( } ) -var ( - commonLdapCLIFlags = []cli.Flag{ +func commonLdapCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Usage: "Authentication name.", @@ -102,8 +102,10 @@ var ( Usage: "The attribute of the user’s LDAP record containing the user’s avatar.", }, } +} - ldapBindDnCLIFlags = append(commonLdapCLIFlags, +func ldapBindDnCLIFlags() []cli.Flag { + return append(commonLdapCLIFlags(), &cli.StringFlag{ Name: "bind-dn", Usage: "The DN to bind to the LDAP server with when searching for the user.", @@ -128,49 +130,59 @@ var ( Name: "page-size", Usage: "Search page size.", }) +} - ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, +func ldapSimpleAuthCLIFlags() []cli.Flag { + return append(commonLdapCLIFlags(), &cli.StringFlag{ Name: "user-dn", Usage: "The user's DN.", }) +} - microcmdAuthAddLdapBindDn = &cli.Command{ +func microcmdAuthAddLdapBindDn() *cli.Command { + return &cli.Command{ Name: "add-ldap", Usage: "Add new LDAP (via Bind DN) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().addLdapBindDn(c) + Action: func(ctx context.Context, cli *cli.Command) error { + return newAuthService().addLdapBindDn(ctx, cli) }, - Flags: ldapBindDnCLIFlags, + Flags: ldapBindDnCLIFlags(), } +} - microcmdAuthUpdateLdapBindDn = &cli.Command{ +func microcmdAuthUpdateLdapBindDn() *cli.Command { + return &cli.Command{ Name: "update-ldap", Usage: "Update existing LDAP (via Bind DN) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().updateLdapBindDn(c) + Action: func(ctx context.Context, cli *cli.Command) error { + return newAuthService().updateLdapBindDn(ctx, cli) }, - Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...), + Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...), } +} - microcmdAuthAddLdapSimpleAuth = &cli.Command{ +func microcmdAuthAddLdapSimpleAuth() *cli.Command { + return &cli.Command{ Name: "add-ldap-simple", Usage: "Add new LDAP (simple auth) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().addLdapSimpleAuth(c) + Action: func(ctx context.Context, cli *cli.Command) error { + return newAuthService().addLdapSimpleAuth(ctx, cli) }, - Flags: ldapSimpleAuthCLIFlags, + Flags: ldapSimpleAuthCLIFlags(), } +} - microcmdAuthUpdateLdapSimpleAuth = &cli.Command{ +func microcmdAuthUpdateLdapSimpleAuth() *cli.Command { + return &cli.Command{ Name: "update-ldap-simple", Usage: "Update existing LDAP (simple auth) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().updateLdapSimpleAuth(c) + Action: func(ctx context.Context, cli *cli.Command) error { + return newAuthService().updateLdapSimpleAuth(ctx, cli) }, - Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...), + Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...), } -) +} // newAuthService creates a service with default functions. func newAuthService() *authService { @@ -183,7 +195,7 @@ func newAuthService() *authService { } // parseAuthSource assigns values on authSource according to command line flags. -func parseAuthSource(c *cli.Context, authSource *auth.Source) { +func parseAuthSource(c *cli.Command, authSource *auth.Source) { if c.IsSet("name") { authSource.Name = c.String("name") } @@ -202,7 +214,7 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) { } // parseLdapConfig assigns values on config according to command line flags. -func parseLdapConfig(c *cli.Context, config *ldap.Source) error { +func parseLdapConfig(c *cli.Command, config *ldap.Source) error { if c.IsSet("name") { config.Name = c.String("name") } @@ -289,7 +301,7 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) { // getAuthSource gets the login source by its id defined in the command line flags. // It returns an error if the id is not set, does not match any source or if the source is not of expected type. -func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) { +func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) { if err := argsSet(c, "id"); err != nil { return nil, err } @@ -307,12 +319,12 @@ func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authTyp } // addLdapBindDn adds a new LDAP via Bind DN authentication source. -func (a *authService) addLdapBindDn(c *cli.Context) error { +func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { return err } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := a.initDB(ctx); err != nil { @@ -336,8 +348,8 @@ func (a *authService) addLdapBindDn(c *cli.Context) error { } // updateLdapBindDn updates a new LDAP via Bind DN authentication source. -func (a *authService) updateLdapBindDn(c *cli.Context) error { - ctx, cancel := installSignals() +func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := a.initDB(ctx); err != nil { @@ -358,12 +370,12 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error { } // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. -func (a *authService) addLdapSimpleAuth(c *cli.Context) error { +func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil { return err } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := a.initDB(ctx); err != nil { @@ -387,8 +399,8 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error { } // updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source. -func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { - ctx, cancel := installSignals() +func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := a.initDB(ctx); err != nil { diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index 7ca0264454..89ce5f4f08 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -8,18 +8,17 @@ import ( "testing" "forgejo.org/models/auth" + "forgejo.org/modules/test" "forgejo.org/services/auth/source/ldap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestAddLdapBindDn(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -226,12 +225,12 @@ func TestAddLdapBindDn(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthAddLdapBindDn.Flags + app := cli.Command{} + app.Flags = microcmdAuthAddLdapBindDn().Flags app.Action = service.addLdapBindDn // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -243,9 +242,7 @@ func TestAddLdapBindDn(t *testing.T) { func TestAddLdapSimpleAuth(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -457,12 +454,12 @@ func TestAddLdapSimpleAuth(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthAddLdapSimpleAuth.Flags + app := cli.Command{} + app.Flags = microcmdAuthAddLdapSimpleAuth().Flags app.Action = service.addLdapSimpleAuth // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -474,9 +471,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { func TestUpdateLdapBindDn(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -920,12 +915,12 @@ func TestUpdateLdapBindDn(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthUpdateLdapBindDn.Flags + app := cli.Command{} + app.Flags = microcmdAuthUpdateLdapBindDn().Flags app.Action = service.updateLdapBindDn // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -937,9 +932,7 @@ func TestUpdateLdapBindDn(t *testing.T) { func TestUpdateLdapSimpleAuth(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -1310,12 +1303,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags + app := cli.Command{} + app.Flags = microcmdAuthUpdateLdapSimpleAuth().Flags app.Action = service.updateLdapSimpleAuth // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go index 58238794b8..abdcd5d48a 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "net/url" @@ -11,11 +12,11 @@ import ( auth_model "forgejo.org/models/auth" "forgejo.org/services/auth/source/oauth2" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - oauthCLIFlags = []cli.Flag{ +func oauthCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", @@ -120,23 +121,27 @@ var ( Usage: "Activate automatic team membership removal depending on groups", }, } +} - microcmdAuthAddOauth = &cli.Command{ +func microcmdAuthAddOauth() *cli.Command { + return &cli.Command{ Name: "add-oauth", Usage: "Add new Oauth authentication source", Action: runAddOauth, - Flags: oauthCLIFlags, + Flags: oauthCLIFlags(), } +} - microcmdAuthUpdateOauth = &cli.Command{ +func microcmdAuthUpdateOauth() *cli.Command { + return &cli.Command{ Name: "update-oauth", Usage: "Update existing Oauth authentication source", Action: runUpdateOauth, - Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...), + Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{idFlag()}, oauthCLIFlags()[1:]...)...), } -) +} -func parseOAuth2Config(c *cli.Context) *oauth2.Source { +func parseOAuth2Config(_ context.Context, c *cli.Command) *oauth2.Source { var customURLMapping *oauth2.CustomURLMapping if c.IsSet("use-custom-urls") { customURLMapping = &oauth2.CustomURLMapping{ @@ -168,15 +173,15 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { } } -func runAddOauth(c *cli.Context) error { - ctx, cancel := installSignals() +func runAddOauth(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { return err } - config := parseOAuth2Config(c) + config := parseOAuth2Config(ctx, c) if config.Provider == "openidConnect" { discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL) if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { @@ -192,12 +197,12 @@ func runAddOauth(c *cli.Context) error { }) } -func runUpdateOauth(c *cli.Context) error { +func runUpdateOauth(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_stmp.go index e166cc38cd..48b3adaac3 100644 --- a/cmd/admin_auth_stmp.go +++ b/cmd/admin_auth_stmp.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "strings" @@ -11,11 +12,11 @@ import ( "forgejo.org/modules/util" "forgejo.org/services/auth/source/smtp" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - smtpCLIFlags = []cli.Flag{ +func smtpCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", @@ -71,23 +72,27 @@ var ( Value: true, }, } +} - microcmdAuthAddSMTP = &cli.Command{ +func microcmdAuthAddSMTP() *cli.Command { + return &cli.Command{ Name: "add-smtp", Usage: "Add new SMTP authentication source", Action: runAddSMTP, - Flags: smtpCLIFlags, + Flags: smtpCLIFlags(), } +} - microcmdAuthUpdateSMTP = &cli.Command{ +func microcmdAuthUpdateSMTP() *cli.Command { + return &cli.Command{ Name: "update-smtp", Usage: "Update existing SMTP authentication source", Action: runUpdateSMTP, - Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...), + Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{idFlag()}, smtpCLIFlags()[1:]...)...), } -) +} -func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { +func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error { if c.IsSet("auth-type") { conf.Auth = c.String("auth-type") validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"} @@ -123,8 +128,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { return nil } -func runAddSMTP(c *cli.Context) error { - ctx, cancel := installSignals() +func runAddSMTP(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { @@ -163,12 +168,12 @@ func runAddSMTP(c *cli.Context) error { }) } -func runUpdateSMTP(c *cli.Context) error { +func runUpdateSMTP(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index 4e7f685843..7bfd12f8f4 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -4,11 +4,13 @@ package cmd import ( + "context" + asymkey_model "forgejo.org/models/asymkey" "forgejo.org/modules/graceful" repo_service "forgejo.org/services/repository" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -25,8 +27,8 @@ var ( } ) -func runRegenerateHooks(_ *cli.Context) error { - ctx, cancel := installSignals() +func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { @@ -35,8 +37,8 @@ func runRegenerateHooks(_ *cli.Context) error { return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext()) } -func runRegenerateKeys(_ *cli.Context) error { - ctx, cancel := installSignals() +func runRegenerateKeys(ctx context.Context, _ *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 967a6ed88a..f4f6fb49af 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -4,18 +4,21 @@ package cmd import ( - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var subcmdUser = &cli.Command{ - Name: "user", - Usage: "Modify users", - Subcommands: []*cli.Command{ - microcmdUserCreate, - microcmdUserList, - microcmdUserChangePassword, - microcmdUserDelete, - microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword, - }, +func subcmdUser() *cli.Command { + return &cli.Command{ + Name: "user", + Usage: "Modify users", + Commands: []*cli.Command{ + microcmdUserCreate(), + microcmdUserList(), + microcmdUserChangePassword(), + microcmdUserDelete(), + microcmdUserGenerateAccessToken(), + microcmdUserMustChangePassword(), + microcmdUserResetMFA(), + }, + } } diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go index 563ad96afd..dd8c9d378a 100644 --- a/cmd/admin_user_change_password.go +++ b/cmd/admin_user_change_password.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" @@ -13,40 +14,42 @@ import ( "forgejo.org/modules/setting" user_service "forgejo.org/services/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserChangePassword = &cli.Command{ - Name: "change-password", - Usage: "Change a user's password", - Action: runChangePassword, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "The user to change password for", +func microcmdUserChangePassword() *cli.Command { + return &cli.Command{ + Name: "change-password", + Usage: "Change a user's password", + Action: runChangePassword, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "The user to change password for", + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Value: "", + Usage: "New password to set for user", + }, + &cli.BoolFlag{ + Name: "must-change-password", + Usage: "User must change password", + Value: true, + }, }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Value: "", - Usage: "New password to set for user", - }, - &cli.BoolFlag{ - Name: "must-change-password", - Usage: "User must change password", - Value: true, - }, - }, + } } -func runChangePassword(c *cli.Context) error { +func runChangePassword(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "username", "password"); err != nil { return err } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index f84254f39c..96431412f6 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "strings" @@ -15,75 +16,76 @@ import ( "forgejo.org/modules/optional" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserCreate = &cli.Command{ - Name: "create", - Usage: "Create a new user in database", - Action: runCreateUser, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Usage: "Username. DEPRECATED: use username instead", +func microcmdUserCreate() *cli.Command { + return &cli.Command{ + Name: "create", + Usage: "Create a new user in database", + Action: runCreateUser, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "Username. DEPRECATED: use username instead", + }, + &cli.StringFlag{ + Name: "username", + Usage: "Username", + }, + &cli.StringFlag{ + Name: "password", + Usage: "User password", + }, + &cli.StringFlag{ + Name: "email", + Usage: "User email address", + }, + &cli.BoolFlag{ + Name: "admin", + Usage: "User is an admin", + }, + &cli.BoolFlag{ + Name: "random-password", + Usage: "Generate a random password for the user", + }, + &cli.BoolFlag{ + Name: "must-change-password", + Usage: "Set this option to false to prevent forcing the user to change their password after initial login", + Value: true, + }, + &cli.IntFlag{ + Name: "random-password-length", + Usage: "Length of the random password to be generated", + Value: 12, + }, + &cli.BoolFlag{ + Name: "access-token", + Usage: "Generate access token for the user", + }, + &cli.StringFlag{ + Name: "access-token-name", + Usage: `Name of the generated access token`, + Value: "gitea-admin", + }, + &cli.StringFlag{ + Name: "access-token-scopes", + Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`, + Value: "all", + }, + &cli.BoolFlag{ + Name: "restricted", + Usage: "Make a restricted user account", + }, + &cli.StringFlag{ + Name: "fullname", + Usage: `The full, human-readable name of the user`, + }, }, - &cli.StringFlag{ - Name: "username", - Usage: "Username", - }, - &cli.StringFlag{ - Name: "password", - Usage: "User password", - }, - &cli.StringFlag{ - Name: "email", - Usage: "User email address", - }, - &cli.BoolFlag{ - Name: "admin", - Usage: "User is an admin", - }, - &cli.BoolFlag{ - Name: "random-password", - Usage: "Generate a random password for the user", - }, - &cli.BoolFlag{ - Name: "must-change-password", - Usage: "Set this option to false to prevent forcing the user to change their password after initial login", - Value: true, - DisableDefaultText: true, - }, - &cli.IntFlag{ - Name: "random-password-length", - Usage: "Length of the random password to be generated", - Value: 12, - }, - &cli.BoolFlag{ - Name: "access-token", - Usage: "Generate access token for the user", - }, - &cli.StringFlag{ - Name: "access-token-name", - Usage: `Name of the generated access token`, - Value: "gitea-admin", - }, - &cli.StringFlag{ - Name: "access-token-scopes", - Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`, - Value: "all", - }, - &cli.BoolFlag{ - Name: "restricted", - Usage: "Make a restricted user account", - }, - &cli.StringFlag{ - Name: "fullname", - Usage: `The full, human-readable name of the user`, - }, - }, + } } -func runCreateUser(c *cli.Context) error { +func runCreateUser(ctx context.Context, c *cli.Command) error { // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. setting.LoadSettings() @@ -108,10 +110,10 @@ func runCreateUser(c *cli.Context) error { username = c.String("username") } else { username = c.String("name") - _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n") + _, _ = fmt.Fprint(c.Root().ErrWriter, "--name flag is deprecated. Use --username instead.\n") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go index 9a4cc22a77..3382c53e5f 100644 --- a/cmd/admin_user_delete.go +++ b/cmd/admin_user_delete.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "strings" @@ -12,41 +13,43 @@ import ( "forgejo.org/modules/storage" user_service "forgejo.org/services/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserDelete = &cli.Command{ - Name: "delete", - Usage: "Delete specific user by id, name or email", - Flags: []cli.Flag{ - &cli.Int64Flag{ - Name: "id", - Usage: "ID of user of the user to delete", +func microcmdUserDelete() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete specific user by id, name or email", + Flags: []cli.Flag{ + &cli.Int64Flag{ + Name: "id", + Usage: "ID of user of the user to delete", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username of the user to delete", + }, + &cli.StringFlag{ + Name: "email", + Aliases: []string{"e"}, + Usage: "Email of the user to delete", + }, + &cli.BoolFlag{ + Name: "purge", + Usage: "Purge user, all their repositories, organizations and comments", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username of the user to delete", - }, - &cli.StringFlag{ - Name: "email", - Aliases: []string{"e"}, - Usage: "Email of the user to delete", - }, - &cli.BoolFlag{ - Name: "purge", - Usage: "Purge user, all their repositories, organizations and comments", - }, - }, - Action: runDeleteUser, + Action: runDeleteUser, + } } -func runDeleteUser(c *cli.Context) error { +func runDeleteUser(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { return errors.New("You must provide the id, username or email of a user to delete") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 1a6c003171..d0f2878297 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -4,49 +4,52 @@ package cmd import ( + "context" "errors" "fmt" auth_model "forgejo.org/models/auth" user_model "forgejo.org/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserGenerateAccessToken = &cli.Command{ - Name: "generate-access-token", - Usage: "Generate an access token for a specific user", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username", +func microcmdUserGenerateAccessToken() *cli.Command { + return &cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + }, + &cli.StringFlag{ + Name: "token-name", + Aliases: []string{"t"}, + Usage: "Token name", + Value: "gitea-admin", + }, + &cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + &cli.StringFlag{ + Name: "scopes", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, + }, }, - &cli.StringFlag{ - Name: "token-name", - Aliases: []string{"t"}, - Usage: "Token name", - Value: "gitea-admin", - }, - &cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - &cli.StringFlag{ - Name: "scopes", - Value: "all", - Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, - }, - }, - Action: runGenerateAccessToken, + Action: runGenerateAccessToken, + } } -func runGenerateAccessToken(c *cli.Context) error { +func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { if !c.IsSet("username") { return errors.New("you must provide a username to generate a token for") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index 6044ce7c3f..ccc4b8c917 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -4,29 +4,32 @@ package cmd import ( + "context" "fmt" "os" "text/tabwriter" user_model "forgejo.org/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserList = &cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", +func microcmdUserList() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, }, - }, + } } -func runListUsers(c *cli.Context) error { - ctx, cancel := installSignals() +func runListUsers(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if err := initDB(ctx); err != nil { @@ -41,7 +44,7 @@ func runListUsers(c *cli.Context) error { w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) if c.IsSet("admin") { - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") + fmt.Fprint(w, "ID\tUsername\tEmail\tIsActive\n") for _, u := range users { if u.IsAdmin { fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) @@ -49,7 +52,7 @@ func runListUsers(c *cli.Context) error { } } else { twofa := user_model.UserList(users).GetTwoFaStatus(ctx) - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") + fmt.Fprint(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") for _, u := range users { fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) } diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go index 920f5c7b21..2ccad56eb9 100644 --- a/cmd/admin_user_must_change_password.go +++ b/cmd/admin_user_must_change_password.go @@ -4,38 +4,41 @@ package cmd import ( + "context" "errors" "fmt" user_model "forgejo.org/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserMustChangePassword = &cli.Command{ - Name: "must-change-password", - Usage: "Set the must change password flag for the provided users or all users", - Action: runMustChangePassword, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "all", - Aliases: []string{"A"}, - Usage: "All users must change password, except those explicitly excluded with --exclude", +func microcmdUserMustChangePassword() *cli.Command { + return &cli.Command{ + Name: "must-change-password", + Usage: "Set the must change password flag for the provided users or all users", + Action: runMustChangePassword, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Aliases: []string{"A"}, + Usage: "All users must change password, except those explicitly excluded with --exclude", + }, + &cli.StringSliceFlag{ + Name: "exclude", + Aliases: []string{"e"}, + Usage: "Do not change the must-change-password flag for these users", + }, + &cli.BoolFlag{ + Name: "unset", + Usage: "Instead of setting the must-change-password flag, unset it", + }, }, - &cli.StringSliceFlag{ - Name: "exclude", - Aliases: []string{"e"}, - Usage: "Do not change the must-change-password flag for these users", - }, - &cli.BoolFlag{ - Name: "unset", - Usage: "Instead of setting the must-change-password flag, unset it", - }, - }, + } } -func runMustChangePassword(c *cli.Context) error { - ctx, cancel := installSignals() +func runMustChangePassword(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() if c.NArg() == 0 && !c.IsSet("all") { diff --git a/cmd/admin_user_reset_mfa.go b/cmd/admin_user_reset_mfa.go new file mode 100644 index 0000000000..8107fd97bf --- /dev/null +++ b/cmd/admin_user_reset_mfa.go @@ -0,0 +1,73 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "fmt" + + auth_model "forgejo.org/models/auth" + user_model "forgejo.org/models/user" + + "github.com/urfave/cli/v3" +) + +func microcmdUserResetMFA() *cli.Command { + return &cli.Command{ + Name: "reset-mfa", + Usage: "Remove all two-factor authentication configurations for a user", + Action: runResetMFA, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "The user to update", + }, + }, + } +} + +func runResetMFA(ctx context.Context, c *cli.Command) error { + if err := argsSet(c, "username"); err != nil { + return err + } + + ctx, cancel := installSignals(ctx) + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + user, err := user_model.GetUserByName(ctx, c.String("username")) + if err != nil { + return err + } + + webAuthnList, err := auth_model.GetWebAuthnCredentialsByUID(ctx, user.ID) + if err != nil { + return err + } + + for _, credential := range webAuthnList { + if _, err := auth_model.DeleteCredential(ctx, credential.ID, user.ID); err != nil { + return err + } + } + + tfaModes, err := auth_model.GetTwoFactorByUID(ctx, user.ID) + if err == nil && tfaModes != nil { + if err := auth_model.DeleteTwoFactorByID(ctx, tfaModes.ID, user.ID); err != nil { + return err + } + } else { + if _, is := err.(auth_model.ErrTwoFactorNotEnrolled); !is { + return err + } + } + + fmt.Printf("%s's two-factor authentication settings have been removed!\n", user.Name) + return nil +} diff --git a/cmd/cert.go b/cmd/cert.go index bf83af389f..f9e3a16f3e 100644 --- a/cmd/cert.go +++ b/cmd/cert.go @@ -6,6 +6,7 @@ package cmd import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -20,47 +21,49 @@ import ( "strings" "time" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdCert represents the available cert sub-command. -var CmdCert = &cli.Command{ - Name: "cert", - Usage: "Generate self-signed certificate", - Description: `Generate a self-signed X.509 certificate for a TLS server. +func cmdCert() *cli.Command { + return &cli.Command{ + Name: "cert", + Usage: "Generate self-signed certificate", + Description: `Generate a self-signed X.509 certificate for a TLS server. Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`, - Action: runCert, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "host", - Value: "", - Usage: "Comma-separated hostnames and IPs to generate a certificate for", + Action: runCert, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "host", + Value: "", + Usage: "Comma-separated hostnames and IPs to generate a certificate for", + }, + &cli.StringFlag{ + Name: "ecdsa-curve", + Value: "", + Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", + }, + &cli.IntFlag{ + Name: "rsa-bits", + Value: 3072, + Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set", + }, + &cli.StringFlag{ + Name: "start-date", + Value: "", + Usage: "Creation date formatted as Jan 1 15:04:05 2011", + }, + &cli.DurationFlag{ + Name: "duration", + Value: 365 * 24 * time.Hour, + Usage: "Duration that certificate is valid for", + }, + &cli.BoolFlag{ + Name: "ca", + Usage: "whether this cert should be its own Certificate Authority", + }, }, - &cli.StringFlag{ - Name: "ecdsa-curve", - Value: "", - Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", - }, - &cli.IntFlag{ - Name: "rsa-bits", - Value: 3072, - Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set", - }, - &cli.StringFlag{ - Name: "start-date", - Value: "", - Usage: "Creation date formatted as Jan 1 15:04:05 2011", - }, - &cli.DurationFlag{ - Name: "duration", - Value: 365 * 24 * time.Hour, - Usage: "Duration that certificate is valid for", - }, - &cli.BoolFlag{ - Name: "ca", - Usage: "whether this cert should be its own Certificate Authority", - }, - }, + } } func publicKey(priv any) any { @@ -89,7 +92,7 @@ func pemBlockForKey(priv any) *pem.Block { } } -func runCert(c *cli.Context) error { +func runCert(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "host"); err != nil { return err } diff --git a/cmd/cmd.go b/cmd/cmd.go index c887d0ed52..85a482b78c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -20,19 +20,21 @@ import ( "forgejo.org/modules/setting" "forgejo.org/modules/util" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // argsSet checks that all the required arguments are set. args is a list of // arguments that must be set in the passed Context. -func argsSet(c *cli.Context, args ...string) error { +func argsSet(c *cli.Command, args ...string) error { for _, a := range args { if !c.IsSet(a) { return errors.New(a + " is not set") } - if util.IsEmptyString(c.String(a)) { - return errors.New(a + " is required") + if s, ok := c.Value(a).(string); ok { + if util.IsEmptyString(s) { + return errors.New(a + " is required") + } } } return nil @@ -73,8 +75,8 @@ If this is the intended configuration file complete the [database] section.`, se return nil } -func installSignals() (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) +func installSignals(ctx context.Context) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) go func() { // install notify signalChannel := make(chan os.Signal, 1) @@ -109,7 +111,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) { log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) } -func globalBool(c *cli.Context, name string) bool { +func globalBool(c *cli.Command, name string) bool { for _, ctx := range c.Lineage() { if ctx.Bool(name) { return true @@ -120,16 +122,16 @@ func globalBool(c *cli.Context, name string) bool { // PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout. // Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever. -func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error { - return func(c *cli.Context) error { +func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(ctx context.Context, cli *cli.Command) (context.Context, error) { + return func(ctx context.Context, cli *cli.Command) (context.Context, error) { level := defaultLevel - if globalBool(c, "quiet") { + if globalBool(cli, "quiet") { level = log.FATAL } - if globalBool(c, "debug") || globalBool(c, "verbose") { + if globalBool(cli, "debug") || globalBool(cli, "verbose") { level = log.TRACE } log.SetConsoleLogger(log.DEFAULT, "console-default", level) - return nil + return ctx, nil } } diff --git a/cmd/docs.go b/cmd/docs.go deleted file mode 100644 index 1dc0980c00..0000000000 --- a/cmd/docs.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/urfave/cli/v2" -) - -// CmdDocs represents the available docs sub-command. -var CmdDocs = &cli.Command{ - Name: "docs", - Usage: "Output CLI documentation", - Description: "A command to output Forgejo's CLI documentation, optionally to a file.", - Action: runDocs, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "man", - Usage: "Output man pages instead", - }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Path to output to instead of stdout (will overwrite if exists)", - }, - }, -} - -func runDocs(ctx *cli.Context) error { - docs, err := ctx.App.ToMarkdown() - if ctx.Bool("man") { - docs, err = ctx.App.ToMan() - } - if err != nil { - return err - } - - if !ctx.Bool("man") { - // Clean up markdown. The following bug was fixed in v2, but is present in v1. - // It affects markdown output (even though the issue is referring to man pages) - // https://github.com/urfave/cli/issues/1040 - firstHashtagIndex := strings.Index(docs, "#") - - if firstHashtagIndex > 0 { - docs = docs[firstHashtagIndex:] - } - } - - out := os.Stdout - if ctx.String("output") != "" { - fi, err := os.Create(ctx.String("output")) - if err != nil { - return err - } - defer fi.Close() - out = fi - } - - _, err = fmt.Fprintln(out, docs) - return err -} diff --git a/cmd/doctor.go b/cmd/doctor.go index 3807623ccd..681794f094 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "fmt" golog "log" "os" @@ -19,80 +20,86 @@ import ( "forgejo.org/modules/setting" "forgejo.org/services/doctor" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdDoctor represents the available doctor sub-command. -var CmdDoctor = &cli.Command{ - Name: "doctor", - Usage: "Diagnose and optionally fix problems, convert or re-create database tables", - Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", +func cmdDoctor() *cli.Command { + return &cli.Command{ + Name: "doctor", + Usage: "Diagnose and optionally fix problems, convert or re-create database tables", + Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Subcommands: []*cli.Command{ - cmdDoctorCheck, - cmdRecreateTable, - cmdDoctorConvert, - }, + Commands: []*cli.Command{ + cmdDoctorCheck(), + cmdRecreateTable(), + cmdDoctorConvert(), + }, + } } -var cmdDoctorCheck = &cli.Command{ - Name: "check", - Usage: "Diagnose and optionally fix problems", - Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Action: runDoctorCheck, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "list", - Usage: "List the available checks", +func cmdDoctorCheck() *cli.Command { + return &cli.Command{ + Name: "check", + Usage: "Diagnose and optionally fix problems", + Description: "A command to diagnose problems with the current Forgejo instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Action: runDoctorCheck, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "List the available checks", + }, + &cli.BoolFlag{ + Name: "default", + Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", + }, + &cli.StringSliceFlag{ + Name: "run", + Usage: "Run the provided checks - (if --default is set, the default checks will also run)", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "Run all the available checks", + }, + &cli.BoolFlag{ + Name: "fix", + Usage: "Automatically fix what we can", + }, + &cli.StringFlag{ + Name: "log-file", + Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, + }, + &cli.BoolFlag{ + Name: "color", + Aliases: []string{"H"}, + Usage: "Use color for outputted information", + }, }, - &cli.BoolFlag{ - Name: "default", - Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", - }, - &cli.StringSliceFlag{ - Name: "run", - Usage: "Run the provided checks - (if --default is set, the default checks will also run)", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "Run all the available checks", - }, - &cli.BoolFlag{ - Name: "fix", - Usage: "Automatically fix what we can", - }, - &cli.StringFlag{ - Name: "log-file", - Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, - }, - &cli.BoolFlag{ - Name: "color", - Aliases: []string{"H"}, - Usage: "Use color for outputted information", - }, - }, + } } -var cmdRecreateTable = &cli.Command{ - Name: "recreate-table", - Usage: "Recreate tables from XORM definitions and copy the data.", - ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "Print SQL commands sent", +func cmdRecreateTable() *cli.Command { + return &cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, }, - }, - Description: `The database definitions Forgejo uses change across versions, sometimes changing default values and leaving old unused columns. + Description: `The database definitions Forgejo uses change across versions, sometimes changing default values and leaving old unused columns. This command will cause Xorm to recreate tables, copying over the data and deleting the old table. You should back-up your database before doing this and ensure that your database is up-to-date first.`, - Action: runRecreateTable, + Action: runRecreateTable, + } } -func runRecreateTable(ctx *cli.Context) error { - stdCtx, cancel := installSignals() +func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error { + stdCtx, cancel := installSignals(stdCtx) defer cancel() // Redirect the default golog to here @@ -143,7 +150,7 @@ func runRecreateTable(ctx *cli.Context) error { }) } -func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { +func setupDoctorDefaultLogger(ctx *cli.Command, colorize bool) { // Silence the default loggers setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) @@ -165,8 +172,8 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { } } -func runDoctorCheck(ctx *cli.Context) error { - stdCtx, cancel := installSignals() +func runDoctorCheck(stdCtx context.Context, ctx *cli.Command) error { + stdCtx, cancel := installSignals(stdCtx) defer cancel() colorize := log.CanColorStdout diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 06883264a3..44bebae154 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -4,25 +4,28 @@ package cmd import ( + "context" "fmt" "forgejo.org/models/db" "forgejo.org/modules/log" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // cmdDoctorConvert represents the available convert sub-command. -var cmdDoctorConvert = &cli.Command{ - Name: "convert", - Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4", - Action: runDoctorConvert, +func cmdDoctorConvert() *cli.Command { + return &cli.Command{ + Name: "convert", + Usage: "Convert the database", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4", + Action: runDoctorConvert, + } } -func runDoctorConvert(ctx *cli.Context) error { - stdCtx, cancel := installSignals() +func runDoctorConvert(stdCtx context.Context, ctx *cli.Command) error { + stdCtx, cancel := installSignals(stdCtx) defer cancel() if err := initDB(stdCtx); err != nil { diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index bfb4b9d803..c3eda8315b 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -11,7 +11,7 @@ import ( "forgejo.org/services/doctor" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestDoctorRun(t *testing.T) { @@ -22,12 +22,12 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) - app := cli.NewApp() - app.Commands = []*cli.Command{cmdDoctorCheck} - err := app.Run([]string{"./gitea", "check", "--run", "test-check"}) + app := cli.Command{} + app.Commands = []*cli.Command{cmdDoctorCheck()} + err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) require.NoError(t, err) - err = app.Run([]string{"./gitea", "check", "--run", "no-such"}) + err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"}) require.ErrorContains(t, err, `unknown checks: "no-such"`) - err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"}) + err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"}) require.ErrorContains(t, err, `unknown checks: "no-such"`) } diff --git a/cmd/dump.go b/cmd/dump.go index bc0b269924..cb01e74196 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -5,6 +5,8 @@ package cmd import ( + "context" + "errors" "fmt" "io" "os" @@ -22,7 +24,7 @@ import ( "code.forgejo.org/go-chi/session" "github.com/mholt/archiver/v3" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error { @@ -83,6 +85,10 @@ func (o *outputType) Set(value string) error { return fmt.Errorf("allowed values are %s", o.Join()) } +func (o *outputType) Get() any { + return o.String() +} + func (o outputType) String() string { if o.selected == "" { return o.Default @@ -96,79 +102,81 @@ var outputTypeEnum = &outputType{ } // CmdDump represents the available dump sub-command. -var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Forgejo files and database", - Description: `Dump compresses all related files and database into zip file. +func cmdDump() *cli.Command { + return &cli.Command{ + Name: "dump", + Usage: "Dump Forgejo files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Forgejo server image to send to maintainer`, - Action: runDump, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Value: fmt.Sprintf("forgejo-dump-%d.zip", time.Now().Unix()), - Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.", + Action: runDump, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Value: fmt.Sprintf("forgejo-dump-%d.zip", time.Now().Unix()), + Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.", + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"V"}, + Usage: "Show process details", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display warnings and errors", + }, + &cli.StringFlag{ + Name: "tempdir", + Aliases: []string{"t"}, + Usage: "Temporary dir path", + }, + &cli.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "Specify the database SQL syntax: sqlite3, mysql, postgres", + }, + &cli.BoolFlag{ + Name: "skip-repository", + Aliases: []string{"R"}, + Usage: "Skip repositories", + }, + &cli.BoolFlag{ + Name: "skip-log", + Aliases: []string{"L"}, + Usage: "Skip logs", + }, + &cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, + &cli.BoolFlag{ + Name: "skip-lfs-data", + Usage: "Skip LFS data", + }, + &cli.BoolFlag{ + Name: "skip-attachment-data", + Usage: "Skip attachment data", + }, + &cli.BoolFlag{ + Name: "skip-package-data", + Usage: "Skip package data", + }, + &cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, + &cli.BoolFlag{ + Name: "skip-repo-archives", + Usage: "Skip repository archives", + }, + &cli.GenericFlag{ + Name: "type", + Value: outputTypeEnum, + Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()), + }, }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"V"}, - Usage: "Show process details", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display warnings and errors", - }, - &cli.StringFlag{ - Name: "tempdir", - Aliases: []string{"t"}, - Usage: "Temporary dir path", - }, - &cli.StringFlag{ - Name: "database", - Aliases: []string{"d"}, - Usage: "Specify the database SQL syntax: sqlite3, mysql, postgres", - }, - &cli.BoolFlag{ - Name: "skip-repository", - Aliases: []string{"R"}, - Usage: "Skip repositories", - }, - &cli.BoolFlag{ - Name: "skip-log", - Aliases: []string{"L"}, - Usage: "Skip logs", - }, - &cli.BoolFlag{ - Name: "skip-custom-dir", - Usage: "Skip custom directory", - }, - &cli.BoolFlag{ - Name: "skip-lfs-data", - Usage: "Skip LFS data", - }, - &cli.BoolFlag{ - Name: "skip-attachment-data", - Usage: "Skip attachment data", - }, - &cli.BoolFlag{ - Name: "skip-package-data", - Usage: "Skip package data", - }, - &cli.BoolFlag{ - Name: "skip-index", - Usage: "Skip bleve index data", - }, - &cli.BoolFlag{ - Name: "skip-repo-archives", - Usage: "Skip repository archives", - }, - &cli.GenericFlag{ - Name: "type", - Value: outputTypeEnum, - Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()), - }, - }, + } } func fatal(format string, args ...any) { @@ -176,7 +184,7 @@ func fatal(format string, args ...any) { log.Fatal(format, args...) } -func runDump(ctx *cli.Context) error { +func runDump(stdCtx context.Context, ctx *cli.Command) error { var file *os.File fileName := ctx.String("file") outType := ctx.String("type") @@ -212,16 +220,16 @@ func runDump(ctx *cli.Context) error { if !setting.InstallLock { log.Error("Is '%s' really the right config path?\n", setting.CustomConf) - return fmt.Errorf("forgejo is not initialized") + return errors.New("forgejo is not initialized") } setting.LoadSettings() // cannot access session settings otherwise verbose := ctx.Bool("verbose") if verbose && ctx.Bool("quiet") { - return fmt.Errorf("--quiet and --verbose cannot both be set") + return errors.New("--quiet and --verbose cannot both be set") } - stdCtx, cancel := installSignals() + stdCtx, cancel := installSignals(stdCtx) defer cancel() err := db.InitEngine(stdCtx) diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index 7c7440dad7..7159d55e99 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -19,73 +19,75 @@ import ( "forgejo.org/services/convert" "forgejo.org/services/migrations" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdDumpRepository represents the available dump repository sub-command. -var CmdDumpRepository = &cli.Command{ - Name: "dump-repo", - Usage: "Dump the repository from git/github/gitea/gitlab", - Description: "This is a command for dumping the repository data.", - Action: runDumpRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "git_service", - Value: "", - Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", - }, - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to store the data", - }, - &cli.StringFlag{ - Name: "clone_addr", - Value: "", - Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", - }, - &cli.StringFlag{ - Name: "auth_username", - Value: "", - Usage: "The username to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_password", - Value: "", - Usage: "The password to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_token", - Value: "", - Usage: "The personal token to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "The data will be stored on a directory with owner name if not empty", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "The data will be stored on a directory with repository name if not empty", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. +func cmdDumpRepository() *cli.Command { + return &cli.Command{ + Name: "dump-repo", + Usage: "Dump the repository from git/github/gitea/gitlab", + Description: "This is a command for dumping the repository data.", + Action: runDumpRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "git_service", + Value: "", + Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", + }, + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to store the data", + }, + &cli.StringFlag{ + Name: "clone_addr", + Value: "", + Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", + }, + &cli.StringFlag{ + Name: "auth_username", + Value: "", + Usage: "The username to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_password", + Value: "", + Usage: "The password to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_token", + Value: "", + Usage: "The personal token to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "The data will be stored on a directory with owner name if not empty", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "The data will be stored on a directory with repository name if not empty", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, }, - }, + } } -func runDumpRepository(ctx *cli.Context) error { +func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error { setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr) // setting.DisableLoggerInit() setting.LoadSettings() // cannot access skip_tls_verify settings otherwise - stdCtx, cancel := installSignals() + stdCtx, cancel := installSignals(stdCtx) defer cancel() if err := initDB(stdCtx); err != nil { diff --git a/cmd/embedded.go b/cmd/embedded.go index c8b9d13d25..8fa76ccef1 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -19,23 +20,25 @@ import ( "forgejo.org/modules/util" "github.com/gobwas/glob" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdEmbedded represents the available extract sub-command. -var ( - CmdEmbedded = &cli.Command{ +func cmdEmbedded() *cli.Command { + return &cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", - Subcommands: []*cli.Command{ - subcmdList, - subcmdView, - subcmdExtract, + Commands: []*cli.Command{ + subcmdList(), + subcmdView(), + subcmdExtract(), }, } +} - subcmdList = &cli.Command{ +func subcmdList() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List files matching the given pattern", Action: runList, @@ -47,8 +50,10 @@ var ( }, }, } +} - subcmdView = &cli.Command{ +func subcmdView() *cli.Command { + return &cli.Command{ Name: "view", Usage: "View a file matching the given pattern", Action: runView, @@ -60,8 +65,10 @@ var ( }, }, } +} - subcmdExtract = &cli.Command{ +func subcmdExtract() *cli.Command { + return &cli.Command{ Name: "extract", Usage: "Extract resources", Action: runExtract, @@ -90,9 +97,9 @@ var ( }, }, } +} - matchedAssetFiles []assetFile -) +var matchedAssetFiles []assetFile type assetFile struct { fs *assetfs.LayeredFS @@ -100,7 +107,7 @@ type assetFile struct { path string } -func initEmbeddedExtractor(c *cli.Context) error { +func initEmbeddedExtractor(_ context.Context, c *cli.Command) error { setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) patterns, err := compileCollectPatterns(c.Args().Slice()) @@ -115,32 +122,32 @@ func initEmbeddedExtractor(c *cli.Context) error { return nil } -func runList(c *cli.Context) error { - if err := runListDo(c); err != nil { +func runList(ctx context.Context, c *cli.Command) error { + if err := runListDo(ctx, c); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runView(c *cli.Context) error { - if err := runViewDo(c); err != nil { +func runView(ctx context.Context, c *cli.Command) error { + if err := runViewDo(ctx, c); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runExtract(c *cli.Context) error { - if err := runExtractDo(c); err != nil { +func runExtract(ctx context.Context, c *cli.Command) error { + if err := runExtractDo(ctx, c); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runListDo(c *cli.Context) error { - if err := initEmbeddedExtractor(c); err != nil { +func runListDo(ctx context.Context, c *cli.Command) error { + if err := initEmbeddedExtractor(ctx, c); err != nil { return err } @@ -151,8 +158,8 @@ func runListDo(c *cli.Context) error { return nil } -func runViewDo(c *cli.Context) error { - if err := initEmbeddedExtractor(c); err != nil { +func runViewDo(ctx context.Context, c *cli.Command) error { + if err := initEmbeddedExtractor(ctx, c); err != nil { return err } @@ -174,8 +181,8 @@ func runViewDo(c *cli.Context) error { return nil } -func runExtractDo(c *cli.Context) error { - if err := initEmbeddedExtractor(c); err != nil { +func runExtractDo(ctx context.Context, c *cli.Command) error { + if err := initEmbeddedExtractor(ctx, c); err != nil { return err } @@ -271,7 +278,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error { return nil } -func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) { +func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) { fs := assetfs.Layered(layer) files, err := fs.ListAllFiles(".", true) if err != nil { diff --git a/cmd/forgejo/actions.go b/cmd/forgejo/actions.go index dbe7398bcf..c445d1aa38 100644 --- a/cmd/forgejo/actions.go +++ b/cmd/forgejo/actions.go @@ -6,6 +6,7 @@ package forgejo import ( "context" "encoding/hex" + "errors" "fmt" "io" "os" @@ -16,14 +17,14 @@ import ( "forgejo.org/modules/setting" private_routers "forgejo.org/routers/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func CmdActions(ctx context.Context) *cli.Command { return &cli.Command{ Name: "actions", Usage: "Commands for managing Forgejo Actions", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ SubcmdActionsGenerateRunnerToken(ctx), SubcmdActionsGenerateRunnerSecret(ctx), SubcmdActionsRegister(ctx), @@ -36,7 +37,7 @@ func SubcmdActionsGenerateRunnerToken(ctx context.Context) *cli.Command { Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Before: prepareWorkPathAndCustomConf(ctx), - Action: func(cliCtx *cli.Context) error { return RunGenerateActionsRunnerToken(ctx, cliCtx) }, + Action: RunGenerateActionsRunnerToken, Flags: []cli.Flag{ &cli.StringFlag{ Name: "scope", @@ -52,7 +53,7 @@ func SubcmdActionsGenerateRunnerSecret(ctx context.Context) *cli.Command { return &cli.Command{ Name: "generate-secret", Usage: "Generate a secret suitable for input to the register subcommand", - Action: func(cliCtx *cli.Context) error { return RunGenerateSecret(ctx, cliCtx) }, + Action: RunGenerateSecret, } } @@ -61,7 +62,7 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command { Name: "register", Usage: "Idempotent registration of a runner using a shared secret", Before: prepareWorkPathAndCustomConf(ctx), - Action: func(cliCtx *cli.Context) error { return RunRegister(ctx, cliCtx) }, + Action: RunRegister, Flags: []cli.Flag{ &cli.StringFlag{ Name: "secret", @@ -105,26 +106,26 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command { } } -func readSecret(ctx context.Context, cliCtx *cli.Context) (string, error) { - if cliCtx.IsSet("secret") { - return cliCtx.String("secret"), nil +func readSecret(ctx context.Context, cli *cli.Command) (string, error) { + if cli.IsSet("secret") { + return cli.String("secret"), nil } - if cliCtx.IsSet("secret-stdin") { + if cli.IsSet("secret-stdin") { buf, err := io.ReadAll(ContextGetStdin(ctx)) if err != nil { return "", err } return string(buf), nil } - if cliCtx.IsSet("secret-file") { - path := cliCtx.String("secret-file") + if cli.IsSet("secret-file") { + path := cli.String("secret-file") buf, err := os.ReadFile(path) if err != nil { return "", err } return string(buf), nil } - return "", fmt.Errorf("at least one of the --secret, --secret-stdin, --secret-file options is required") + return "", errors.New("at least one of the --secret, --secret-stdin, --secret-file options is required") } func validateSecret(secret string) error { @@ -138,18 +139,18 @@ func validateSecret(secret string) error { return nil } -func getLabels(cliCtx *cli.Context) (*[]string, error) { - if !cliCtx.Bool("keep-labels") { - lblValue := strings.Split(cliCtx.String("labels"), ",") +func getLabels(cli *cli.Command) (*[]string, error) { + if !cli.Bool("keep-labels") { + lblValue := strings.Split(cli.String("labels"), ",") return &lblValue, nil } - if cliCtx.String("labels") != "" { - return nil, fmt.Errorf("--labels and --keep-labels should not be used together") + if cli.String("labels") != "" { + return nil, errors.New("--labels and --keep-labels should not be used together") } return nil, nil } -func RunRegister(ctx context.Context, cliCtx *cli.Context) error { +func RunRegister(ctx context.Context, cli *cli.Command) error { var cancel context.CancelFunc if !ContextGetNoInit(ctx) { ctx, cancel = installSignals(ctx) @@ -161,17 +162,17 @@ func RunRegister(ctx context.Context, cliCtx *cli.Context) error { } setting.MustInstalled() - secret, err := readSecret(ctx, cliCtx) + secret, err := readSecret(ctx, cli) if err != nil { return err } if err := validateSecret(secret); err != nil { return err } - scope := cliCtx.String("scope") - name := cliCtx.String("name") - version := cliCtx.String("version") - labels, err := getLabels(cliCtx) + scope := cli.String("scope") + name := cli.String("name") + version := cli.String("version") + labels, err := getLabels(cli) if err != nil { return err } @@ -209,7 +210,7 @@ func RunRegister(ctx context.Context, cliCtx *cli.Context) error { return nil } -func RunGenerateSecret(ctx context.Context, cliCtx *cli.Context) error { +func RunGenerateSecret(ctx context.Context, cli *cli.Command) error { runner := actions_model.ActionRunner{} if err := runner.GenerateToken(); err != nil { return err @@ -220,7 +221,7 @@ func RunGenerateSecret(ctx context.Context, cliCtx *cli.Context) error { return nil } -func RunGenerateActionsRunnerToken(ctx context.Context, cliCtx *cli.Context) error { +func RunGenerateActionsRunnerToken(ctx context.Context, cli *cli.Command) error { if !ContextGetNoInit(ctx) { var cancel context.CancelFunc ctx, cancel = installSignals(ctx) @@ -229,7 +230,7 @@ func RunGenerateActionsRunnerToken(ctx context.Context, cliCtx *cli.Context) err setting.MustInstalled() - scope := cliCtx.String("scope") + scope := cli.String("scope") respText, extra := private.GenerateActionsRunnerToken(ctx, scope) if extra.HasError() { diff --git a/cmd/forgejo/actions_test.go b/cmd/forgejo/actions_test.go index b58f52184c..11315239f7 100644 --- a/cmd/forgejo/actions_test.go +++ b/cmd/forgejo/actions_test.go @@ -4,14 +4,13 @@ package forgejo import ( + "context" "fmt" "testing" - "forgejo.org/services/context" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestActions_getLabels(t *testing.T) { @@ -54,21 +53,21 @@ func TestActions_getLabels(t *testing.T) { }, } - flags := SubcmdActionsRegister(context.Context{}).Flags + flags := SubcmdActionsRegister(t.Context()).Flags for _, c := range cases { t.Run(fmt.Sprintf("args: %v", c.args), func(t *testing.T) { // Create a copy of command to test var result *resultType - app := cli.NewApp() + app := cli.Command{} app.Flags = flags - app.Action = func(ctx *cli.Context) error { + app.Action = func(_ context.Context, ctx *cli.Command) error { labels, err := getLabels(ctx) result = &resultType{labels, err} return nil } // Run it - _ = app.Run(c.args) + _ = app.Run(t.Context(), c.args) // Test the results require.NotNil(t, result) diff --git a/cmd/forgejo/f3.go b/cmd/forgejo/f3.go index bfd14cd1a4..c4aafeac58 100644 --- a/cmd/forgejo/f3.go +++ b/cmd/forgejo/f3.go @@ -20,7 +20,7 @@ import ( f3_cmd "code.forgejo.org/f3/gof3/v3/cmd" f3_logger "code.forgejo.org/f3/gof3/v3/logger" f3_util "code.forgejo.org/f3/gof3/v3/util" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func CmdF3(ctx context.Context) *cli.Command { @@ -28,21 +28,21 @@ func CmdF3(ctx context.Context) *cli.Command { return &cli.Command{ Name: "f3", Usage: "F3", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ SubcmdF3Mirror(ctx), }, } } func SubcmdF3Mirror(ctx context.Context) *cli.Command { - mirrorCmd := f3_cmd.CreateCmdMirror(ctx) + mirrorCmd := f3_cmd.CreateCmdMirror() mirrorCmd.Before = prepareWorkPathAndCustomConf(ctx) f3Action := mirrorCmd.Action - mirrorCmd.Action = func(c *cli.Context) error { return runMirror(ctx, c, f3Action) } + mirrorCmd.Action = func(ctx context.Context, cli *cli.Command) error { return runMirror(ctx, cli, f3Action) } return mirrorCmd } -func runMirror(ctx context.Context, c *cli.Context, action cli.ActionFunc) error { +func runMirror(ctx context.Context, c *cli.Command, action cli.ActionFunc) error { setting.LoadF3Setting() if !setting.F3.Enabled { return errors.New("F3 is disabled, it is not ready to be used and is only present for development purposes") @@ -69,7 +69,7 @@ func runMirror(ctx context.Context, c *cli.Context, action cli.ActionFunc) error } } - err := action(c) + err := action(ctx, c) if panicError, ok := err.(f3_util.PanicError); ok { log.Debug("F3 Stack trace\n%s", panicError.Stack()) } diff --git a/cmd/forgejo/forgejo.go b/cmd/forgejo/forgejo.go index 3b95c80991..171ef1a71d 100644 --- a/cmd/forgejo/forgejo.go +++ b/cmd/forgejo/forgejo.go @@ -16,7 +16,7 @@ import ( "forgejo.org/modules/private" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) type key int @@ -34,7 +34,7 @@ func CmdForgejo(ctx context.Context) *cli.Command { Name: "forgejo-cli", Usage: "Forgejo CLI", Flags: []cli.Flag{}, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ CmdActions(ctx), CmdF3(ctx), }, @@ -147,12 +147,12 @@ func handleCliResponseExtra(ctx context.Context, extra private.ResponseExtra) er return cli.Exit(extra.Error, 1) } -func prepareWorkPathAndCustomConf(ctx context.Context) func(c *cli.Context) error { - return func(c *cli.Context) error { +func prepareWorkPathAndCustomConf(ctx context.Context) func(ctx context.Context, cli *cli.Command) (context.Context, error) { + return func(ctx context.Context, cli *cli.Command) (context.Context, error) { if !ContextGetNoInit(ctx) { var args setting.ArgWorkPathAndCustomConf // from children to parent, check the global flags - for _, curCtx := range c.Lineage() { + for _, curCtx := range cli.Lineage() { if curCtx.IsSet("work-path") && args.WorkPath == "" { args.WorkPath = curCtx.String("work-path") } @@ -165,6 +165,6 @@ func prepareWorkPathAndCustomConf(ctx context.Context) func(c *cli.Context) erro } setting.InitWorkPathAndCommonConfig(os.Getenv, args) } - return nil + return ctx, nil } } diff --git a/cmd/generate.go b/cmd/generate.go index dcbdcd0353..7076ae541f 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -5,56 +5,65 @@ package cmd import ( + "context" "fmt" "os" "forgejo.org/modules/generate" "github.com/mattn/go-isatty" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - // CmdGenerate represents the available generate sub-command. - CmdGenerate = &cli.Command{ +// CmdGenerate represents the available generate sub-command. +func cmdGenerate() *cli.Command { + return &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", - Subcommands: []*cli.Command{ - subcmdSecret, + Commands: []*cli.Command{ + subcmdSecret(), }, } +} - subcmdSecret = &cli.Command{ +func subcmdSecret() *cli.Command { + return &cli.Command{ Name: "secret", Usage: "Generate a secret token", - Subcommands: []*cli.Command{ - microcmdGenerateInternalToken, - microcmdGenerateLfsJwtSecret, - microcmdGenerateSecretKey, + Commands: []*cli.Command{ + microcmdGenerateInternalToken(), + microcmdGenerateLfsJwtSecret(), + microcmdGenerateSecretKey(), }, } +} - microcmdGenerateInternalToken = &cli.Command{ +func microcmdGenerateInternalToken() *cli.Command { + return &cli.Command{ Name: "INTERNAL_TOKEN", Usage: "Generate a new INTERNAL_TOKEN", Action: runGenerateInternalToken, } +} - microcmdGenerateLfsJwtSecret = &cli.Command{ +func microcmdGenerateLfsJwtSecret() *cli.Command { + return &cli.Command{ Name: "JWT_SECRET", Aliases: []string{"LFS_JWT_SECRET"}, Usage: "Generate a new JWT_SECRET", Action: runGenerateLfsJwtSecret, } +} - microcmdGenerateSecretKey = &cli.Command{ +func microcmdGenerateSecretKey() *cli.Command { + return &cli.Command{ Name: "SECRET_KEY", Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } -) +} -func runGenerateInternalToken(c *cli.Context) error { +func runGenerateInternalToken(ctx context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() if err != nil { return err @@ -63,25 +72,25 @@ func runGenerateInternalToken(c *cli.Context) error { fmt.Printf("%s", internalToken) if isatty.IsTerminal(os.Stdout.Fd()) { - fmt.Printf("\n") + fmt.Println() } return nil } -func runGenerateLfsJwtSecret(c *cli.Context) error { +func runGenerateLfsJwtSecret(ctx context.Context, c *cli.Command) error { _, jwtSecretBase64 := generate.NewJwtSecret() fmt.Printf("%s", jwtSecretBase64) if isatty.IsTerminal(os.Stdout.Fd()) { - fmt.Printf("\n") + fmt.Print("\n") } return nil } -func runGenerateSecretKey(c *cli.Context) error { +func runGenerateSecretKey(ctx context.Context, c *cli.Command) error { secretKey, err := generate.NewSecretKey() if err != nil { return err @@ -90,7 +99,7 @@ func runGenerateSecretKey(c *cli.Context) error { fmt.Printf("%s", secretKey) if isatty.IsTerminal(os.Stdout.Fd()) { - fmt.Printf("\n") + fmt.Print("\n") } return nil diff --git a/cmd/hook.go b/cmd/hook.go index 935c1b08ea..909cdfdf84 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -21,29 +21,31 @@ import ( repo_module "forgejo.org/modules/repository" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const ( hookBatchSize = 30 ) -var ( - // CmdHook represents the available hooks sub-command. - CmdHook = &cli.Command{ +// CmdHook represents the available hooks sub-command. +func cmdHook() *cli.Command { + return &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), - Subcommands: []*cli.Command{ - subcmdHookPreReceive, - subcmdHookUpdate, - subcmdHookPostReceive, - subcmdHookProcReceive, + Commands: []*cli.Command{ + subcmdHookPreReceive(), + subcmdHookUpdate(), + subcmdHookPostReceive(), + subcmdHookProcReceive(), }, } +} - subcmdHookPreReceive = &cli.Command{ +func subcmdHookPreReceive() *cli.Command { + return &cli.Command{ Name: "pre-receive", Usage: "Delegate pre-receive Git hook", Description: "This command should only be called by Git", @@ -54,7 +56,10 @@ var ( }, }, } - subcmdHookUpdate = &cli.Command{ +} + +func subcmdHookUpdate() *cli.Command { + return &cli.Command{ Name: "update", Usage: "Delegate update Git hook", Description: "This command should only be called by Git", @@ -65,7 +70,10 @@ var ( }, }, } - subcmdHookPostReceive = &cli.Command{ +} + +func subcmdHookPostReceive() *cli.Command { + return &cli.Command{ Name: "post-receive", Usage: "Delegate post-receive Git hook", Description: "This command should only be called by Git", @@ -76,8 +84,11 @@ var ( }, }, } - // Note: new hook since git 2.29 - subcmdHookProcReceive = &cli.Command{ +} + +// Note: new hook since git 2.29 +func subcmdHookProcReceive() *cli.Command { + return &cli.Command{ Name: "proc-receive", Usage: "Delegate proc-receive Git hook", Description: "This command should only be called by Git", @@ -88,7 +99,7 @@ var ( }, }, } -) +} type delayWriter struct { internal io.Writer @@ -161,11 +172,11 @@ func (n *nilWriter) WriteString(s string) (int, error) { return len(s), nil } -func runHookPreReceive(c *cli.Context) error { +func runHookPreReceive(ctx context.Context, c *cli.Command) error { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { return nil } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), true) @@ -247,7 +258,7 @@ Forgejo or set your environment appropriately.`, "") newCommitIDs[count] = newCommitID refFullNames[count] = refFullName count++ - fmt.Fprintf(out, "*") + fmt.Fprint(out, "*") if count >= hookBatchSize { fmt.Fprintf(out, " Checking %d references\n", count) @@ -263,10 +274,10 @@ Forgejo or set your environment appropriately.`, "") lastline = 0 } } else { - fmt.Fprintf(out, ".") + fmt.Fprint(out, ".") } if lastline >= hookBatchSize { - fmt.Fprintf(out, "\n") + fmt.Fprint(out, "\n") lastline = 0 } } @@ -283,7 +294,7 @@ Forgejo or set your environment appropriately.`, "") return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error) } } else if lastline > 0 { - fmt.Fprintf(out, "\n") + fmt.Fprint(out, "\n") } fmt.Fprintf(out, "Checked %d references in total\n", total) @@ -291,13 +302,13 @@ Forgejo or set your environment appropriately.`, "") } // runHookUpdate process the update hook: https://git-scm.com/docs/githooks#update -func runHookUpdate(c *cli.Context) error { +func runHookUpdate(ctx context.Context, c *cli.Command) error { // Now if we're an internal don't do anything else if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { return nil } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() if c.NArg() != 3 { @@ -323,8 +334,8 @@ func runHookUpdate(c *cli.Context) error { return fail(ctx, fmt.Sprintf("The modification of %s is skipped as it's an internal reference.", refFullName), "") } -func runHookPostReceive(c *cli.Context) error { - ctx, cancel := installSignals() +func runHookPostReceive(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), true) @@ -399,7 +410,7 @@ Forgejo or set your environment appropriately.`, "") continue } - fmt.Fprintf(out, ".") + fmt.Fprint(out, ".") oldCommitIDs[count] = string(fields[0]) newCommitIDs[count] = string(fields[1]) refFullNames[count] = git.RefName(fields[2]) @@ -487,8 +498,8 @@ func hookPrintResults(results []private.HookPostReceiveBranchResult) { } } -func runHookProcReceive(c *cli.Context) error { - ctx, cancel := installSignals() +func runHookProcReceive(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), true) diff --git a/cmd/hook_test.go b/cmd/hook_test.go index 89bd3cf737..82ed392fb8 100644 --- a/cmd/hook_test.go +++ b/cmd/hook_test.go @@ -19,7 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // Capture what's being written into a standard file descriptor. @@ -134,14 +134,14 @@ func TestDelayWriter(t *testing.T) { defer ts.Close() defer test.MockVariableValue(&setting.LocalURL, ts.URL+"/")() - app := cli.NewApp() - app.Commands = []*cli.Command{subcmdHookPreReceive} + app := cli.Command{} + app.Commands = []*cli.Command{subcmdHookPreReceive()} t.Run("Should delay", func(t *testing.T) { defer test.MockVariableValue(&setting.Git.VerbosePushDelay, time.Millisecond*500)() finish := captureOutput(t, os.Stdout) - err = app.Run([]string{"./forgejo", "pre-receive"}) + err = app.Run(t.Context(), []string{"./forgejo", "pre-receive"}) require.NoError(t, err) out := finish() @@ -153,7 +153,7 @@ func TestDelayWriter(t *testing.T) { defer test.MockVariableValue(&setting.Git.VerbosePushDelay, time.Second*5)() finish := captureOutput(t, os.Stdout) - err = app.Run([]string{"./forgejo", "pre-receive"}) + err = app.Run(t.Context(), []string{"./forgejo", "pre-receive"}) require.NoError(t, err) out := finish() @@ -163,15 +163,15 @@ func TestDelayWriter(t *testing.T) { } func TestRunHookUpdate(t *testing.T) { - app := cli.NewApp() - app.Commands = []*cli.Command{subcmdHookUpdate} + app := cli.Command{} + app.Commands = []*cli.Command{subcmdHookUpdate()} t.Run("Removal of internal reference", func(t *testing.T) { defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() defer test.MockVariableValue(&setting.IsProd, false)() finish := captureOutput(t, os.Stderr) - err := app.Run([]string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"}) + err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"}) out := finish() require.Error(t, err) @@ -183,7 +183,7 @@ func TestRunHookUpdate(t *testing.T) { defer test.MockVariableValue(&setting.IsProd, false)() finish := captureOutput(t, os.Stderr) - err := app.Run([]string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000001"}) + err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/pull/1/head", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000001"}) out := finish() require.Error(t, err) @@ -191,12 +191,12 @@ func TestRunHookUpdate(t *testing.T) { }) t.Run("Removal of branch", func(t *testing.T) { - err := app.Run([]string{"./forgejo", "update", "refs/head/main", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"}) + err := app.Run(t.Context(), []string{"./forgejo", "update", "refs/head/main", "0a51ae26bc73c47e2f754560c40904cf14ed51a9", "0000000000000000000000000000000000000000"}) require.NoError(t, err) }) t.Run("Not enough arguments", func(t *testing.T) { - err := app.Run([]string{"./forgejo", "update"}) + err := app.Run(t.Context(), []string{"./forgejo", "update"}) require.NoError(t, err) }) } diff --git a/cmd/keys.go b/cmd/keys.go index 2d241984c4..00901903f4 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "strings" @@ -11,45 +12,47 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdKeys represents the available keys sub-command -var CmdKeys = &cli.Command{ - Name: "keys", - Usage: "(internal) Should only be called by SSH server", - Description: "Queries the Forgejo database to get the authorized command for a given ssh key fingerprint", - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runKeys, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "expected", - Aliases: []string{"e"}, - Value: "git", - Usage: "Expected user for whom provide key commands", +func cmdKeys() *cli.Command { + return &cli.Command{ + Name: "keys", + Usage: "(internal) Should only be called by SSH server", + Description: "Queries the Forgejo database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runKeys, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "expected", + Aliases: []string{"e"}, + Value: "git", + Usage: "Expected user for whom provide key commands", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "Username trying to log in by SSH", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", + }, + &cli.StringFlag{ + Name: "content", + Aliases: []string{"k"}, + Value: "", + Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "Username trying to log in by SSH", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", - }, - &cli.StringFlag{ - Name: "content", - Aliases: []string{"k"}, - Value: "", - Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", - }, - }, + } } -func runKeys(c *cli.Context) error { +func runKeys(ctx context.Context, c *cli.Command) error { if !c.IsSet("username") { return errors.New("No username provided") } @@ -68,7 +71,7 @@ func runKeys(c *cli.Context) error { return errors.New("No key type and content provided") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), true) @@ -78,6 +81,6 @@ func runKeys(c *cli.Context) error { if extra.Error != nil { return extra.Error } - _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text)) + _, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text)) return nil } diff --git a/cmd/mailer.go b/cmd/mailer.go index cddebd6d36..d05d6c849b 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -4,16 +4,17 @@ package cmd import ( + "context" "fmt" "forgejo.org/modules/private" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -func runSendMail(c *cli.Context) error { - ctx, cancel := installSignals() +func runSendMail(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setting.MustInstalled() diff --git a/cmd/main.go b/cmd/main.go index 9d559e078f..65cde47884 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,7 +14,7 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // cmdHelp is our own help subcommand with more information @@ -25,18 +25,18 @@ func cmdHelp() *cli.Command { Aliases: []string{"h"}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(c *cli.Context) (err error) { - lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil} + Action: func(ctx context.Context, c *cli.Command) (err error) { + lineage := c.Lineage() // The order is from child to parent: help, doctor, Forgejo targetCmdIdx := 0 - if c.Command.Name == "help" { + if c.Name == "help" { targetCmdIdx = 1 } - if lineage[targetCmdIdx+1].Command != nil { - err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name) + if targetCmdIdx+1 < len(lineage) { + err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1], lineage[targetCmdIdx].Name) } else { err = cli.ShowAppHelp(c) } - _, _ = fmt.Fprintf(c.App.Writer, ` + _, _ = fmt.Fprintf(c.Root().Writer, ` DEFAULT CONFIGURATION: AppPath: %s WorkPath: %s @@ -77,25 +77,25 @@ func appGlobalFlags() []cli.Flag { } } -func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) { - command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...) +func prepareSubcommandWithConfig(command *cli.Command, globalFlags func() []cli.Flag) { + command.Flags = append(globalFlags(), command.Flags...) command.Action = prepareWorkPathAndCustomConf(command.Action) command.HideHelp = true if command.Name != "help" { - command.Subcommands = append(command.Subcommands, cmdHelp()) + command.Commands = append(command.Commands, cmdHelp()) } - for i := range command.Subcommands { - prepareSubcommandWithConfig(command.Subcommands[i], globalFlags) + for i := range command.Commands { + prepareSubcommandWithConfig(command.Commands[i], globalFlags) } } // prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config // It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times -func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error { - return func(ctx *cli.Context) error { +func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(_ context.Context, _ *cli.Command) error { + return func(ctx context.Context, cli *cli.Command) error { var args setting.ArgWorkPathAndCustomConf // from children to parent, check the global flags - for _, curCtx := range ctx.Lineage() { + for _, curCtx := range cli.Lineage() { if curCtx.IsSet("work-path") && args.WorkPath == "" { args.WorkPath = curCtx.String("work-path") } @@ -107,15 +107,15 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) } } setting.InitWorkPathAndCommonConfig(os.Getenv, args) - if ctx.Bool("help") || action == nil { + if cli.Bool("help") || action == nil { // the default behavior of "urfave/cli": "nil action" means "show help" - return cmdHelp().Action(ctx) + return cmdHelp().Action(ctx, cli) } - return action(ctx) + return action(ctx, cli) } } -func NewMainApp(version, versionExtra string) *cli.App { +func NewMainApp(version, versionExtra string) *cli.Command { path, err := os.Executable() if err != nil { panic(err) @@ -124,7 +124,7 @@ func NewMainApp(version, versionExtra string) *cli.App { subCmdsStandalone := make([]*cli.Command, 0, 10) subCmdWithConfig := make([]*cli.Command, 0, 10) - globalFlags := make([]cli.Flag, 0, 10) + globalFlags := func() []cli.Flag { return []cli.Flag{} } // // If the executable is forgejo-cli, provide a Forgejo specific CLI @@ -133,14 +133,16 @@ func NewMainApp(version, versionExtra string) *cli.App { if executable == "forgejo-cli" { subCmdsStandalone = append(subCmdsStandalone, forgejo.CmdActions(context.Background())) subCmdWithConfig = append(subCmdWithConfig, forgejo.CmdF3(context.Background())) - globalFlags = append(globalFlags, []cli.Flag{ - &cli.BoolFlag{ - Name: "quiet", - }, - &cli.BoolFlag{ - Name: "verbose", - }, - }...) + globalFlags = func() []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + }, + &cli.BoolFlag{ + Name: "verbose", + }, + } + } } else { // // Otherwise provide a Gitea compatible CLI which includes Forgejo @@ -149,55 +151,54 @@ func NewMainApp(version, versionExtra string) *cli.App { // binary and rename it to forgejo if they want. // subCmdsStandalone = append(subCmdsStandalone, forgejo.CmdForgejo(context.Background())) - subCmdWithConfig = append(subCmdWithConfig, CmdActions) + subCmdWithConfig = append(subCmdWithConfig, cmdActions()) } return innerNewMainApp(version, versionExtra, subCmdsStandalone, subCmdWithConfig, globalFlags) } -func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmdWithConfigArgs []*cli.Command, globalFlagsArgs []cli.Flag) *cli.App { - app := cli.NewApp() - app.HelpName = "forgejo" - app.Name = "Forgejo" +func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmdWithConfigArgs []*cli.Command, globalFlagsArgs func() []cli.Flag) *cli.Command { + app := &cli.Command{} + app.Name = "forgejo" app.Usage = "Beyond coding. We forge." app.Description = `By default, forgejo will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` app.Version = version + versionExtra - app.EnableBashCompletion = true + app.EnableShellCompletion = true // these sub-commands need to use config file subCmdWithConfig := []*cli.Command{ cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config" - CmdWeb, - CmdServ, - CmdHook, - CmdKeys, - CmdDump, - CmdAdmin, - CmdMigrate, - CmdDoctor, - CmdManager, - CmdEmbedded, - CmdMigrateStorage, - CmdDumpRepository, - CmdRestoreRepository, + cmdWeb(), + cmdServ(), + cmdHook(), + cmdKeys(), + cmdDump(), + cmdAdmin(), + cmdMigrate(), + cmdDoctor(), + cmdManager(), + cmdEmbedded(), + cmdMigrateStorage(), + cmdDumpRepository(), + cmdRestoreRepository(), } subCmdWithConfig = append(subCmdWithConfig, subCmdWithConfigArgs...) // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ - CmdCert, - CmdGenerate, - CmdDocs, + cmdCert(), + cmdGenerate(), } subCmdStandalone = append(subCmdStandalone, subCmdsStandaloneArgs...) - app.DefaultCommand = CmdWeb.Name + app.DefaultCommand = cmdWeb().Name - globalFlags := appGlobalFlags() - globalFlags = append(globalFlags, globalFlagsArgs...) + globalFlags := func() []cli.Flag { + return append(appGlobalFlags(), globalFlagsArgs()...) + } app.Flags = append(app.Flags, cli.VersionFlag) - app.Flags = append(app.Flags, globalFlags...) + app.Flags = append(app.Flags, globalFlags()...) app.HideHelp = true // use our own help action to show helps (with more information like default config) app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { @@ -210,8 +211,8 @@ func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmd return app } -func RunMainApp(app *cli.App, args ...string) error { - err := app.Run(args) +func RunMainApp(app *cli.Command, args ...string) error { + err := app.Run(context.Background(), args) if err == nil { return nil } @@ -220,7 +221,7 @@ func RunMainApp(app *cli.App, args ...string) error { cli.OsExiter(1) return err } - _, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err) + _, _ = fmt.Fprintf(app.Root().ErrWriter, "Command error: %v\n", err) cli.OsExiter(1) return err } diff --git a/cmd/main_test.go b/cmd/main_test.go index 1ff71005e3..737150c62f 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -4,6 +4,8 @@ package cmd import ( + "context" + "errors" "fmt" "io" "path/filepath" @@ -16,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestMain(m *testing.M) { @@ -27,10 +29,10 @@ func makePathOutput(workPath, customPath, customConf string) string { return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) } -func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App { +func newTestApp(testCmdAction func(_ context.Context, ctx *cli.Command) error) *cli.Command { app := NewMainApp("version", "version-extra") testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction} - prepareSubcommandWithConfig(testCmd, appGlobalFlags()) + prepareSubcommandWithConfig(testCmd, appGlobalFlags) app.Commands = append(app.Commands, testCmd) app.DefaultCommand = testCmd.Name return app @@ -42,7 +44,7 @@ type runResult struct { ExitCode int } -func runTestApp(app *cli.App, args ...string) (runResult, error) { +func runTestApp(app *cli.Command, args ...string) (runResult, error) { outBuf := new(strings.Builder) errBuf := new(strings.Builder) app.Writer = outBuf @@ -65,7 +67,6 @@ func TestCliCmd(t *testing.T) { defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") cli.CommandHelpTemplate = "(command help template)" - cli.AppHelpTemplate = "(app help template)" cli.SubcommandHelpTemplate = "(subcommand help template)" cases := []struct { @@ -109,12 +110,17 @@ func TestCliCmd(t *testing.T) { }, } - app := newTestApp(func(ctx *cli.Context) error { - _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) - return nil - }) for _, c := range cases { t.Run(c.cmd, func(t *testing.T) { + defer test.MockProtect(&setting.AppWorkPath)() + defer test.MockProtect(&setting.CustomPath)() + defer test.MockProtect(&setting.CustomConf)() + + app := newTestApp(func(_ context.Context, ctx *cli.Command) error { + _, _ = fmt.Fprint(ctx.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) + return nil + }) + for k, v := range c.env { t.Setenv(k, v) } @@ -122,34 +128,34 @@ func TestCliCmd(t *testing.T) { r, err := runTestApp(app, args...) require.NoError(t, err, c.cmd) assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd+"\n"+r.Stdout) }) } } func TestCliCmdError(t *testing.T) { - app := newTestApp(func(ctx *cli.Context) error { return fmt.Errorf("normal error") }) + app := newTestApp(func(_ context.Context, ctx *cli.Command) error { return errors.New("normal error") }) r, err := runTestApp(app, "./gitea", "test-cmd") require.Error(t, err) assert.Equal(t, 1, r.ExitCode) assert.Empty(t, r.Stdout) assert.Equal(t, "Command error: normal error\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) }) + app = newTestApp(func(_ context.Context, ctx *cli.Command) error { return cli.Exit("exit error", 2) }) r, err = runTestApp(app, "./gitea", "test-cmd") require.Error(t, err) assert.Equal(t, 2, r.ExitCode) assert.Empty(t, r.Stdout) assert.Equal(t, "exit error\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return nil }) + app = newTestApp(func(_ context.Context, ctx *cli.Command) error { return nil }) r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") require.Error(t, err) assert.Equal(t, 1, r.ExitCode) - assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout) - assert.Empty(t, r.Stderr) // the cli package's strange behavior, the error message is not in stderr .... + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) + assert.Empty(t, r.Stdout) - app = newTestApp(func(ctx *cli.Context) error { return nil }) + app = newTestApp(func(_ context.Context, ctx *cli.Command) error { return nil }) r, err = runTestApp(app, "./gitea", "test-cmd") require.NoError(t, err) assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called diff --git a/cmd/manager.go b/cmd/manager.go index 56089947fd..029329b44e 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -4,30 +4,34 @@ package cmd import ( + "context" "os" "time" "forgejo.org/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - // CmdManager represents the manager command - CmdManager = &cli.Command{ +// CmdManager represents the manager command +func cmdManager() *cli.Command { + return &cli.Command{ Name: "manager", Usage: "Manage the running forgejo process", Description: "This is a command for managing the running forgejo process", - Subcommands: []*cli.Command{ - subcmdShutdown, - subcmdRestart, - subcmdReloadTemplates, - subcmdFlushQueues, - subcmdLogging, - subCmdProcesses, + Commands: []*cli.Command{ + subcmdShutdown(), + subcmdRestart(), + subcmdReloadTemplates(), + subcmdFlushQueues(), + subcmdLogging(), + subCmdProcesses(), }, } - subcmdShutdown = &cli.Command{ +} + +func subcmdShutdown() *cli.Command { + return &cli.Command{ Name: "shutdown", Usage: "Gracefully shutdown the running process", Flags: []cli.Flag{ @@ -37,7 +41,10 @@ var ( }, Action: runShutdown, } - subcmdRestart = &cli.Command{ +} + +func subcmdRestart() *cli.Command { + return &cli.Command{ Name: "restart", Usage: "Gracefully restart the running process - (not implemented for windows servers)", Flags: []cli.Flag{ @@ -47,7 +54,10 @@ var ( }, Action: runRestart, } - subcmdReloadTemplates = &cli.Command{ +} + +func subcmdReloadTemplates() *cli.Command { + return &cli.Command{ Name: "reload-templates", Usage: "Reload template files in the running process", Flags: []cli.Flag{ @@ -57,7 +67,10 @@ var ( }, Action: runReloadTemplates, } - subcmdFlushQueues = &cli.Command{ +} + +func subcmdFlushQueues() *cli.Command { + return &cli.Command{ Name: "flush-queues", Usage: "Flush queues in the running process", Action: runFlushQueues, @@ -76,7 +89,10 @@ var ( }, }, } - subCmdProcesses = &cli.Command{ +} + +func subCmdProcesses() *cli.Command { + return &cli.Command{ Name: "processes", Usage: "Display running processes within the current process", Action: runProcesses, @@ -106,10 +122,10 @@ var ( }, }, } -) +} -func runShutdown(c *cli.Context) error { - ctx, cancel := installSignals() +func runShutdown(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -117,8 +133,8 @@ func runShutdown(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runRestart(c *cli.Context) error { - ctx, cancel := installSignals() +func runRestart(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -126,8 +142,8 @@ func runRestart(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runReloadTemplates(c *cli.Context) error { - ctx, cancel := installSignals() +func runReloadTemplates(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -135,8 +151,8 @@ func runReloadTemplates(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runFlushQueues(c *cli.Context) error { - ctx, cancel := installSignals() +func runFlushQueues(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -144,8 +160,8 @@ func runFlushQueues(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runProcesses(c *cli.Context) error { - ctx, cancel := installSignals() +func runProcesses(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index c6db2bb05a..c543afe872 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -11,11 +12,11 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - defaultLoggingFlags = []cli.Flag{ +func defaultLoggingFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "logger", Usage: `Logger name - will default to "default"`, @@ -56,11 +57,13 @@ var ( Name: "debug", }, } +} - subcmdLogging = &cli.Command{ +func subcmdLogging() *cli.Command { + return &cli.Command{ Name: "logging", Usage: "Adjust logging commands", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "pause", Usage: "Pause logging (Forgejo will buffer logs up to a certain point and will drop them after that point)", @@ -104,11 +107,11 @@ var ( }, { Name: "add", Usage: "Add a logger", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "file", Usage: "Add a file logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.StringFlag{ Name: "filename", Aliases: []string{"f"}, @@ -152,7 +155,7 @@ var ( }, { Name: "conn", Usage: "Add a net conn logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.BoolFlag{ Name: "reconnect-on-message", Aliases: []string{"R"}, @@ -193,10 +196,10 @@ var ( }, }, } -) +} -func runRemoveLogger(c *cli.Context) error { - ctx, cancel := installSignals() +func runRemoveLogger(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -210,8 +213,8 @@ func runRemoveLogger(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runAddConnLogger(c *cli.Context) error { - ctx, cancel := installSignals() +func runAddConnLogger(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -237,11 +240,11 @@ func runAddConnLogger(c *cli.Context) error { if c.IsSet("reconnect-on-message") { vals["reconnectOnMsg"] = c.Bool("reconnect-on-message") } - return commonAddLogger(c, mode, vals) + return commonAddLogger(ctx, c, mode, vals) } -func runAddFileLogger(c *cli.Context) error { - ctx, cancel := installSignals() +func runAddFileLogger(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -270,10 +273,10 @@ func runAddFileLogger(c *cli.Context) error { if c.IsSet("compression-level") { vals["compressionLevel"] = c.Int("compression-level") } - return commonAddLogger(c, mode, vals) + return commonAddLogger(ctx, c, mode, vals) } -func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { +func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error { if len(c.String("level")) > 0 { vals["level"] = log.LevelFromString(c.String("level")).String() } @@ -300,15 +303,15 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { if c.IsSet("writer") { writer = c.String("writer") } - ctx, cancel := installSignals() + ctx, cancel := installSignals(ctx) defer cancel() extra := private.AddLogger(ctx, logger, writer, mode, vals) return handleCliResponseExtra(extra) } -func runPauseLogging(c *cli.Context) error { - ctx, cancel := installSignals() +func runPauseLogging(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -317,8 +320,8 @@ func runPauseLogging(c *cli.Context) error { return nil } -func runResumeLogging(c *cli.Context) error { - ctx, cancel := installSignals() +func runResumeLogging(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -327,8 +330,8 @@ func runResumeLogging(c *cli.Context) error { return nil } -func runReleaseReopenLogging(c *cli.Context) error { - ctx, cancel := installSignals() +func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) @@ -337,8 +340,8 @@ func runReleaseReopenLogging(c *cli.Context) error { return nil } -func runSetLogSQL(c *cli.Context) error { - ctx, cancel := installSignals() +func runSetLogSQL(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setup(ctx, c.Bool("debug"), false) diff --git a/cmd/migrate.go b/cmd/migrate.go index c192ca1966..5a485d17f9 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -11,19 +11,21 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdMigrate represents the available migrate sub-command. -var CmdMigrate = &cli.Command{ - Name: "migrate", - Usage: "Migrate the database", - Description: "This is a command for migrating the database, so that you can run 'forgejo admin user create' before starting the server.", - Action: runMigrate, +func cmdMigrate() *cli.Command { + return &cli.Command{ + Name: "migrate", + Usage: "Migrate the database", + Description: "This is a command for migrating the database, so that you can run 'forgejo admin user create' before starting the server.", + Action: runMigrate, + } } -func runMigrate(ctx *cli.Context) error { - stdCtx, cancel := installSignals() +func runMigrate(stdCtx context.Context, ctx *cli.Command) error { + stdCtx, cancel := installSignals(stdCtx) defer cancel() if err := initDB(stdCtx); err != nil { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 6a04dd48ae..d741a883e3 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -22,79 +22,81 @@ import ( "forgejo.org/modules/setting" "forgejo.org/modules/storage" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "xorm.io/xorm" ) // CmdMigrateStorage represents the available migrate storage sub-command. -var CmdMigrateStorage = &cli.Command{ - Name: "migrate-storage", - Usage: "Migrate the storage", - Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", - Action: runMigrateStorage, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", +func cmdMigrateStorage() *cli.Command { + return &cli.Command{ + Name: "migrate-storage", + Usage: "Migrate the storage", + Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", + Action: runMigrateStorage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", + }, + &cli.StringFlag{ + Name: "storage", + Aliases: []string{"s"}, + Value: "", + Usage: "New storage type: local (default) or minio", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "New storage placement if store is local (leave blank for default)", + }, + &cli.StringFlag{ + Name: "minio-endpoint", + Value: "", + Usage: "Minio storage endpoint", + }, + &cli.StringFlag{ + Name: "minio-access-key-id", + Value: "", + Usage: "Minio storage accessKeyID", + }, + &cli.StringFlag{ + Name: "minio-secret-access-key", + Value: "", + Usage: "Minio storage secretAccessKey", + }, + &cli.StringFlag{ + Name: "minio-bucket", + Value: "", + Usage: "Minio storage bucket", + }, + &cli.StringFlag{ + Name: "minio-location", + Value: "", + Usage: "Minio storage location to create bucket", + }, + &cli.StringFlag{ + Name: "minio-base-path", + Value: "", + Usage: "Minio storage base path on the bucket", + }, + &cli.BoolFlag{ + Name: "minio-use-ssl", + Usage: "Enable SSL for minio", + }, + &cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + &cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, }, - &cli.StringFlag{ - Name: "storage", - Aliases: []string{"s"}, - Value: "", - Usage: "New storage type: local (default) or minio", - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Value: "", - Usage: "New storage placement if store is local (leave blank for default)", - }, - &cli.StringFlag{ - Name: "minio-endpoint", - Value: "", - Usage: "Minio storage endpoint", - }, - &cli.StringFlag{ - Name: "minio-access-key-id", - Value: "", - Usage: "Minio storage accessKeyID", - }, - &cli.StringFlag{ - Name: "minio-secret-access-key", - Value: "", - Usage: "Minio storage secretAccessKey", - }, - &cli.StringFlag{ - Name: "minio-bucket", - Value: "", - Usage: "Minio storage bucket", - }, - &cli.StringFlag{ - Name: "minio-location", - Value: "", - Usage: "Minio storage location to create bucket", - }, - &cli.StringFlag{ - Name: "minio-base-path", - Value: "", - Usage: "Minio storage base path on the bucket", - }, - &cli.BoolFlag{ - Name: "minio-use-ssl", - Usage: "Enable SSL for minio", - }, - &cli.BoolFlag{ - Name: "minio-insecure-skip-verify", - Usage: "Skip SSL verification", - }, - &cli.StringFlag{ - Name: "minio-checksum-algorithm", - Value: "", - Usage: "Minio checksum algorithm (default/md5)", - }, - }, + } } func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error { @@ -182,8 +184,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora }) } -func runMigrateStorage(ctx *cli.Context) error { - stdCtx, cancel := installSignals() +func runMigrateStorage(stdCtx context.Context, ctx *cli.Command) error { + stdCtx, cancel := installSignals(stdCtx) defer cancel() if err := initDB(stdCtx); err != nil { diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 1e53ce26ba..0e9f0bb50a 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -4,52 +4,55 @@ package cmd import ( + "context" "strings" "forgejo.org/modules/private" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdRestoreRepository represents the available restore a repository sub-command. -var CmdRestoreRepository = &cli.Command{ - Name: "restore-repo", - Usage: "Restore the repository from disk", - Description: "This is a command for restoring the repository data.", - Action: runRestoreRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to restore from", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "Restore destination owner name", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "Restore destination repository name", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be restored, one or more units should be separated as comma. +func cmdRestoreRepository() *cli.Command { + return &cli.Command{ + Name: "restore-repo", + Usage: "Restore the repository from disk", + Description: "This is a command for restoring the repository data.", + Action: runRestoreRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to restore from", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "Restore destination owner name", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "Restore destination repository name", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, + &cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, - &cli.BoolFlag{ - Name: "validation", - Usage: "Sanity check the content of the files before trying to load them", - }, - }, + } } -func runRestoreRepository(c *cli.Context) error { - ctx, cancel := installSignals() +func runRestoreRepository(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() setting.MustInstalled() diff --git a/cmd/serv.go b/cmd/serv.go index 4b288632d2..1fac2d13f5 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -33,7 +33,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/kballard/go-shellquote" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const ( @@ -41,20 +41,22 @@ const ( ) // CmdServ represents the available serv sub-command. -var CmdServ = &cli.Command{ - Name: "serv", - Usage: "(internal) Should only be called by SSH shell", - Description: "Serv provides access auth for repositories", - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runServ, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "enable-pprof", +func cmdServ() *cli.Command { + return &cli.Command{ + Name: "serv", + Usage: "(internal) Should only be called by SSH shell", + Description: "Serv provides access auth for repositories", + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runServ, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-pprof", + }, + &cli.BoolFlag{ + Name: "debug", + }, }, - &cli.BoolFlag{ - Name: "debug", - }, - }, + } } func setup(ctx context.Context, debug, gitNeeded bool) { @@ -131,8 +133,8 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { return nil } -func runServ(c *cli.Context) error { - ctx, cancel := installSignals() +func runServ(ctx context.Context, c *cli.Command) error { + ctx, cancel := installSignals(ctx) defer cancel() // FIXME: This needs to internationalised @@ -194,7 +196,7 @@ func runServ(c *cli.Context) error { if git.CheckGitVersionAtLeast("2.29") == nil { // for AGit Flow if cmd == "ssh_info" { - fmt.Print(`{"type":"gitea","version":1}`) + fmt.Print(`{"type":"agit","version":1}`) return nil } } diff --git a/cmd/web.go b/cmd/web.go index 1e67b2e922..87965a7c1e 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -26,48 +26,50 @@ import ( "forgejo.org/routers/install" "github.com/felixge/fgprof" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // PIDFile could be set from build tag var PIDFile = "/run/gitea.pid" // CmdWeb represents the available web sub-command. -var CmdWeb = &cli.Command{ - Name: "web", - Usage: "Start the Forgejo web server", - Description: `The Forgejo web server is the only thing you need to run, +func cmdWeb() *cli.Command { + return &cli.Command{ + Name: "web", + Usage: "Start the Forgejo web server", + Description: `The Forgejo web server is the only thing you need to run, and it takes care of all the other things for you`, - Before: PrepareConsoleLoggerLevel(log.INFO), - Action: runWeb, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Temporary port number to prevent conflict", + Before: PrepareConsoleLoggerLevel(log.INFO), + Action: runWeb, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Temporary port number to prevent conflict", + }, + &cli.StringFlag{ + Name: "install-port", + Value: "3000", + Usage: "Temporary port number to run the install page on to prevent conflict", + }, + &cli.StringFlag{ + Name: "pid", + Aliases: []string{"P"}, + Value: PIDFile, + Usage: "Custom pid file path", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display Fatal logging errors until logging is set-up", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, - &cli.StringFlag{ - Name: "install-port", - Value: "3000", - Usage: "Temporary port number to run the install page on to prevent conflict", - }, - &cli.StringFlag{ - Name: "pid", - Aliases: []string{"P"}, - Value: PIDFile, - Usage: "Custom pid file path", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display Fatal logging errors until logging is set-up", - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Set initial logging to TRACE level until logging is properly set-up", - }, - }, + } } func runHTTPRedirector() { @@ -128,7 +130,7 @@ func showWebStartupMessage(msg string) { } } -func serveInstall(ctx *cli.Context) error { +func serveInstall(_ context.Context, ctx *cli.Command) error { showWebStartupMessage("Prepare to run install page") routers.InitWebInstallPage(graceful.GetManager().HammerContext()) @@ -161,7 +163,7 @@ func serveInstall(ctx *cli.Context) error { return nil } -func serveInstalled(ctx *cli.Context) error { +func serveInstalled(_ context.Context, ctx *cli.Command) error { setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() setting.MustInstalled() @@ -233,7 +235,7 @@ func servePprof() { finished() } -func runWeb(ctx *cli.Context) error { +func runWeb(ctx context.Context, cli *cli.Command) error { defer func() { if panicked := recover(); panicked != nil { log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) @@ -251,12 +253,12 @@ func runWeb(ctx *cli.Context) error { } // Set pid file setting - if ctx.IsSet("pid") { - createPIDFile(ctx.String("pid")) + if cli.IsSet("pid") { + createPIDFile(cli.String("pid")) } if !setting.InstallLock { - if err := serveInstall(ctx); err != nil { + if err := serveInstall(ctx, cli); err != nil { return err } } else { @@ -267,7 +269,7 @@ func runWeb(ctx *cli.Context) error { go servePprof() } - return serveInstalled(ctx) + return serveInstalled(ctx, cli) } func setPort(port string) error { diff --git a/contrib/autocompletion/bash_autocomplete b/contrib/autocompletion/bash_autocomplete index 5cb62f26a7..58844938a6 100755 --- a/contrib/autocompletion/bash_autocomplete +++ b/contrib/autocompletion/bash_autocomplete @@ -1,4 +1,3 @@ -#! /bin/bash # Heavily inspired by https://github.com/urfave/cli _cli_bash_autocomplete() { @@ -7,9 +6,9 @@ _cli_bash_autocomplete() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" if [[ "$cur" == "-"* ]]; then - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-shell-completion ) else - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-shell-completion ) fi COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 diff --git a/contrib/autocompletion/zsh_autocomplete b/contrib/autocompletion/zsh_autocomplete index b3b40df503..0fd1a0b175 100644 --- a/contrib/autocompletion/zsh_autocomplete +++ b/contrib/autocompletion/zsh_autocomplete @@ -9,9 +9,9 @@ _cli_zsh_autocomplete() { local cur cur=${words[-1]} if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}") else - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-shell-completion)}") fi if [[ "${opts[1]}" != "" ]]; then diff --git a/contrib/environment-to-ini/README b/contrib/environment-to-ini/README index f1d3f2ae83..e4caf25666 100644 --- a/contrib/environment-to-ini/README +++ b/contrib/environment-to-ini/README @@ -1,45 +1,46 @@ Environment To Ini ================== -Multiple docker users have requested that the Gitea docker is changed -to permit arbitrary configuration via environment variables. +This tool allows defining Forgejo's entire configuration via environment +variables, mostly geared towards usage in Docker. -Gitea needs to use an ini file for configuration because the running -environment that starts the docker may not be the same as that used -by the hooks. An ini file also gives a good default and means that -users do not have to completely provide a full environment. +Forgejo needs to use an INI file for configuration because the running +environment that starts the container may not be the same as the one used +by the hooks. An INI file also gives a good default and means that +users do not have to provide the entire set of environment variables. With those caveats above, this command provides a generic way of converting suitably structured environment variables into any ini value. -To use the command is very simple just run it and the default gitea -app.ini will be rewritten to take account of the variables provided, -however there are various options to give slightly different -behavior and these can be interrogated with the `-h` option. +When run, `environment-to-ini` will write the config files based on the +environment variables provided. +Check with the `-h` flag for several options to alter this behaviour. -The environment variables should be of the form: +Environment variables of the form "FORGEJO__SECTION_NAME__KEY_NAME" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value as provided. - GITEA__SECTION_NAME__KEY_NAME - -Note, SECTION_NAME in the notation above is case-insensitive. +Environment variables of the form "FORGEJO__SECTION_NAME__KEY_NAME__FILE" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value loaded from the specified file. Environment variables are usually restricted to a reduced character set "0-9A-Z_" - in order to allow the setting of sections with characters outside of that set, they should be escaped as following: -"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names -can be escaped as a UTF8 byte string if necessary. E.g. to configure: +"_0X2E_" for ".". The entire section and key names can be escaped as +a UTF8 byte string if necessary. E.g. to configure: - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ + """ + ... + [log.console] + COLORIZE=false + STDERR=true + ... + """ -You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" -and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found +You would set the environment variables: "FORGEJO__LOG_0x2E_CONSOLE__COLORIZE=false" +and "FORGEJO__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found on the configuration cheat sheet. To build locally, run: diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index 95f34527ac..e8e799b5f3 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -4,16 +4,17 @@ package main import ( + "context" "os" "forgejo.org/modules/log" "forgejo.org/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func main() { - app := cli.NewApp() + app := cli.Command{} app.Name = "environment-to-ini" app.Usage = "Use provided environment to update configuration ini" app.Description = `As a helper to allow docker users to update the forgejo configuration @@ -72,13 +73,13 @@ func main() { }, } app.Action = runEnvironmentToIni - err := app.Run(os.Args) + err := app.Run(context.Background(), os.Args) if err != nil { log.Fatal("Failed to run app with %s: %v", os.Args, err) } } -func runEnvironmentToIni(c *cli.Context) error { +func runEnvironmentToIni(ctx context.Context, c *cli.Command) error { // the config system may change the environment variables, so get a copy first, to be used later env := append([]string{}, os.Environ()...) setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ diff --git a/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet b/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet index 31b7d4f9b2..108cab0eb1 100644 --- a/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet +++ b/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet @@ -408,7 +408,7 @@ local addIssueLabelsOverrides(labels) = regex: '', type: 'query', multi: true, - allValue: '.+' + allValue: '.+', }, ) .addTemplate( @@ -423,7 +423,7 @@ local addIssueLabelsOverrides(labels) = regex: '', type: 'query', multi: true, - allValue: '.+' + allValue: '.+', }, ) .addTemplate( diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 41599d411e..1b8d4c6697 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -183,7 +183,7 @@ RUN_USER = ; git ;; ;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, ;; for system SSH this setting has no effect -;SSH_SERVER_KEY_EXCHANGES = curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 +;SSH_SERVER_KEY_EXCHANGES = mlkem768x25519-sha256, curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 ;; ;; For the built-in SSH server, choose the MACs to support for SSH connections, ;; for system SSH this setting has no effect @@ -1025,6 +1025,10 @@ LEVEL = Info ;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS. ;DEFAULT_FORK_REPO_UNITS = repo.code,repo.pulls ;; +;; Comma separated list of default mirror repo units. +;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS. +;DEFAULT_MIRROR_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.wiki,repo.projects,repo.packages +;; ;; Prefix archive files by placing them in a directory named after the repository ;PREFIX_ARCHIVE_FILES = true ;; diff --git a/eslint.config.mjs b/eslint.config.mjs index 5737bed623..28cfa80089 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,5 @@ import eslintCommunityEslintPluginEslintComments from '@eslint-community/eslint-plugin-eslint-comments'; -import stylisticEslintPluginJs from '@stylistic/eslint-plugin-js'; +import stylisticEslintPlugin from '@stylistic/eslint-plugin'; import vitest from '@vitest/eslint-plugin'; import arrayFunc from 'eslint-plugin-array-func'; import eslintPluginImportX from 'eslint-plugin-import-x'; @@ -26,7 +26,7 @@ export default tseslint.config( { plugins: { '@eslint-community/eslint-comments': eslintCommunityEslintPluginEslintComments, - '@stylistic/js': stylisticEslintPluginJs, + '@stylistic': stylisticEslintPlugin, '@vitest': vitest, 'array-func': arrayFunc, 'no-jquery': noJquery, @@ -69,62 +69,62 @@ export default tseslint.config( '@eslint-community/eslint-comments/no-unused-enable': [2], '@eslint-community/eslint-comments/no-use': [0], '@eslint-community/eslint-comments/require-description': [0], - '@stylistic/js/array-bracket-newline': [0], - '@stylistic/js/array-bracket-spacing': [2, 'never'], - '@stylistic/js/array-element-newline': [0], - '@stylistic/js/arrow-parens': [2, 'always'], + '@stylistic/array-bracket-newline': [0], + '@stylistic/array-bracket-spacing': [2, 'never'], + '@stylistic/array-element-newline': [0], + '@stylistic/arrow-parens': [2, 'always'], - '@stylistic/js/arrow-spacing': [2, { + '@stylistic/arrow-spacing': [2, { before: true, after: true, }], - '@stylistic/js/block-spacing': [0], + '@stylistic/block-spacing': [0], - '@stylistic/js/brace-style': [2, '1tbs', { + '@stylistic/brace-style': [2, '1tbs', { allowSingleLine: true, }], - '@stylistic/js/comma-dangle': [2, 'always-multiline'], + '@stylistic/comma-dangle': [2, 'always-multiline'], - '@stylistic/js/comma-spacing': [2, { + '@stylistic/comma-spacing': [2, { before: false, after: true, }], - '@stylistic/js/comma-style': [2, 'last'], - '@stylistic/js/computed-property-spacing': [2, 'never'], - '@stylistic/js/dot-location': [2, 'property'], - '@stylistic/js/eol-last': [2], - '@stylistic/js/function-call-spacing': [2, 'never'], - '@stylistic/js/function-call-argument-newline': [0], - '@stylistic/js/function-paren-newline': [0], - '@stylistic/js/generator-star-spacing': [0], - '@stylistic/js/implicit-arrow-linebreak': [0], + '@stylistic/comma-style': [2, 'last'], + '@stylistic/computed-property-spacing': [2, 'never'], + '@stylistic/dot-location': [2, 'property'], + '@stylistic/eol-last': [2], + '@stylistic/function-call-spacing': [2, 'never'], + '@stylistic/function-call-argument-newline': [0], + '@stylistic/function-paren-newline': [0], + '@stylistic/generator-star-spacing': [0], + '@stylistic/implicit-arrow-linebreak': [0], - '@stylistic/js/indent': [2, 2, { + '@stylistic/indent': [2, 2, { ignoreComments: true, SwitchCase: 1, }], - '@stylistic/js/key-spacing': [2], - '@stylistic/js/keyword-spacing': [2], - '@stylistic/js/linebreak-style': [2, 'unix'], - '@stylistic/js/lines-around-comment': [0], - '@stylistic/js/lines-between-class-members': [0], - '@stylistic/js/max-len': [0], - '@stylistic/js/max-statements-per-line': [0], - '@stylistic/js/multiline-ternary': [0], - '@stylistic/js/new-parens': [2], - '@stylistic/js/newline-per-chained-call': [0], - '@stylistic/js/no-confusing-arrow': [0], - '@stylistic/js/no-extra-parens': [0], - '@stylistic/js/no-extra-semi': [2], - '@stylistic/js/no-floating-decimal': [0], - '@stylistic/js/no-mixed-operators': [0], - '@stylistic/js/no-mixed-spaces-and-tabs': [2], + '@stylistic/key-spacing': [2], + '@stylistic/keyword-spacing': [2], + '@stylistic/linebreak-style': [2, 'unix'], + '@stylistic/lines-around-comment': [0], + '@stylistic/lines-between-class-members': [0], + '@stylistic/max-len': [0], + '@stylistic/max-statements-per-line': [0], + '@stylistic/multiline-ternary': [0], + '@stylistic/new-parens': [2], + '@stylistic/newline-per-chained-call': [0], + '@stylistic/no-confusing-arrow': [0], + '@stylistic/no-extra-parens': [0], + '@stylistic/no-extra-semi': [2], + '@stylistic/no-floating-decimal': [0], + '@stylistic/no-mixed-operators': [0], + '@stylistic/no-mixed-spaces-and-tabs': [2], - '@stylistic/js/no-multi-spaces': [2, { + '@stylistic/no-multi-spaces': [2, { ignoreEOLComments: true, exceptions: { @@ -132,60 +132,60 @@ export default tseslint.config( }, }], - '@stylistic/js/no-multiple-empty-lines': [2, { + '@stylistic/no-multiple-empty-lines': [2, { max: 1, maxEOF: 0, maxBOF: 0, }], - '@stylistic/js/no-tabs': [2], - '@stylistic/js/no-trailing-spaces': [2], - '@stylistic/js/no-whitespace-before-property': [2], - '@stylistic/js/nonblock-statement-body-position': [2], - '@stylistic/js/object-curly-newline': [0], - '@stylistic/js/object-curly-spacing': [2, 'never'], - '@stylistic/js/object-property-newline': [0], - '@stylistic/js/one-var-declaration-per-line': [0], - '@stylistic/js/operator-linebreak': [2, 'after'], - '@stylistic/js/padded-blocks': [2, 'never'], - '@stylistic/js/padding-line-between-statements': [0], - '@stylistic/js/quote-props': [0], + '@stylistic/no-tabs': [2], + '@stylistic/no-trailing-spaces': [2], + '@stylistic/no-whitespace-before-property': [2], + '@stylistic/nonblock-statement-body-position': [2], + '@stylistic/object-curly-newline': [0], + '@stylistic/object-curly-spacing': [2, 'never'], + '@stylistic/object-property-newline': [0], + '@stylistic/one-var-declaration-per-line': [0], + '@stylistic/operator-linebreak': [2, 'after'], + '@stylistic/padded-blocks': [2, 'never'], + '@stylistic/padding-line-between-statements': [0], + '@stylistic/quote-props': [0], - '@stylistic/js/quotes': [2, 'single', { + '@stylistic/quotes': [2, 'single', { avoidEscape: true, allowTemplateLiterals: true, }], - '@stylistic/js/rest-spread-spacing': [2, 'never'], + '@stylistic/rest-spread-spacing': [2, 'never'], - '@stylistic/js/semi': [2, 'always', { + '@stylistic/semi': [2, 'always', { omitLastInOneLineBlock: true, }], - '@stylistic/js/semi-spacing': [2, { + '@stylistic/semi-spacing': [2, { before: false, after: true, }], - '@stylistic/js/semi-style': [2, 'last'], - '@stylistic/js/space-before-blocks': [2, 'always'], + '@stylistic/semi-style': [2, 'last'], + '@stylistic/space-before-blocks': [2, 'always'], - '@stylistic/js/space-before-function-paren': [2, { + '@stylistic/space-before-function-paren': [2, { anonymous: 'ignore', named: 'never', asyncArrow: 'always', }], - '@stylistic/js/space-in-parens': [2, 'never'], - '@stylistic/js/space-infix-ops': [2], - '@stylistic/js/space-unary-ops': [2], - '@stylistic/js/spaced-comment': [2, 'always'], - '@stylistic/js/switch-colon-spacing': [2], - '@stylistic/js/template-curly-spacing': [2, 'never'], - '@stylistic/js/template-tag-spacing': [2, 'never'], - '@stylistic/js/wrap-iife': [2, 'inside'], - '@stylistic/js/wrap-regex': [0], - '@stylistic/js/yield-star-spacing': [2, 'after'], + '@stylistic/space-in-parens': [2, 'never'], + '@stylistic/space-infix-ops': [2], + '@stylistic/space-unary-ops': [2], + '@stylistic/spaced-comment': [2, 'always'], + '@stylistic/switch-colon-spacing': [2], + '@stylistic/template-curly-spacing': [2, 'never'], + '@stylistic/template-tag-spacing': [2, 'never'], + '@stylistic/wrap-iife': [2, 'inside'], + '@stylistic/wrap-regex': [0], + '@stylistic/yield-star-spacing': [2, 'after'], 'accessor-pairs': [2], 'array-callback-return': [2, { diff --git a/flake.lock b/flake.lock index 90672733d5..dcf7755013 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1733392399, - "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", + "lastModified": 1749285348, + "narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", + "rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index daa66d1d04..01b23258b9 100644 --- a/flake.nix +++ b/flake.nix @@ -3,35 +3,20 @@ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { - nixpkgs, - flake-utils, - ... - }: + outputs = + { + nixpkgs, + flake-utils, + ... + }: flake-utils.lib.eachDefaultSystem ( - system: let + system: + let pkgs = nixpkgs.legacyPackages.${system}; - in { - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - # generic - git - git-lfs - gnumake - gnused - gnutar - gzip - - # frontend - nodejs_20 - - # backend - gofumpt - sqlite - go - gopls - ]; - }; + in + { + devShells.default = import ./shell.nix { inherit pkgs; }; + formatter = pkgs.nixfmt-rfc-style; } ); } diff --git a/go.mod b/go.mod index 943e798209..510ec9c3ae 100644 --- a/go.mod +++ b/go.mod @@ -2,30 +2,31 @@ module forgejo.org go 1.24 -toolchain go1.24.2 +toolchain go1.24.4 require ( - code.forgejo.org/f3/gof3/v3 v3.10.6 + code.forgejo.org/f3/gof3/v3 v3.11.0 code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 + code.forgejo.org/forgejo/go-rpmutils v1.0.0 code.forgejo.org/forgejo/levelqueue v1.0.0 code.forgejo.org/forgejo/reply v1.0.2 - code.forgejo.org/go-chi/binding v1.0.0 - code.forgejo.org/go-chi/cache v1.0.0 - code.forgejo.org/go-chi/captcha v1.0.1 - code.forgejo.org/go-chi/session v1.0.1 + code.forgejo.org/go-chi/binding v1.0.1 + code.forgejo.org/go-chi/cache v1.0.1 + code.forgejo.org/go-chi/captcha v1.0.2 + code.forgejo.org/go-chi/session v1.0.2 code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/sdk/gitea v0.21.0 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - connectrpc.com/connect v1.17.0 + connectrpc.com/connect v1.18.1 github.com/42wim/httpsig v1.2.3 github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/ProtonMail/go-crypto v1.1.6 + github.com/ProtonMail/go-crypto v1.3.0 github.com/PuerkitoBio/goquery v1.10.3 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2 github.com/alecthomas/chroma/v2 v2.18.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb - github.com/blevesearch/bleve/v2 v2.5.1 + github.com/blevesearch/bleve/v2 v2.5.2 github.com/buildkite/terminal-to-html/v3 v3.16.8 github.com/caddyserver/certmagic v0.23.0 github.com/chi-middleware/proxy v1.1.1 @@ -40,14 +41,14 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.2.0 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.2 github.com/go-git/go-git/v5 v5.13.2 github.com/go-ldap/ldap/v3 v3.4.6 - github.com/go-openapi/spec v0.20.14 - github.com/go-sql-driver/mysql v1.9.1 + github.com/go-openapi/spec v0.21.0 + github.com/go-sql-driver/mysql v1.9.3 github.com/go-webauthn/webauthn v0.13.0 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f @@ -62,8 +63,8 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 - github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 - github.com/jhillyerd/enmime/v2 v2.1.0 + github.com/inbucket/html2text v0.9.0 + github.com/jhillyerd/enmime/v2 v2.2.0 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.18.0 @@ -75,7 +76,7 @@ require ( github.com/meilisearch/meilisearch-go v0.31.0 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/minio/minio-go/v7 v7.0.91 + github.com/minio/minio-go/v7 v7.0.94 github.com/msteinert/pam/v2 v2.1.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.8.0 @@ -86,27 +87,25 @@ require ( github.com/prometheus/client_golang v1.21.1 github.com/redis/go-redis/v9 v9.8.0 github.com/robfig/cron/v3 v3.0.1 - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 - github.com/sassoftware/go-rpmutils v0.4.0 - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 - github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 + github.com/sergi/go-diff v1.4.0 github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.0 github.com/ulikunitz/xz v0.5.12 - github.com/urfave/cli/v2 v2.27.6 + github.com/urfave/cli/v3 v3.3.3 github.com/valyala/fastjson v1.6.4 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.12 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - gitlab.com/gitlab-org/api/client-go v0.126.0 - go.uber.org/mock v0.5.1 - golang.org/x/crypto v0.38.0 + gitlab.com/gitlab-org/api/client-go v0.130.1 + go.uber.org/mock v0.5.2 + golang.org/x/crypto v0.39.0 golang.org/x/image v0.27.0 - golang.org/x/net v0.40.0 + golang.org/x/net v0.41.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.14.0 + golang.org/x/sync v0.15.0 golang.org/x/sys v0.33.0 - golang.org/x/text v0.25.0 + golang.org/x/text v0.26.0 google.golang.org/protobuf v1.36.4 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 @@ -121,7 +120,6 @@ require ( dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect - github.com/DataDog/zstd v1.5.5 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect github.com/andybalholm/brotli v1.1.1 // indirect @@ -146,14 +144,13 @@ require ( github.com/blevesearch/zapx/v13 v13.4.2 // indirect github.com/blevesearch/zapx/v14 v14.4.2 // indirect github.com/blevesearch/zapx/v15 v15.4.2 // indirect - github.com/blevesearch/zapx/v16 v16.2.3 // indirect + github.com/blevesearch/zapx/v16 v16.2.4 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.3.8 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect @@ -161,7 +158,7 @@ require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect @@ -170,9 +167,9 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/swag v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-webauthn/x v0.1.21 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -195,7 +192,7 @@ require ( github.com/libdns/libdns v1.0.0-beta.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/markbates/going v1.0.3 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mholt/acmez/v3 v3.1.2 // indirect github.com/miekg/dns v1.1.63 // indirect @@ -208,8 +205,11 @@ require ( github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nwaples/rardecode v1.1.3 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.0.7 // indirect github.com/onsi/ginkgo v1.16.5 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -221,15 +221,13 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/xid v1.6.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect + github.com/tinylib/msgp v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.etcd.io/bbolt v1.4.0 // indirect @@ -237,18 +235,16 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.34.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 -replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 - -replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.26.0 +replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.28.0 replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1 diff --git a/go.sum b/go.sum index 7f6f6ecb32..53558fddd7 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,15 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -code.forgejo.org/f3/gof3/v3 v3.10.6 h1:Ru/Iz+pqM8IPi7atUHE7+q7v3O3DRbYgMFqrFTsO1m8= -code.forgejo.org/f3/gof3/v3 v3.10.6/go.mod h1:K6lQCWQIyN/5rjP/OJL9fMA6fd++satndE20w/I6Kss= +code.forgejo.org/f3/gof3/v3 v3.11.0 h1:f/xToKwqTgxG6PYxvewywjDQyCcyHEEJ6sZqUitFsAE= +code.forgejo.org/f3/gof3/v3 v3.11.0/go.mod h1:4FaRUNSQGBiD1M0DuB0yNv+Z2wMtlOeckgygHSSq4KQ= code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 h1:HTZl3CBk3ABNYtFI6TPLvJgGKFIhKT5CBk0sbOtkDKU= code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:PphB88CPbx601QrWPMZATeorACeVmQlyv3u+uUMbSaM= -code.forgejo.org/forgejo/act v1.26.0 h1:6mTmoaw7d/WpYiw/Pw6AaypxFdgJog5OFi/PMEgEbxs= -code.forgejo.org/forgejo/act v1.26.0/go.mod h1:HFDFrXPrqfM9aH2RCnMiBdo/3ThxDmZjp58InPjGOfo= +code.forgejo.org/forgejo/act v1.28.0 h1:96njNC7C1YNyjWq5OWvLZMF/nw0PMthzIA8Nwbnn7jo= +code.forgejo.org/forgejo/act v1.28.0/go.mod h1:dFuiwAmD5vyrzecysHB2kL/GM3wRpoVPl+WdbCTC8Bs= code.forgejo.org/forgejo/archiver/v3 v3.5.1 h1:UmmbA7D5550uf71SQjarmrn6yKwOGxtEjb3jaYYtmSE= code.forgejo.org/forgejo/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +code.forgejo.org/forgejo/go-rpmutils v1.0.0 h1:RZGGeKt70p/WaIEL97pyT6uiiEIoN8/aLmS5Z6WmX0M= +code.forgejo.org/forgejo/go-rpmutils v1.0.0/go.mod h1:cg+VbgLXfrDPza9T+kBsMb3TVmmzPN4XseT6gDGLSUk= code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:RArF5AsF9LH4nEoJxqRxcP5r8hhRfWcId84G82YbqzA= code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= code.forgejo.org/forgejo/levelqueue v1.0.0 h1:9krYpU6BM+j/1Ntj6m+VCAIu0UNnne1/UfU/XgPpLuE= @@ -16,22 +18,22 @@ code.forgejo.org/forgejo/reply v1.0.2 h1:dMhQCHV6/O3L5CLWNTol+dNzDAuyCK88z4J/lCd code.forgejo.org/forgejo/reply v1.0.2/go.mod h1:RyZUfzQLc+fuLIGjTSQWDAJWPiL4WtKXB/FifT5fM7U= code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616 h1:kEZL84+02jY9RxXM4zHBWZ3Fml0B09cmP1LGkDsCfIA= code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -code.forgejo.org/go-chi/binding v1.0.0 h1:EIDJtk9brK7WsT7rvS/D4cxX8XlnhY3LMy8ex1jeHu0= -code.forgejo.org/go-chi/binding v1.0.0/go.mod h1:fWwqaHj0H1/KeCpBqdvKunflq8pYfciEHI5v3UUeE2E= -code.forgejo.org/go-chi/cache v1.0.0 h1:akLfGxNlHcacmtutovNtYFSTMsbdcp5MGjAEsP4pxnE= -code.forgejo.org/go-chi/cache v1.0.0/go.mod h1:OVlZ/TqDYJ+RUJ+R+J+OLxtlyjo3pbjBeK7LAWAB+Vk= -code.forgejo.org/go-chi/captcha v1.0.1 h1:/oe1fvGOpdyyeGijg3oMYNOYLvEovNvp79Y3gLe3qbk= -code.forgejo.org/go-chi/captcha v1.0.1/go.mod h1:6EbjSVVa7WoZFENgwK/hLAJZq+HBXtgRsjnIngILC8Y= -code.forgejo.org/go-chi/session v1.0.1 h1:RNkcJQZJBqlvJoIFXSth87b3kMFZLDBA18VcitD+Z0Y= -code.forgejo.org/go-chi/session v1.0.1/go.mod h1:y69sjS984wc7k4xyu77yNE5HKeSlBoQW8VSGdsK7RAs= +code.forgejo.org/go-chi/binding v1.0.1 h1:coKNI+X1NzRN7X85LlrpvBRqk0TXpJ+ja28vusQWEuY= +code.forgejo.org/go-chi/binding v1.0.1/go.mod h1:oTFFDg/dkwFbmVuusiULB1OlrIJM95cOGK7Nc3GYcoo= +code.forgejo.org/go-chi/cache v1.0.1 h1:w6IsDcPbeEnEYZn7M2HJe3/3/Ehtcw/72VjcVK7+lBw= +code.forgejo.org/go-chi/cache v1.0.1/go.mod h1:K3aQSyRIN4xiuqV1kanfQ6O4ToDpzDpY3bNOyGjFe3U= +code.forgejo.org/go-chi/captcha v1.0.2 h1:vyHDPXkpjDv8bLO9NqtWzZayzstD/WpJ5xwEkAaqZGQ= +code.forgejo.org/go-chi/captcha v1.0.2/go.mod h1:lxiPLcJ76UCZHoH31/Wbum4GUi2NgjfFZLrJkKv1lLE= +code.forgejo.org/go-chi/session v1.0.2 h1:pG+AXre9L9VXJmTaADXkmeEPuRalhmBXyv6tG2Rvjcc= +code.forgejo.org/go-chi/session v1.0.2/go.mod h1:HnEGyBny7WPzCiVLP2vzL5ssma+3gCSl/vLpuVNYrqc= code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU= code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= -connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= -connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -46,13 +48,11 @@ github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= -github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= -github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= @@ -87,8 +87,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/blevesearch/bleve/v2 v2.5.1 h1:cc/O++W2Hcjp1SU5ETHeE+QYWv2oV88ldYEPowdmg8M= -github.com/blevesearch/bleve/v2 v2.5.1/go.mod h1:9g/wnbWKm9AgXrU8Ecqi+IDdqjUHWymwkQRDg+5tafU= +github.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8= +github.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo= github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y= github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg= @@ -121,13 +121,13 @@ github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= -github.com/blevesearch/zapx/v16 v16.2.3 h1:7Y0r+a3diEvlazsncexq1qoFOcBd64xwMS7aDm4lo1s= -github.com/blevesearch/zapx/v16 v16.2.3/go.mod h1:wVJ+GtURAaRG9KQAMNYyklq0egV+XJlGcXNCE0OFjjA= +github.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww= +github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -150,10 +150,8 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= -github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -194,8 +192,8 @@ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTe github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -215,8 +213,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= @@ -239,16 +237,16 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= -github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= -github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= -github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= -github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -343,12 +341,12 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= -github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+qks= +github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jhillyerd/enmime/v2 v2.1.0 h1:c8Qwi5Xq5EdtMN6byQWoZ/8I2RMTo6OJ7Xay+s1oPO0= -github.com/jhillyerd/enmime/v2 v2.1.0/go.mod h1:EJ74dcRbBcqHSP2TBu08XRoy6y3Yx0cevwb1YkGMEmQ= +github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8= +github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -384,8 +382,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 h1:F/3FfGmKdiKFa8kL3YrpZ7pe9H4l4AzA1pbaOUnRvPI= -github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0/go.mod h1:JEfTc3+2DF9Z4PXhLLvXL42zexJyh8rIq3OzUj/0rAk= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= @@ -393,12 +389,10 @@ github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= @@ -415,8 +409,8 @@ github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= -github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= +github.com/minio/minio-go/v7 v7.0.94 h1:1ZoksIKPyaSt64AVOyaQvhDOgVC3MfZsWM6mZXRUGtM= +github.com/minio/minio-go/v7 v7.0.94/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -440,8 +434,12 @@ github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWk github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw= +github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -459,6 +457,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -497,17 +497,11 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= -github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= -github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -531,12 +525,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= +github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -545,8 +541,6 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= @@ -564,8 +558,8 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -gitlab.com/gitlab-org/api/client-go v0.126.0 h1:VV5TdkF6pMbEdFGvbR2CwEgJwg6qdg1u3bj5eD2tiWk= -gitlab.com/gitlab-org/api/client-go v0.126.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAjE7UeNatu2VWHRf4/LE= +gitlab.com/gitlab-org/api/client-go v0.130.1 h1:1xF5C5Zq3sFeNg3PzS2z63oqrxifne3n/OnbI7nptRc= +gitlab.com/gitlab-org/api/client-go v0.130.1/go.mod h1:ZhSxLAWadqP6J9lMh40IAZOlOxBLPRh7yFOXR/bMJWM= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -573,8 +567,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= -go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -591,8 +585,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= @@ -603,8 +597,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -620,8 +614,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -633,8 +627,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -653,7 +647,6 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -684,10 +677,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -695,8 +688,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 3f0283db7f..ade43881cf 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ import ( _ "forgejo.org/modules/markup/markdown" _ "forgejo.org/modules/markup/orgmode" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // these flags will be set by the build flags diff --git a/manifest.scm b/manifest.scm new file mode 100644 index 0000000000..f9605bc2d9 --- /dev/null +++ b/manifest.scm @@ -0,0 +1,38 @@ +;;; Copyright 2025 The Forgejo Authors. All rights reserved. +;;; SPDX-License-Identifier: MIT +;;; +;;; Commentary: +;;; +;;; This is a GNU Guix manifest that can be used to create a +;;; development environment to build and test Forgejo. +;;; +;;; The following is a usage example to create a containerized +;;; environment, with HOME shared for the Go cache and the network +;;; made available to fetch required Go and Node dependencies. +;;; +#| +guix shell -CNF --share=$HOME -m manifest.scm +export GOTOOLCHAIN=local # to use the Go binary from Guix +export CC=gcc CGO_ENABLED=1 +export TAGS="timetzdata sqlite sqlite_unlock_notify" +make clean +make -j$(nproc) +make test -j$(nproc) # run unit tests +make test-sqlite -j$(nproc) # run integration tests +make watch # run an instance/rebuild on changes +|# +(specifications->manifest + (list "bash-minimal" + "coreutils" + "findutils" + "gcc-toolchain" + "git" ;libpcre support is required + "git-lfs" + "gnupg" + "go" + "grep" + "make" + "node" + "nss-certs" + "openssh" + "sed")) diff --git a/models/actions/run.go b/models/actions/run.go index 61159bc929..55def805ed 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" "fmt" "slices" "strings" @@ -54,6 +55,7 @@ type ActionRun struct { PreviousDuration time.Duration Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` + NotifyEmail bool } func init() { @@ -222,29 +224,38 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork var hasWaiting bool for _, v := range jobs { id, job := v.Job() - needs := job.Needs() - if err := v.SetJob(id, job.EraseNeeds()); err != nil { - return err + status := StatusFailure + payload := []byte{} + needs := []string{} + name := run.Title + runsOn := []string{} + if job != nil { + needs = job.Needs() + if err := v.SetJob(id, job.EraseNeeds()); err != nil { + return err + } + payload, _ = v.Marshal() + + if len(needs) > 0 || run.NeedApproval { + status = StatusBlocked + } else { + status = StatusWaiting + hasWaiting = true + } + name, _ = util.SplitStringAtByteN(job.Name, 255) + runsOn = job.RunsOn() } - payload, _ := v.Marshal() - status := StatusWaiting - if len(needs) > 0 || run.NeedApproval { - status = StatusBlocked - } else { - hasWaiting = true - } - job.Name, _ = util.SplitStringAtByteN(job.Name, 255) runJobs = append(runJobs, &ActionRunJob{ RunID: run.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, CommitSHA: run.CommitSHA, IsForkPullRequest: run.IsForkPullRequest, - Name: job.Name, + Name: name, WorkflowPayload: payload, JobID: id, Needs: needs, - RunsOn: job.RunsOn(), + RunsOn: runsOn, Status: status, }) } @@ -346,7 +357,7 @@ func UpdateRunWithoutNotification(ctx context.Context, run *ActionRun, cols ...s return err } if affected == 0 { - return fmt.Errorf("run has changed") + return errors.New("run has changed") // It's impossible that the run is not found, since Gitea never deletes runs. } diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index cbcb4beb8e..afc754f26a 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -54,6 +54,8 @@ type FindRunJobOptions struct { CommitSHA string Statuses []Status UpdatedBefore timeutil.TimeStamp + Events []string // []webhook_module.HookEventType + RunNumber int64 } func (opts FindRunJobOptions) ToConds() builder.Cond { @@ -76,5 +78,11 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) } + if len(opts.Events) > 0 { + cond = cond.And(builder.In("event", opts.Events)) + } + if opts.RunNumber > 0 { + cond = cond.And(builder.Eq{"`index`": opts.RunNumber}) + } return cond } diff --git a/models/actions/runner.go b/models/actions/runner.go index 4d5056b425..bece1ae301 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -16,6 +16,7 @@ import ( repo_model "forgejo.org/models/repo" "forgejo.org/models/shared/types" user_model "forgejo.org/models/user" + "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/timeutil" "forgejo.org/modules/translation" @@ -353,3 +354,53 @@ func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { } return res.RowsAffected() } + +func DeleteOfflineRunners(ctx context.Context, olderThan timeutil.TimeStamp, globalOnly bool) error { + log.Info("Doing: DeleteOfflineRunners") + + if olderThan.AsTime().After(timeutil.TimeStampNow().AddDuration(-RunnerOfflineTime).AsTime()) { + return fmt.Errorf("invalid `cron.cleanup_offline_runners.older_than`value: must be at least %q", RunnerOfflineTime) + } + + cond := builder.Or( + // never online + builder.And(builder.Eq{"last_online": 0}, builder.Lt{"created": olderThan}), + // was online but offline + builder.And(builder.Gt{"last_online": 0}, builder.Lt{"last_online": olderThan}), + ) + + if globalOnly { + cond = builder.And(cond, builder.Eq{"owner_id": 0}, builder.Eq{"repo_id": 0}) + } + + if err := db.Iterate( + ctx, + cond, + func(ctx context.Context, r *ActionRunner) error { + if err := DeleteRunner(ctx, r); err != nil { + return fmt.Errorf("DeleteOfflineRunners: %w", err) + } + lastOnline := r.LastOnline.AsTime() + olderThanTime := olderThan.AsTime() + if !lastOnline.IsZero() && lastOnline.Before(olderThanTime) { + log.Info( + "Deleted runner [ID: %d, Name: %s], last online %s ago", + r.ID, r.Name, olderThanTime.Sub(lastOnline).String(), + ) + } else { + log.Info( + "Deleted runner [ID: %d, Name: %s], unused since %s ago", + r.ID, r.Name, olderThanTime.Sub(r.Created.AsTime()).String(), + ) + } + + return nil + }, + ); err != nil { + return err + } + + log.Info("Finished: DeleteOfflineRunners") + + return nil +} diff --git a/models/actions/runner_test.go b/models/actions/runner_test.go index 0623e66046..1916c35a76 100644 --- a/models/actions/runner_test.go +++ b/models/actions/runner_test.go @@ -6,10 +6,12 @@ import ( "encoding/binary" "fmt" "testing" + "time" auth_model "forgejo.org/models/auth" "forgejo.org/models/db" "forgejo.org/models/unittest" + "forgejo.org/modules/timeutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -73,3 +75,68 @@ func TestDeleteRunner(t *testing.T) { idAsBinary[6], idAsBinary[7]) assert.Equal(t, idAsHexadecimal, after.UUID[19:]) } + +func TestDeleteOfflineRunnersRunnerGlobalOnly(t *testing.T) { + baseTime := time.Date(2024, 5, 19, 7, 40, 32, 0, time.UTC) + timeutil.MockSet(baseTime) + defer timeutil.MockUnset() + + require.NoError(t, unittest.PrepareTestDatabase()) + + olderThan := timeutil.TimeStampNow().Add(-timeutil.Hour) + + require.NoError(t, DeleteOfflineRunners(db.DefaultContext, olderThan, true)) + + // create at test base time + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 12345678}) + // last_online test base time + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000001}) + // created one month ago but a repo + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000002}) + // last online one hour ago + unittest.AssertNotExistsBean(t, &ActionRunner{ID: 10000003}) + // last online 10 seconds ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000004}) + // created 1 month ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000005}) + // created 1 hour ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000006}) + // last online 1 hour ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000007}) +} + +func TestDeleteOfflineRunnersAll(t *testing.T) { + baseTime := time.Date(2024, 5, 19, 7, 40, 32, 0, time.UTC) + timeutil.MockSet(baseTime) + defer timeutil.MockUnset() + + require.NoError(t, unittest.PrepareTestDatabase()) + + olderThan := timeutil.TimeStampNow().Add(-timeutil.Hour) + + require.NoError(t, DeleteOfflineRunners(db.DefaultContext, olderThan, false)) + + // create at test base time + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 12345678}) + // last_online test base time + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000001}) + // created one month ago + unittest.AssertNotExistsBean(t, &ActionRunner{ID: 10000002}) + // last online one hour ago + unittest.AssertNotExistsBean(t, &ActionRunner{ID: 10000003}) + // last online 10 seconds ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000004}) + // created 1 month ago + unittest.AssertNotExistsBean(t, &ActionRunner{ID: 10000005}) + // created 1 hour ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000006}) + // last online 1 hour ago + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000007}) +} + +func TestDeleteOfflineRunnersErrorOnInvalidOlderThanValue(t *testing.T) { + baseTime := time.Date(2024, 5, 19, 7, 40, 32, 0, time.UTC) + timeutil.MockSet(baseTime) + defer timeutil.MockUnset() + require.Error(t, DeleteOfflineRunners(db.DefaultContext, timeutil.TimeStampNow(), false)) +} diff --git a/models/actions/status.go b/models/actions/status.go index f4357af731..e42c221121 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -34,6 +34,15 @@ var statusNames = map[Status]string{ StatusBlocked: "blocked", } +var nameToStatus = make(map[string]Status, len(statusNames)) + +func init() { + // Populate name to status lookup map + for status, name := range statusNames { + nameToStatus[name] = status + } +} + // String returns the string name of the Status func (s Status) String() string { return statusNames[s] @@ -102,3 +111,8 @@ func (s Status) AsResult() runnerv1.Result { } return runnerv1.Result_RESULT_UNSPECIFIED } + +func StatusFromString(name string) (Status, bool) { + status, exists := nameToStatus[name] + return status, exists +} diff --git a/models/activities/action.go b/models/activities/action.go index f4be7d23e2..8592f81414 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -7,6 +7,7 @@ package activities import ( "context" + "errors" "fmt" "net/url" "path" @@ -441,6 +442,12 @@ func (a *Action) GetIssueContent(ctx context.Context) string { return a.Issue.Content } +func GetActivityByID(ctx context.Context, id int64) (*Action, error) { + var act Action + _, err := db.GetEngine(ctx).ID(id).Get(&act) + return &act, err +} + // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { db.ListOptions @@ -458,7 +465,7 @@ type GetFeedsOptions struct { // GetFeeds returns actions according to the provided options func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { - return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") + return nil, 0, errors.New("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") } cond, err := activityQueryCondition(ctx, opts) @@ -466,11 +473,8 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, err } - sess := db.GetEngine(ctx).Where(cond). - Select("`action`.*"). // this line will avoid select other joined table's columns - Join("INNER", "repository", "`repository`.id = `action`.repo_id") - opts.SetDefaultValues() + sess := db.GetEngine(ctx).Where(cond) sess = db.SetSessionPagination(sess, &opts) actions := make([]*Action, 0, opts.PageSize) @@ -597,13 +601,14 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) } // NotifyWatchers creates batch of actions for every watcher. -func NotifyWatchers(ctx context.Context, actions ...*Action) error { +func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { var watchers []*repo_model.Watch var repo *repo_model.Repository var err error var permCode []bool var permIssue []bool var permPR []bool + var out []Action e := db.GetEngine(ctx) @@ -614,14 +619,14 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { // Add feeds for user self and all watchers. watchers, err = repo_model.GetWatchers(ctx, act.RepoID) if err != nil { - return fmt.Errorf("get watchers: %w", err) + return nil, fmt.Errorf("get watchers: %w", err) } // Be aware that optimizing this correctly into the `GetWatchers` SQL // query is for most cases less performant than doing this. blockedDoerUserIDs, err := user_model.ListBlockedByUsersID(ctx, act.ActUserID) if err != nil { - return fmt.Errorf("user_model.ListBlockedByUsersID: %w", err) + return nil, fmt.Errorf("user_model.ListBlockedByUsersID: %w", err) } if len(blockedDoerUserIDs) > 0 { @@ -636,8 +641,9 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { // Add feed for actioner. act.UserID = act.ActUserID if _, err = e.Insert(act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) + return nil, fmt.Errorf("insert new actioner: %w", err) } + out = append(out, *act) if repoChanged { act.loadRepo(ctx) @@ -645,7 +651,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { // check repo owner exist. if err := act.Repo.LoadOwner(ctx); err != nil { - return fmt.Errorf("can't get repo owner: %w", err) + return nil, fmt.Errorf("can't get repo owner: %w", err) } } else if act.Repo == nil { act.Repo = repo @@ -656,7 +662,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { act.ID = 0 act.UserID = act.Repo.Owner.ID if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) + return nil, fmt.Errorf("insert new actioner: %w", err) } } @@ -709,26 +715,29 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { } if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new action: %w", err) + return nil, fmt.Errorf("insert new action: %w", err) } } } - return nil + return out, nil } // NotifyWatchersActions creates batch of actions for every watcher. -func NotifyWatchersActions(ctx context.Context, acts []*Action) error { +func NotifyWatchersActions(ctx context.Context, acts []*Action) ([]Action, error) { ctx, committer, err := db.TxContext(ctx) if err != nil { - return err + return nil, err } defer committer.Close() + var out []Action for _, act := range acts { - if err := NotifyWatchers(ctx, act); err != nil { - return err + as, err := NotifyWatchers(ctx, act) + if err != nil { + return nil, err } + out = append(out, as...) } - return committer.Commit() + return out, committer.Commit() } // DeleteIssueActions delete all actions related with issueID diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 8b9b2f6929..47dbd8ac2d 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -197,7 +197,8 @@ func TestNotifyWatchers(t *testing.T) { RepoID: 1, OpType: activities_model.ActionStarRepo, } - require.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action)) + _, err := activities_model.NotifyWatchers(db.DefaultContext, action) + require.NoError(t, err) // One watchers are inactive, thus action is only created for user 8, 1, 4, 11 unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ @@ -226,24 +227,6 @@ func TestNotifyWatchers(t *testing.T) { }) } -func TestGetFeedsCorrupted(t *testing.T) { - require.NoError(t, unittest.PrepareTestDatabase()) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ID: 8, - RepoID: 1700, - }) - - actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ - RequestedUser: user, - Actor: user, - IncludePrivate: true, - }) - require.NoError(t, err) - assert.Empty(t, actions) - assert.Equal(t, int64(0), count) -} - func TestConsistencyUpdateAction(t *testing.T) { if !setting.Database.Type.IsSQLite3() { t.Skip("Test is only for SQLite database.") diff --git a/models/activities/federated_user_activity.go b/models/activities/federated_user_activity.go new file mode 100644 index 0000000000..1ff3a855d0 --- /dev/null +++ b/models/activities/federated_user_activity.go @@ -0,0 +1,106 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "context" + "fmt" + + "forgejo.org/models/db" + user_model "forgejo.org/models/user" + "forgejo.org/modules/json" + "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +type FederatedUserActivity struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL"` + ActorID int64 + ActorURI string + Actor *user_model.User `xorm:"-"` // transient + NoteContent string `xorm:"TEXT"` + NoteURL string `xorm:"VARCHAR(255)"` + OriginalNote string `xorm:"TEXT"` + Created timeutil.TimeStamp `xorm:"created"` +} + +func init() { + db.RegisterModel(new(FederatedUserActivity)) +} + +func NewFederatedUserActivity(userID, actorID int64, actorURI, noteContent, noteURL string, originalNote ap.Activity) (FederatedUserActivity, error) { + jsonString, err := json.Marshal(originalNote) + if err != nil { + return FederatedUserActivity{}, err + } + result := FederatedUserActivity{ + UserID: userID, + ActorID: actorID, + ActorURI: actorURI, + NoteContent: noteContent, + NoteURL: noteURL, + OriginalNote: string(jsonString), + } + if valid, err := validation.IsValid(result); !valid { + return FederatedUserActivity{}, err + } + return result, nil +} + +func (federatedUserActivity FederatedUserActivity) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.UserID, "UserID")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorID, "ActorID")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorURI, "ActorURI")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteContent, "NoteContent")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteURL, "NoteURL")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.OriginalNote, "OriginalNote")...) + return result +} + +func CreateUserActivity(ctx context.Context, federatedUserActivity *FederatedUserActivity) error { + if valid, err := validation.IsValid(federatedUserActivity); !valid { + return err + } + _, err := db.GetEngine(ctx).Insert(federatedUserActivity) + return err +} + +type GetFollowingFeedsOptions struct { + db.ListOptions +} + +func GetFollowingFeeds(ctx context.Context, actorID int64, opts GetFollowingFeedsOptions) ([]*FederatedUserActivity, int64, error) { + log.Debug("user_id = %s", actorID) + sess := db.GetEngine(ctx).Where("user_id = ?", actorID) + opts.SetDefaultValues() + sess = db.SetSessionPagination(sess, &opts) + + actions := make([]*FederatedUserActivity, 0, opts.PageSize) + count, err := sess.FindAndCount(&actions) + if err != nil { + return nil, 0, fmt.Errorf("FindAndCount: %w", err) + } + for _, act := range actions { + if err := act.loadActor(ctx); err != nil { + return nil, 0, err + } + } + return actions, count, err +} + +func (federatedUserActivity *FederatedUserActivity) loadActor(ctx context.Context) error { + log.Debug("for activity %s", federatedUserActivity) + actorUser, _, err := user_model.GetFederatedUserByUserID(ctx, federatedUserActivity.ActorID) + if err != nil { + return err + } + federatedUserActivity.Actor = actorUser + + return nil +} diff --git a/models/activities/federated_user_activity_test.go b/models/activities/federated_user_activity_test.go new file mode 100644 index 0000000000..9bf4f77984 --- /dev/null +++ b/models/activities/federated_user_activity_test.go @@ -0,0 +1,24 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "testing" + + "forgejo.org/modules/validation" +) + +func Test_FederatedUserActivityValidation(t *testing.T) { + sut := FederatedUserActivity{} + sut.UserID = 13 + sut.ActorID = 33 + sut.ActorURI = "33" + sut.NoteContent = "Any content!" + sut.NoteURL = "https://example.org/note/17" + sut.OriginalNote = "federatedUserActivityNote-17" + + if res, _ := validation.IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } +} diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index 0cc3f759c6..11badb77e2 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -13,6 +13,12 @@ import ( "forgejo.org/modules/timeutil" ) +const ( + // contributionsMaxAgeSeconds How old data to retrieve for the heatmap. + // 371 days to cover the entire heatmap (53 *full* weeks) + contributionsMaxAgeSeconds = 32054400 +) + // UserHeatmapData represents the data needed to create a heatmap type UserHeatmapData struct { Timestamp timeutil.TimeStamp `json:"timestamp"` @@ -62,7 +68,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi Select(groupBy+" AS timestamp, count(user_id) as contributions"). Table("action"). Where(cond). - And("created_unix > ?", timeutil.TimeStampNow()-31536000). + And("created_unix >= ?", timeutil.TimeStampNow()-contributionsMaxAgeSeconds). GroupBy("timestamp"). OrderBy("timestamp"). Find(&hdata) diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index d922f9a78b..34308cb3d4 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -54,6 +54,10 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { "multiple actions performed with two grouped together", 10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`, }, + { + "test cutoff within", + 40, 40, 1, `[{"timestamp":1577404800,"contributions":1}]`, + }, } // Prepare require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index b7e10ce85c..64866da076 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -5,6 +5,7 @@ package asymkey import ( "context" + "errors" "fmt" "strings" "time" @@ -209,7 +210,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified // deleteGPGKey does the actual key deletion func deleteGPGKey(ctx context.Context, keyID string) (int64, error) { if keyID == "" { - return 0, fmt.Errorf("empty KeyId forbidden") // Should never happen but just to be sure + return 0, errors.New("empty KeyId forbidden") // Should never happen but just to be sure } // Delete imported key n, err := db.GetEngine(ctx).Where("key_id=?", keyID).Delete(new(GPGKeyImport)) diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go index db1912c316..5b8a22fe63 100644 --- a/models/asymkey/gpg_key_common.go +++ b/models/asymkey/gpg_key_common.go @@ -7,6 +7,7 @@ import ( "bytes" "crypto" "encoding/base64" + "errors" "fmt" "hash" "io" @@ -75,7 +76,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) { // Check type pkey, ok := p.(*packet.PublicKey) if !ok { - return nil, fmt.Errorf("key is not a public key") + return nil, errors.New("key is not a public key") } return pkey, nil } @@ -122,15 +123,15 @@ func readArmoredSign(r io.Reader) (body io.Reader, err error) { func extractSignature(s string) (*packet.Signature, error) { r, err := readArmoredSign(strings.NewReader(s)) if err != nil { - return nil, fmt.Errorf("Failed to read signature armor") + return nil, errors.New("Failed to read signature armor") } p, err := packet.Read(r) if err != nil { - return nil, fmt.Errorf("Failed to read signature packet") + return nil, errors.New("Failed to read signature packet") } sig, ok := p.(*packet.Signature) if !ok { - return nil, fmt.Errorf("Packet is not a signature") + return nil, errors.New("Packet is not a signature") } return sig, nil } diff --git a/models/asymkey/gpg_key_object_verification.go b/models/asymkey/gpg_key_object_verification.go index ccd31a38b1..745ed04869 100644 --- a/models/asymkey/gpg_key_object_verification.go +++ b/models/asymkey/gpg_key_object_verification.go @@ -6,6 +6,7 @@ package asymkey import ( "context" + "errors" "fmt" "hash" "strings" @@ -316,7 +317,7 @@ func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, si func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { // Check if key can sign if !k.CanSign { - return fmt.Errorf("key can not sign") + return errors.New("key can not sign") } // Decode key pkey, err := base64DecPubKey(k.Content) diff --git a/models/asymkey/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go index 305e464b4b..8177db6439 100644 --- a/models/asymkey/ssh_key_parse.go +++ b/models/asymkey/ssh_key_parse.go @@ -10,6 +10,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/pem" + "errors" "fmt" "math/big" "os" @@ -93,7 +94,7 @@ func parseKeyString(content string) (string, error) { block, _ := pem.Decode([]byte(content)) if block == nil { - return "", fmt.Errorf("failed to parse PEM block containing the public key") + return "", errors.New("failed to parse PEM block containing the public key") } if strings.Contains(block.Type, "PRIVATE") { return "", ErrKeyIsPrivate @@ -226,7 +227,7 @@ func SSHNativeParsePublicKey(keyLine string) (string, int, error) { // The ssh library can parse the key, so next we find out what key exactly we have. switch pkeyType { - case ssh.KeyAlgoDSA: + case ssh.KeyAlgoDSA: //nolint:staticcheck rawPub := struct { Name string P, Q, G, Y *big.Int diff --git a/models/db/log.go b/models/db/log.go index b94af8e39c..387709cc50 100644 --- a/models/db/log.go +++ b/models/db/log.go @@ -69,6 +69,9 @@ func (l *XORMLogBridge) Warn(v ...any) { // Warnf show warning log func (l *XORMLogBridge) Warnf(format string, v ...any) { + if format == "Table %s Column %s db default is %s, struct default is %s" || format == "Table %s Column %s db nullable is %v, struct nullable is %v" { + return + } l.Log(stackLevel, log.WARN, format, v...) } diff --git a/models/error.go b/models/error.go index e8962f386b..ebaa8a135d 100644 --- a/models/error.go +++ b/models/error.go @@ -414,7 +414,7 @@ func IsErrSHAOrCommitIDNotProvided(err error) bool { } func (err ErrSHAOrCommitIDNotProvided) Error() string { - return "a SHA or commit ID must be proved when updating a file" + return "a SHA or commit ID must be provided when updating a file" } // ErrInvalidMergeStyle represents an error if merging with disabled merge strategy diff --git a/models/fixtures/TestGetUsedForUser/action_artifact.yaml b/models/fixtures/TestGetUsedForUser/action_artifact.yaml new file mode 100644 index 0000000000..db5392126d --- /dev/null +++ b/models/fixtures/TestGetUsedForUser/action_artifact.yaml @@ -0,0 +1,17 @@ +- + id: 1001 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 693147180559 + file_compressed_size: 693147180559 + content_encoding: "application/zip" + artifact_path: "big-file.zip" + artifact_name: "big-file" + status: 4 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index b2febb4ed8..a97e94fbf4 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -59,14 +59,6 @@ created_unix: 1603011540 # grouped with id:7 - id: 8 - user_id: 1 - op_type: 12 # close issue - act_user_id: 1 - repo_id: 1700 # dangling intentional - is_private: false - created_unix: 1603011541 - -- id: 9 user_id: 34 op_type: 12 # close issue act_user_id: 34 @@ -74,3 +66,11 @@ is_private: false created_unix: 1680454039 content: '4|' # issueId 5 + +- id: 10 + user_id: 40 + op_type: 1 # create repo + act_user_id: 40 + repo_id: 60 # public + is_private: false + created_unix: 1577404800 # end of heatmap \ No newline at end of file diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 7a7bf34197..5b6f89ae0e 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -471,3 +471,64 @@ need_approval: 0 approved_by: 0 event_payload: '{"head_commit":{"id":"5f22f7d0d95d614d25a5b68592adb345a4b5c7fd"}}' + + +# GET action run(s) test +- + id: 892 + title: "successful push run" + repo_id: 63 + owner_id: 2 + workflow_id: "success.yaml" + index: 1 + trigger_user_id: 2 + ref: "refs/heads/main" + commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee" + event: "push" + is_fork_pull_request: 0 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 + +- + id: 893 + title: "failed pull_request run" + repo_id: 63 + owner_id: 2 + workflow_id: "failed.yaml" + index: 2 + trigger_user_id: 2 + ref: "refs/heads/bugfix-1" + commit_sha: "35c5cddfc19397501ec8f4f7bb808a7c8f04445f" + event: "pull_request" + is_fork_pull_request: 0 + status: 2 # failure + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 + +- + id: 894 + title: "running workflow_dispatch run" + repo_id: 63 + owner_id: 2 + workflow_id: "running.yaml" + index: 3 + trigger_user_id: 2 + ref: "refs/heads/main" + commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee" + event: "workflow_dispatch" + is_fork_pull_request: 0 + status: 6 # running + started: 1683636528 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_runner.yml b/models/fixtures/action_runner.yml index 94deac998e..fcf26d49b6 100644 --- a/models/fixtures/action_runner.yml +++ b/models/fixtures/action_runner.yml @@ -18,3 +18,122 @@ created: 1716104432 updated: 1716104432 deleted: ~ +- id: 10000001 + uuid: 10d3b248-6460-4bf5-b819-1f5b3109e10f + name: global-online + version: v6.3.1+7-gc4c0ca0 + owner_id: 0 + repo_id: 0 + description: "" + base: 0 + repo_range: "" + token_hash: 7e9ed71f64e98ce1f70e94c63f3cb6c41a8cb0b90de3e1daf7ec5c35361d60ed44da67c5ac393b7aaf443dcfc766007dc828 + token_salt: WUcgZWl7mW + last_online: 1716104422 + last_active: 0 + agent_labels: '["docker"]' + created: 1716104431 + updated: 1716104422 + deleted: ~ +- id: 10000002 + uuid: 1d188484-dd97-4a70-b707-5e87b578ab6b + name: repo-never-used + version: v6.3.1+7-gc4c0ca0 + owner_id: 0 + repo_id: 1 + description: "" + base: 0 + repo_range: "" + token_hash: 51e88c17ac8b54dd101dc2e4f530a71643c703adba7170f4b1a28f1cb483b4cfb107798c521e0532ef3c6480b64518a5c6a5 + token_salt: 4rh8ncXYIO + last_online: 0 + last_active: 0 + agent_labels: '["docker"]' + created: 1713512432 + updated: 1713512432 + deleted: ~ +- id: 10000003 + uuid: 7a039c6b-b0b2-4cf5-a93d-715d617f99e2 + name: global-offline + version: v6.3.1+7-gc4c0ca0 + owner_id: 0 + repo_id: 0 + description: "" + base: 0 + repo_range: "" + token_hash: c76960c56bc6069f0d1648991ec626500abe8c15286f5c355d565c3b5ba945d7d6f1272a6c77849e592528179511b94f5d69 + token_salt: TFMe2jhOkB + last_online: 1715499632 + last_active: 0 + agent_labels: '["docker"]' + created: 1715499632 + updated: 1715499632 + deleted: ~ +- id: 10000004 + uuid: 93ca7fdd-faca-4df6-a474-8345263ef10b + name: user-online + version: v6.3.1+7-gc4c0ca0 + owner_id: 1 + repo_id: 0 + description: "" + base: 0 + repo_range: "" + token_hash: 6ddf7f0f2301d2b3f66418145dc497a6d09fa6586e659afcb5ae2a0c5b639561d795aff8062537db9df73b396842ea826134 + token_salt: QcdGuReAp4 + last_online: 1716104422 + last_active: 0 + agent_labels: '["docker"]' + created: 1716104431 + updated: 1716104422 + deleted: ~ +- id: 10000005 + uuid: a8534df6-c4be-40f4-9714-903b69d973d9 + name: user-never-used + version: v6.3.1+7-gc4c0ca0 + owner_id: 1 + repo_id: 0 + description: desc + base: 0 + repo_range: "" + token_hash: 4441de7defcfc3d21baa608dec66a562cf23307abddaabdbb836907ac5f48c8780c354891916c525b79ec7af8e95be7a09b4 + token_salt: ONNqIOnj3t + last_online: 0 + last_active: 0 + agent_labels: '["docker"]' + created: 1713512433 + updated: 1713512433 + deleted: ~ +- id: 10000006 + uuid: e1c5bb6c-de68-4335-8955-5192f76708ac + name: orga-fresh-created + version: v6.3.1+7-gc4c0ca0 + owner_id: 35 + repo_id: 0 + description: "" + base: 0 + repo_range: "" + token_hash: a61f9ee48c6847d243ace0a8936efe80af9277c7bc46d6da6e03d1d406608b8023ee66600ad24f0effaa8e3338f92ac97ac9 + token_salt: fZJKjrFGWA + last_online: 0 + last_active: 0 + agent_labels: '["docker"]' + created: 1716100832 + updated: 1716100832 + deleted: ~ +- id: 10000007 + uuid: ff755f06-948e-479b-8031-5b3e9f123e32 + name: orga-offline + version: v6.3.1+7-gc4c0ca0 + owner_id: 35 + repo_id: 0 + description: "" + base: 0 + repo_range: "" + token_hash: 9372efb38f9b64efe65065380abe2f24ef34a59d9619f4cdc08f1151e9849f0b6e722aa10538e8730288de6e2f09acdac695 + token_salt: TnU7iiIdCb + last_online: 1716100832 + last_active: 0 + agent_labels: '["docker"]' + created: 1736085520 + updated: 1716100832 + deleted: ~ diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index f4121284a6..34407d6f81 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -113,3 +113,344 @@ review_id: 22 assignee_id: 5 created_unix: 946684817 + +- + id: 13 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["4ca8bcaf27e28504df7bf996819665986b01c847","96cef4a7b72b3c208340ae6f0cf55a93e9077c93","c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2"]}' + created_unix: 1688672373 + +- + id: 14 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["23576dd018294e476c06e569b6b0f170d0558705"]}' + created_unix: 1688672374 + +- + id: 15 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["3e64625bd6eb5bcba69ac97de6c8f507402df861", "c704db5794097441aa2d9dd834d5b7e2f8f08108"]}' + created_unix: 1688672375 + +- + id: 16 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["811d46c7e518f4f180afb862c0db5cb8c80529ce", "747ddb3506a4fa04a7747808eb56ae16f9e933dc", "837d5c8125633d7d258f93b998e867eab0145520", "1978192d98bb1b65e11c2cf37da854fbf94bffd6"]}' + created_unix: 1688672376 + +- + id: 17 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":true,"commit_ids":["1978192d98bb1b65e11c2cf37da854fbf94bffd6", "9b93963cf6de4dc33f915bb67f192d099c301f43"]}' + created_unix: 1749734240 + +- + id: 2000 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 1 + old_milestone_id: 0 + created_unix: 946684820 + +- + id: 2001 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 2 + old_milestone_id: 1 + created_unix: 946684920 + +- + id: 2002 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 0 + old_milestone_id: 2 + created_unix: 946685020 + +- + id: 2003 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 10 # not exsting milestone + old_milestone_id: 0 + created_unix: 946685080 + +- + id: 2010 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 1 + old_project_id: 0 + created_unix: 946685120 + +- + id: 2011 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 2 + old_project_id: 1 + created_unix: 946685220 + +- + id: 2012 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 0 + old_project_id: 2 + created_unix: 946685320 + +- + id: 2013 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 10 # not existing project + old_project_id: 0 + created_unix: 946685420 + +- + id: 2020 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 1 # add label + created_unix: 946685520 + +- + id: 2021 + type: 7 # label + poster_id: 1 + issue_id: 1 + label_id: 2 + content: 1 # add label + created_unix: 946685620 + +- + id: 2022 + type: 7 # label + poster_id: 2 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 0 # remove label + created_unix: 946685720 + +- + id: 2023 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 1 # add label + created_unix: 946685720 + +- + id: 2024 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 2 + content: 0 # remove label + created_unix: 946685720 + +- + id: 2025 + type: 7 # label + poster_id: 2 + issue_id: 1 # in repo_id 1 + label_id: 2 + content: 1 # add label + created_unix: 946685820 + +- + id: 2026 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 0 # remove label + created_unix: 946685920 + +- + id: 2027 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 2 + content: 0 # remove label + created_unix: 946685920 + +- id: 2040 + type: 9 # assignee + poster_id: 1 + issue_id: 1 # in repo_id 1 + assignee_id: 1 # self + removed_assignee: false # add assignee + created_unix: 946688020 + +- id: 2041 + type: 9 # assignee + poster_id: 2 + issue_id: 1 # in repo_id 1 + assignee_id: 1 + removed_assignee: true # remove assignee + created_unix: 946688120 + +- id: 2042 + type: 9 # assignee + poster_id: 1 + issue_id: 1 # in repo_id 1 + assignee_id: 2 + removed_assignee: false # add assignee + created_unix: 946688220 + +- id: 2043 + type: 9 # assignee + poster_id: 2 + issue_id: 1 # in repo_id 1 + assignee_id: 2 # self + removed_assignee: true # remove assignee + created_unix: 946688320 + +- id: 2050 + type: 23 # lock + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688420 + +- id: 2051 + type: 24 # unlock + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688520 + +- id: 2052 + type: 23 # lock + poster_id: 1 + issue_id: 1 # in repo_id 1 + content: "Too heated" + created_unix: 946688620 + +- id: 2053 + type: 24 # unlock + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688720 + +- id: 2060 + type: 36 # pin + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688820 + +- id: 2061 + type: 37 # unpin + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688920 + +- id: 2070 + type: 2 # close + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946689020 + +- id: 2071 + type: 1 # reopen + poster_id: 2 + issue_id: 1 # in repo_id 1 + created_unix: 946689220 + +- id: 2072 + type: 2 # close + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + created_unix: 946689320 + +- id: 2073 + type: 1 # reopen + poster_id: 2 + issue_id: 2 # pull in repo_id 1 + created_unix: 946689420 + +- id: 2080 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 5 # issue in repo_id 1 + created_unix: 946689500 + +- id: 2081 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 2 # pull in repo_id 1 + created_unix: 946689600 + +- id: 2082 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 32 + ref_issue_id: 16 # issue in repo_id 32 + created_unix: 946689700 + +- id: 2083 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 10 + ref_issue_id: 8 # pull in repo_id 10 + created_unix: 946689800 + +- id: 2090 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 1 # issue in repo_id 1 + created_unix: 946689900 + +- id: 2091 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 2 # pull in repo_id 1 + created_unix: 946690000 + +- id: 2092 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 32 + ref_issue_id: 16 # issue in repo_id 32 + created_unix: 946690050 + +- id: 2093 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 10 + ref_issue_id: 8 # pull in repo_id 10 + created_unix: 946690100 diff --git a/models/fixtures/follow.yml b/models/fixtures/follow.yml index b8d35828bf..da3d4a60c1 100644 --- a/models/fixtures/follow.yml +++ b/models/fixtures/follow.yml @@ -17,3 +17,13 @@ id: 4 user_id: 31 follow_id: 33 + +- + id: 5 + user_id: 4 + follow_id: 8 + +- + id: 6 + user_id: 5 + follow_id: 8 diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index fc0e03bca1..c62d451868 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -18,7 +18,7 @@ id: 2 hook_id: 1 uuid: uuid2 - is_delivered: false + is_delivered: true - id: 3 @@ -40,4 +40,4 @@ id: 4 hook_id: 3 uuid: uuid4 - is_delivered: false + is_delivered: true diff --git a/models/fixtures/pull_auto_merge.yml b/models/fixtures/pull_auto_merge.yml new file mode 100644 index 0000000000..ca780a73aa --- /dev/null +++ b/models/fixtures/pull_auto_merge.yml @@ -0,0 +1 @@ +[] # empty diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index cd49a51796..773f238645 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -795,3 +795,10 @@ type: 10 config: "{}" created_unix: 946684810 + +- + id: 115 + repo_id: 63 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 0ba4d06e14..2f104eed65 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -31,6 +31,8 @@ close_issues_via_commit_in_any_branch: false created_unix: 1731254961 updated_unix: 1731254961 + topics: '[]' + - id: 2 owner_id: 2 @@ -61,7 +63,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: true - + topics: '[]' - id: 3 owner_id: 3 @@ -94,6 +96,7 @@ close_issues_via_commit_in_any_branch: false created_unix: 1700000001 updated_unix: 1700000001 + topics: '[]' - id: 4 @@ -125,6 +128,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 5 @@ -158,6 +162,7 @@ close_issues_via_commit_in_any_branch: false created_unix: 1700000002 updated_unix: 1700000002 + topics: '[]' - id: 6 @@ -190,6 +195,7 @@ close_issues_via_commit_in_any_branch: false created_unix: 1710000001 updated_unix: 1710000001 + topics: '[]' - id: 7 @@ -222,6 +228,7 @@ close_issues_via_commit_in_any_branch: false created_unix: 1710000003 updated_unix: 1710000003 + topics: '[]' - id: 8 @@ -254,6 +261,7 @@ close_issues_via_commit_in_any_branch: false created_unix: 1710000002 updated_unix: 1710000002 + topics: '[]' - id: 9 @@ -284,6 +292,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 10 @@ -315,6 +324,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 11 @@ -346,6 +356,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 12 @@ -376,6 +387,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 13 @@ -406,6 +418,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 14 @@ -437,6 +450,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 15 @@ -468,6 +482,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 16 @@ -499,6 +514,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 17 @@ -529,6 +545,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 18 @@ -559,6 +576,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 19 @@ -589,6 +607,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 20 @@ -619,6 +638,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 21 @@ -649,6 +669,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 22 @@ -679,6 +700,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 23 @@ -709,6 +731,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 24 @@ -739,6 +762,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 25 @@ -769,6 +793,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 26 @@ -799,6 +824,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 27 @@ -829,6 +855,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 28 @@ -859,6 +886,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 29 @@ -889,6 +917,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 30 @@ -919,6 +948,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 31 @@ -950,6 +980,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 32 # org public repo @@ -982,6 +1013,7 @@ close_issues_via_commit_in_any_branch: false created_unix: 1700000003 updated_unix: 1700000003 + topics: '[]' - id: 33 @@ -1013,6 +1045,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 34 @@ -1043,6 +1076,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 35 @@ -1073,6 +1107,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 36 @@ -1104,6 +1139,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 37 @@ -1135,6 +1171,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 38 @@ -1166,6 +1203,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 39 @@ -1197,6 +1235,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 40 @@ -1228,6 +1267,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 41 @@ -1259,6 +1299,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 42 @@ -1290,6 +1331,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 43 @@ -1320,6 +1362,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 44 @@ -1351,6 +1394,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 45 @@ -1381,6 +1425,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 46 @@ -1412,6 +1457,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 47 @@ -1443,6 +1489,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 48 @@ -1474,6 +1521,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 49 @@ -1506,6 +1554,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 50 @@ -1537,6 +1586,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 51 @@ -1568,6 +1618,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 52 @@ -1599,6 +1650,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 53 @@ -1627,6 +1679,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 54 @@ -1639,6 +1692,7 @@ is_archived: false is_private: true status: 0 + topics: '[]' - id: 55 @@ -1651,6 +1705,7 @@ is_private: true num_issues: 1 status: 0 + topics: '[]' - id: 56 @@ -1664,6 +1719,7 @@ is_private: true status: 0 num_issues: 0 + topics: '[]' - id: 57 @@ -1677,6 +1733,7 @@ is_private: false status: 0 num_issues: 0 + topics: '[]' - id: 58 # org public repo @@ -1708,6 +1765,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 1059 @@ -1721,6 +1779,7 @@ is_private: false status: 0 num_issues: 0 + topics: '[]' - id: 59 @@ -1734,6 +1793,7 @@ is_private: true status: 0 num_issues: 0 + topics: '[]' - id: 60 @@ -1765,6 +1825,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 61 @@ -1796,6 +1857,7 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' - id: 62 owner_id: 2 @@ -1826,3 +1888,36 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' + +- + id: 63 + owner_id: 2 + owner_name: user2 + lower_name: test_action_run_search + name: test_action_run_search + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: true + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + is_fsck_enabled: true + close_issues_via_commit_in_any_branch: false + topics: '[]' diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index 149fe90888..a863f1203a 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -239,3 +239,15 @@ num_members: 2 includes_all_repositories: false can_create_org_repo: false + +- + id: 25 + org_id: 17 + lower_name: super-user + name: super-user + description: "" + authorize: 3 + num_repos: 0 + num_members: 0 + includes_all_repositories: 0 + can_create_org_repo: 0 diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml index e8f8d0e422..4d282a7eb5 100644 --- a/models/fixtures/team_unit.yml +++ b/models/fixtures/team_unit.yml @@ -329,3 +329,10 @@ team_id: 22 type: 3 access_mode: 1 + +- + id: 84 + org_id: 17 + team_id: 25 + type: 3 + access_mode: 3 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 630505b8b4..00aa182540 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -53,6 +53,7 @@ login_source: 0 login_name: user2 type: 0 + website: https://keyoxide.org/eb114f5e6c0dc2bcdd183550a4b61a2dc5923710 salt: ZogKvWdyEx max_repo_creation: -1 is_active: true @@ -69,7 +70,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 17 + num_repos: 18 num_teams: 0 num_members: 0 visibility: 0 @@ -93,7 +94,7 @@ login_name: org3 type: 1 salt: ZogKvWdyEx - max_repo_creation: -1 + max_repo_creation: 1000 is_active: false is_admin: false is_restricted: false @@ -143,7 +144,7 @@ avatar_email: user4@example.com use_custom_avatar: true num_followers: 0 - num_following: 1 + num_following: 2 num_stars: 0 num_repos: 0 num_teams: 0 @@ -181,7 +182,7 @@ avatar_email: user5@example.com use_custom_avatar: true num_followers: 0 - num_following: 0 + num_following: 1 num_stars: 0 num_repos: 1 num_teams: 0 @@ -294,7 +295,7 @@ avatar: "" avatar_email: user8@example.com use_custom_avatar: true - num_followers: 1 + num_followers: 3 num_following: 1 num_stars: 0 num_repos: 0 @@ -641,7 +642,7 @@ num_following: 0 num_stars: 0 num_repos: 2 - num_teams: 3 + num_teams: 4 num_members: 4 visibility: 0 repo_admin_change_team_access: false diff --git a/models/forgefed/federationhost.go b/models/forgefed/federationhost.go index 29f1b7d28e..978847bd95 100644 --- a/models/forgefed/federationhost.go +++ b/models/forgefed/federationhost.go @@ -6,6 +6,7 @@ package forgefed import ( "database/sql" "fmt" + "net/url" "strings" "time" @@ -17,9 +18,9 @@ import ( // swagger:model type FederationHost struct { ID int64 `xorm:"pk autoincr"` - HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"` + HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` + HostPort uint16 `xorm:" UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` NodeInfo NodeInfo `xorm:"extends NOT NULL"` - HostPort uint16 `xorm:"NOT NULL DEFAULT 443"` HostSchema string `xorm:"NOT NULL DEFAULT 'https'"` LatestActivity time.Time `xorm:"NOT NULL"` KeyID sql.NullString `xorm:"key_id UNIQUE"` @@ -42,6 +43,13 @@ func NewFederationHost(hostFqdn string, nodeInfo NodeInfo, port uint16, schema s return result, nil } +func (host FederationHost) AsURL() url.URL { + return url.URL{ + Scheme: host.HostSchema, + Host: fmt.Sprintf("%v:%v", host.HostFqdn, host.HostPort), + } +} + // Validate collects error strings in a slice and returns this func (host FederationHost) Validate() []string { var result []string diff --git a/models/forgefed/federationhost_test.go b/models/forgefed/federationhost_test.go index 824495c9cb..d11affbae0 100644 --- a/models/forgefed/federationhost_test.go +++ b/models/forgefed/federationhost_test.go @@ -35,7 +35,7 @@ func Test_FederationHostValidation(t *testing.T) { HostSchema: "https", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid: HostFqdn empty") + t.Error("sut should be invalid: HostFqdn empty") } sut = FederationHost{ @@ -48,7 +48,7 @@ func Test_FederationHostValidation(t *testing.T) { HostSchema: "https", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid: HostFqdn too long (len=256)") + t.Error("sut should be invalid: HostFqdn too long (len=256)") } sut = FederationHost{ @@ -59,7 +59,7 @@ func Test_FederationHostValidation(t *testing.T) { HostSchema: "https", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid: NodeInfo invalid") + t.Error("sut should be invalid: NodeInfo invalid") } sut = FederationHost{ @@ -72,7 +72,7 @@ func Test_FederationHostValidation(t *testing.T) { HostSchema: "https", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid: Future timestamp") + t.Error("sut should be invalid: Future timestamp") } sut = FederationHost{ @@ -85,6 +85,6 @@ func Test_FederationHostValidation(t *testing.T) { HostSchema: "https", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid: HostFqdn lower case") + t.Error("sut should be invalid: HostFqdn lower case") } } diff --git a/models/forgefed/nodeinfo.go b/models/forgefed/nodeinfo.go index 2461b5e499..38f51304c5 100644 --- a/models/forgefed/nodeinfo.go +++ b/models/forgefed/nodeinfo.go @@ -17,12 +17,14 @@ type ( ) const ( - ForgejoSourceType SoftwareNameType = "forgejo" - GiteaSourceType SoftwareNameType = "gitea" + ForgejoSourceType SoftwareNameType = "forgejo" + GiteaSourceType SoftwareNameType = "gitea" + MastodonSourceType SoftwareNameType = "mastodon" + GoToSocialSourceType SoftwareNameType = "gotosocial" ) var KnownSourceTypes = []any{ - ForgejoSourceType, GiteaSourceType, + ForgejoSourceType, GiteaSourceType, MastodonSourceType, GoToSocialSourceType, } // ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------ diff --git a/models/forgefed/nodeinfo_test.go b/models/forgefed/nodeinfo_test.go index 9e37e77100..a0c9781b90 100644 --- a/models/forgefed/nodeinfo_test.go +++ b/models/forgefed/nodeinfo_test.go @@ -4,7 +4,7 @@ package forgefed import ( - "fmt" + "errors" "reflect" "strings" "testing" @@ -28,7 +28,7 @@ func Test_NodeInfoWellKnownUnmarshalJSON(t *testing.T) { }, "empty": { item: []byte(``), - wantErr: fmt.Errorf("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""), + wantErr: errors.New("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""), }, } @@ -74,7 +74,7 @@ func Test_NewNodeInfoWellKnown(t *testing.T) { _, err := NewNodeInfoWellKnown([]byte(`invalid`)) if err == nil { - t.Errorf("error was expected here") + t.Error("error was expected here") } } @@ -87,6 +87,6 @@ func Test_NewNodeInfo(t *testing.T) { _, err := NewNodeInfo([]byte(`invalid`)) if err == nil { - t.Errorf("error was expected here") + t.Error("error was expected here") } } diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 73226c525f..737350b019 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -5,6 +5,7 @@ package forgejo_migrations //nolint:revive import ( "context" + "errors" "fmt" "os" @@ -98,6 +99,16 @@ var migrations = []*Migration{ NewMigration("Add public key information to `FederatedUser` and `FederationHost`", AddPublicKeyInformationForFederation), // v29 -> v30 NewMigration("Migrate `User.NormalizedFederatedURI` column to extract port & schema into FederatedHost", MigrateNormalizedFederatedURI), + // v30 -> v31 + NewMigration("Normalize repository.topics to empty slice instead of null", SetTopicsAsEmptySlice), + // v31 -> v32 + NewMigration("Migrate maven package name concatenation", ChangeMavenArtifactConcatenation), + // v32 -> v33 + NewMigration("Add federated user activity tables, update the `federated_user` table & add indexes", FederatedUserActivityMigration), + // v33 -> v34 + NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun), + // v34 -> v35 + NewMigration("Add index to `stopped` column in `action_run` table", AddIndexToActionRunStopped), } // GetCurrentDBVersion returns the current Forgejo database version. @@ -130,7 +141,7 @@ func EnsureUpToDate(x *xorm.Engine) error { } if currentDB < 0 { - return fmt.Errorf("database has not been initialized") + return errors.New("database has not been initialized") } expected := ExpectedVersion() diff --git a/models/forgejo_migrations/v31.go b/models/forgejo_migrations/v31.go new file mode 100644 index 0000000000..fdcab21b1a --- /dev/null +++ b/models/forgejo_migrations/v31.go @@ -0,0 +1,58 @@ +// Copyright 2025 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func SetTopicsAsEmptySlice(x *xorm.Engine) error { + var err error + switch x.Dialect().URI().DBType { + case schemas.MYSQL: + _, err = x.Exec("UPDATE `repository` SET topics = '[]' WHERE topics IS NULL OR topics = 'null'") + case schemas.SQLITE: + _, err = x.Exec("UPDATE `repository` SET topics = '[]' WHERE topics IS NULL OR topics = 'null'") + case schemas.POSTGRES: + _, err = x.Exec("UPDATE `repository` SET topics = '[]' WHERE topics IS NULL OR topics::text = 'null'") + } + + if err != nil { + return err + } + + if x.Dialect().URI().DBType == schemas.SQLITE { + sessMigration := x.NewSession() + defer sessMigration.Close() + if err := sessMigration.Begin(); err != nil { + return err + } + _, err = sessMigration.Exec("ALTER TABLE `repository` RENAME COLUMN `topics` TO `topics_backup`") + if err != nil { + return err + } + _, err = sessMigration.Exec("ALTER TABLE `repository` ADD COLUMN `topics` TEXT NOT NULL DEFAULT '[]'") + if err != nil { + return err + } + _, err = sessMigration.Exec("UPDATE `repository` SET `topics` = `topics_backup`") + if err != nil { + return err + } + _, err = sessMigration.Exec("ALTER TABLE `repository` DROP COLUMN `topics_backup`") + if err != nil { + return err + } + + return sessMigration.Commit() + } + + type Repository struct { + ID int64 `xorm:"pk autoincr"` + Topics []string `xorm:"TEXT JSON NOT NULL"` + } + + return x.Sync(new(Repository)) +} diff --git a/models/forgejo_migrations/v31_test.go b/models/forgejo_migrations/v31_test.go new file mode 100644 index 0000000000..5b4aac2a60 --- /dev/null +++ b/models/forgejo_migrations/v31_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "testing" + + migration_tests "forgejo.org/models/migrations/test" + + "github.com/stretchr/testify/require" +) + +func Test_SetTopicsAsEmptySlice(t *testing.T) { + type Repository struct { + ID int64 `xorm:"pk autoincr"` + Topics []string `xorm:"TEXT JSON"` + } + + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(Repository)) + defer deferable() + if x == nil || t.Failed() { + return + } + + require.NoError(t, SetTopicsAsEmptySlice(x)) + + var repos []Repository + require.NoError(t, x.Find(&repos)) + + for _, repo := range repos { + if repo.ID == 2 { + require.Equal(t, []string{"go", "dev"}, repo.Topics, "Valid topics should remain unchanged") + } else { + require.Equal(t, []string{}, repo.Topics, "NULL topics should be set to empty array") + } + } +} diff --git a/models/forgejo_migrations/v32.go b/models/forgejo_migrations/v32.go new file mode 100644 index 0000000000..bed335ab6b --- /dev/null +++ b/models/forgejo_migrations/v32.go @@ -0,0 +1,414 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "encoding/xml" + "fmt" + "regexp" + "slices" + "sort" + "strconv" + "strings" + + "forgejo.org/models/packages" + "forgejo.org/modules/json" + "forgejo.org/modules/log" + "forgejo.org/modules/packages/maven" + packages_service "forgejo.org/services/packages" + + "golang.org/x/net/context" + "xorm.io/xorm" +) + +var getPackage = packages_service.GetPackageFileStream + +type Snapshot struct { + baseVersion string + date string + time string + build int +} + +type Metadata struct { + XMLName xml.Name `xml:"metadata"` + ModelVersion string `xml:"modelVersion,attr"` + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` +} + +type mavenPackageResult struct { + PackageFile *packages.PackageFile `xorm:"extends"` + PackageVersion *packages.PackageVersion `xorm:"extends"` + Package *packages.Package `xorm:"extends"` + PackageName string `xorm:"-"` + Snapshot *Snapshot `xorm:"-"` + GroupID string `xorm:"-"` + ArtifactID string `xorm:"-"` +} + +// ChangeMavenArtifactConcatenation resolves old dash-concatenated Maven coordinates and regenerates metadata. +// Note: runs per-owner in a single transaction; failures roll back all owners. +func ChangeMavenArtifactConcatenation(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + // get unique owner IDs of Maven packages + var ownerIDs []*int64 + if err := sess. + Table("package"). + Select("package.owner_id"). + Where("package.type = 'maven'"). + GroupBy("package.owner_id"). + OrderBy("package.owner_id DESC"). + Find(&ownerIDs); err != nil { + return err + } + + for _, id := range ownerIDs { + if err := fixMavenArtifactPerOwner(sess, id); err != nil { + log.Error("owner %d migration failed: %v", id, err) + return err // rollback all + } + } + + return sess.Commit() +} + +func fixMavenArtifactPerOwner(sess *xorm.Session, ownerID *int64) error { + results, err := getMavenPackageResultsToUpdate(sess, ownerID) + if err != nil { + return err + } + + if err = resolvePackageCollisions(results, sess); err != nil { + return err + } + + if err = processPackageVersions(results, sess); err != nil { + return err + } + + return processPackageFiles(results, sess) +} + +// processPackageFiles updates Maven package files and versions in the database +// Returns an error if any database or processing operation fails. +func processPackageFiles(results []*mavenPackageResult, sess *xorm.Session) error { + processedVersion := make(map[string][]*mavenPackageResult) + + for _, r := range results { + if r.Snapshot != nil { + key := fmt.Sprintf("%s:%s", r.PackageName, r.PackageVersion.LowerVersion) + processedVersion[key] = append(processedVersion[key], r) + } + + // Only update version_id when it differs + if r.PackageVersion.ID != r.PackageFile.VersionID { + pattern := strings.TrimSuffix(r.PackageFile.Name, ".pom") + "%" + // Per routers/api/packages/maven/maven.go:338, POM files already have the `IsLead`, so no update needed for this prop + if _, err := sess.Exec("UPDATE package_file SET version_id = ? WHERE version_id = ? and name like ?", r.PackageVersion.ID, r.PackageFile.VersionID, pattern); err != nil { + return err + } + } + } + + // If maven-metadata.xml is missing (snapshot path collision), skip regeneration + // Without this metadata, Maven cannot resolve snapshot details + for _, packageResults := range processedVersion { + sort.Slice(packageResults, func(i, j int) bool { + return packageResults[i].Snapshot.build > packageResults[j].Snapshot.build + }) + + rs := packageResults[0] + + pf, md, err := parseMetadata(sess, rs) + if err != nil { + return err + } + + if pf != nil && md != nil && md.GroupID == rs.GroupID && md.ArtifactID == rs.ArtifactID { + if pf.VersionID != rs.PackageFile.VersionID { + if _, err := sess.ID(pf.ID).Cols("version_id").Update(pf); err != nil { + return err + } + } + continue + } + + log.Warn("no maven-metadata.xml found for (id: %d) [%s:%s]", rs.PackageVersion.ID, rs.PackageName, rs.PackageVersion.Version) + } + + return nil +} + +// parseMetadata retrieves metadata for a Maven package file from the database and decodes it into a Metadata object. +// Returns the associated PackageFile, Metadata, and any error encountered during processing. +func parseMetadata(sess *xorm.Session, snapshot *mavenPackageResult) (*packages.PackageFile, *Metadata, error) { + ctx := context.Background() + + var pf packages.PackageFile + found, err := sess.Table(pf). + Where("version_id = ?", snapshot.PackageFile.VersionID). // still the old id + And("lower_name = ?", "maven-metadata.xml"). + Get(&pf) + if err != nil { + return nil, nil, err + } + + if !found { + return nil, nil, nil + } + + s, _, _, err := getPackage(ctx, &pf) + if err != nil { + return nil, nil, err + } + + defer s.Close() + dec := xml.NewDecoder(s) + var m Metadata + if err := dec.Decode(&m); err != nil { + return nil, nil, err + } + + return &pf, &m, nil +} + +// processPackageVersions processes Maven package versions by updating metadata or inserting new records as necessary. +// It avoids redundant updates by tracking already processed versions using a map. Returns an error on failure. +func processPackageVersions(results []*mavenPackageResult, sess *xorm.Session) error { + processedVersion := make(map[string]int64) + + for _, r := range results { + key := fmt.Sprintf("%s:%s", r.PackageName, r.PackageVersion.Version) + + if id, ok := processedVersion[key]; ok { + r.PackageVersion.ID = id + continue + } + + // for non collisions, just update the metadata + if r.PackageVersion.PackageID == r.Package.ID { + if _, err := sess.ID(r.PackageVersion.ID).Cols("metadata_json").Update(r.PackageVersion); err != nil { + return err + } + } else { + log.Info("Create new maven package version for %s:%s", r.PackageName, r.PackageVersion.Version) + r.PackageVersion.ID = 0 + r.PackageVersion.PackageID = r.Package.ID + if _, err := sess.Insert(r.PackageVersion); err != nil { + return err + } + } + + processedVersion[key] = r.PackageVersion.ID + } + + return nil +} + +// getMavenPackageResultsToUpdate retrieves Maven package results that need updates based on the owner ID. +// It processes POM metadata, fixes package inconsistencies, and filters corrupted package versions. +func getMavenPackageResultsToUpdate(sess *xorm.Session, ownerID *int64) ([]*mavenPackageResult, error) { + ctx := context.Background() + var candidates []*mavenPackageResult + if err := sess. + Table("package_file"). + Select("package_file.*, package_version.*, package.*"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where("package_file.lower_name LIKE ?", "%.pom"). + And("package.type = ?", "maven"). + And("package.owner_id = ?", ownerID). + OrderBy("package_version.id DESC, package_file.id DESC"). + Find(&candidates); err != nil { + return nil, err + } + + var results []*mavenPackageResult + var corruptedVersionIDs []int64 + + // fetch actual metadata from blob as all packages needs to be fixed following the new string concatenation + for _, r := range candidates { + if err := processPomMetadata(ctx, r); err != nil { + // Skip corrupted versions; admin intervention may be needed to repair these files. + log.Warn("Failed to process package file [id: %d] ignoring package version[%d]: %v", r.PackageFile.ID, r.PackageVersion.ID, err) + + corruptedVersionIDs = append(corruptedVersionIDs, r.PackageVersion.ID) + + continue + } + + results = append(results, r) + log.Debug("Resolved id [%d] from [%s:%s] to [%s:%s] [Snapshot: %v]", r.Package.ID, r.Package.Name, r.PackageVersion.Version, r.PackageName, r.PackageVersion.Version, r.Snapshot) + } + + for _, corruptedVersionID := range corruptedVersionIDs { + for i := 0; i < len(results); { + if corruptedVersionID == results[i].PackageVersion.ID { + results = append(results[:i], results[i+1:]...) + } else { + i++ + } + } + } + + return results, nil +} + +// resolvePackageCollisions handles name collisions by keeping the first existing record and inserting new Package records for subsequent collisions. +// Returns a map from PackageName to its resolved Package.ID. +func resolvePackageCollisions(results []*mavenPackageResult, sess *xorm.Session) error { + // Group new names by lowerName + collisions := make(map[string][]string) + for _, r := range results { + names := collisions[r.Package.LowerName] + if !slices.Contains(names, r.PackageName) { + collisions[r.Package.LowerName] = append(names, r.PackageName) + } + } + + pkgIDByName := make(map[string]int64) + var err error + + for _, r := range results { + list := collisions[r.Package.LowerName] + + // update to the upcoming package name which is colon separated + r.Package.Name = r.PackageName + r.Package.LowerName = r.PackageName + + // exiting entry + if id, ok := pkgIDByName[r.PackageName]; ok { + r.Package.ID = id + // first package kept the current id + } else if list[0] == r.PackageName { + pkgIDByName[r.PackageName] = r.Package.ID + + if _, err = sess.ID(r.Package.ID).Cols("name", "lower_name").Update(r.Package); err != nil { + return err + } + // create a new entry + } else { + log.Info("Create new maven package for %s", r.Package.Name) + + r.Package.ID = 0 + if _, err = sess.Insert(r.Package); err != nil { + return err + } + + pkgIDByName[r.PackageName] = r.Package.ID + } + } + + return nil +} + +// processPomMetadata processes a Maven package file, parses its POM metadata, and updates PackageVersion information. +func processPomMetadata(ctx context.Context, mpr *mavenPackageResult) error { + s, _, _, err := getPackage(ctx, mpr.PackageFile) + if err != nil { + return fmt.Errorf("unable to get package stream: %v", err) + } + defer s.Close() + + actualPom, err := maven.ParsePackageMetaData(s) + if err != nil { + return fmt.Errorf("failed to parse POM metadata: %v", err) + } + + raw, err := json.Marshal(actualPom) + if err != nil { + return fmt.Errorf("failed to marshal metadata: %v", err) + } + + var currentPom *maven.Metadata + if err = json.Unmarshal([]byte(mpr.PackageVersion.MetadataJSON), ¤tPom); err != nil { + return fmt.Errorf("failed to unmarshal metadata: %v", err) + } + + // since the rest api can also be (ab)used to upload artifacts wrong, just ignore them + if isInvalidMatch(currentPom, actualPom) { + return fmt.Errorf("artifact mismatch: actual [%s] expected [%s]", actualPom.ArtifactID, currentPom.ArtifactID) + } + + // this will also fix packages that missed its groupID + // Ref: https://codeberg.org/forgejo/forgejo/pulls/6329 + mpr.PackageVersion.MetadataJSON = string(raw) + + // Since Maven packages are case-sensitive, avoid potential clashes and clean-ups + // by enforcing consistent case handling similar to RPM packages. + mpr.PackageName = fmt.Sprintf("%s:%s", actualPom.GroupID, actualPom.ArtifactID) + + mpr.GroupID = actualPom.GroupID + mpr.ArtifactID = actualPom.ArtifactID + + if strings.HasSuffix(mpr.PackageVersion.Version, "-SNAPSHOT") { + snap, err := extraSnapshotDetails(currentPom, actualPom, mpr) + if err != nil { + return err + } + mpr.Snapshot = snap + } else { + // only snapshots are affected but kept in case of not complete fixtures + expectedFileName := fmt.Sprintf("%s-%s.pom", actualPom.ArtifactID, mpr.PackageVersion.Version) + if mpr.PackageFile.Name != expectedFileName { + log.Warn("invalid package file name - this is a collision which needs to be resolved expected [%s], actual [%s]", expectedFileName, mpr.PackageFile.Name) + } + } + + return nil +} + +// extraSnapshotDetails extracts detailed snapshot information +// Returns a Snapshot object encapsulating the extracted details or an error if the filename is invalid or parsing fails. +func extraSnapshotDetails(currentPom, actualPom *maven.Metadata, mpr *mavenPackageResult) (*Snapshot, error) { + pattern := `^%s-` + + `(?P[\d\.]+)-` + + `(?P\d{8})\.` + + `(?P\d{6})-` + + `(?P\d+)\.pom$` + re := regexp.MustCompile(fmt.Sprintf(pattern, regexp.QuoteMeta(currentPom.ArtifactID))) + + if re.FindStringSubmatch(mpr.PackageFile.Name) == nil { + log.Warn("invalid package file name - this is a collision which needs to be resolved %s", mpr.PackageFile.Name) + } + + re = regexp.MustCompile(fmt.Sprintf(pattern, regexp.QuoteMeta(actualPom.ArtifactID))) + match := re.FindStringSubmatch(mpr.PackageFile.Name) + + if match == nil { + return nil, fmt.Errorf("invalid snapshot filename: %s", mpr.PackageFile.Name) + } + + baseIdx := re.SubexpIndex("baseVersion") + dateIdx := re.SubexpIndex("date") + timeIdx := re.SubexpIndex("time") + buildIdx := re.SubexpIndex("build") + + buildNum, _ := strconv.Atoi(match[buildIdx]) + + return &Snapshot{ + baseVersion: match[baseIdx], + date: match[dateIdx], + time: match[timeIdx], + build: buildNum, + }, nil +} + +// isInvalidMatch returns true if the stored metadata’s groupID:artifactID +// differs from actual values—accounting for an earlier bug that sometimes omitted the groupID. +func isInvalidMatch(current, actual *maven.Metadata) bool { + bare := fmt.Sprintf("-%s", actual.ArtifactID) + full := fmt.Sprintf("%s-%s", actual.GroupID, actual.ArtifactID) + currentID := fmt.Sprintf("%s-%s", current.GroupID, current.ArtifactID) + + return currentID != full && currentID != bare +} diff --git a/models/forgejo_migrations/v32_test.go b/models/forgejo_migrations/v32_test.go new file mode 100644 index 0000000000..cd33de2608 --- /dev/null +++ b/models/forgejo_migrations/v32_test.go @@ -0,0 +1,369 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "bytes" + "context" + "io" + "net/url" + "strings" + "testing" + + migration_tests "forgejo.org/models/migrations/test" + "forgejo.org/models/packages" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type readSeekCloser struct { + *bytes.Reader +} + +func (rsc readSeekCloser) Close() error { + // No resources to close, so we simply provide a no-op implementation. + return nil +} + +func StringToReadSeekCloser(s string) io.ReadSeekCloser { + return readSeekCloser{Reader: bytes.NewReader([]byte(s))} +} + +func Test_ChangeMavenArtifactConcatenation(t *testing.T) { + getPackage = func(ctx context.Context, pf *packages.PackageFile) (io.ReadSeekCloser, *url.URL, *packages.PackageFile, error) { + var data string + + switch pf.BlobID { + case 1: + data = `4.0.0pomcom.exampleparent-project1.0-SNAPSHOT` + case 3: + data = `4.0.0com.exampleparent-projectsub-module1.0-SNAPSHOT` + case 6: + data = `4.0.0pomcom.exampleparent-project7.0.0` + case 7: + data = `4.0.0com.exampleparent-projectsub-module7.0.0` + case 9: + data = `4.0.0pomcom.exampleparent-project7.0.0` + case 11: + data = `4.0.0pomfoo-bar1.0-SNAPSHOT` + case 13: + data = `4.0.0pomfoo-bar7.0.0` + case 14: + data = `4.0.0pomfoo-bar1.0-SNAPSHOT` + case 16: + data = `4.0.0pomfoo-bar7.0.0` + case 20: + data = `4.0.0pomcom.exampleparent-project8.0.0` + case 21: + data = `4.0.0com.exampleparent-projectsub-module8.0.0` + case 23: + data = `4.0.0pomcom.exampleparent-project8.0.0` + case 26: + data = `4.0.0pomfoo-bar8.0.0` + case 28: + data = `4.0.0pomfoo-bar8.0.0` + case 32: + data = `4.0.0pomcom.exampleparent-project9.0.0` + case 33: + data = `4.0.0com.exampleparent-projectsub-module9.0.0` + case 35: + data = `4.0.0pomcom.exampleparent-project9.0.0` + case 38: + data = `4.0.0pomfoo-bar9.0.0` + case 40: + data = `4.0.0pomfoo-bar9.0.0` + case 44: + data = `4.0.0pomcom.exampleparent-project10.0.0` + case 45: + data = `4.0.0com.exampleparent-projectsub-module10.0.0` + case 47: + data = `4.0.0pomcom.exampleparent-project10.0.0` + case 50: + data = `4.0.0pomfoo-bar10.0.0` + case 52: + data = `4.0.0pomfoo-bar10.0.0` + case 56: + data = `4.0.0pomcom.exampleparent-project11.0.0` + case 57: + data = `4.0.0com.exampleparent-projectsub-module11.0.0` + case 59: + data = `4.0.0pomcom.exampleparent-project11.0.0` + case 62: + data = `4.0.0pomfoo-bar11.0.0` + case 64: + data = `4.0.0pomfoo-bar11.0.0` + case 66: + data = `4.0.0com.brokenbr-parentbr-rest-webmvc` + case 68: + data = `4.0.0com.brokenbr-parentbr-openapi-base` + case 72: + data = `4.0.0pomde.loosetielt-parent-kotlincom.brokenbr-root1.2.4` + case 74: + data = `4.0.0com.brokenbr-parentbr-repo-jooq` + case 76: + data = `4.0.0com.brokenbr-parentbr-repo-in-memory` + case 78: + data = `4.0.0pomcom.brokenbr-rootbr-parent` + case 79: + data = `4.0.0pomgroupbar-art11.0.0` + case 80: + data = `4.0.0pomgroup-barart11.0.0` + case 55: + data = `com.examplesub-module1.0-SNAPSHOT` + case 53: + data = `com.exampleparent-project1.0-SNAPSHOT` + case 63: + data = `foo-bar1.0-SNAPSHOT` + default: + t.Fatalf("Unknown package file type: %d", pf.BlobID) + } + + return StringToReadSeekCloser(data), nil, nil, nil + } + + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(packages.Package), new(packages.PackageFile), new(packages.PackageVersion), new(packages.PackageBlob)) + defer deferable() + if x == nil || t.Failed() { + return + } + + cnt, err := x.Table("package").Count() + require.NoError(t, err) + assert.EqualValues(t, 8, cnt) + + cnt, err = x.Table("package_file").Count() + require.NoError(t, err) + assert.EqualValues(t, 87, cnt) + + cnt, err = x.Table("package_version").Count() + require.NoError(t, err) + assert.EqualValues(t, 31, cnt) + + cnt, err = x.Table("package_blob").Count() + require.NoError(t, err) + assert.EqualValues(t, 80, cnt) + + require.NoError(t, ChangeMavenArtifactConcatenation(x)) + + var pks []*packages.Package + require.NoError(t, x.OrderBy("id").Find(&pks)) + validatePackages(t, pks) + + var pvs []*packages.PackageVersion + require.NoError(t, x.OrderBy("id").Find(&pvs)) + validatePackageVersions(t, pvs) + + var pfs []*packages.PackageFile + require.NoError(t, x.OrderBy("id").Find(&pfs)) + validatePackageFiles(t, pfs) +} + +func validatePackages(t *testing.T, pbs []*packages.Package) { + assertPackage := func(id, ownerID, repoID int64, name string) { + pb := pbs[id-1] + + require.Equal(t, id, pb.ID) + require.Equal(t, ownerID, pb.OwnerID) + require.Equal(t, repoID, pb.RepoID) + require.Equal(t, name, pb.Name) + require.Equal(t, name, pb.LowerName) + require.Equal(t, packages.TypeMaven, pb.Type) + } + + require.Len(t, pbs, 10) + + assertPackage(1, 2, 0, "com.example:parent-project") + assertPackage(2, 2, 0, "com.example:sub-module") + assertPackage(3, 1, 0, "com.example:parent-project") + assertPackage(4, 1, 0, "com.example:sub-module") + assertPackage(5, 1, 0, "foo:-bar") + assertPackage(6, 8, 54, "com.broken:br-rest-webmvc") + // broken poms completely ignored as it is impossible to look up the correct metadata + assertPackage(7, 8, 54, "com.broken-br-openapi-base") + assertPackage(8, 1, 0, "group-bar:art") + + // new created entries + assertPackage(9, 1, 0, "group:bar-art") + assertPackage(10, 1, 0, "foo-:bar") +} + +func validatePackageVersions(t *testing.T, pvs []*packages.PackageVersion) { + require.Len(t, pvs, 38) + + assertPackageVersion := func(id, packageId, creatorId, createdUnix int64, version, metadata string) { + pv := pvs[id-1] + + require.Equal(t, id, pv.ID) + require.Equal(t, packageId, pv.PackageID) + + require.Equal(t, creatorId, pv.CreatorID) + require.Equal(t, version, pv.Version) + require.Equal(t, strings.ToLower(version), pv.LowerVersion) + require.JSONEq(t, metadata, pv.MetadataJSON) + } + + assertPackageVersion(1, 1, 1, 1746256357, "1.0-SNAPSHOT", `{"artifact_id":"parent-project","group_id":"com.example"}`) + assertPackageVersion(2, 2, 1, 1746256358, "1.0-SNAPSHOT", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(3, 1, 1, 1746256360, "7.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + // added groupId + assertPackageVersion(4, 2, 1, 1746256361, "7.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(5, 3, 1, 1746256364, "7.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + // added groupId + assertPackageVersion(6, 4, 1, 1746256365, "7.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(7, 5, 1, 1746256367, "1.0-SNAPSHOT", `{"artifact_id":"-bar","group_id":"foo"}`) + assertPackageVersion(8, 5, 1, 1746256370, "7.0.0", `{"artifact_id":"-bar","group_id":"foo"}`) + assertPackageVersion(9, 1, 1, 1746256389, "8.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + // added groupId + assertPackageVersion(10, 2, 1, 1746256390, "8.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(11, 3, 1, 1746256393, "8.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + // added groupId + assertPackageVersion(12, 4, 1, 1746256394, "8.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(13, 5, 1, 1746256399, "8.0.0", `{"artifact_id":"-bar","group_id":"foo"}`) + assertPackageVersion(14, 1, 1, 1746256419, "9.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + // added groupId + assertPackageVersion(15, 2, 1, 1746256420, "9.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(16, 3, 1, 1746256423, "9.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + // added groupId + assertPackageVersion(17, 4, 1, 1746256424, "9.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(18, 5, 1, 1746256429, "9.0.0", `{"artifact_id":"-bar","group_id":"foo"}`) + assertPackageVersion(19, 1, 1, 1746256449, "10.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + assertPackageVersion(20, 2, 1, 1746256450, "10.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(21, 3, 1, 1746256452, "10.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + assertPackageVersion(22, 4, 1, 1746256453, "10.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(23, 5, 1, 1746256459, "10.0.0", `{"artifact_id":"-bar","group_id":"foo"}`) + assertPackageVersion(24, 1, 1, 1746256478, "11.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + assertPackageVersion(25, 2, 1, 1746256479, "11.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(26, 3, 1, 1746256482, "11.0.0", `{"artifact_id":"parent-project","group_id":"com.example"}`) + assertPackageVersion(27, 4, 1, 1746256483, "11.0.0", `{"artifact_id":"sub-module","group_id":"com.example"}`) + assertPackageVersion(28, 5, 1, 1746256488, "11.0.0", `{"artifact_id":"-bar","group_id":"foo"}`) + // should be untouched at all. fixtures doesn't contain names + assertPackageVersion(29, 7, 6, 1746256488, "1.2.4", `{"group_id":"com.broken","artifact_id":"br-root","name":"Foo"}`) + // added group name + assertPackageVersion(30, 6, 6, 1746256488, "1.2.4", `{"artifact_id":"br-rest-webmvc", "group_id":"com.broken"}`) + assertPackageVersion(31, 8, 1, 1746256488, "11.0.0", `{"artifact_id":"art","group_id":"group-bar"}`) + + // new entries + assertPackageVersion(32, 9, 1, 1746256488, "11.0.0", `{"artifact_id":"bar-art","group_id":"group"}`) + assertPackageVersion(33, 10, 1, 1746256488, "11.0.0", `{"artifact_id":"bar","group_id":"foo-"}`) + assertPackageVersion(34, 10, 1, 1746256488, "10.0.0", `{"artifact_id":"bar","group_id":"foo-"}`) + assertPackageVersion(35, 10, 1, 1746256488, "9.0.0", `{"artifact_id":"bar","group_id":"foo-"}`) + assertPackageVersion(36, 10, 1, 1746256488, "8.0.0", `{"artifact_id":"bar","group_id":"foo-"}`) + assertPackageVersion(37, 10, 1, 1746256488, "7.0.0", `{"artifact_id":"bar","group_id":"foo-"}`) + assertPackageVersion(38, 10, 1, 1746256488, "1.0-SNAPSHOT", `{"artifact_id":"bar","group_id":"foo-"}`) +} + +func validatePackageFiles(t *testing.T, pfs []*packages.PackageFile) { + assertPackageVersion := func(pos, id, versionId, blobId, createdUnix int64, name string, isLead bool) { + pf := pfs[pos] + + require.Equal(t, id, pf.ID) + require.Equal(t, versionId, pf.VersionID) + require.Equal(t, blobId, pf.BlobID) + require.Equal(t, name, pf.Name) + require.Equal(t, strings.ToLower(name), pf.LowerName) + require.Empty(t, pf.CompositeKey) + require.Equal(t, isLead, pf.IsLead) + require.EqualValues(t, createdUnix, pf.CreatedUnix) + + require.Empty(t, pf.CompositeKey) + } + assertPackageVersion(0, 1, 1, 1, 1746256357, "parent-project-1.0-20250503.071237-1.pom", true) + assertPackageVersion(1, 3, 2, 3, 1746256358, "sub-module-1.0-20250503.071237-1.pom", true) + assertPackageVersion(2, 4, 2, 4, 1746256358, "sub-module-1.0-20250503.071237-1.jar", false) + assertPackageVersion(3, 6, 3, 6, 1746256360, "parent-project-7.0.0.pom", true) + assertPackageVersion(4, 7, 4, 7, 1746256361, "sub-module-7.0.0.pom", true) + assertPackageVersion(5, 8, 4, 8, 1746256361, "sub-module-7.0.0.jar", false) + assertPackageVersion(6, 9, 5, 9, 1746256364, "parent-project-7.0.0.pom", true) + assertPackageVersion(7, 10, 6, 7, 1746256365, "sub-module-7.0.0.pom", true) + assertPackageVersion(8, 11, 6, 10, 1746256365, "sub-module-7.0.0.jar", false) + // new versionId 7 -> 38 + assertPackageVersion(9, 12, 38, 11, 1746256367, "bar-1.0-20250503.071248-1.pom", true) + // new versionId 37 + assertPackageVersion(10, 14, 37, 13, 1746256370, "bar-7.0.0.pom", true) + assertPackageVersion(11, 15, 7, 14, 1746256373, "-bar-1.0-20250503.071253-2.pom", true) + assertPackageVersion(12, 17, 8, 16, 1746256375, "-bar-7.0.0.pom", true) + assertPackageVersion(13, 18, 1, 1, 1746256385, "parent-project-1.0-20250503.071306-2.pom", true) + assertPackageVersion(14, 20, 2, 3, 1746256386, "sub-module-1.0-20250503.071306-2.pom", true) + assertPackageVersion(15, 21, 2, 18, 1746256386, "sub-module-1.0-20250503.071306-2.jar", false) + assertPackageVersion(16, 23, 9, 20, 1746256389, "parent-project-8.0.0.pom", true) + assertPackageVersion(17, 24, 10, 21, 1746256390, "sub-module-8.0.0.pom", true) + assertPackageVersion(18, 25, 10, 22, 1746256390, "sub-module-8.0.0.jar", false) + assertPackageVersion(19, 26, 11, 23, 1746256393, "parent-project-8.0.0.pom", true) + assertPackageVersion(20, 27, 12, 21, 1746256394, "sub-module-8.0.0.pom", true) + assertPackageVersion(21, 28, 12, 24, 1746256394, "sub-module-8.0.0.jar", false) + // new versionId 7 -> 38 + assertPackageVersion(22, 29, 38, 11, 1746256397, "bar-1.0-20250503.071317-3.pom", true) + assertPackageVersion(23, 31, 36, 26, 1746256399, "bar-8.0.0.pom", true) + assertPackageVersion(24, 32, 7, 14, 1746256402, "-bar-1.0-20250503.071323-4.pom", true) + assertPackageVersion(25, 34, 13, 28, 1746256405, "-bar-8.0.0.pom", true) + assertPackageVersion(26, 35, 1, 1, 1746256415, "parent-project-1.0-20250503.071335-3.pom", true) + assertPackageVersion(27, 37, 2, 3, 1746256416, "sub-module-1.0-20250503.071335-3.pom", true) + assertPackageVersion(28, 38, 2, 30, 1746256416, "sub-module-1.0-20250503.071335-3.jar", false) + assertPackageVersion(29, 40, 14, 32, 1746256419, "parent-project-9.0.0.pom", true) + assertPackageVersion(30, 41, 15, 33, 1746256420, "sub-module-9.0.0.pom", true) + assertPackageVersion(31, 42, 15, 34, 1746256420, "sub-module-9.0.0.jar", false) + assertPackageVersion(32, 43, 16, 35, 1746256423, "parent-project-9.0.0.pom", true) + assertPackageVersion(33, 44, 17, 33, 1746256424, "sub-module-9.0.0.pom", true) + assertPackageVersion(34, 45, 17, 36, 1746256424, "sub-module-9.0.0.jar", false) + // new versionId 7 -> 38 + assertPackageVersion(35, 46, 38, 11, 1746256427, "bar-1.0-20250503.071347-5.pom", true) + // new versionId 18 -> 35 + assertPackageVersion(36, 48, 35, 38, 1746256429, "bar-9.0.0.pom", true) + assertPackageVersion(37, 49, 7, 14, 1746256432, "-bar-1.0-20250503.071353-6.pom", true) + assertPackageVersion(38, 51, 18, 40, 1746256435, "-bar-9.0.0.pom", true) + assertPackageVersion(39, 52, 1, 1, 1746256445, "parent-project-1.0-20250503.071405-4.pom", true) + assertPackageVersion(40, 54, 2, 3, 1746256446, "sub-module-1.0-20250503.071405-4.pom", true) + assertPackageVersion(41, 55, 2, 42, 1746256446, "sub-module-1.0-20250503.071405-4.jar", false) + assertPackageVersion(42, 57, 19, 44, 1746256449, "parent-project-10.0.0.pom", true) + assertPackageVersion(43, 58, 20, 45, 1746256450, "sub-module-10.0.0.pom", true) + assertPackageVersion(44, 59, 20, 46, 1746256450, "sub-module-10.0.0.jar", false) + assertPackageVersion(45, 60, 21, 47, 1746256452, "parent-project-10.0.0.pom", true) + assertPackageVersion(46, 61, 22, 45, 1746256453, "sub-module-10.0.0.pom", true) + assertPackageVersion(47, 62, 22, 48, 1746256453, "sub-module-10.0.0.jar", false) + // new versionId 7 -> 38 + assertPackageVersion(48, 63, 38, 11, 1746256456, "bar-1.0-20250503.071416-7.pom", true) + // new versionId 34 + assertPackageVersion(49, 65, 34, 50, 1746256459, "bar-10.0.0.pom", true) + assertPackageVersion(50, 66, 7, 14, 1746256461, "-bar-1.0-20250503.071422-8.pom", true) + assertPackageVersion(51, 68, 23, 52, 1746256464, "-bar-10.0.0.pom", true) + assertPackageVersion(52, 69, 1, 1, 1746256474, "parent-project-1.0-20250503.071435-5.pom", true) + assertPackageVersion(53, 70, 1, 53, 1746256474, "maven-metadata.xml", false) + assertPackageVersion(54, 71, 2, 3, 1746256475, "sub-module-1.0-20250503.071435-5.pom", true) + assertPackageVersion(55, 72, 2, 54, 1746256475, "sub-module-1.0-20250503.071435-5.jar", false) + assertPackageVersion(56, 73, 2, 55, 1746256476, "maven-metadata.xml", false) + assertPackageVersion(57, 74, 24, 56, 1746256478, "parent-project-11.0.0.pom", true) + assertPackageVersion(58, 75, 25, 57, 1746256479, "sub-module-11.0.0.pom", true) + assertPackageVersion(59, 76, 25, 58, 1746256479, "sub-module-11.0.0.jar", false) + assertPackageVersion(60, 77, 26, 59, 1746256482, "parent-project-11.0.0.pom", true) + assertPackageVersion(61, 78, 27, 57, 1746256483, "sub-module-11.0.0.pom", true) + assertPackageVersion(62, 79, 27, 60, 1746256483, "sub-module-11.0.0.jar", false) + // new versionId 7 -> 38 + assertPackageVersion(63, 80, 38, 11, 1746256486, "bar-1.0-20250503.071446-9.pom", true) + // new versionId 33 + assertPackageVersion(64, 82, 33, 62, 1746256488, "bar-11.0.0.pom", true) + assertPackageVersion(65, 83, 7, 14, 1746256491, "-bar-1.0-20250503.071451-10.pom", true) + assertPackageVersion(66, 84, 7, 63, 1746256491, "maven-metadata.xml", false) + assertPackageVersion(67, 85, 28, 64, 1746256494, "-bar-11.0.0.pom", true) + assertPackageVersion(68, 86, 29, 75, 174625649444986, "br-repo-jooq-1.2.4-sources.jar", false) + assertPackageVersion(69, 87, 29, 65, 174625649446161, "br-rest-webmvc-1.2.4.jar", false) + assertPackageVersion(70, 88, 29, 68, 174625649444734, "br-openapi-base-1.2.4.pom", true) + assertPackageVersion(71, 89, 29, 69, 174625649444746, "br-openapi-base-1.2.4.jar", false) + assertPackageVersion(72, 90, 29, 70, 174625649444775, "br-openapi-base-1.2.4-sources.jar", false) + assertPackageVersion(73, 91, 29, 78, 174625649444852, "br-parent-1.2.4.pom", true) + assertPackageVersion(74, 92, 29, 76, 174625649444900, "br-repo-in-memory-1.2.4.pom", true) + assertPackageVersion(75, 93, 29, 73, 174625649444911, "br-repo-in-memory-1.2.4.jar", false) + assertPackageVersion(76, 94, 29, 77, 174625649444922, "br-repo-in-memory-1.2.4-sources.jar", false) + assertPackageVersion(77, 95, 29, 74, 174625649444953, "br-repo-jooq-1.2.4.pom", true) + assertPackageVersion(78, 96, 29, 67, 174625649444969, "br-repo-jooq-1.2.4.jar", false) + assertPackageVersion(79, 97, 29, 71, 174625649446161, "br-rest-webmvc-1.2.4-sources.jar", false) + assertPackageVersion(80, 98, 29, 66, 174625649446195, "br-rest-webmvc-1.2.4.pom", true) + assertPackageVersion(81, 99, 29, 72, 174625649446217, "br-root-1.2.4.pom", true) + assertPackageVersion(82, 100, 30, 66, 174625649446311, "br-rest-webmvc-1.2.4.pom", true) + assertPackageVersion(83, 101, 30, 65, 174625649446312, "br-rest-webmvc-1.2.4.jar", false) + assertPackageVersion(84, 102, 30, 71, 174625649446312, "br-rest-webmvc-1.2.4-sources.jar", false) + // new versionId 31 -> 32 + assertPackageVersion(85, 103, 32, 79, 1746280832, "bar-art-11.0.0.pom", true) + assertPackageVersion(86, 104, 31, 80, 1746280843, "art-11.0.0.pom", true) +} diff --git a/models/forgejo_migrations/v33.go b/models/forgejo_migrations/v33.go new file mode 100644 index 0000000000..272035fc23 --- /dev/null +++ b/models/forgejo_migrations/v33.go @@ -0,0 +1,126 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgejo_migrations //nolint:revive + +import ( + "fmt" + + "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func dropOldFederationHostIndexes(x *xorm.Engine) { + // drop unique index on HostFqdn + type FederationHost struct { + ID int64 `xorm:"pk autoincr"` + HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"` + } + + err := x.DropIndexes(FederationHost{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } +} + +func addFederatedUserActivityTables(x *xorm.Engine) { + type FederatedUserActivity struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` + ActorID int64 + ActorURI string + NoteContent string `xorm:"TEXT"` + NoteURL string `xorm:"VARCHAR(255)"` + OriginalNote string `xorm:"TEXT"` + Created timeutil.TimeStamp `xorm:"created"` + } + + // add unique index on HostFqdn+HostPort + type FederationHost struct { + ID int64 `xorm:"pk autoincr"` + HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` + HostPort uint16 `xorm:"UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` + } + + type FederatedUserFollower struct { + ID int64 `xorm:"pk autoincr"` + + FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` + FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` + } + + // Add InboxPath to FederatedUser & add index fo UserID + type FederatedUser struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` + InboxPath string + } + + var err error + + err = x.Sync(&FederationHost{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + err = x.Sync(&FederatedUserActivity{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + err = x.Sync(&FederatedUserFollower{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + err = x.Sync(&FederatedUser{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + // Migrate + sessMigration := x.NewSession() + defer sessMigration.Close() + if err := sessMigration.Begin(); err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + federatedUsers := make([]*FederatedUser, 0) + err = sessMigration.OrderBy("id").Find(&federatedUsers) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + for _, federatedUser := range federatedUsers { + if federatedUser.InboxPath != "" { + log.Info("migration[33]: This user was already migrated: %v", federatedUser) + } else { + // Migrate User.InboxPath + sql := "UPDATE `federated_user` SET `inbox_path` = ? WHERE `id` = ?" + if _, err := sessMigration.Exec(sql, fmt.Sprintf("/api/v1/activitypub/user-id/%v/inbox", federatedUser.UserID), federatedUser.ID); err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + } + } + + err = sessMigration.Commit() + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } +} + +func FederatedUserActivityMigration(x *xorm.Engine) error { + dropOldFederationHostIndexes(x) + addFederatedUserActivityTables(x) + return nil +} diff --git a/models/forgejo_migrations/v33_test.go b/models/forgejo_migrations/v33_test.go new file mode 100644 index 0000000000..664c704bbc --- /dev/null +++ b/models/forgejo_migrations/v33_test.go @@ -0,0 +1,46 @@ +// Copyright 2025 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "testing" + "time" + + migration_tests "forgejo.org/models/migrations/test" + "forgejo.org/modules/log" + ft "forgejo.org/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_FederatedUserActivityMigration(t *testing.T) { + lc, cl := ft.NewLogChecker(log.DEFAULT, log.WARN) + lc.Filter("migration[33]") + defer cl() + + // intentionally conflicting definition + type FederatedUser struct { + ID int64 `xorm:"pk autoincr"` + UserID string + } + + // Prepare TestEnv + x, deferable := migration_tests.PrepareTestEnv(t, 0, + new(FederatedUser), + ) + sessTest := x.NewSession() + sessTest.Insert(FederatedUser{UserID: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"}) + sessTest.Commit() + defer deferable() + if x == nil || t.Failed() { + return + } + + require.NoError(t, FederatedUserActivityMigration(x)) + logFiltered, _ := lc.Check(5 * time.Second) + assert.NotEmpty(t, logFiltered) +} diff --git a/models/forgejo_migrations/v34.go b/models/forgejo_migrations/v34.go new file mode 100644 index 0000000000..9e958b934f --- /dev/null +++ b/models/forgejo_migrations/v34.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import "xorm.io/xorm" + +func AddNotifyEmailToActionRun(x *xorm.Engine) error { + type ActionRun struct { + ID int64 + NotifyEmail bool + } + return x.Sync(new(ActionRun)) +} diff --git a/models/forgejo_migrations/v35.go b/models/forgejo_migrations/v35.go new file mode 100644 index 0000000000..0fb3b43e2c --- /dev/null +++ b/models/forgejo_migrations/v35.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func AddIndexToActionRunStopped(x *xorm.Engine) error { + type ActionRun struct { + ID int64 + Stopped timeutil.TimeStamp `xorm:"index"` + } + + return x.Sync(&ActionRun{}) +} diff --git a/models/git/commit_status.go b/models/git/commit_status.go index a679703ffd..60a0aa5a4f 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -179,25 +179,6 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string { return lang.TrString("repo.commitstatus." + status.State.String()) } -// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions -func (status *CommitStatus) HideActionsURL(ctx context.Context) { - if status.RepoID == 0 { - return - } - - if status.Repo == nil { - if err := status.loadRepository(ctx); err != nil { - log.Error("loadRepository: %v", err) - return - } - } - - prefix := fmt.Sprintf("%s/actions", status.Repo.Link()) - if strings.HasPrefix(status.TargetURL, prefix) { - status.TargetURL = "" - } -} - // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus { if len(statuses) == 0 { @@ -453,11 +434,19 @@ type SignCommitWithStatuses struct { *asymkey_model.SignCommit } -// ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state -func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses { - newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits)) +// ParseCommitsWithStatus converts git commits into SignCommitWithStatuses (checks signature and calculates its worst status state) +func ParseCommitsWithStatus(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses { + commitsWithSignature := asymkey_model.ParseCommitsWithSignature( + ctx, + user_model.ValidateCommitsWithEmails(ctx, commits), + repo.GetTrustModel(), + func(user *user_model.User) (bool, error) { + return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID) + }, + ) - for _, c := range oldCommits { + commitsWithStatus := make([]*SignCommitWithStatuses, 0, len(commitsWithSignature)) + for _, c := range commitsWithSignature { commit := &SignCommitWithStatuses{ SignCommit: c, } @@ -469,43 +458,12 @@ func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.Sig commit.Status = CalcCommitStatus(statuses) } - newCommits = append(newCommits, commit) + commitsWithStatus = append(commitsWithStatus, commit) } - return newCommits + return commitsWithStatus } // hashCommitStatusContext hash context func hashCommitStatusContext(context string) string { return fmt.Sprintf("%x", sha1.Sum([]byte(context))) } - -// ConvertFromGitCommit converts git commits into SignCommitWithStatuses -func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses { - return ParseCommitsWithStatus(ctx, - asymkey_model.ParseCommitsWithSignature( - ctx, - user_model.ValidateCommitsWithEmails(ctx, commits), - repo.GetTrustModel(), - func(user *user_model.User) (bool, error) { - return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID) - }, - ), - repo, - ) -} - -// CommitStatusesHideActionsURL hide Gitea Actions urls -func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) { - idToRepos := make(map[int64]*repo_model.Repository) - for _, status := range statuses { - if status == nil { - continue - } - - if status.Repo == nil { - status.Repo = idToRepos[status.RepoID] - } - status.HideActionsURL(ctx) - idToRepos[status.RepoID] = status.Repo - } -} diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index c062bbbbb9..ce6c0d4673 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -4,11 +4,9 @@ package git_test import ( - "fmt" "testing" "time" - actions_model "forgejo.org/models/actions" "forgejo.org/models/db" git_model "forgejo.org/models/git" repo_model "forgejo.org/models/repo" @@ -246,26 +244,3 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { assert.Equal(t, "compliance/lint-backend", contexts[0]) } } - -func TestCommitStatusesHideActionsURL(t *testing.T) { - require.NoError(t, unittest.PrepareTestDatabase()) - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791, RepoID: repo.ID}) - require.NoError(t, run.LoadAttributes(db.DefaultContext)) - - statuses := []*git_model.CommitStatus{ - { - RepoID: repo.ID, - TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.Index), - }, - { - RepoID: repo.ID, - TargetURL: "https://mycicd.org/1", - }, - } - - git_model.CommitStatusesHideActionsURL(db.DefaultContext, statuses) - assert.Empty(t, statuses[0].TargetURL) - assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL) -} diff --git a/models/issues/comment.go b/models/issues/comment.go index dc38f83f79..a81221caf4 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -652,8 +652,11 @@ func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error { if c.Issue.Repo.Owner.IsOrganization() { c.AssigneeTeam, err = organization.GetTeamByID(ctx, c.AssigneeTeamID) - if err != nil && !organization.IsErrTeamNotExist(err) { - return err + if err != nil { + if !organization.IsErrTeamNotExist(err) { + return err + } + c.AssigneeTeam = organization.NewGhostTeam() } } } @@ -799,7 +802,7 @@ func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { } defer closer.Close() - c.Commits = git_model.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) + c.Commits = git_model.ParseCommitsWithStatus(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) c.CommitsNum = int64(len(c.Commits)) } diff --git a/models/issues/issue.go b/models/issues/issue.go index d6a0029638..5edebb4105 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -6,6 +6,7 @@ package issues import ( "context" + "errors" "fmt" "html/template" "regexp" @@ -804,7 +805,7 @@ func (issue *Issue) MovePin(ctx context.Context, newPosition int) error { } if newPosition < 1 { - return fmt.Errorf("The Position can't be lower than 1") + return errors.New("The Position can't be lower than 1") } dbctx, committer, err := db.TxContext(ctx) diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index bf4b89ee0b..529f0c15d4 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -48,7 +48,9 @@ type IssuesOptions struct { //nolint UpdatedBeforeUnix int64 // prioritize issues from this repo PriorityRepoID int64 - IsArchived optional.Option[bool] + // if this issue index (not ID) exists and matches the filters, *and* priorityrepo sort is used, show it first + PriorityIssueIndex int64 + IsArchived optional.Option[bool] // If combined with AllPublic, then private as well as public issues // that matches the criteria will be returned, if AllPublic is false @@ -60,12 +62,14 @@ type IssuesOptions struct { //nolint // applySorts sort an issues-related session based on the provided // sortType string -func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { +func applySorts(sess *xorm.Session, sortType string, priorityRepoID, priorityIssueIndex int64) { switch sortType { case "oldest": sess.Asc("issue.created_unix").Asc("issue.id") case "recentupdate": sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") + case "recentclose": + sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id") case "leastupdate": sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") case "mostcomment": @@ -95,8 +99,11 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { case "priorityrepo": sess.OrderBy("CASE "+ "WHEN issue.repo_id = ? THEN 1 "+ - "ELSE 2 END ASC", priorityRepoID). - Desc("issue.created_unix"). + "ELSE 2 END ASC", priorityRepoID) + if priorityIssueIndex != 0 { + sess.OrderBy("issue.index = ? DESC", priorityIssueIndex) + } + sess.Desc("issue.created_unix"). Desc("issue.id") case "project-column-sorting": sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") @@ -468,7 +475,7 @@ func Issues(ctx context.Context, opts *IssuesOptions) (IssueList, error) { Join("INNER", "repository", "`issue`.repo_id = `repository`.id") applyLimit(sess, opts) applyConditions(sess, opts) - applySorts(sess, opts.SortType, opts.PriorityRepoID) + applySorts(sess, opts.SortType, opts.PriorityRepoID, opts.PriorityIssueIndex) issues := IssueList{} if err := sess.Find(&issues); err != nil { @@ -492,7 +499,7 @@ func IssueIDs(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Co } applyLimit(sess, opts) - applySorts(sess, opts.SortType, opts.PriorityRepoID) + applySorts(sess, opts.SortType, opts.PriorityRepoID, opts.PriorityIssueIndex) var res []int64 total, err := sess.Select("`issue`.id").Table(&Issue{}).FindAndCount(&res) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index d15533390e..aca9069205 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -6,6 +6,7 @@ package issues import ( "context" + "errors" "fmt" "strings" @@ -338,10 +339,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue } if opts.Issue.Index <= 0 { - return fmt.Errorf("no issue index provided") + return errors.New("no issue index provided") } if opts.Issue.ID > 0 { - return fmt.Errorf("issue exist") + return errors.New("issue exist") } opts.Issue.Created = timeutil.TimeStampNanoNow() diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 52433e735d..67a23246cf 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -67,6 +67,13 @@ type Milestone struct { TotalTrackedTime int64 `xorm:"-"` } +// Ghost milestone is a milestone which has been deleted +const GhostMilestoneID = -1 + +func (m *Milestone) IsGhost() bool { + return m.ID == GhostMilestoneID +} + func init() { db.RegisterModel(new(Milestone)) } diff --git a/models/issues/pull.go b/models/issues/pull.go index c46961447c..188ef00814 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -5,7 +5,9 @@ package issues import ( + "bufio" "context" + "errors" "fmt" "io" "regexp" @@ -795,7 +797,7 @@ func (pr *PullRequest) GetWorkInProgressPrefix(ctx context.Context) string { // UpdateCommitDivergence update Divergence of a pull request func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error { if pr.ID == 0 { - return fmt.Errorf("pull ID is 0") + return errors.New("pull ID is 0") } pr.CommitsAhead = ahead pr.CommitsBehind = behind @@ -922,31 +924,30 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr * return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } -// GetCodeOwnersFromContent returns the code owners configuration -// Return empty slice if files missing +// GetCodeOwnersFromReader returns the code owners configuration // Return warning messages on parsing errors // We're trying to do the best we can when parsing a file. // Invalid lines are skipped. Non-existent users and teams too. -func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) { - if len(data) == 0 { - return nil, nil - } +func GetCodeOwnersFromReader(ctx context.Context, rc io.ReadCloser, truncated bool) ([]*CodeOwnerRule, []string) { + defer rc.Close() + scanner := bufio.NewScanner(rc) - rules := make([]*CodeOwnerRule, 0) - lines := strings.Split(data, "\n") - warnings := make([]string, 0) + var rules []*CodeOwnerRule + var warnings []string + line := 0 + for scanner.Scan() { + line++ - for i, line := range lines { - tokens := TokenizeCodeOwnersLine(line) + tokens := TokenizeCodeOwnersLine(scanner.Text()) if len(tokens) == 0 { continue } else if len(tokens) < 2 { - warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1)) + warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", line)) continue } rule, wr := ParseCodeOwnersLine(ctx, tokens) for _, w := range wr { - warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) + warnings = append(warnings, fmt.Sprintf("Line: %d: %s", line, w)) } if rule == nil { continue @@ -954,6 +955,12 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul rules = append(rules, rule) } + if err := scanner.Err(); err != nil { + warnings = append(warnings, err.Error()) + } + if truncated { + warnings = append(warnings, fmt.Sprintf("File too big: truncated while on line %d", line)) + } return rules, warnings } diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index a448673454..ddb813cf44 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -149,10 +149,11 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio } findSession := listPullRequestStatement(ctx, baseRepoID, opts) - applySorts(findSession, opts.SortType, 0) + applySorts(findSession, opts.SortType, 0, 0) findSession = db.SetSessionPagination(findSession, opts) prs := make([]*PullRequest, 0, opts.PageSize) - return prs, maxResults, findSession.Find(&prs) + found := findSession.Find(&prs) + return prs, maxResults, found } // PullRequestList defines a list of pull requests diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 5a8e8d8aaf..3682f6fd25 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -79,6 +79,47 @@ func TestPullRequestsNewest(t *testing.T) { } } +func TestPullRequests_Closed_RecentSortType(t *testing.T) { + // Issue ID | Closed At. | Updated At + // 2 | 1707270001 | 1707270001 + // 3 | 1707271000 | 1707279999 + // 11 | 1707279999 | 1707275555 + tests := []struct { + sortType string + expectedIssueIDOrder []int64 + }{ + {"recentupdate", []int64{3, 11, 2}}, + {"recentclose", []int64{11, 3, 2}}, + } + + require.NoError(t, unittest.PrepareTestDatabase()) + _, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2") + require.NoError(t, err) + _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3") + require.NoError(t, err) + _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11") + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.sortType, func(t *testing.T) { + prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + }, + State: "closed", + SortType: test.sortType, + }) + require.NoError(t, err) + + if assert.Len(t, prs, len(test.expectedIssueIDOrder)) { + for i := range test.expectedIssueIDOrder { + assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID) + } + } + }) + } +} + func TestLoadRequestedReviewers(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/issues/review.go b/models/issues/review.go index db5cd65e2e..584704d3e8 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -5,6 +5,7 @@ package issues import ( "context" + "errors" "fmt" "slices" "strings" @@ -349,7 +350,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error review.Type = ReviewTypeRequest review.ReviewerTeamID = opts.ReviewerTeam.ID } else { - return nil, fmt.Errorf("provide either reviewer or reviewer team") + return nil, errors.New("provide either reviewer or reviewer team") } if _, err := sess.Insert(review); err != nil { @@ -908,7 +909,7 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us // the PR writer , offfcial reviewer and poster can do it func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) { if doer == nil || issue == nil { - return false, fmt.Errorf("issue or doer is nil") + return false, errors.New("issue or doer is nil") } if doer.ID != issue.PosterID { @@ -945,11 +946,11 @@ func DeleteReview(ctx context.Context, r *Review) error { defer committer.Close() if r.ID == 0 { - return fmt.Errorf("review is not allowed to be 0") + return errors.New("review is not allowed to be 0") } if r.Type == ReviewTypeRequest { - return fmt.Errorf("review request can not be deleted using this method") + return errors.New("review request can not be deleted using this method") } opts := FindCommentsOptions{ diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index 897ad016ab..7f7edda53b 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -74,7 +74,7 @@ func RecreateTable(sess *xorm.Session, bean any) error { } newTableColumns := table.Columns() if len(newTableColumns) == 0 { - return fmt.Errorf("no columns in new table") + return errors.New("no columns in new table") } hasID := false for _, column := range newTableColumns { diff --git a/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package.yml b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package.yml new file mode 100644 index 0000000000..df6fd6348f --- /dev/null +++ b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package.yml @@ -0,0 +1,66 @@ +- id: 1 + owner_id: 2 + repo_id: 0 + type: maven + name: com.example-parent-project + lower_name: com.example-parent-project + semver_compatible: 0 + is_internal: 0 +- id: 2 + owner_id: 2 + repo_id: 0 + type: maven + name: com.example-sub-module + lower_name: com.example-sub-module + semver_compatible: 0 + is_internal: 0 +- id: 3 + owner_id: 1 + repo_id: 0 + type: maven + name: com.example-parent-project + lower_name: com.example-parent-project + semver_compatible: 0 + is_internal: 0 +- id: 4 + owner_id: 1 + repo_id: 0 + type: maven + name: com.example-sub-module + lower_name: com.example-sub-module + semver_compatible: 0 + is_internal: 0 +- id: 5 + owner_id: 1 + repo_id: 0 + type: maven + name: foo--bar + lower_name: foo--bar + semver_compatible: 0 + is_internal: 0 +# broken uploads +- id: 6 + owner_id: 8 + repo_id: 54 + type: maven + name: com.broken-br-rest-webmvc + lower_name: com.broken-br-rest-webmvc + semver_compatible: 0 + is_internal: 0 +- id: 7 + owner_id: 8 + repo_id: 54 + type: maven + name: com.broken-br-openapi-base + lower_name: com.broken-br-openapi-base + semver_compatible: 0 + is_internal: 0 +# collision +- id: 8 + owner_id: 1 + repo_id: 0 + type: maven + name: group-bar-art + lower_name: group-bar-art + semver_compatible: 0 + is_internal: 0 diff --git a/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_blob.yml b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_blob.yml new file mode 100644 index 0000000000..69e38d4968 --- /dev/null +++ b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_blob.yml @@ -0,0 +1,641 @@ +- id: 1 + size: 1038 + hash_md5: 6096f13928b6de3103a2bd4857fcf1fa + hash_sha1: bbdfeca76d178834b5750cd8d14b8a698847c554 + hash_sha256: 82d2245520562132935dc1d0caa181df125587917de9f14ed9dfeb2f2f88f0f0 + hash_sha512: 898896789a69a87252b44e13c253ecadfbe04867c8319931eccddf4fef57b04b066061792b515c17d4480e3bbafc82590781a0c3fd43c6a64abe0e5cff05e057 + created_unix: 1746256357 + hash_blake2b: null +- id: 2 + size: 598 + hash_md5: 390df8dad491a0aba1b5098801735d5b + hash_sha1: e96e2d1909a9dc88f6a34cdd2e34e2308d9a0283 + hash_sha256: a292499ce09f7ec89fbcf97d7816dd7f9c90e3deb0163be5704a28e3eb0dc819 + hash_sha512: 8ddb3201adbdb4bdf97d27f563db097a032f910fefcf64de1b09d533aaa44affb8622b037143ea620df1010bdba56e5206dff6bd9de4450e43ba214ac2bf67d9 + created_unix: 1746256357 + hash_blake2b: null +- id: 3 + size: 763 + hash_md5: bb4f70724a5c4197d91f9bf1e36e49c3 + hash_sha1: 6fd741578f83b0f1d10dbc0a53f339e790d93c16 + hash_sha256: e6e07ed923d3c316d4174449be423171bc46a8c6dad0c864d3d2b916eda5bb5f + hash_sha512: 773336c44e8cd5aa92c3b3c921418392661ae5a4c51305341e2766d39ad978e3dfdf4f4992bbcc88bdce3fb824b6a75b1e86a161211940d55e6a4715dab924c6 + created_unix: 1746256358 + hash_blake2b: null +- id: 4 + size: 2099 + hash_md5: 72dbbe63f0c475783e53cc8760183dbc + hash_sha1: c1f6bff52587f6cc11dd26549873e5dd4bfdf4df + hash_sha256: d79571250439111a13090e69b5dd5e3ba02f980fcf6aca46943f854a0762145f + hash_sha512: bda94c619b66d9c01e75385b2e77be630b38e70d8a4163894c059550d9c6bd8300656f9dbf6c81c06112061b18c4bdc32102019ed1dd151556bd02ba433175d4 + created_unix: 1746256358 + hash_blake2b: null +- id: 5 + size: 765 + hash_md5: dba88ff4468713b2de10b2151115a3cd + hash_sha1: f2ba30eda8fd818a8e435a720c242acd77db4498 + hash_sha256: 5fc713fca3c8daeef084fc9bebc08c0dc49dfccbbd6da774180f6b57dab2a0b1 + hash_sha512: 5b494fffce128d83a5a5891c2615709efa8ed2ab7bf557ba33c4d6cc4cc2386af11e74e0db28c162bb402a6e9cdfd270f2dadb033832e9cf313c6ac5ee5493b9 + created_unix: 1746256358 + hash_blake2b: null +- id: 6 + size: 1031 + hash_md5: 32aec2cc3a9369dbd7fd062267ddc0de + hash_sha1: 611b0d7b0e10574a87ca3fee6172f3b77b3fc824 + hash_sha256: d53eaadc924150a5e5b7be4a0fdbbf20d2c996b01bb01f4388570815166fbabd + hash_sha512: f1b516945b8faf99e4943171c9b149114753b7cbe53707f901c7fa07f810662a8e2fe1a9dd4a7b1ee8989c4c26499c3e4ce5e233cadf5ee897dc5b4839353071 + created_unix: 1746256360 + hash_blake2b: null +- id: 7 + size: 749 + hash_md5: dadab7af17fffd4ba67b2161f0573840 + hash_sha1: 8438861bf54c263273cfb3d85bd4aa060d983cfe + hash_sha256: 28acf9ad32b66ee2e02fe58e6c61aafd12fc299fdb9add83a6043244a5e211a6 + hash_sha512: bf009a26ab2f863fe4571840fd5ea3586ceb02270e1d52319ae65a96709aa0b81cbbf95cbd28add5b8803e20431579a1e61cd98dc089febf4f471d309efb124a + created_unix: 1746256361 + hash_blake2b: null +- id: 8 + size: 2081 + hash_md5: 65f26423829a7cda51535dfd8649927e + hash_sha1: c9436ace1e28aca3c546be746e8986f66a3beafd + hash_sha256: 9c2c053ac72e9fb571b6cd7a31019759126b5fae331753d8594cb8a0aa210e58 + hash_sha512: 185728651b566828e8ee69cbd4d9fb331403c0f2e01bbf2e5be65d8e70a049390c78a08e5eff5ead7c070efaff7e3e7b4d13e8ca48d94f9b7a714be954259d94 + created_unix: 1746256361 + hash_blake2b: null +- id: 9 + size: 1016 + hash_md5: 8c19574fa57e395ce6f35d8d5644048d + hash_sha1: 0dea7bcebea9d4f506da68a5c3a592ab6bbb8150 + hash_sha256: c31fcee243ed68fa575c7ffbacfacfdcb24001bd86e261475220d91603d4995c + hash_sha512: eb92ec4f81e9beba3473f4504f8bb387aeac632a70a5016b27b6527ac70dcc4c940fb940a225452892701b8e6ecfdcd3858b6bd74b87f94a3b5f5bb1de369645 + created_unix: 1746256364 + hash_blake2b: null +- id: 10 + size: 2081 + hash_md5: 42e97d6c79d8c7dfa4f80c90534a3a7e + hash_sha1: edeb74a2a1a05ad4851629b90b65463559976995 + hash_sha256: 228213798b1af7033aed779446cb5a657b765f72dc47626ca2a0ad7aaca846e6 + hash_sha512: 65d83d6a96e8b59ec0ae35a184a8bbe844e572eb7b2c66b022f92925dc31edcad02312e59d1312d944ee1ace163729e6d0ec95b1c97163c61a19b40b18156cca + created_unix: 1746256365 + hash_blake2b: null +- id: 11 + size: 947 + hash_md5: a1aa1f55f9579b5b81d2b2c797677d3a + hash_sha1: 27f82af8e6580ad7fecad111665666cb08f90274 + hash_sha256: ec8b8d74a0074f391e5b9fb25f4924c5dc30e8767df07096f7ea9b4f038fe0a2 + hash_sha512: eb59b7154f208bfbc5da207b7fbe8288917d637bd2ad09f60e525d1208ab41a5a312d349715b527012f46b32351494bff7f5c6510b4521fda0c9164afc87b50c + created_unix: 1746256367 + hash_blake2b: null +- id: 12 + size: 580 + hash_md5: a4e26488fa83b5945d1c0373fe07d6c0 + hash_sha1: 891cde56160342b16e4e3eab187487b1e7d4c155 + hash_sha256: 8d27bf11781cad6d864da15e7b04080a6b1c4f8908d2e5baba79b855f6049194 + hash_sha512: ea05ff7b6e8566609063f3492c0b2551ae1275912b8c370c5345fadda9e3a408d0879c749a94160e180161eb7727a93d7a3a8aaa1dde892f75748fc4d62fece5 + created_unix: 1746256368 + hash_blake2b: null +- id: 13 + size: 940 + hash_md5: 2b1ec34161144a7100b6988347f80e4c + hash_sha1: 29e71c96e109efccb8a7f756c1828e16b19d5303 + hash_sha256: 97b8e8d2bf656e30a60328054fe47cb901ebe3aa7a4c30fa7f4cdbe1b2600663 + hash_sha512: 8fb46a5898cb29eebac4a73b7c02dbcfe5c596c8f273b8e4483ea5778a3475718e792ec50b031879146e6072e396b022fdc6ee1cbf83369acdc0ee341cc9090c + created_unix: 1746256370 + hash_blake2b: null +- id: 14 + size: 947 + hash_md5: 086cba5b2139e72ea02ba0ba40bb1cef + hash_sha1: e62e69071a629c0c46465e132b530b33da90a49d + hash_sha256: 51c4395507768e97e7e6426f6acf0b054eb64fcdb5cff64b78680a013c8dbe14 + hash_sha512: ced379957d362b70a60f188d6c8c24ef2b3bc9828b32314a629574dea760c17225f1ef91875e9906ba4063fcff522fcd1d8c47d0a9a1ee9c6fe6d28d35219aa3 + created_unix: 1746256373 + hash_blake2b: null +- id: 15 + size: 580 + hash_md5: 4091fac7a4f1754f86ddcc52734d5628 + hash_sha1: 5478354b11895f4495337e6c1ffd1480a05c4558 + hash_sha256: 930a7c95b67d8f6bcef5587a522b945f16840d7466ca45408b43a320ae28ec11 + hash_sha512: 93b69bc6c84fd03e66d42c49620d1efed6f3354598b82937528347b67088ed4042705b85989cbb9ce59c256cfc9fef8a706c3f149511643ceafbc88d660404fb + created_unix: 1746256373 + hash_blake2b: null +- id: 16 + size: 940 + hash_md5: 0714ceb54fbe397bd632a61e66498265 + hash_sha1: d66dc74c7f5a092c266aaa18d236840e2ab1b7e7 + hash_sha256: 6a8557a171e8808ed59b3e2c95f9499851e4a222654fa7320d3658c094bbc40d + hash_sha512: a6b34065742e12611f3df651d7b3c859d7f4d058bfb7aee3901e69755aa13342720530a8d4a8253f2696389db0d2df6ecb0d4068d7c747d7f480ec2fcb3846c1 + created_unix: 1746256375 + hash_blake2b: null +- id: 17 + size: 598 + hash_md5: 65e94a8db881efaf0944e26c8c8262f0 + hash_sha1: 60b143474f8dd311ccdd3eb08a1589a3f8b757df + hash_sha256: 9ffad7e3c4c868a02c08b5956e67a4ae86b3a516ef56ca3345d6e8416e7585fd + hash_sha512: 1441415dce05f8f2d44027bc5c78133e8a757454d3b26171e576d154c23b81d97ae8ed59861ee072e15ece88dfb187d53cd052972af46d35880cabc9fe31d670 + created_unix: 1746256385 + hash_blake2b: null +- id: 18 + size: 2099 + hash_md5: 8e3ec93159d2190db2f80778756d33af + hash_sha1: 65a0719f6c8a19768852a7314fc3672c6f992347 + hash_sha256: 2bdd4a5ebe1a6ac3043cb56485051a8b3332a16b6461899c3fd298156ebae4e6 + hash_sha512: e33122f943f62ba64bdf16c9249495e643b836c756252de9d001069de6367792bfdfe22e00f689663b88e661309cd5d1af2a7dc6ab24a96e5542db065dfa5230 + created_unix: 1746256386 + hash_blake2b: null +- id: 19 + size: 765 + hash_md5: 823b9b5f66afb761bfd3db0d5f7e7b35 + hash_sha1: 822357b0ba18bb302272bd77227361beaac414d8 + hash_sha256: 255da7936e3a5df32cd3ef0d8af7ecb9b273aee15d08932806ffa3feb0a15368 + hash_sha512: ef0a455f0c7eb01403c51d85676a95d912622af4ea428fbf44818bcfd1b0a06d628decd27079f77d7913255ebfa0837411dbabf1b87d08caee3ca950a752f04a + created_unix: 1746256386 + hash_blake2b: null +- id: 20 + size: 1031 + hash_md5: a3952027a659d6199f4b5f6534465bd4 + hash_sha1: e14f78cd8cf1561cce5dabbee03778be99f298f1 + hash_sha256: 1bc987782dc3aa48b2fedca48c13bbc752d7e55c642d5b2194db88940652cef0 + hash_sha512: 34298f6f1cccddd5489d6bee98a3bb16efdfe78edf38c71c2641aa49e316bc8efc1fda3423026343336d1e76211ac535678c7f0b144b6580cb3eae8c9dcd37f2 + created_unix: 1746256389 + hash_blake2b: null +- id: 21 + size: 749 + hash_md5: 35decba84a82a8b52e9b81c29574ed82 + hash_sha1: 1397366dbecb13de99f8caf9eb758afd23951824 + hash_sha256: 060fb4fddb2d6b85cc5d49c531aedebdafd10638447ec7582122c2d6a05874bf + hash_sha512: 4dad39a4cf8d99f866492181bbda829a58123d1e5c70df06f83b2dc9690ccec149795f493ff814a7f2e32003033b47ea8d37ddbcce970f1a248a92b1ed791769 + created_unix: 1746256390 + hash_blake2b: null +- id: 22 + size: 2081 + hash_md5: 8f02a5a705e82c7b4cd279714095f43e + hash_sha1: 0cd14ac45ee96a739e74921b11fd39145240be23 + hash_sha256: bbc981db64f0cb352740e76df76452ccc1c0a5c0bc73929d63cbd12033976cf3 + hash_sha512: 66bb3adec4666246c27844f89eaed600863a6e93143064d4edba1494efc474e095997e462480bd928680f090639205b8917486815181f3399115f21ce1233ad4 + created_unix: 1746256390 + hash_blake2b: null +- id: 23 + size: 1016 + hash_md5: 9f446caee85b293c8b7aafadf38973d4 + hash_sha1: 446a2c6fc2dda6ad2e00818afdff8771211ed08d + hash_sha256: 1379abf81c435e90cc4e9658bd7d6fad4437bf92bbc8610c2e0ed12e57e03150 + hash_sha512: ed7a09670cd9cc74ce977c2a24f6ed98e2d72de7abc37ccf077c56f78320bcb37fdc702743837ded3d25d8b4dcab75e0559649486ba589a657a1b72954c2d8fb + created_unix: 1746256393 + hash_blake2b: null +- id: 24 + size: 2081 + hash_md5: 799454b1553262175f9f151a037e922e + hash_sha1: d56f9adcd116fd93d11d3692440bd6bd033c9e5a + hash_sha256: 4c6051a3051e44e747f38cc3fe96d20a2bc4272dc25cc0c374eca4d3bd45884b + hash_sha512: 7243936532054ee059bf8bc996a23e80ff5ec90ce5724235ec91e13ed501693dfe881f78e70dde01cb2f3ad1de428fdf0c86a740dc473f7401159bb1719ad59a + created_unix: 1746256394 + hash_blake2b: null +- id: 25 + size: 580 + hash_md5: 7ca05a7b4ef9c35504122ea71260a7ac + hash_sha1: dc9091893ec24b36d8e88ec1d64e58fed315744c + hash_sha256: f6cb7d1d54e02ddf93e1f1d9a9131f12770d19cc2432982f4af33a42f9d2f8ea + hash_sha512: d011cb7f41761ec032fa1c0ecd5733fe152d07a498f7d048e7ff0fe9e04cbc472594795ee427c86d71e104cc36bb4765fe652457bf02f6cfafb39bce1554e9e5 + created_unix: 1746256397 + hash_blake2b: null +- id: 26 + size: 940 + hash_md5: 979ff1d9d17988d15d74a9125f2ec53f + hash_sha1: aa540f051f67d77d27edc2468addd77f50a9e134 + hash_sha256: 1983f79e2cc3b39feada90a938d6b06772e87ec74a40540a347bad3f367824fb + hash_sha512: 2c8b53b796990c028cafe26d20183b4a3ddb0221274756d68a81c8cd3f00dc1de2e8346a9e9f83ccc119e94bc058576f96bc633be6a6499806877c42912d1208 + created_unix: 1746256399 + hash_blake2b: null +- id: 27 + size: 580 + hash_md5: e8c6c698b114078242994350e2c59cfe + hash_sha1: 965f728ce742af25f364f840e05b9cd8c21e4505 + hash_sha256: b83a283c38737abcbb23462117ef35e6ee4a4d86513a2440bb4d41fa0e117dc9 + hash_sha512: 418aaf3bcd26cf63856e36ddba8c972ea99a764c25ed9fcfe6003236f6cc1e812885bf153d6f8a1b9fe712a3af04d4d99ea4d87a57ce06663f285cc1388a34b5 + created_unix: 1746256402 + hash_blake2b: null +- id: 28 + size: 940 + hash_md5: 4831302596f19f696b56d59564d1991d + hash_sha1: 92e6531a2e7bc42727d4ff455efa8f1408c38c55 + hash_sha256: b24b3aeb82d6dd3d532f6047e0b697b062fb10f2c4e1849b11eb1a7494450578 + hash_sha512: ef61c607cec6ab378cbf2522c985fe699ab2843072068e0809f06ae02c7d9b8476663104ffded3af3c96335d0db817bb78dbbc062dc48ed84b104826cb4642a6 + created_unix: 1746256405 + hash_blake2b: null +- id: 29 + size: 598 + hash_md5: 56a57f536a07cfd465c022e69bfc064d + hash_sha1: 4fd298455cddb9a0381410e5e05bcebb54d66639 + hash_sha256: e8b37be762d1ab0fc8bbc297ba1b27c507d0098a287973c3b7003772839d81ce + hash_sha512: e1ed956616b3314eb247cf24586b36ce90435635ae40b57814c7a6cf0ef92c38bf13fbc2f07895ca6f67dfb9c3b9c0b4cf5556d69320b424dca989d1e4f74856 + created_unix: 1746256415 + hash_blake2b: null +- id: 30 + size: 2099 + hash_md5: 4bc4c156c6a6a6d421e6b2a0f9bad601 + hash_sha1: 7c8746c62bb8418d1570fd0884cf9f846e055ffa + hash_sha256: 3f56bd9d542d9b944c47fb946277d72445ef2d34dfee108ea3a8c2d06dbca50b + hash_sha512: 7b47cc1e588c8e358cc29aa2135bcccb0f919bfbd9bc26b8342a352f4fdde3f3d2b5ad935c1efd6fc2d9e7cec5b1dc5c3e6630c9b3b979e47ff4fbce7863ea3d + created_unix: 1746256416 + hash_blake2b: null +- id: 31 + size: 765 + hash_md5: 5c30fe5aa7e261d6c6575cc8265697a1 + hash_sha1: cdc6f4ed95e723af264f3b6aae46bd0a98deacea + hash_sha256: fc9e302e423b982dad68720d5f940452a312cf3c287a96dc66582460265a1b01 + hash_sha512: 079c9cdc9a919dac4225f42c26ffb34d4d115db8ab5e51cbcdd2b248f06935dfcb169f0a9e04f49b143615dafd5a6742ed231a1be13bc5c814af75ae173bd0e0 + created_unix: 1746256416 + hash_blake2b: null +- id: 32 + size: 1031 + hash_md5: 0a379e399f7918290e303348c8cd6b07 + hash_sha1: ab53de2814344cfd8998d209c6927a415df147ff + hash_sha256: 2267f735bd581417efae14f797ece94779fcea40ab28ac700734c0ee38b284e2 + hash_sha512: 967ebc462c40ecda18ad36abba96534459d8c653c14628ea2d8a2f1e18b313b2cf07d36080cce0aed921780b7717913ab1a77063632845d0daebb4d93bee595e + created_unix: 1746256419 + hash_blake2b: null +- id: 33 + size: 749 + hash_md5: 861a7d2013e965382bb9404175aa4f44 + hash_sha1: caf88fc255776f9460c5bd689973a66f528d447f + hash_sha256: 9c592110208bcd6624661c36180ce96f238dbe6c6f9ad9749a0d3df828172743 + hash_sha512: f7a6a16bf8c972fc12871030e3b681c1341b2eccc047f64904b2e6139f8f41015387d517e1ae0e9a19fde8dd0c3d9188db3e433120618f096e7894d3646c765e + created_unix: 1746256420 + hash_blake2b: null +- id: 34 + size: 2081 + hash_md5: d05558896004f742e8bbe6bbb318deda + hash_sha1: 2537f2f2127f5bfeb43ab064aaabb8278ad4bc08 + hash_sha256: 4e931208c50f458bd523ef5ef55c5367335c62d558be4e24615dd200a75c378d + hash_sha512: 8a4fece3a1dd51c870c160d526e58fc96c3282dd770a1e9ce38b39492cda676dd48ecf258ba3282ed081997142503967a7114646481cde6d5b7eb7be49fcff37 + created_unix: 1746256420 + hash_blake2b: null +- id: 35 + size: 1016 + hash_md5: 835a5a3fe19145a134e4a0508fbb9411 + hash_sha1: 707c30bed611042d57c52d0db2bcd0f609016a5a + hash_sha256: 650248c779adbb13c81316902d4b7b27ef294530b0bbd484a3f3a1d369ca995b + hash_sha512: 81810ccaf38ad57a9e384336f872d2f39da9474abd648024e8da6733c9d0fd214757563cc7c8a7dcff12cd3e64ba4b694afa6854a6f55bd4e5a53152eef6a7f2 + created_unix: 1746256423 + hash_blake2b: null +- id: 36 + size: 2081 + hash_md5: 599464902943b0e7499180d1c2547754 + hash_sha1: cab101f1bb3a73e91c3d75f7897067fb29c7747a + hash_sha256: 5e960e634845737a49b2bf758496e2a42cfb227ab24455a9d182bde7f597d4a6 + hash_sha512: 759dde21a878216a6e629bdbd91e2fa4dcc04b7c56116683b5c7433eba277ca3f17daf918c4575130dd14a6d2dcf453bc7861f7e99b7abf150c3b058a299743e + created_unix: 1746256424 + hash_blake2b: null +- id: 37 + size: 580 + hash_md5: 6e23997166dfb6e2bf3cddb0d7a7f6ff + hash_sha1: a9972937cb840e3c961fa00ed8512c45fa9d75a3 + hash_sha256: c95111d06b75e32cf4d507b818544dbb5245bc0b7dd8b3a1f5d8479772a06b1e + hash_sha512: ca549fae977e7fcda088e3211d362a3636c47bd914e2c5ce08ddff2703e60b06dbd66339bae819c881425364f679f1839b1e0685f3bced32747a969d00775a8c + created_unix: 1746256427 + hash_blake2b: null +- id: 38 + size: 940 + hash_md5: bda9707822e39ed52a6d0b9bb5e9af96 + hash_sha1: f6ea322c0b9c6211747129636a59ff95e5dbb165 + hash_sha256: 364f44514fb924cfcddc84c31bbf3dccf9060d9e3ef1e3e6dd28f5789a79e4c6 + hash_sha512: 7636858249476a81c886c529997eb55ef8c36c52430f0e98050f628fcd440dc09e57f43f88e272f827f86c95451aa4b1927a8d860f970427d1df8348590e7869 + created_unix: 1746256429 + hash_blake2b: null +- id: 39 + size: 580 + hash_md5: 5b373c943e67125442c5a573eece9404 + hash_sha1: 84b40e3373b8c172d8c302dd304f792838a3f788 + hash_sha256: d6355300d7e51c649efe19b3419d0dec57d42d4c75079e4ca06ea081f8260f5c + hash_sha512: bd926b535bd19f60d69d76a858db2c48f6057223bc23a3575a4fb49fc9ea455250f2bdedad09efdb098c4ac16c6e3d2451a6c182ef191f076276de790448506f + created_unix: 1746256432 + hash_blake2b: null +- id: 40 + size: 940 + hash_md5: e3817ea0b2f58dd5cc570c0d337a2ecd + hash_sha1: 2acc853133cb0ddb46a546fb31f7b4f23df1c676 + hash_sha256: 775271730e0c4b923a81f539ce97d1b21a1db78ca201065dc886fd42d4e0251b + hash_sha512: cb28a65b2f5f7e1e9ec45f6151ed322ee751de5266f413940655de7d739ebf2f7ac9168ccd9676e5b1696d3dcfb57d0bbc404141dfb321a2d28c1a6dcf32b0a7 + created_unix: 1746256435 + hash_blake2b: null +- id: 41 + size: 598 + hash_md5: fb3df5314eda77cfc0da782d86a1574e + hash_sha1: 874c3e4aeaad0dcb91f955357c008ed3de52883f + hash_sha256: a1f80fc25f3a4cacc21c4ab3e263cc722f7e44bc829a38e7fa927853737cbf9c + hash_sha512: b27519f9792ac72b715ff591895deba46d4c6455ea04a4b416e8c5a13eb234176ebd85a21cee5b473f3baa679c6c03cfb72d6d552dee7baa9e7d75535c776557 + created_unix: 1746256445 + hash_blake2b: null +- id: 42 + size: 2099 + hash_md5: f12303e4cfd6cb446c8411f8c2c5c54d + hash_sha1: 74af4b51471a501f0fd65ff19498cda87c8252d3 + hash_sha256: 9fde3ad1d6dc79a40d9ef1c41bb22d00db53df558c0f4d305aaddc0dc2cc0bba + hash_sha512: 0f19704ff822157723138bbbf027d418a601cbe0fd21db16478d6e870ad74b80084d5aa35d529a2c6555237cfea19a6c1f955df0c4b00b5ed94f0f1b42465bd6 + created_unix: 1746256446 + hash_blake2b: null +- id: 43 + size: 765 + hash_md5: 0ee9f9dbd150f2fba1f2d151243a23ea + hash_sha1: 521db5e9e0e2d25e8b80c9a168ece7263e0b548c + hash_sha256: 034554ebaf63c4c20d9290e86b45375616b4e289a28e480f58e25a791c361639 + hash_sha512: 23ba0f0847083365fbbd0c4de0d71a6560e50781f413e1546a11e9d9d4aad7daee1116b5b94844ee0a431f4d70796561a337a648ec052ac3c5e4bad5accf73ea + created_unix: 1746256446 + hash_blake2b: null +- id: 44 + size: 1032 + hash_md5: c4c544a801fb99281c6c473c22e0545b + hash_sha1: 1c686f728522cb8387a8415a259c62c1af8a0fd1 + hash_sha256: ce8c82a53d379498784a981f3c69d847947420fb7e1077f5a2b3d9f41fb7c8ed + hash_sha512: d9d1bb2a1bcc7886f8c2fd5a413cd954fe2043d4a81dbb1f9ace06a1676684047a7ef2bdc27c9d3f9b800657dbf7226acc0f32b71fc6dc0c9e2662a5c64f7fea + created_unix: 1746256449 + hash_blake2b: null +- id: 45 + size: 751 + hash_md5: 89b82f20b8be3ea17a068b46e7497428 + hash_sha1: 27f1389a68febf6786e0d79c698dd1d66c9abfd2 + hash_sha256: db4ea60e526f85e9829a45e2d75c1f48509e7429a79f7a5d24bc536596c66ab5 + hash_sha512: b441c6ba00bfcca18adaa976ac37bbf9cab17cac8e7d24e54bc40122dec33781faec7b07db97b3dcaefec7ff8a4251e96728772995d91d196691d8e3494b5768 + created_unix: 1746256450 + hash_blake2b: null +- id: 46 + size: 2081 + hash_md5: 96d24b1f1bec73579c5dbf85ee52b332 + hash_sha1: 4f407843baaa58116c1022b73f5e411f1c531840 + hash_sha256: 6866d5d85796b5fc8576310f8ed1d9d9c85825727007809cfc9ff666f1dbdbf0 + hash_sha512: 8c7c62afaeec008d7c6bc8e390f4adbb11c4ca11337aa1c6832ba83de19f0c75d52ea4fd72a822acdc4f567158120119ed121e0b68213db7a7a7a4f3a731f8fb + created_unix: 1746256450 + hash_blake2b: null +- id: 47 + size: 1017 + hash_md5: 71a491bc2a2bbb08ec5b3682d09c3f1f + hash_sha1: a126e70bed9837bf4609f32d74b20d2ca3e47b59 + hash_sha256: 2bcacea4eefed11c6f6a23da44ddd575689460262af046a37872e32aa76b79c3 + hash_sha512: d647d5f7122d76928ac349bdb147fad8b2cd68b7be75a4f580c768feaf86c5e1f7ec484786b6c81324b9df00329cc89217cb00d379138b40adfc81f74d06e6bb + created_unix: 1746256452 + hash_blake2b: null +- id: 48 + size: 2081 + hash_md5: 26e05a44a2e1989b418aac50feda163e + hash_sha1: 224cefa64086537d1d571644abc2180803128035 + hash_sha256: ea09c61dff5dec488b378b2ce0d4dd64891394606aa318dd57d5f2158d7441fd + hash_sha512: 4ca6c77760a11e44f7826b3a23bd5b607ab1e323f47aa76acd9226f1771551fa4dd45c58002c8d3ba6fc4867ed17ad34edb1695a9ab94a31afb8255862633976 + created_unix: 1746256453 + hash_blake2b: null +- id: 49 + size: 580 + hash_md5: a9e65f66ed8ba4769f366fc93142ae07 + hash_sha1: c170e29851dc3fb44e115a907de7152efd5fb85e + hash_sha256: 8ae9c630848588ab6bafebbc6ebcb3b6fe329350e89894852db5c55cc52a4c2c + hash_sha512: e0bdaf7430a5176640c0fd57910e1b3e98b4dcc0d34a5a4e898be6c30ccd2d3b59c22dcf94619e3349003ba7b7882ab43216d6175471becffd874143d74ace17 + created_unix: 1746256456 + hash_blake2b: null +- id: 50 + size: 941 + hash_md5: accc00b46ba0c6abc545ed79c2a2f88b + hash_sha1: 44a47e8601fe1785bbabc21dbc9efc61ff16d47f + hash_sha256: 6f514c55dca294288b5821f415cd24ba11649f001edcfe219e1052d252fd49d8 + hash_sha512: 9ee7ebd7280fdb51f46610a7923212236b668e7a62a083bae7f2e2f1472d06cb0a9d0f38069e5537fa3b8689f13abeb3e38a1e8432d55f247a06c0522e74374e + created_unix: 1746256459 + hash_blake2b: null +- id: 51 + size: 580 + hash_md5: 700171de0c83b136769d88af94c1b9b3 + hash_sha1: c3baca0fb897aeb9462f3718369a30515a68e415 + hash_sha256: 96eec3e81eebd724a3ef0e6215e337543b3979f2a3355a193933dc7489aa4616 + hash_sha512: bfb5bb19da1aa65d7cc78ad4f3c57769aefef90ffd5edab3ecd0db5272550bb0666de00acb2a053e03a9473e09d9a77b5026e46bbe18cf669ec4e953b49256c9 + created_unix: 1746256462 + hash_blake2b: null +- id: 52 + size: 941 + hash_md5: 613b6c98a027fc929ecdb06482859626 + hash_sha1: 39878c4f57ff81bcdc2ce191c44a9a9b31384646 + hash_sha256: 78fe74176a46873c6fe0c519270641ea45b3e879b1d4caa595bf67dc69d760b0 + hash_sha512: 16c23e52c02fa0d30dd60704299a375eb6ca75c3fb471fa3501720e53118254bc8888dbf44f75c3cdb2a98ac0ff9ae6fff72e9026fa9a241eaa25841f4fbdaaf + created_unix: 1746256464 + hash_blake2b: null +- id: 53 + size: 598 + hash_md5: f3015c6cc30b064cd13392a252b61038 + hash_sha1: 5b6a6ba01238ec187d18ed02f080456d9b0d8416 + hash_sha256: c331e46fb09c0bcc4aa8019b39d9ed7ddfacbcd139407557f6089f36e5861c57 + hash_sha512: 64d91206800c8fcc22e449591952289fb83eff85dcc2bf898f2daab66d40ff4e8f9b665335b7a6bd2c0663d78a2ddf1debef923f3306f6813dae98caf02a0f95 + created_unix: 1746256474 + hash_blake2b: 1ae012597dbf2d40644c82e06dc6267d88638571b0c669c62e59d15463c289e170f602420e0147da1f9524cea5095f63868bc36ab5630383360e8101b3796d49 +- id: 54 + size: 2099 + hash_md5: 84d83643c58073eb48d88ae591920e50 + hash_sha1: ef931c2b87b9e2c10b7bbb2bb592cb5dbe82aed7 + hash_sha256: 8b554f322b8fce952f2221bf08be8447b8606e2ce54d8846b5edcc80ec1fbafd + hash_sha512: a2b1a5321f67ed8a324e17903497fac845169cbbb49cc3dfdad9800b164877a7d8f5fcc8f9d6a8e2b394cb13c9a512b27e39149f3b017dd28f26cfcb168fc4c6 + created_unix: 1746256475 + hash_blake2b: e4ca11cdd35926a89604589782e538c8140911fef2dad8a934685b33ad841e3cc96ea9d36c90ae413147ce2cabe6c5517045320049fbb3b0069f3620df753bfc +- id: 55 + size: 765 + hash_md5: 1e9a236068db2f888b41ad1737e26764 + hash_sha1: 03c5b9eb6e2fea31d3e774c51d3719227810414b + hash_sha256: b117af5eaca1f723b7fc3646853298bbffddbb02c63af5cf09d853f0585afb89 + hash_sha512: c24ff96a33c8eb5f96a212923e1381d41e2813be978772f407aac30090c8657558427b40cbc129dc8b43b96c988ab888703ef5d5e139dff32bf727041a90fca5 + created_unix: 1746256476 + hash_blake2b: f4549d79377a1d01fd8c0a19d4a015dfcc50b58820d57ec7bd14fdc5fbaa8166fdd11432200d728d5f1052e54cab2069c8f80818616377c1baa1013fc7679356 +- id: 56 + size: 1032 + hash_md5: 6cda2dee8f57ccc1730f40917a6ec85c + hash_sha1: 9689d59af6cf615be6ac21106dcbcc79250b2909 + hash_sha256: 012e2bbd675130a45b6b6255142254428a86b35c5cffd17546331e9f1bbadb55 + hash_sha512: 06b79141428bb93d1ba14a82f132dd068ac4b68db3e55029a25ff4bd2be6274a33898f263dc225eed1186462f7eb3fa4e7a24925e996dda0dd1bdbd265eef6b4 + created_unix: 1746256478 + hash_blake2b: 0e148046a9984fe45d2109d227ed31b58329afc532eee4817d2113584ff034aa1c5ff09d44240c715d9a80a1a3ea89480bfb425e173df154aedde6398e2ec347 +- id: 57 + size: 751 + hash_md5: 00c8d9ca6462b0e95a30325307a1d855 + hash_sha1: 525a79e7dfe6553e5e4b4057e80e090d48b43c84 + hash_sha256: 5b74c950ced8330e3dae432f6c5b0330c6c8e3a72f706f2d8128db989640a02a + hash_sha512: 0777a9af5eb936fe5c1a28bb5df26c06db7e290f858cb1ac2e7717236c9be949e8bb214d4007b865ed9d22776b5abd4f8f1e16f862beb9d2db7388f16c9f0ac4 + created_unix: 1746256479 + hash_blake2b: 65ed24d1a672a0ec358359f0f9b1dcefce0b64fb8ebaf0342dcf5fd9826b3664a95ac54628a9c245ddfac0843ff36f70de3dff288d5782c54539e1a95c0466d4 +- id: 58 + size: 2082 + hash_md5: 610e6b411ad6a885613f3df4e022e8c2 + hash_sha1: 5090aa11b88cc5c22895fdcd9e303eb5f89b414f + hash_sha256: b87955056b214c1404e58c6e60d2e8b203c249ed141ba80ef6e2d22e4d62d187 + hash_sha512: c70e98ee7f6b22b427aed9232abdab8380385c841cb6d93af25328bc0a4ed0d1aa4342b363850fde06a1c4f68b0b63c2da99fa36e5934aed5314cdcf6e47e830 + created_unix: 1746256479 + hash_blake2b: e3d37152d65d1874f932f18ad0325b8bad850dd220e6f81f99ce58b3117a7f9ee40a3467992d8be78988af7434a5303f264cb5bdf4d4ab66752904a55fe4ac44 +- id: 59 + size: 1017 + hash_md5: 155aada9969270a2d3cc9e7cbd72398d + hash_sha1: 6f7140f45bbf17a98d8b51f87c6672c30cbfe114 + hash_sha256: 3ff912d0c3e4a91d6f5394d8af0a361ca7f183ff6f8a606a32e68e80d4341300 + hash_sha512: 04558b2154b25463060f6ac999426e816682c7b6e866bf3b9198b4b6cf41288f762439871858831bee627559906e920b478c21525e3ddf85b9896d55840775c5 + created_unix: 1746256482 + hash_blake2b: 485c3a16ff1036dfbc05cc55bbb175d5e782b16a97bc4b47fb29b3af824abab016f808fc6cdc96690c4f657b4acadd69bbcf0dec7dc478c91dd09137b7cb3e2f +- id: 60 + size: 2082 + hash_md5: dc9d45bb32e8f3490634bd513cbf9ece + hash_sha1: ea91296f560ab8eb978b7ed0db8efe2fd0e8b413 + hash_sha256: acf6b1fc9b5b93d9832b6c59e58feb352fd8b07e04356b4b60bbc35dd77c3aac + hash_sha512: ff31a45035f8092995bd6ff8e55e6fa35cdcaa4aeaf296da0382657d080541f49687a5adeccbde9ebac0fdbe814ac1e080e7f266bb38acfacd038ce0a6b6a632 + created_unix: 1746256483 + hash_blake2b: e24f6cd27d8702026d8d750734fd28d36b1621d8bc7bad18b403b61d0390829a31db679aeea559b7b3f7df27212cad968415d67a72c33abf5744cd61e96e8080 +- id: 61 + size: 580 + hash_md5: efddf19ae15f14a76ad89882c5687f1e + hash_sha1: 4241c3e18d179d31a02032e6fbb31014adf9f3f5 + hash_sha256: eee31b528413c52085cc0b0aa0245ac472cfd364e80fde1c5386e78e75372890 + hash_sha512: cb752b230dd53a380573dcbd2d406a497ad9279b4fff5fe7de493c34d354de35f343788673830f59912651c1eab81da04513f3ecef0ab6a30dc2f1b39fd9701c + created_unix: 1746256486 + hash_blake2b: 02729bb20121140d8f8904adc712dd7432fb1117bba8a3d878b86aa59afdb3e6d26e3d597b072b0364666fea48c2c5721d5d3bdabb12241267d7fbed0668c927 +- id: 62 + size: 941 + hash_md5: 0aeb1040c1716c32a6ee5a3da7e4b7ca + hash_sha1: b7cca777ecd4e4d969f2e4c5e91d30fc7cb75070 + hash_sha256: e2f0da45a23db0feddb37e44fbf6600e69187530e52234fe1bcc7ce3d1bd2a28 + hash_sha512: c3394a2adb2bc062cd0015751b5a0d64c7e2600a9dd15b7153782bc9c45f57a734bd6ef7d12d35f48bf2b281e5949d8846fd61612f162c227d19a8a0de1f2ce9 + created_unix: 1746256488 + hash_blake2b: 3fe0bc2e796722fa3c26146ab7f5690eba647ce9463e94266998c68cefc2a834092f92ecddd70d98d1e8774808ebd87df429248c61e73144332b8cd8a571b4b0 +- id: 63 + size: 582 + hash_md5: 1262bc37cc2b04b123ae5c1e7a739c4c + hash_sha1: 2990625d65fed9184ea3da0553722c128dc962c4 + hash_sha256: 49ef9afc4951add83afa570b9fef916c5bd22d236fd74752dbfb29944dac4db9 + hash_sha512: 9012ae4e297da75320c057c78220f34c64889a1905c49467f4fce7400f87b2957ffa9b1f93b7048746d84320081ec7aa69338e2d57422e53cf3c06a5365f87ef + created_unix: 1746256491 + hash_blake2b: a822759b7592218d9571f96ed3039dc934aab2b175476530926ff8bd874b94b05a9c6a275d8b3c2b50fc2efa67fd6045edb659bad9aa11e83044e53cc54564ec +- id: 64 + size: 941 + hash_md5: 53561653378116758da59c1e5682d024 + hash_sha1: ac27f3a6e7bd048aaa784273b6bfc92030708065 + hash_sha256: 1da9c5d5281b8467a86fe7fb015f3ebf1807d3d975c8555959399b91628d7b8f + hash_sha512: a97b7265b7df742c903d68b2358f0d14cfc2511c20d2c61a8f2a24505a5fbeaff624b2dd47eca988a3466f55ce7910c0831e7c1617b41a5e560dd584f8b8c28d + created_unix: 1746256494 + hash_blake2b: b6a911084595fdcddbab8c70e5482a2cf038bfde93b27258978a42465d894b92725f9abc81646d28a9c4b80a773c6347aaf43cf7d2067135a15edc2f1ad40b08 +# broken uploads +- id: 65 + size: 66356 + hash_md5: 298a5b68e9783c9f193f3818b6362c0b + hash_sha1: 20eecad725f7c40a579fd06f438fa366e9dc5887 + hash_sha256: 6b6802be078be4c07b38b9d0a156521459c81d2cc742be8cf115398890baf95c + hash_sha512: 983db2a7ccdf88c8765bacaf6da98eaea04aebd42e1e67943172e7e862c8f62a88cfc96adaf59bb3ad65d889c39e7281f55eb7d706b281782e4c9d0690091d2f + created_unix: 1734346161 + hash_blake2b: null +- id: 66 + size: 2968 + hash_md5: fa67973e250e94fb4a888ebdb4fa86d7 + hash_sha1: dce5f1b461883dd50f9191d52aaab01a5a06e578 + hash_sha256: aae8a7ee5a446ffbb59eb3e3a70ff8809b61cdc235105397e8550b30d12d145b + hash_sha512: 6b2f0e33da8204e72903a8f13f6ab4b77ef6b387453777e308088d8c44b925c1b1d226bd8836e32af89ce96a586de0ac0d7ebd351a9534fde7de73634b2fa802 + created_unix: 1734346195 + hash_blake2b: null +- id: 67 + size: 64135 + hash_md5: d9c454fa89301aed28ffc712cd230c56 + hash_sha1: 9c4b65d14fda070c3248e9ad6da78ef118cd7bec + hash_sha256: 9a41e5fec8381a75c41c0e4cc5df56546bbc0ac70019e9ec3716bdf3f7c0da0e + hash_sha512: 04e706e34c62602ab129dcb3e817fe4d3b6843b823686c375cebc3cb6adfb758227cf02aca015a808d340a3aeda7424e9ce35de14456d1a8d40874782239f250 + created_unix: 1734344969 + hash_blake2b: null +- id: 68 + size: 1053 + hash_md5: d686ee5f6ce4b91cc317354f49cde759 + hash_sha1: 8ff8b4d60a1062fe468ff5c310833151932ec628 + hash_sha256: fd61ac88b2ad7760fd4798613bae50994a85e5346002409837f221a5120de03f + hash_sha512: d1705bb7dad6b70a6a9041276bfc41e1e265ac0b8ba65f0ee3bd680e3ad681d813f555b82e81ba162eaae37e5453846fb569c9d64498a69efcfbfa42bd75b82e + created_unix: 1734344734 + hash_blake2b: null +- id: 69 + size: 24995 + hash_md5: 555c619b35ccc37ef448e0627e1c91bc + hash_sha1: 2715bef2d294e8ef9bd8142b8cb92b4f20930792 + hash_sha256: 90769f198e34910cf842b36e1ae790e801656fa9dc8c072c8ce06a031c195dcb + hash_sha512: 1d1f5f1badc779bb387259dcd6bf4deec7b8d711e67b62b2c6c85c1f2a6aab3ed463c82e7f5c7d2060bddbd6797ffd070b4120a7d66eea8a57ee345de2822c8e + created_unix: 1734344746 + hash_blake2b: null +- id: 70 + size: 6307 + hash_md5: 70833f96aba4e4aae6a66459e1f48640 + hash_sha1: e049c72bc8f5990c1e63c0a9ed17c12b0096efe6 + hash_sha256: 55f6f783455b4737078b1b7ec9361299ec336ad0982fdc70e8f861c69be847ca + hash_sha512: 8ac7ab63762560227892717c23a1ba2cd6b3f0a59cd8cfcaa86076c65e63dcbac8f0c47213dab21677f7583b672487ddbff28a4aa2446581e25f5d98edc70eca + created_unix: 1734344775 + hash_blake2b: null +- id: 71 + size: 14826 + hash_md5: 426d22a7b0b57a8a00681191c4f22ae6 + hash_sha1: 69781f39415518ae823d8745ce4720e743172019 + hash_sha256: 0fd63dcc4af83babbdff690441ea9182a640ed9d40b1312cdb1b457b2a03f38f + hash_sha512: 681fa091abad08a33950f6a308f46ead8c9af8529a42032835c03c960f4578a744b44bd4773aef73438b25b42cc51302498424aead7198eff623f96de6914176 + created_unix: 1734346161 + hash_blake2b: null +- id: 72 + size: 863 + hash_md5: 9acc0ca46cfe4c2adfe0b6e5062a8a15 + hash_sha1: 0d946b74eac616923103506ee350d82f6c9066bd + hash_sha256: f6641da8427411dcf13c15e7e10cabef802383d859301ffc2de0a49b0f213fc3 + hash_sha512: 4ca12e3af7ca08d58307c610042c31775683011b47345064f213312211134cfe5919c71ba4211019565052364262ced0d9b7dffd058199a36c741f2fef39e2af + created_unix: 1734346217 + hash_blake2b: null +- id: 73 + size: 10272 + hash_md5: 6a4f701abfb602e3838ba300f9cb6ccc + hash_sha1: 0e30a16b8e1b74c51abd08e33dc5341404b71e3b + hash_sha256: 0c3e7c7143843491b221af0225ad4205b48ab871aa66ff00bce4d3954b9e0987 + hash_sha512: b917580c12f09044e117adca76bd1551df5fb92af1860851700b232d07445b2e8293bb73a9c18bfecc6e6d5dbb6df86d954d6a5d93d0442847f6168125fc91be + created_unix: 1734344911 + hash_blake2b: null +- id: 74 + size: 4229 + hash_md5: 260b6982c5690d674ac546b3bcc743dc + hash_sha1: 8e3214aba795ae5849386dac742deb01caf04b8b + hash_sha256: ee2e1fe649bf8fc670b9ea5f34815d66c85ba7f8fcd861a557afb0aa9679f652 + hash_sha512: 6c58c7d0eca2943f3a4a5a2785b961e00d18bf1a9d246879b487120e27f986a0364edb842748875f25244df1c9f939fb705086ad437fce4890f7c8e2b31a79ab + created_unix: 1734344953 + hash_blake2b: null +- id: 75 + size: 12410 + hash_md5: c511f1c9b10762bd9f907e834d7d01cb + hash_sha1: 4998c52d0d23dd3d218e2d9d336f24c0c073601e + hash_sha256: cdcf5d6a2c273647885dc4d1a3da4d64922042a4a0bb16beaf561f29b7c59209 + hash_sha512: 6db8db82f436cec9066b4174a280e0e5bbd21cc829cb2beaf2071fe6ff1f8c77a5a7e5e8a168a41551a21c98bf03e6441375c87dc64a0ef2646720f348d3927d + created_unix: 1734344986 + hash_blake2b: null +- id: 76 + size: 770 + hash_md5: f123e1549bdb0cf99f89a8944f25d8f0 + hash_sha1: 01507b6530a5b134a5c660765e0f552cdb5131d2 + hash_sha256: 81bbd81ba91c24024c7d4790fa1cd5c25acfb7b9c7c4fa9814e0d9f399b6b622 + hash_sha512: 0738901ad6678a9ab202d5a0356d8bc49c1ed1ec21922718ff706b0990446f78b87adf3bfd25273db8a8910dd7b19f8c232f469b1f65dacffafb6fde29182104 + created_unix: 1734344900 + hash_blake2b: null +- id: 77 + size: 4034 + hash_md5: 4bf84ceb6e877a385f40ad51166bfc1a + hash_sha1: eaf6225714366c3a05d4c56c20b034efd8a5f037 + hash_sha256: f375ec85318662e1789bd34750e578cb2fcf99c2bf005ecc06fabf64e23daafc + hash_sha512: 470c5ed2bf14a7ed426453ff026ad2269bbd503ba8c2cd767c89793530c77a7e7e503c68ee542d125bbceda3b49cdc5c846931981eac739a865ffb0cabbe4d89 + created_unix: 1734344922 + hash_blake2b: null +- id: 78 + size: 6088 + hash_md5: 0a4d805af108af059f9af308168ddcfb + hash_sha1: bef8a000beaec4bbe5d68b008868d76c90f3f25d + hash_sha256: 5b055f5fa634434bdcde8b8174c783d768abb5a253ccef648bb522af603836ae + hash_sha512: 9254409431fa52da3144960c272e7aaa0bb646297601cbc6bf88f6c736903aa70943cb56fdbddfac096c10444de25bfd53084b2cf3139ef5b650aae38dd42c26 + created_unix: 1734344852 + hash_blake2b: null +- id: 79 + size: 946 + hash_md5: 3f78acb32897fd77fe467b9776744c12 + hash_sha1: 3d70df43777d56ad3388ad0036979fcb77443b4c + hash_sha256: f9b8b7d3050abecc367bc1523cf5ec17282b5d7181ee833ced319cf382f92616 + hash_sha512: 2886114ebaef9a77b21d7240591d376a0ef898986882e25e86e0633d465bb9e8096355a4ca6273eb9cf532f02efadd5195b786249986d7c2fafe40f94a8c5ca9 + created_unix: 1746280832 + hash_blake2b: cef2628cf51cca9907bdcc69f4e6cbc65c121d37eccb9eccc29fb11179a09af67dccbe0eb04fcd2b87b23925f96082f33feae5c9c550307ab6c6f260aa532f8b +- id: 80 + size: 946 + hash_md5: 222067385a69840b897fb1177a404876 + hash_sha1: d56d3378e8492c6ceee3f967f41123fe59082770 + hash_sha256: 3710e14622091c60701b5ee6357d80681ad3b236baf639d2f848887f0cf681a4 + hash_sha512: 2de5f0f547b60272ce3da6933309f7c967b23a839c0b7321ae952c229241091033e0640c9e64a199b10936ea64bbc3520ff56a5b6cf339856ca3d089bd5d5487 + created_unix: 1746280843 + hash_blake2b: e7e3034bcbc26fba242f2d1c8ec4f653e175178e303b1d120adfed0eec2f6c674b29d5fec4ac2ecb862f491682af480c7d3234be633ad9b288cfdc29cd020beb diff --git a/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_file.yml b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_file.yml new file mode 100644 index 0000000000..2d7bd135b3 --- /dev/null +++ b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_file.yml @@ -0,0 +1,698 @@ +- id: 1 + version_id: 1 + blob_id: 1 + name: parent-project-1.0-20250503.071237-1.pom + lower_name: parent-project-1.0-20250503.071237-1.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256357 +- id: 3 + version_id: 2 + blob_id: 3 + name: sub-module-1.0-20250503.071237-1.pom + lower_name: sub-module-1.0-20250503.071237-1.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256358 +- id: 4 + version_id: 2 + blob_id: 4 + name: sub-module-1.0-20250503.071237-1.jar + lower_name: sub-module-1.0-20250503.071237-1.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256358 +- id: 6 + version_id: 3 + blob_id: 6 + name: parent-project-7.0.0.pom + lower_name: parent-project-7.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256360 +- id: 7 + version_id: 4 + blob_id: 7 + name: sub-module-7.0.0.pom + lower_name: sub-module-7.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256361 +- id: 8 + version_id: 4 + blob_id: 8 + name: sub-module-7.0.0.jar + lower_name: sub-module-7.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256361 +- id: 9 + version_id: 5 + blob_id: 9 + name: parent-project-7.0.0.pom + lower_name: parent-project-7.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256364 +- id: 10 + version_id: 6 + blob_id: 7 + name: sub-module-7.0.0.pom + lower_name: sub-module-7.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256365 +- id: 11 + version_id: 6 + blob_id: 10 + name: sub-module-7.0.0.jar + lower_name: sub-module-7.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256365 +- id: 12 + version_id: 7 + blob_id: 11 + name: bar-1.0-20250503.071248-1.pom + lower_name: bar-1.0-20250503.071248-1.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256367 +- id: 14 + version_id: 8 + blob_id: 13 + name: bar-7.0.0.pom + lower_name: bar-7.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256370 +- id: 15 + version_id: 7 + blob_id: 14 + name: -bar-1.0-20250503.071253-2.pom + lower_name: -bar-1.0-20250503.071253-2.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256373 +- id: 17 + version_id: 8 + blob_id: 16 + name: -bar-7.0.0.pom + lower_name: -bar-7.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256375 +- id: 18 + version_id: 1 + blob_id: 1 + name: parent-project-1.0-20250503.071306-2.pom + lower_name: parent-project-1.0-20250503.071306-2.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256385 +- id: 20 + version_id: 2 + blob_id: 3 + name: sub-module-1.0-20250503.071306-2.pom + lower_name: sub-module-1.0-20250503.071306-2.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256386 +- id: 21 + version_id: 2 + blob_id: 18 + name: sub-module-1.0-20250503.071306-2.jar + lower_name: sub-module-1.0-20250503.071306-2.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256386 +- id: 23 + version_id: 9 + blob_id: 20 + name: parent-project-8.0.0.pom + lower_name: parent-project-8.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256389 +- id: 24 + version_id: 10 + blob_id: 21 + name: sub-module-8.0.0.pom + lower_name: sub-module-8.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256390 +- id: 25 + version_id: 10 + blob_id: 22 + name: sub-module-8.0.0.jar + lower_name: sub-module-8.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256390 +- id: 26 + version_id: 11 + blob_id: 23 + name: parent-project-8.0.0.pom + lower_name: parent-project-8.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256393 +- id: 27 + version_id: 12 + blob_id: 21 + name: sub-module-8.0.0.pom + lower_name: sub-module-8.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256394 +- id: 28 + version_id: 12 + blob_id: 24 + name: sub-module-8.0.0.jar + lower_name: sub-module-8.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256394 +- id: 29 + version_id: 7 + blob_id: 11 + name: bar-1.0-20250503.071317-3.pom + lower_name: bar-1.0-20250503.071317-3.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256397 +- id: 31 + version_id: 13 + blob_id: 26 + name: bar-8.0.0.pom + lower_name: bar-8.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256399 +- id: 32 + version_id: 7 + blob_id: 14 + name: -bar-1.0-20250503.071323-4.pom + lower_name: -bar-1.0-20250503.071323-4.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256402 +- id: 34 + version_id: 13 + blob_id: 28 + name: -bar-8.0.0.pom + lower_name: -bar-8.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256405 +- id: 35 + version_id: 1 + blob_id: 1 + name: parent-project-1.0-20250503.071335-3.pom + lower_name: parent-project-1.0-20250503.071335-3.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256415 +- id: 37 + version_id: 2 + blob_id: 3 + name: sub-module-1.0-20250503.071335-3.pom + lower_name: sub-module-1.0-20250503.071335-3.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256416 +- id: 38 + version_id: 2 + blob_id: 30 + name: sub-module-1.0-20250503.071335-3.jar + lower_name: sub-module-1.0-20250503.071335-3.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256416 +- id: 40 + version_id: 14 + blob_id: 32 + name: parent-project-9.0.0.pom + lower_name: parent-project-9.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256419 +- id: 41 + version_id: 15 + blob_id: 33 + name: sub-module-9.0.0.pom + lower_name: sub-module-9.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256420 +- id: 42 + version_id: 15 + blob_id: 34 + name: sub-module-9.0.0.jar + lower_name: sub-module-9.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256420 +- id: 43 + version_id: 16 + blob_id: 35 + name: parent-project-9.0.0.pom + lower_name: parent-project-9.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256423 +- id: 44 + version_id: 17 + blob_id: 33 + name: sub-module-9.0.0.pom + lower_name: sub-module-9.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256424 +- id: 45 + version_id: 17 + blob_id: 36 + name: sub-module-9.0.0.jar + lower_name: sub-module-9.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256424 +- id: 46 + version_id: 7 + blob_id: 11 + name: bar-1.0-20250503.071347-5.pom + lower_name: bar-1.0-20250503.071347-5.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256427 +- id: 48 + version_id: 18 + blob_id: 38 + name: bar-9.0.0.pom + lower_name: bar-9.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256429 +- id: 49 + version_id: 7 + blob_id: 14 + name: -bar-1.0-20250503.071353-6.pom + lower_name: -bar-1.0-20250503.071353-6.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256432 +- id: 51 + version_id: 18 + blob_id: 40 + name: -bar-9.0.0.pom + lower_name: -bar-9.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256435 +- id: 52 + version_id: 1 + blob_id: 1 + name: parent-project-1.0-20250503.071405-4.pom + lower_name: parent-project-1.0-20250503.071405-4.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256445 +- id: 54 + version_id: 2 + blob_id: 3 + name: sub-module-1.0-20250503.071405-4.pom + lower_name: sub-module-1.0-20250503.071405-4.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256446 +- id: 55 + version_id: 2 + blob_id: 42 + name: sub-module-1.0-20250503.071405-4.jar + lower_name: sub-module-1.0-20250503.071405-4.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256446 +- id: 57 + version_id: 19 + blob_id: 44 + name: parent-project-10.0.0.pom + lower_name: parent-project-10.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256449 +- id: 58 + version_id: 20 + blob_id: 45 + name: sub-module-10.0.0.pom + lower_name: sub-module-10.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256450 +- id: 59 + version_id: 20 + blob_id: 46 + name: sub-module-10.0.0.jar + lower_name: sub-module-10.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256450 +- id: 60 + version_id: 21 + blob_id: 47 + name: parent-project-10.0.0.pom + lower_name: parent-project-10.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256452 +- id: 61 + version_id: 22 + blob_id: 45 + name: sub-module-10.0.0.pom + lower_name: sub-module-10.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256453 +- id: 62 + version_id: 22 + blob_id: 48 + name: sub-module-10.0.0.jar + lower_name: sub-module-10.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256453 +- id: 63 + version_id: 7 + blob_id: 11 + name: bar-1.0-20250503.071416-7.pom + lower_name: bar-1.0-20250503.071416-7.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256456 +- id: 65 + version_id: 23 + blob_id: 50 + name: bar-10.0.0.pom + lower_name: bar-10.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256459 +- id: 66 + version_id: 7 + blob_id: 14 + name: -bar-1.0-20250503.071422-8.pom + lower_name: -bar-1.0-20250503.071422-8.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256461 +- id: 68 + version_id: 23 + blob_id: 52 + name: -bar-10.0.0.pom + lower_name: -bar-10.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256464 +- id: 69 + version_id: 1 + blob_id: 1 + name: parent-project-1.0-20250503.071435-5.pom + lower_name: parent-project-1.0-20250503.071435-5.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256474 +- id: 70 + version_id: 1 + blob_id: 53 + name: maven-metadata.xml + lower_name: maven-metadata.xml + composite_key: "" + is_lead: 0 + created_unix: 1746256474 +- id: 71 + version_id: 2 + blob_id: 3 + name: sub-module-1.0-20250503.071435-5.pom + lower_name: sub-module-1.0-20250503.071435-5.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256475 +- id: 72 + version_id: 2 + blob_id: 54 + name: sub-module-1.0-20250503.071435-5.jar + lower_name: sub-module-1.0-20250503.071435-5.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256475 +- id: 73 + version_id: 2 + blob_id: 55 + name: maven-metadata.xml + lower_name: maven-metadata.xml + composite_key: "" + is_lead: 0 + created_unix: 1746256476 +- id: 74 + version_id: 24 + blob_id: 56 + name: parent-project-11.0.0.pom + lower_name: parent-project-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256478 +- id: 75 + version_id: 25 + blob_id: 57 + name: sub-module-11.0.0.pom + lower_name: sub-module-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256479 +- id: 76 + version_id: 25 + blob_id: 58 + name: sub-module-11.0.0.jar + lower_name: sub-module-11.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256479 +- id: 77 + version_id: 26 + blob_id: 59 + name: parent-project-11.0.0.pom + lower_name: parent-project-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256482 +- id: 78 + version_id: 27 + blob_id: 57 + name: sub-module-11.0.0.pom + lower_name: sub-module-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256483 +- id: 79 + version_id: 27 + blob_id: 60 + name: sub-module-11.0.0.jar + lower_name: sub-module-11.0.0.jar + composite_key: "" + is_lead: 0 + created_unix: 1746256483 +- id: 80 + version_id: 7 + blob_id: 11 + name: bar-1.0-20250503.071446-9.pom + lower_name: bar-1.0-20250503.071446-9.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256486 +- id: 82 + version_id: 28 + blob_id: 62 + name: bar-11.0.0.pom + lower_name: bar-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256488 +- id: 83 + version_id: 7 + blob_id: 14 + name: -bar-1.0-20250503.071451-10.pom + lower_name: -bar-1.0-20250503.071451-10.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256491 +- id: 84 + version_id: 7 + blob_id: 63 + name: maven-metadata.xml + lower_name: maven-metadata.xml + composite_key: "" + is_lead: 0 + created_unix: 1746256491 +- id: 85 + version_id: 28 + blob_id: 64 + name: -bar-11.0.0.pom + lower_name: -bar-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746256494 +# broken uploads +- id: 86 + version_id: 29 + blob_id: 75 + name: br-repo-jooq-1.2.4-sources.jar + lower_name: br-repo-jooq-1.2.4-sources.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649444986 +- id: 87 + version_id: 29 + blob_id: 65 + name: br-rest-webmvc-1.2.4.jar + lower_name: br-rest-webmvc-1.2.4.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649446161 +- id: 88 + version_id: 29 + blob_id: 68 + name: br-openapi-base-1.2.4.pom + lower_name: br-openapi-base-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649444734 +- id: 89 + version_id: 29 + blob_id: 69 + name: br-openapi-base-1.2.4.jar + lower_name: br-openapi-base-1.2.4.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649444746 +- id: 90 + version_id: 29 + blob_id: 70 + name: br-openapi-base-1.2.4-sources.jar + lower_name: br-openapi-base-1.2.4-sources.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649444775 +- id: 91 + version_id: 29 + blob_id: 78 + name: br-parent-1.2.4.pom + lower_name: br-parent-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649444852 +- id: 92 + version_id: 29 + blob_id: 76 + name: br-repo-in-memory-1.2.4.pom + lower_name: br-repo-in-memory-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649444900 +- id: 93 + version_id: 29 + blob_id: 73 + name: br-repo-in-memory-1.2.4.jar + lower_name: br-repo-in-memory-1.2.4.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649444911 +- id: 94 + version_id: 29 + blob_id: 77 + name: br-repo-in-memory-1.2.4-sources.jar + lower_name: br-repo-in-memory-1.2.4-sources.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649444922 +- id: 95 + version_id: 29 + blob_id: 74 + name: br-repo-jooq-1.2.4.pom + lower_name: br-repo-jooq-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649444953 +- id: 96 + version_id: 29 + blob_id: 67 + name: br-repo-jooq-1.2.4.jar + lower_name: br-repo-jooq-1.2.4.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649444969 +- id: 97 + version_id: 29 + blob_id: 71 + name: br-rest-webmvc-1.2.4-sources.jar + lower_name: br-rest-webmvc-1.2.4-sources.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649446161 +- id: 98 + version_id: 29 + blob_id: 66 + name: br-rest-webmvc-1.2.4.pom + lower_name: br-rest-webmvc-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649446195 +- id: 99 + version_id: 29 + blob_id: 72 + name: br-root-1.2.4.pom + lower_name: br-root-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649446217 +- id: 100 + version_id: 30 + blob_id: 66 + name: br-rest-webmvc-1.2.4.pom + lower_name: br-rest-webmvc-1.2.4.pom + composite_key: "" + is_lead: 1 + created_unix: 174625649446311 +- id: 101 + version_id: 30 + blob_id: 65 + name: br-rest-webmvc-1.2.4.jar + lower_name: br-rest-webmvc-1.2.4.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649446312 +- id: 102 + version_id: 30 + blob_id: 71 + name: br-rest-webmvc-1.2.4-sources.jar + lower_name: br-rest-webmvc-1.2.4-sources.jar + composite_key: "" + is_lead: 0 + created_unix: 174625649446312 +# collision +- id: 103 + version_id: 31 + blob_id: 79 + name: bar-art-11.0.0.pom + lower_name: bar-art-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746280832 +- id: 104 + version_id: 31 + blob_id: 80 + name: art-11.0.0.pom + lower_name: art-11.0.0.pom + composite_key: "" + is_lead: 1 + created_unix: 1746280843 diff --git a/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_version.yml b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_version.yml new file mode 100644 index 0000000000..dbdb427adf --- /dev/null +++ b/models/migrations/fixtures/Test_ChangeMavenArtifactConcatenation/package_version.yml @@ -0,0 +1,281 @@ +- id: 1 + package_id: 1 + creator_id: 1 + version: 1.0-SNAPSHOT + lower_version: 1.0-snapshot + created_unix: 1746256357 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 2 + package_id: 2 + creator_id: 1 + version: 1.0-SNAPSHOT + lower_version: 1.0-snapshot + created_unix: 1746256358 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 3 + package_id: 1 + creator_id: 1 + version: 7.0.0 + lower_version: 7.0.0 + created_unix: 1746256360 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 4 + package_id: 2 + creator_id: 1 + version: 7.0.0 + lower_version: 7.0.0 + created_unix: 1746256361 + is_internal: 0 + metadata_json: '{"artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 5 + package_id: 3 + creator_id: 1 + version: 7.0.0 + lower_version: 7.0.0 + created_unix: 1746256364 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 6 + package_id: 4 + creator_id: 1 + version: 7.0.0 + lower_version: 7.0.0 + created_unix: 1746256365 + is_internal: 0 + metadata_json: '{"artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 7 + package_id: 5 + creator_id: 1 + version: 1.0-SNAPSHOT + lower_version: 1.0-snapshot + created_unix: 1746256367 + is_internal: 0 + metadata_json: '{"group_id":"foo","artifact_id":"-bar","name":"Example :: Parent "}' + download_count: 0 +- id: 8 + package_id: 5 + creator_id: 1 + version: 7.0.0 + lower_version: 7.0.0 + created_unix: 1746256370 + is_internal: 0 + metadata_json: '{"group_id":"foo","artifact_id":"-bar","name":"Example :: Parent "}' + download_count: 0 +- id: 9 + package_id: 1 + creator_id: 1 + version: 8.0.0 + lower_version: 8.0.0 + created_unix: 1746256389 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 10 + package_id: 2 + creator_id: 1 + version: 8.0.0 + lower_version: 8.0.0 + created_unix: 1746256390 + is_internal: 0 + metadata_json: '{"artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 11 + package_id: 3 + creator_id: 1 + version: 8.0.0 + lower_version: 8.0.0 + created_unix: 1746256393 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 12 + package_id: 4 + creator_id: 1 + version: 8.0.0 + lower_version: 8.0.0 + created_unix: 1746256394 + is_internal: 0 + metadata_json: '{"artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 13 + package_id: 5 + creator_id: 1 + version: 8.0.0 + lower_version: 8.0.0 + created_unix: 1746256399 + is_internal: 0 + metadata_json: '{"group_id":"foo","artifact_id":"-bar","name":"Example :: Parent "}' + download_count: 0 +- id: 14 + package_id: 1 + creator_id: 1 + version: 9.0.0 + lower_version: 9.0.0 + created_unix: 1746256419 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 15 + package_id: 2 + creator_id: 1 + version: 9.0.0 + lower_version: 9.0.0 + created_unix: 1746256420 + is_internal: 0 + metadata_json: '{"artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 16 + package_id: 3 + creator_id: 1 + version: 9.0.0 + lower_version: 9.0.0 + created_unix: 1746256423 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 17 + package_id: 4 + creator_id: 1 + version: 9.0.0 + lower_version: 9.0.0 + created_unix: 1746256424 + is_internal: 0 + metadata_json: '{"artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 18 + package_id: 5 + creator_id: 1 + version: 9.0.0 + lower_version: 9.0.0 + created_unix: 1746256429 + is_internal: 0 + metadata_json: '{"group_id":"foo","artifact_id":"-bar","name":"Example :: Parent "}' + download_count: 0 +- id: 19 + package_id: 1 + creator_id: 1 + version: 10.0.0 + lower_version: 10.0.0 + created_unix: 1746256449 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 20 + package_id: 2 + creator_id: 1 + version: 10.0.0 + lower_version: 10.0.0 + created_unix: 1746256450 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 21 + package_id: 3 + creator_id: 1 + version: 10.0.0 + lower_version: 10.0.0 + created_unix: 1746256452 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 22 + package_id: 4 + creator_id: 1 + version: 10.0.0 + lower_version: 10.0.0 + created_unix: 1746256453 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 23 + package_id: 5 + creator_id: 1 + version: 10.0.0 + lower_version: 10.0.0 + created_unix: 1746256459 + is_internal: 0 + metadata_json: '{"group_id":"foo","artifact_id":"-bar","name":"Example :: Parent "}' + download_count: 0 +- id: 24 + package_id: 1 + creator_id: 1 + version: 11.0.0 + lower_version: 11.0.0 + created_unix: 1746256478 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 25 + package_id: 2 + creator_id: 1 + version: 11.0.0 + lower_version: 11.0.0 + created_unix: 1746256479 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 26 + package_id: 3 + creator_id: 1 + version: 11.0.0 + lower_version: 11.0.0 + created_unix: 1746256482 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"parent-project","name":"Example :: Parent "}' + download_count: 0 +- id: 27 + package_id: 4 + creator_id: 1 + version: 11.0.0 + lower_version: 11.0.0 + created_unix: 1746256483 + is_internal: 0 + metadata_json: '{"group_id":"com.example","artifact_id":"sub-module","name":"Example :: SubModule ","dependencies":[{"group_id":"junit","artifact_id":"junit","version":"3.8.1"}]}' + download_count: 0 +- id: 28 + package_id: 5 + creator_id: 1 + version: 11.0.0 + lower_version: 11.0.0 + created_unix: 1746256488 + is_internal: 0 + metadata_json: '{"group_id":"foo","artifact_id":"-bar","name":"Example :: Parent "}' + download_count: 0 +# broken uploads +- id: 29 + package_id: 7 + creator_id: 6 + version: 1.2.4 + lower_version: 1.2.4 + created_unix: 1746256488 + is_internal: 0 + metadata_json: '{"group_id":"com.broken","artifact_id":"br-root","name":"Foo"}' + download_count: 0 +- id: 30 + package_id: 6 + creator_id: 6 + version: 1.2.4 + lower_version: 1.2.4 + created_unix: 1746256488 + is_internal: 0 + metadata_json: '{"artifact_id":"br-rest-webmvc","name":"Foo :: REST :: Web MVC","dependencies":[{"group_id":"com.broken","artifact_id":"br-base"},{"group_id":"jakarta.servlet","artifact_id":"jakarta.servlet-api"},{"group_id":"jakarta.validation","artifact_id":"jakarta.validation-api"},{"group_id":"org.springframework","artifact_id":"spring-webmvc"},{"group_id":"org.springframework.hateoas","artifact_id":"spring-hateoas"},{"group_id":"org.springframework.boot","artifact_id":"spring-boot-starter-test"},{"group_id":"com.broken","artifact_id":"br-test"},{"group_id":"com.broken","artifact_id":"br-repo-in-memory"},{"group_id":"org.hibernate.validator","artifact_id":"hibernate-validator"},{"group_id":"org.glassfish.expressly","artifact_id":"expressly"}]}' + download_count: 0 +# collision +- id: 31 + package_id: 8 + creator_id: 1 + version: 11.0.0 + lower_version: 11.0.0 + created_unix: 1746256488 + is_internal: 0 + metadata_json: '{"group_id":"group-bar","artifact_id":"art","name":"Example :: Parent "}' + download_count: 0 diff --git a/models/migrations/fixtures/Test_SetTopicsAsEmptySlice/repository.yml b/models/migrations/fixtures/Test_SetTopicsAsEmptySlice/repository.yml new file mode 100644 index 0000000000..e37b0b25b5 --- /dev/null +++ b/models/migrations/fixtures/Test_SetTopicsAsEmptySlice/repository.yml @@ -0,0 +1,9 @@ +# type Repository struct { +# ID int64 `xorm:"pk autoincr"` +# Topics []string `xorm:"TEXT JSON"` +# } +- + id: 1 +- + id: 2 + topics: '["go", "dev"]' diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index aea9b593bd..2a5b97f519 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -6,6 +6,7 @@ package migrations import ( "context" + "errors" "fmt" "forgejo.org/models/db" @@ -412,7 +413,7 @@ func EnsureUpToDate(x *xorm.Engine) error { } if currentDB < 0 { - return fmt.Errorf("database has not been initialized") + return errors.New("database has not been initialized") } if minDBVersion > currentDB { diff --git a/models/migrations/v1_13/v151.go b/models/migrations/v1_13/v151.go index 60339962cb..ff584fff67 100644 --- a/models/migrations/v1_13/v151.go +++ b/models/migrations/v1_13/v151.go @@ -5,6 +5,7 @@ package v1_13 //nolint import ( "context" + "errors" "fmt" "strings" @@ -83,7 +84,7 @@ func SetDefaultPasswordToArgon2(x *xorm.Engine) error { newTableColumns := table.Columns() if len(newTableColumns) == 0 { - return fmt.Errorf("no columns in new table") + return errors.New("no columns in new table") } hasID := false for _, column := range newTableColumns { diff --git a/models/migrations/v1_14/v158.go b/models/migrations/v1_14/v158.go index 9849d5a9ea..3fa27cfecd 100644 --- a/models/migrations/v1_14/v158.go +++ b/models/migrations/v1_14/v158.go @@ -4,7 +4,7 @@ package v1_14 //nolint import ( - "fmt" + "errors" "strconv" "forgejo.org/modules/log" @@ -72,7 +72,7 @@ func UpdateCodeCommentReplies(x *xorm.Engine) error { case setting.Database.Type.IsSQLite3(): sqlCmd = sqlSelect + sqlTail + " LIMIT " + strconv.Itoa(batchSize) + " OFFSET " + strconv.Itoa(start) default: - return fmt.Errorf("Unsupported database type") + return errors.New("Unsupported database type") } if err := sess.SQL(sqlCmd).Find(&comments); err != nil { diff --git a/models/migrations/v1_14/v177_test.go b/models/migrations/v1_14/v177_test.go index 199a71186a..bffc6f92e3 100644 --- a/models/migrations/v1_14/v177_test.go +++ b/models/migrations/v1_14/v177_test.go @@ -73,12 +73,12 @@ func Test_DeleteOrphanedIssueLabels(t *testing.T) { // Now test what is left if _, ok := postMigration[2]; ok { - t.Errorf("Orphaned Label[2] survived the migration") + t.Error("Orphaned Label[2] survived the migration") return } if _, ok := postMigration[5]; ok { - t.Errorf("Orphaned Label[5] survived the migration") + t.Error("Orphaned Label[5] survived the migration") return } diff --git a/models/migrations/v1_17/v222.go b/models/migrations/v1_17/v222.go index c9a33f007d..ae910cbcb6 100644 --- a/models/migrations/v1_17/v222.go +++ b/models/migrations/v1_17/v222.go @@ -5,6 +5,7 @@ package v1_17 //nolint import ( "context" + "errors" "fmt" "forgejo.org/models/migrations/base" @@ -29,7 +30,7 @@ func DropOldCredentialIDColumn(x *xorm.Engine) error { } if !credentialIDBytesExists { // looks like 221 hasn't properly run - return fmt.Errorf("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration") + return errors.New("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration") } // Create webauthnCredential table diff --git a/models/migrations/v1_21/v264.go b/models/migrations/v1_21/v264.go index 88eaf0d918..5615600072 100644 --- a/models/migrations/v1_21/v264.go +++ b/models/migrations/v1_21/v264.go @@ -5,7 +5,7 @@ package v1_21 //nolint import ( "context" - "fmt" + "errors" "forgejo.org/models/db" "forgejo.org/modules/timeutil" @@ -57,7 +57,7 @@ func AddBranchTable(x *xorm.Engine) error { if err != nil { return err } else if !has { - return fmt.Errorf("no admin user found") + return errors.New("no admin user found") } branches := make([]Branch, 0, 100) diff --git a/models/organization/TestFindOrgs/org_user.yml b/models/organization/TestFindOrgs/org_user.yml new file mode 100644 index 0000000000..79b6fc613e --- /dev/null +++ b/models/organization/TestFindOrgs/org_user.yml @@ -0,0 +1,5 @@ +- + id: 1000 + uid: 4 + org_id: 22 + is_public: true diff --git a/models/organization/org_list.go b/models/organization/org_list.go index 4bca3cdb99..e387936473 100644 --- a/models/organization/org_list.go +++ b/models/organization/org_list.go @@ -26,6 +26,7 @@ type SearchOrganizationsOptions struct { type FindOrgOptions struct { db.ListOptions UserID int64 + IncludeLimited bool IncludePrivate bool } @@ -43,7 +44,11 @@ func (opts FindOrgOptions) ToConds() builder.Cond { cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) } if !opts.IncludePrivate { - cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) + if !opts.IncludeLimited { + cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) + } else { + cond = cond.And(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) + } } return cond } diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go index 780616b71f..170e2bf131 100644 --- a/models/organization/org_list_test.go +++ b/models/organization/org_list_test.go @@ -27,6 +27,7 @@ func TestCountOrganizations(t *testing.T) { } func TestFindOrgs(t *testing.T) { + defer unittest.OverrideFixtures("models/organization/TestFindOrgs")() require.NoError(t, unittest.PrepareTestDatabase()) orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ @@ -34,8 +35,14 @@ func TestFindOrgs(t *testing.T) { IncludePrivate: true, }) require.NoError(t, err) - if assert.Len(t, orgs, 1) { - assert.EqualValues(t, 3, orgs[0].ID) + if assert.Len(t, orgs, 2) { + if orgs[0].ID == 22 { + assert.EqualValues(t, 22, orgs[0].ID) + assert.EqualValues(t, 3, orgs[1].ID) + } else { + assert.EqualValues(t, 3, orgs[0].ID) + assert.EqualValues(t, 22, orgs[1].ID) + } } orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ @@ -50,6 +57,14 @@ func TestFindOrgs(t *testing.T) { IncludePrivate: true, }) require.NoError(t, err) + assert.EqualValues(t, 2, total) + + total, err = db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ + UserID: 4, + IncludePrivate: false, + IncludeLimited: true, + }) + require.NoError(t, err) assert.EqualValues(t, 1, total) } diff --git a/models/organization/team.go b/models/organization/team.go index 450082077f..c78eff39fb 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -292,3 +292,11 @@ func FixInconsistentOwnerTeams(ctx context.Context) (int64, error) { return int64(len(teamIDs)), nil } + +func NewGhostTeam() *Team { + return &Team{ + ID: -1, + Name: "Ghost team", + LowerName: "ghost team", + } +} diff --git a/models/packages/package.go b/models/packages/package.go index 3b01d0b1ea..bdd1c74cad 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -201,6 +201,15 @@ type Package struct { IsInternal bool `xorm:"NOT NULL DEFAULT false"` } +func ResolvePackageName(name string, t Type) string { + switch t { + case TypeMaven: + return name + default: + return strings.ToLower(name) + } +} + // TryInsertPackage inserts a package. If a package exists already, ErrDuplicatePackage is returned func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) { e := db.GetEngine(ctx) @@ -272,7 +281,7 @@ func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name var cond builder.Cond = builder.Eq{ "package.owner_id": ownerID, "package.type": packageType, - "package.lower_name": strings.ToLower(name), + "package.lower_name": ResolvePackageName(name, packageType), "package.is_internal": false, } diff --git a/models/packages/package_file.go b/models/packages/package_file.go index d4bcc2859a..5034e1f29d 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -87,8 +87,8 @@ func GetFileForVersionByID(ctx context.Context, versionID, fileID int64) (*Packa return pf, nil } -// GetFileForVersionByName gets a file of a version by name -func GetFileForVersionByName(ctx context.Context, versionID int64, name, key string) (*PackageFile, error) { +// GetFileForVersionByNameMatchCase gets a file of a version by name +func GetFileForVersionByNameMatchCase(ctx context.Context, versionID int64, name, key string) (*PackageFile, error) { if name == "" { return nil, ErrPackageFileNotExist } @@ -97,7 +97,7 @@ func GetFileForVersionByName(ctx context.Context, versionID int64, name, key str has, err := db.GetEngine(ctx).Where(builder.Eq{ "version_id": versionID, - "lower_name": strings.ToLower(name), + "lower_name": name, "composite_key": key, }).Get(pf) if err != nil { @@ -109,6 +109,11 @@ func GetFileForVersionByName(ctx context.Context, versionID int64, name, key str return pf, nil } +// GetFileForVersionByName gets a file of a version by name +func GetFileForVersionByName(ctx context.Context, versionID int64, name, key string) (*PackageFile, error) { + return GetFileForVersionByNameMatchCase(ctx, versionID, strings.ToLower(name), key) +} + // DeleteFileByID deletes a file func DeleteFileByID(ctx context.Context, fileID int64) error { _, err := db.GetEngine(ctx).ID(fileID).Delete(&PackageFile{}) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 7d1f664b62..87a97143f3 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -211,10 +211,13 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond { cond = cond.And(builder.Eq{"package.id": opts.PackageID}) } if opts.Name.Value != "" { + // potential drawback if Type is all / undefined + name := ResolvePackageName(opts.Name.Value, opts.Type) + if opts.Name.ExactMatch { - cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)}) + cond = cond.And(builder.Eq{"package.lower_name": name}) } else { - cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)}) + cond = cond.And(builder.Like{"package.lower_name", name}) } } if opts.Version.Value != "" { diff --git a/models/project/column.go b/models/project/column.go index 52917cb9fd..20de39357b 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -145,7 +145,7 @@ func NewColumn(ctx context.Context, column *Column) error { return err } if res.ColumnCount >= maxProjectColumns { - return fmt.Errorf("NewBoard: maximum number of columns reached") + return errors.New("NewBoard: maximum number of columns reached") } column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) _, err := db.GetEngine(ctx).Insert(column) @@ -170,7 +170,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error { } if column.Default { - return fmt.Errorf("deleteColumnByID: cannot delete default column") + return errors.New("deleteColumnByID: cannot delete default column") } // move all issues to the default column diff --git a/models/project/issue.go b/models/project/issue.go index 9e9db19004..d404033446 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -5,7 +5,7 @@ package project import ( "context" - "fmt" + "errors" "forgejo.org/models/db" "forgejo.org/modules/log" @@ -73,7 +73,7 @@ func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueI return err } if int(count) != len(sortedIssueIDs) { - return fmt.Errorf("all issues have to be added to a project first") + return errors.New("all issues have to be added to a project first") } for sorting, issueID := range sortedIssueIDs { @@ -88,7 +88,7 @@ func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueI func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error { if c.ProjectID != newColumn.ProjectID { - return fmt.Errorf("columns have to be in the same project") + return errors.New("columns have to be in the same project") } if c.ID == newColumn.ID { diff --git a/models/pull/automerge.go b/models/pull/automerge.go index 63f572309b..dcc1f39271 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -10,6 +10,7 @@ import ( "forgejo.org/models/db" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/log" "forgejo.org/modules/timeutil" ) @@ -58,13 +59,15 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, return ErrAlreadyScheduledToAutoMerge{PullID: pullID} } - _, err := db.GetEngine(ctx).Insert(&AutoMerge{ + scheduledPRM, err := db.GetEngine(ctx).Insert(&AutoMerge{ DoerID: doer.ID, PullID: pullID, MergeStyle: style, Message: message, DeleteBranchAfterMerge: deleteBranch, }) + log.Trace("ScheduleAutoMerge %+v for PR %d", scheduledPRM, pullID) + return err } @@ -81,6 +84,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe return false, nil, err } + log.Trace("GetScheduledMergeByPullID found %+v for PR %d", scheduledPRM, pullID) + scheduledPRM.Doer = doer return true, scheduledPRM, nil } @@ -94,6 +99,8 @@ func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error { return db.ErrNotExist{Resource: "auto_merge", ID: pullID} } + log.Trace("DeleteScheduledAutoMerge %+v for PR %d", scheduledPRM, pullID) + _, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{}) return err } diff --git a/models/quota/main_test.go b/models/quota/main_test.go new file mode 100644 index 0000000000..ec0a0e0013 --- /dev/null +++ b/models/quota/main_test.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package quota + +import ( + "testing" + + "forgejo.org/models/unittest" + + _ "forgejo.org/models" + _ "forgejo.org/models/actions" + _ "forgejo.org/models/activities" + _ "forgejo.org/models/forgefed" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/models/quota/used.go b/models/quota/used.go index 4a1bc84c0a..22815165f6 100644 --- a/models/quota/used.go +++ b/models/quota/used.go @@ -131,7 +131,8 @@ func createQueryFor(ctx context.Context, userID int64, q string) db.Engine { case "artifacts": session = session. Table("action_artifact"). - Join("INNER", "`repository`", "`action_artifact`.repo_id = `repository`.id") + Join("INNER", "`repository`", "`action_artifact`.repo_id = `repository`.id"). + Where("`action_artifact`.status != ?", action_model.ArtifactStatusExpired) case "packages": session = session. Table("package_version"). diff --git a/models/quota/used_test.go b/models/quota/used_test.go new file mode 100644 index 0000000000..82cc5b9bcc --- /dev/null +++ b/models/quota/used_test.go @@ -0,0 +1,23 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package quota + +import ( + "testing" + + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetUsedForUser(t *testing.T) { + defer unittest.OverrideFixtures("models/fixtures/TestGetUsedForUser/")() + require.NoError(t, unittest.PrepareTestDatabase()) + + used, err := GetUsedForUser(t.Context(), 5) + require.NoError(t, err) + + assert.EqualValues(t, 4096, used.Size.Assets.Artifacts) +} diff --git a/models/repo/TestSearchRepositoryIDsByCondition/repository.yml b/models/repo/TestSearchRepositoryIDsByCondition/repository.yml index 9ce830783d..b10fbc9226 100644 --- a/models/repo/TestSearchRepositoryIDsByCondition/repository.yml +++ b/models/repo/TestSearchRepositoryIDsByCondition/repository.yml @@ -28,3 +28,4 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' diff --git a/models/repo/attachment.go b/models/repo/attachment.go index 3bf51e80ca..6d903be5f8 100644 --- a/models/repo/attachment.go +++ b/models/repo/attachment.go @@ -5,6 +5,7 @@ package repo import ( "context" + "errors" "fmt" "net/url" "path" @@ -232,9 +233,9 @@ func DeleteAttachmentsByComment(ctx context.Context, commentID int64, remove boo // UpdateAttachmentByUUID Updates attachment via uuid func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...string) error { if attach.UUID == "" { - return fmt.Errorf("attachment uuid should be not blank") + return errors.New("attachment uuid should be not blank") } - if attach.ExternalURL != "" && !validation.IsValidExternalURL(attach.ExternalURL) { + if attach.ExternalURL != "" && !validation.IsValidReleaseAssetURL(attach.ExternalURL) { return ErrInvalidExternalURL{ExternalURL: attach.ExternalURL} } _, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach) @@ -243,7 +244,7 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str // UpdateAttachment updates the given attachment in database func UpdateAttachment(ctx context.Context, atta *Attachment) error { - if atta.ExternalURL != "" && !validation.IsValidExternalURL(atta.ExternalURL) { + if atta.ExternalURL != "" && !validation.IsValidReleaseAssetURL(atta.ExternalURL) { return ErrInvalidExternalURL{ExternalURL: atta.ExternalURL} } sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count") diff --git a/models/repo/following_repo_test.go b/models/repo/following_repo_test.go index cff125dabe..4bac23e77a 100644 --- a/models/repo/following_repo_test.go +++ b/models/repo/following_repo_test.go @@ -26,6 +26,6 @@ func Test_FollowingRepoValidation(t *testing.T) { URI: "http://localhost:3000/api/v1/activitypub/repo-id/1", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid") + t.Error("sut should be invalid") } } diff --git a/models/repo/pushmirror_test.go b/models/repo/pushmirror_test.go index c11cbd26ef..fbef835372 100644 --- a/models/repo/pushmirror_test.go +++ b/models/repo/pushmirror_test.go @@ -40,8 +40,6 @@ func TestPushMirrorsIterate(t *testing.T) { Interval: 0, }) - time.Sleep(1 * time.Millisecond) - repo_model.PushMirrorsIterate(db.DefaultContext, 1, func(idx int, bean any) error { m, ok := bean.(*repo_model.PushMirror) assert.True(t, ok) diff --git a/models/repo/repo.go b/models/repo/repo.go index 9db1fd9039..688c03b3d5 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -6,6 +6,7 @@ package repo import ( "context" + "errors" "fmt" "html/template" "net" @@ -182,7 +183,7 @@ type Repository struct { StatsIndexerStatus *RepoIndexerStatus `xorm:"-"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` - Topics []string `xorm:"TEXT JSON"` + Topics []string `xorm:"TEXT JSON NOT NULL"` ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"` TrustModel TrustModelType @@ -195,6 +196,13 @@ type Repository struct { ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"` } +// BeforeInsert will be invoked by XORM before updating a record +func (repo *Repository) BeforeInsert() { + if repo.Topics == nil { + repo.Topics = []string{} + } +} + func init() { db.RegisterModel(new(Repository)) } @@ -820,7 +828,7 @@ func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error pathSegments := getRepositoryURLPathSegments(repoURL) if len(pathSegments) != 2 { - return nil, fmt.Errorf("unknown or malformed repository URL") + return nil, errors.New("unknown or malformed repository URL") } ownerName := pathSegments[0] diff --git a/models/unit/unit.go b/models/unit/unit.go index 6251d44c9b..a14f3ff364 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -1,4 +1,5 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package unit @@ -69,7 +70,7 @@ func (u Type) LogString() string { } var ( - // AllRepoUnitTypes contains all the unit types + // AllRepoUnitTypes contains all units AllRepoUnitTypes = []Type{ TypeCode, TypeIssues, @@ -83,7 +84,7 @@ var ( TypeActions, } - // DefaultRepoUnits contains the default unit types + // DefaultRepoUnits contains default units for regular repos DefaultRepoUnits = []Type{ TypeCode, TypeIssues, @@ -95,12 +96,22 @@ var ( TypeActions, } - // ForkRepoUnits contains the default unit types for forks + // ForkRepoUnits contains default units for forks DefaultForkRepoUnits = []Type{ TypeCode, TypePullRequests, } + // DefaultMirrorRepoUnits contains default units for mirrors + DefaultMirrorRepoUnits = []Type{ + TypeCode, + TypeIssues, + TypeReleases, + TypeWiki, + TypeProjects, + TypePackages, + } + // NotAllowedDefaultRepoUnits contains units that can't be default NotAllowedDefaultRepoUnits = []Type{ TypeExternalWiki, @@ -172,6 +183,8 @@ func LoadUnitConfig() error { if len(DefaultRepoUnits) == 0 { return errors.New("no default repository units found") } + + // Default fork repo units setDefaultForkRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultForkRepoUnits...) if len(invalidKeys) > 0 { log.Warn("Invalid keys in default fork repo units: %s", strings.Join(invalidKeys, ", ")) @@ -181,6 +194,16 @@ func LoadUnitConfig() error { return errors.New("no default fork repository units found") } + // Default mirror repo units + setDefaultMirrorRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultMirrorRepoUnits...) + if len(invalidKeys) > 0 { + log.Warn("Invalid keys in default mirror repo units: %s", strings.Join(invalidKeys, ", ")) + } + DefaultMirrorRepoUnits = validateDefaultRepoUnits(DefaultMirrorRepoUnits, setDefaultMirrorRepoUnits) + if len(DefaultMirrorRepoUnits) == 0 { + return errors.New("no default mirror repository units found") + } + // Collect the allowed repo unit groups. Mutually exclusive units are // grouped together. AllowedRepoUnitGroups = [][]Type{} diff --git a/models/user/federated_user.go b/models/user/federated_user.go index c1833c7de3..d2a9c34c9e 100644 --- a/models/user/federated_user.go +++ b/models/user/federated_user.go @@ -11,19 +11,21 @@ import ( type FederatedUser struct { ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` KeyID sql.NullString `xorm:"key_id UNIQUE"` PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"` - NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! + InboxPath string + NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! } -func NewFederatedUser(userID int64, externalID string, federationHostID int64, normalizedOriginalURL string) (FederatedUser, error) { +func NewFederatedUser(userID int64, externalID string, federationHostID int64, inboxPath, normalizedOriginalURL string) (FederatedUser, error) { result := FederatedUser{ UserID: userID, ExternalID: externalID, FederationHostID: federationHostID, + InboxPath: inboxPath, NormalizedOriginalURL: normalizedOriginalURL, } if valid, err := validation.IsValid(result); !valid { @@ -32,10 +34,11 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64, n return result, nil } -func (user FederatedUser) Validate() []string { +func (federatedUser FederatedUser) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...) - result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...) - result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.UserID, "UserID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.ExternalID, "ExternalID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.FederationHostID, "FederationHostID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.InboxPath, "InboxPath")...) return result } diff --git a/models/user/federated_user_follower.go b/models/user/federated_user_follower.go new file mode 100644 index 0000000000..db72c9b5ce --- /dev/null +++ b/models/user/federated_user_follower.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import "forgejo.org/modules/validation" + +type FederatedUserFollower struct { + ID int64 `xorm:"pk autoincr"` + FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` + FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` +} + +func NewFederatedUserFollower(followedUserID, federatedUserID int64) (FederatedUserFollower, error) { + result := FederatedUserFollower{ + FollowedUserID: followedUserID, + FollowingUserID: federatedUserID, + } + if valid, err := validation.IsValid(result); !valid { + return FederatedUserFollower{}, err + } + return result, nil +} + +func (user FederatedUserFollower) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(user.FollowedUserID, "FollowedUserID")...) + result = append(result, validation.ValidateNotEmpty(user.FollowingUserID, "FollowingUserID")...) + return result +} diff --git a/models/user/federated_user_follower_test.go b/models/user/federated_user_follower_test.go new file mode 100644 index 0000000000..e57ba01308 --- /dev/null +++ b/models/user/federated_user_follower_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "forgejo.org/modules/validation" + + "github.com/stretchr/testify/assert" +) + +func Test_FederatedUserFollowerValidation(t *testing.T) { + sut := FederatedUserFollower{ + FollowedUserID: 12, + FollowingUserID: 1, + } + res, err := validation.IsValid(sut) + assert.Truef(t, res, "sut should be valid but was %q", err) + + sut = FederatedUserFollower{ + FollowedUserID: 1, + } + res, _ = validation.IsValid(sut) + assert.False(t, res, "sut should be invalid") +} diff --git a/models/user/federated_user_test.go b/models/user/federated_user_test.go index 374236f6d3..be18339670 100644 --- a/models/user/federated_user_test.go +++ b/models/user/federated_user_test.go @@ -14,6 +14,7 @@ func Test_FederatedUserValidation(t *testing.T) { UserID: 12, ExternalID: "12", FederationHostID: 1, + InboxPath: "/api/v1/activitypub/user-id/12/inbox", } if res, err := validation.IsValid(sut); !res { t.Errorf("sut should be valid but was %q", err) @@ -22,8 +23,9 @@ func Test_FederatedUserValidation(t *testing.T) { sut = FederatedUser{ ExternalID: "12", FederationHostID: 1, + InboxPath: "/api/v1/activitypub/user-id/12/inbox", } if res, _ := validation.IsValid(sut); res { - t.Errorf("sut should be invalid") + t.Error("sut should be invalid") } } diff --git a/models/user/follow.go b/models/user/follow.go index 5be0f73c35..e32c226385 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -11,6 +11,7 @@ import ( ) // Follow represents relations of user and their followers. +// TODO: We should unify Activity-pub-following and classical following (see models/user/user_repository.go#IsFollowingAp) type Follow struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"UNIQUE(follow)"` diff --git a/models/user/setting.go b/models/user/setting.go index a915119ad2..e547d24e9f 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -5,6 +5,7 @@ package user import ( "context" + "errors" "fmt" "strings" @@ -114,10 +115,10 @@ func GetUserAllSettings(ctx context.Context, uid int64) (map[string]*Setting, er func validateUserSettingKey(key string) error { if len(key) == 0 { - return fmt.Errorf("setting key must be set") + return errors.New("setting key must be set") } if strings.ToLower(key) != key { - return fmt.Errorf("setting key should be lowercase") + return errors.New("setting key should be lowercase") } return nil } diff --git a/models/user/user.go b/models/user/user.go index d75fe56a20..eedd1db80e 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -15,6 +15,7 @@ import ( "net/url" "path/filepath" "regexp" + "runtime/trace" "strings" "time" "unicode" @@ -397,7 +398,8 @@ func (u *User) SetPassword(passwd string) (err error) { } // ValidatePassword checks if the given password matches the one belonging to the user. -func (u *User) ValidatePassword(passwd string) bool { +func (u *User) ValidatePassword(ctx context.Context, passwd string) bool { + defer trace.StartRegion(ctx, "Validate user password").End() return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt) } diff --git a/models/user/user_repository.go b/models/user/user_repository.go index 299d3af64a..3f24efb1fb 100644 --- a/models/user/user_repository.go +++ b/models/user/user_repository.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package user @@ -8,12 +8,14 @@ import ( "fmt" "forgejo.org/models/db" + "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/validation" ) func init() { db.RegisterModel(new(FederatedUser)) + db.RegisterModel(new(FederatedUserFollower)) } func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error { @@ -30,7 +32,12 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat if err != nil { return err } - defer committer.Close() + defer func() { + err := committer.Close() + if err != nil { + log.Error("Error closing committer: %v", err) + } + }() if err := CreateUser(ctx, user, &overwrite); err != nil { return err @@ -50,6 +57,14 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat return committer.Commit() } +func (federatedUser *FederatedUser) UpdateFederatedUser(ctx context.Context) error { + if _, err := validation.IsValid(federatedUser); err != nil { + return err + } + _, err := db.GetEngine(ctx).ID(federatedUser.ID).Cols("inbox_path").Update(federatedUser) + return err +} + func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { federatedUser := new(FederatedUser) user := new(User) @@ -75,6 +90,41 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID return user, federatedUser, nil } +func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { + user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID) + if err != nil { + return nil, nil, err + } else if federatedUser == nil { + return nil, nil, fmt.Errorf("FederatedUser for externalId = %v and federationHostId = %v does not exist", externalID, federationHostID) + } + return user, federatedUser, nil +} + +func GetFederatedUserByUserID(ctx context.Context, userID int64) (*User, *FederatedUser, error) { + federatedUser := new(FederatedUser) + user := new(User) + has, err := db.GetEngine(ctx).Where("user_id=?", userID).Get(federatedUser) + if err != nil { + return nil, nil, err + } else if !has { + return nil, nil, fmt.Errorf("Federated user %v does not exist", federatedUser.UserID) + } + has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user) + if err != nil { + return nil, nil, err + } else if !has { + return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID) + } + + if res, err := validation.IsValid(*user); !res { + return nil, nil, err + } + if res, err := validation.IsValid(*federatedUser); !res { + return nil, nil, err + } + return user, federatedUser, nil +} + func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *FederatedUser, error) { federatedUser := new(FederatedUser) user := new(User) @@ -101,7 +151,85 @@ func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *Federa return user, federatedUser, nil } +func UpdateFederatedUser(ctx context.Context, federatedUser *FederatedUser) error { + if res, err := validation.IsValid(federatedUser); !res { + return err + } + _, err := db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser) + return err +} + func DeleteFederatedUser(ctx context.Context, userID int64) error { _, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID}) return err } + +func GetFollowersForUser(ctx context.Context, user *User) ([]*FederatedUserFollower, error) { + if res, err := validation.IsValid(user); !res { + return nil, err + } + followers := make([]*FederatedUserFollower, 0, 8) + + err := db.GetEngine(ctx). + Where("followed_user_id = ?", user.ID). + Find(&followers) + if err != nil { + return nil, err + } + for _, element := range followers { + if res, err := validation.IsValid(*element); !res { + return nil, err + } + } + return followers, nil +} + +func AddFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) (*FederatedUserFollower, error) { + if res, err := validation.IsValid(followedUser); !res { + return nil, err + } + if res, err := validation.IsValid(followingUser); !res { + return nil, err + } + + federatedUserFollower, err := NewFederatedUserFollower(followedUser.ID, followingUser.UserID) + if err != nil { + return nil, err + } + _, err = db.GetEngine(ctx).Insert(&federatedUserFollower) + if err != nil { + return nil, err + } + + return &federatedUserFollower, err +} + +func RemoveFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) error { + if res, err := validation.IsValid(followedUser); !res { + return err + } + if res, err := validation.IsValid(followingUser); !res { + return err + } + + _, err := db.GetEngine(ctx).Delete(&FederatedUserFollower{ + FollowedUserID: followedUser.ID, + FollowingUserID: followingUser.UserID, + }) + return err +} + +// TODO: We should unify Activity-pub-following and classical following (see models/user/follow.go) +func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) { + if res, err := validation.IsValid(followedUser); !res { + return false, err + } + if res, err := validation.IsValid(followingUser); !res { + return false, err + } + + return db.GetEngine(ctx).Get(&FederatedUserFollower{ + FollowedUserID: followedUser.ID, + FollowingUserID: followingUser.UserID, + }) +} diff --git a/models/user/user_system.go b/models/user/user_system.go index 82805cc8ee..11f54591b7 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -12,6 +12,13 @@ import ( "forgejo.org/modules/structs" ) +// IsSystem returns true if the user has a fixed +// negative ID, is never stored in the database and +// is generated on the fly when needed. +func (u *User) IsSystem() bool { + return u.IsGhost() || u.IsActions() +} + const ( GhostUserID = -1 GhostUserName = "Ghost" diff --git a/models/user/user_test.go b/models/user/user_test.go index 7263f9510c..fd9d05653f 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -148,7 +148,7 @@ func TestAPActorID_APActorID(t *testing.T) { assert.Equal(t, expected, url) } -func TestAPActorKeyID(t *testing.T) { +func TestKeyID(t *testing.T) { user := user_model.User{ID: 1} url := user.APActorKeyID() expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key" @@ -267,7 +267,7 @@ func TestHashPasswordDeterministic(t *testing.T) { r2 := u.Passwd assert.NotEqual(t, r1, r2) - assert.True(t, u.ValidatePassword(pass)) + assert.True(t, u.ValidatePassword(t.Context(), pass)) } } } @@ -324,7 +324,7 @@ func TestCreateUserInvalidEmail(t *testing.T) { err := user_model.CreateUser(db.DefaultContext, user) require.Error(t, err) - assert.True(t, validation.IsErrEmailCharIsNotSupported(err)) + assert.True(t, validation.IsErrEmailInvalid(err)) } func TestCreateUserEmailAlreadyUsed(t *testing.T) { @@ -616,11 +616,8 @@ func TestGetAllAdmins(t *testing.T) { } func Test_ValidateUser(t *testing.T) { - oldSetting := setting.Service.AllowedUserVisibilityModesSlice - defer func() { - setting.Service.AllowedUserVisibilityModesSlice = oldSetting - }() - setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true} + defer test.MockVariableValue(&setting.Service.AllowedUserVisibilityModesSlice, []bool{true, false, true})() + kases := map[*user_model.User]bool{ {ID: 1, Visibility: structs.VisibleTypePublic}: true, {ID: 2, Visibility: structs.VisibleTypeLimited}: false, @@ -632,11 +629,8 @@ func Test_ValidateUser(t *testing.T) { } func Test_NormalizeUserFromEmail(t *testing.T) { - oldSetting := setting.Service.AllowDotsInUsernames - defer func() { - setting.Service.AllowDotsInUsernames = oldSetting - }() - setting.Service.AllowDotsInUsernames = true + defer test.MockVariableValue(&setting.Service.AllowDotsInUsernames, true)() + testCases := []struct { Input string Expected string @@ -702,12 +696,7 @@ func TestDisabledUserFeatures(t *testing.T) { testValues := container.SetOf(setting.UserFeatureDeletion, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) - - oldSetting := setting.Admin.ExternalUserDisableFeatures - defer func() { - setting.Admin.ExternalUserDisableFeatures = oldSetting - }() - setting.Admin.ExternalUserDisableFeatures = testValues + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, testValues)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 356f5fdcc9..1f81caf424 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -299,6 +299,24 @@ func (w *Webhook) HasPackageEvent() bool { (w.ChooseEvents && w.Package) } +// HasActionRunFailureEvent returns if hook enabled action failure event. +func (w *Webhook) HasActionRunFailureEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.ActionRunFailure) +} + +// HasActionRunRecoverEvent returns if hook enabled action recover event. +func (w *Webhook) HasActionRunRecoverEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.ActionRunRecover) +} + +// HasActionRunSuccessEvent returns if hook enabled action success event. +func (w *Webhook) HasActionRunSuccessEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.ActionRunSuccess) +} + // HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event. func (w *Webhook) HasPullRequestReviewRequestEvent() bool { return w.SendEverything || @@ -337,6 +355,9 @@ func (w *Webhook) EventCheckers() []struct { {w.HasReleaseEvent, webhook_module.HookEventRelease}, {w.HasPackageEvent, webhook_module.HookEventPackage}, {w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest}, + {w.HasActionRunFailureEvent, webhook_module.HookEventActionRunFailure}, + {w.HasActionRunRecoverEvent, webhook_module.HookEventActionRunRecover}, + {w.HasActionRunSuccessEvent, webhook_module.HookEventActionRunSuccess}, } } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index e70c3b2557..60cd2b333b 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -74,7 +74,8 @@ func TestWebhook_EventsArray(t *testing.T) { "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release", - "package", "pull_request_review_request", + "package", "pull_request_review_request", "action_run_failure", + "action_run_recover", "action_run_success", }, (&Webhook{ HookEvent: &webhook_module.HookEvent{SendEverything: true}, @@ -89,15 +90,78 @@ func TestWebhook_EventsArray(t *testing.T) { } func TestCreateWebhook(t *testing.T) { - hook := &Webhook{ - RepoID: 3, - URL: "https://www.example.com/unit_test", - ContentType: ContentTypeJSON, - Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`, - } - unittest.AssertNotExistsBean(t, hook) - require.NoError(t, CreateWebhook(db.DefaultContext, hook)) - unittest.AssertExistsAndLoadBean(t, hook) + t.Run("Some chosen events 1", func(t *testing.T) { + hook := &Webhook{ + RepoID: 3, + URL: "https://www.example.com/unit_test", + ContentType: ContentTypeJSON, + Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"create":false,"push":true,"pull_request":true}}`, + } + unittest.AssertNotExistsBean(t, hook) + require.NoError(t, CreateWebhook(db.DefaultContext, hook)) + hookFromDb := unittest.AssertExistsAndLoadBean(t, hook) + assert.Equal(t, []string{ + string(webhook_module.HookEventPush), + string(webhook_module.HookEventPullRequest), + }, hookFromDb.EventsArray()) + }) + + t.Run("Some chosen events 2", func(t *testing.T) { + hook := &Webhook{ + RepoID: 3, + URL: "https://www.example.com/unit_test", + ContentType: ContentTypeJSON, + Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"action_run_recover":false,"action_run_success":true}}`, + } + unittest.AssertNotExistsBean(t, hook) + require.NoError(t, CreateWebhook(db.DefaultContext, hook)) + hookFromDb := unittest.AssertExistsAndLoadBean(t, hook) + assert.Equal(t, []string{string(webhook_module.HookEventActionRunSuccess)}, hookFromDb.EventsArray()) + }) + + t.Run("All events", func(t *testing.T) { + hook := &Webhook{ + RepoID: 3, + URL: "https://www.example.com/unit_test", + ContentType: ContentTypeJSON, + Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"create":true,"delete":true,"fork":true,"issues":true,"issue_assign":true,"issue_label":true,"issue_milestone":true,"issue_comment":true,"push":true,"pull_request":true,"pull_request_assign":true,"pull_request_label":true,"pull_request_milestone":true,"pull_request_comment":true,"pull_request_review":true,"pull_request_sync":true,"pull_request_review_request":true,"wiki":true,"repository":true,"release":true,"package":true,"action_run_failure":true,"action_run_recover":true,"action_run_success":true}}`, + } + unittest.AssertNotExistsBean(t, hook) + require.NoError(t, CreateWebhook(db.DefaultContext, hook)) + hookFromDb := unittest.AssertExistsAndLoadBean(t, hook) + assert.Equal(t, []string{ + string(webhook_module.HookEventCreate), + string(webhook_module.HookEventDelete), + string(webhook_module.HookEventFork), + string(webhook_module.HookEventPush), + string(webhook_module.HookEventIssues), + string(webhook_module.HookEventIssueAssign), + string(webhook_module.HookEventIssueLabel), + string(webhook_module.HookEventIssueMilestone), + string(webhook_module.HookEventIssueComment), + string(webhook_module.HookEventPullRequest), + string(webhook_module.HookEventPullRequestAssign), + string(webhook_module.HookEventPullRequestLabel), + string(webhook_module.HookEventPullRequestMilestone), + string(webhook_module.HookEventPullRequestComment), + string(webhook_module.HookEventPullRequestReviewApproved), + string(webhook_module.HookEventPullRequestReviewRejected), + string(webhook_module.HookEventPullRequestReviewComment), + string(webhook_module.HookEventPullRequestSync), + string(webhook_module.HookEventWiki), + string(webhook_module.HookEventRepository), + string(webhook_module.HookEventRelease), + string(webhook_module.HookEventPackage), + string(webhook_module.HookEventPullRequestReviewRequest), + // these aren't webhook event types + // string(webhook_module.HookEventSchedule), + // string(webhook_module.HookEventWorkflowDispatch), + string(webhook_module.HookEventActionRunFailure), + string(webhook_module.HookEventActionRunRecover), + string(webhook_module.HookEventActionRunSuccess), + }, + hookFromDb.EventsArray()) + }) } func TestGetWebhookByRepoID(t *testing.T) { diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 43948cce5c..7ae4557ed6 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -122,6 +122,14 @@ func DetectWorkflows( events, err := GetEventsFromContent(content) if err != nil { log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) + dwf := &DetectedWorkflow{ + EntryName: entry.Name(), + TriggerEvent: &jobparser.Event{ + Name: triggedEvent.Event(), + }, + Content: content, + } + workflows = append(workflows, dwf) continue } for _, evt := range events { @@ -315,6 +323,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ } case "paths": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) @@ -328,6 +340,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa } } case "paths-ignore": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index b85ed7fd56..9068ce31c3 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -150,6 +150,24 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on: workflow_dispatch", expected: true, }, + { + desc: "push to tag matches workflow with paths condition (should skip paths check)", + triggeredEvent: webhook_module.HookEventPush, + payload: &api.PushPayload{ + Ref: "refs/tags/v1.0.0", + Before: "0000000", + Commits: []*api.PayloadCommit{ + { + ID: "abcdef123456", + Added: []string{"src/main.go"}, + Message: "Release v1.0.0", + }, + }, + }, + commit: nil, + yamlOn: "on:\n push:\n paths:\n - src/**", + expected: true, + }, } for _, tc := range testCases { diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 8d54ae5e4a..48c6728f43 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -5,10 +5,10 @@ package assetfs import ( "context" + "errors" "fmt" "io" "io/fs" - "net/http" "os" "path/filepath" "slices" @@ -25,7 +25,7 @@ import ( // Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem type Layer struct { name string - fs http.FileSystem + fs fs.FS localPath string } @@ -34,10 +34,18 @@ func (l *Layer) Name() string { } // Open opens the named file. The caller is responsible for closing the file. -func (l *Layer) Open(name string) (http.File, error) { +func (l *Layer) Open(name string) (fs.File, error) { return l.fs.Open(name) } +func (l *Layer) ReadDir(name string) ([]fs.DirEntry, error) { + dirEntries, err := fs.ReadDir(l.fs, name) + if err != nil && errors.Is(err, fs.ErrNotExist) { + err = nil + } + return dirEntries, err +} + // Local returns a new Layer with the given name, it serves files from the given local path. func Local(name, base string, sub ...string) *Layer { // TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before @@ -48,11 +56,18 @@ func Local(name, base string, sub ...string) *Layer { panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err)) } root := util.FilePathJoinAbs(base, sub...) - return &Layer{name: name, fs: http.Dir(root), localPath: root} + fsRoot, err := os.OpenRoot(root) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil + } + panic(fmt.Sprintf("Unable to open layer %q", err)) + } + return &Layer{name: name, fs: fsRoot.FS(), localPath: root} } // Bindata returns a new Layer with the given name, it serves files from the given bindata asset. -func Bindata(name string, fs http.FileSystem) *Layer { +func Bindata(name string, fs fs.FS) *Layer { return &Layer{name: name, fs: fs} } @@ -65,11 +80,11 @@ type LayeredFS struct { // Layered returns a new LayeredFS with the given layers. The first layer is the top layer. func Layered(layers ...*Layer) *LayeredFS { - return &LayeredFS{layers: layers} + return &LayeredFS{layers: slices.DeleteFunc(layers, func(layer *Layer) bool { return layer == nil })} } // Open opens the named file. The caller is responsible for closing the file. -func (l *LayeredFS) Open(name string) (http.File, error) { +func (l *LayeredFS) Open(name string) (fs.File, error) { for _, layer := range l.layers { f, err := layer.Open(name) if err == nil || !os.IsNotExist(err) { @@ -102,29 +117,18 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { return nil, "", fs.ErrNotExist } -func shouldInclude(info fs.FileInfo, fileMode ...bool) bool { +func shouldInclude(info fs.DirEntry, fileMode ...bool) bool { if util.CommonSkip(info.Name()) { return false } if len(fileMode) == 0 { return true } else if len(fileMode) == 1 { - return fileMode[0] == !info.Mode().IsDir() + return fileMode[0] == !info.IsDir() } panic("too many arguments for fileMode in shouldInclude") } -func readDir(layer *Layer, name string) ([]fs.FileInfo, error) { - f, err := layer.Open(name) - if os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - defer f.Close() - return f.Readdir(-1) -} - // ListFiles lists files/directories in the given directory. The fileMode controls the returned files. // * omitted: all files and directories will be returned. // * true: only files will be returned. @@ -133,7 +137,7 @@ func readDir(layer *Layer, name string) ([]fs.FileInfo, error) { func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) { fileSet := make(container.Set[string]) for _, layer := range l.layers { - infos, err := readDir(layer, name) + infos, err := layer.ReadDir(name) if err != nil { return nil, err } @@ -162,7 +166,7 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err var list func(dir string) error list = func(dir string) error { for _, layer := range layers { - infos, err := readDir(layer, dir) + infos, err := layer.ReadDir(dir) if err != nil { return err } diff --git a/modules/auth/openid/discovery_cache_test.go b/modules/auth/openid/discovery_cache_test.go index 5a7f450937..77ce46c4d5 100644 --- a/modules/auth/openid/discovery_cache_test.go +++ b/modules/auth/openid/discovery_cache_test.go @@ -23,14 +23,14 @@ func (s *testDiscoveredInfo) OpLocalID() string { } func TestTimedDiscoveryCache(t *testing.T) { - dc := newTimedDiscoveryCache(1 * time.Second) + dc := newTimedDiscoveryCache(100 * time.Millisecond) // Put some initial values dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"}) // Make sure we can retrieve them if di := dc.Get("foo"); di == nil { - t.Errorf("Expected a result, got nil") + t.Error("Expected a result, got nil") } else if di.OpEndpoint() != "opEndpoint" || di.OpLocalID() != "opLocalID" || di.ClaimedID() != "claimedID" { t.Errorf("Expected opEndpoint opLocalID claimedID, got %v %v %v", di.OpEndpoint(), di.OpLocalID(), di.ClaimedID()) } @@ -41,9 +41,9 @@ func TestTimedDiscoveryCache(t *testing.T) { } // Sleep one second and try retrieve again - time.Sleep(1 * time.Second) + time.Sleep(100 * time.Millisecond) if di := dc.Get("foo"); di != nil { - t.Errorf("Expected a nil, got a result") + t.Error("Expected a nil, got a result") } } diff --git a/modules/auth/webauthn/webauthn_test.go b/modules/auth/webauthn/webauthn_test.go index 552b698984..5ad01de602 100644 --- a/modules/auth/webauthn/webauthn_test.go +++ b/modules/auth/webauthn/webauthn_test.go @@ -7,19 +7,19 @@ import ( "testing" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" ) func TestInit(t *testing.T) { - setting.Domain = "domain" - setting.AppName = "AppName" - setting.AppURL = "https://domain/" - rpOrigin := []string{"https://domain"} + defer test.MockVariableValue(&setting.Domain, "domain")() + defer test.MockVariableValue(&setting.AppName, "AppName")() + defer test.MockVariableValue(&setting.AppURL, "https://domain/")() Init() assert.Equal(t, setting.Domain, WebAuthn.Config.RPID) assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName) - assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins) + assert.Equal(t, []string{"https://domain"}, WebAuthn.Config.RPOrigins) } diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 40471565d6..13e8ec88e6 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -8,6 +8,7 @@ package identicon import ( "crypto/sha256" + "errors" "fmt" "image" "image/color" @@ -29,7 +30,7 @@ type Identicon struct { // fore all possible foreground colors. only one foreground color will be picked randomly for one image func New(size int, back color.Color, fore ...color.Color) (*Identicon, error) { if len(fore) == 0 { - return nil, fmt.Errorf("foreground is not set") + return nil, errors.New("foreground is not set") } if size < minImageSize { diff --git a/modules/cache/cache.go b/modules/cache/cache.go index 9ad4b5cd90..aa69a0d3a7 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -4,6 +4,7 @@ package cache import ( + "errors" "fmt" "strconv" "time" @@ -48,7 +49,7 @@ const ( func Test() (time.Duration, error) { if conn == nil { - return 0, fmt.Errorf("default cache not initialized") + return 0, errors.New("default cache not initialized") } testData := fmt.Sprintf("%x", make([]byte, 500)) @@ -63,10 +64,10 @@ func Test() (time.Duration, error) { } testVal := conn.Get(testCacheKey) if testVal == nil { - return 0, fmt.Errorf("expect cache hit but got none") + return 0, errors.New("expect cache hit but got none") } if testVal != testData { - return 0, fmt.Errorf("expect cache to return same value as stored but got other") + return 0, errors.New("expect cache to return same value as stored but got other") } return time.Since(start), nil diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go index f7bc400598..aa28c98452 100644 --- a/modules/cache/cache_test.go +++ b/modules/cache/cache_test.go @@ -4,7 +4,7 @@ package cache import ( - "fmt" + "errors" "testing" "time" @@ -45,7 +45,7 @@ func TestGetString(t *testing.T) { createTestCache() data, err := GetString("key", func() (string, error) { - return "", fmt.Errorf("some error") + return "", errors.New("some error") }) require.Error(t, err) assert.Empty(t, data) @@ -70,7 +70,7 @@ func TestGetString(t *testing.T) { assert.Equal(t, "some data", data) data, err = GetString("key", func() (string, error) { - return "", fmt.Errorf("some error") + return "", errors.New("some error") }) require.NoError(t, err) assert.Equal(t, "some data", data) @@ -81,7 +81,7 @@ func TestGetInt(t *testing.T) { createTestCache() data, err := GetInt("key", func() (int, error) { - return 0, fmt.Errorf("some error") + return 0, errors.New("some error") }) require.Error(t, err) assert.Equal(t, 0, data) @@ -106,7 +106,7 @@ func TestGetInt(t *testing.T) { assert.Equal(t, 100, data) data, err = GetInt("key", func() (int, error) { - return 0, fmt.Errorf("some error") + return 0, errors.New("some error") }) require.NoError(t, err) assert.Equal(t, 100, data) @@ -117,7 +117,7 @@ func TestGetInt64(t *testing.T) { createTestCache() data, err := GetInt64("key", func() (int64, error) { - return 0, fmt.Errorf("some error") + return 0, errors.New("some error") }) require.Error(t, err) assert.EqualValues(t, 0, data) @@ -142,7 +142,7 @@ func TestGetInt64(t *testing.T) { assert.EqualValues(t, 100, data) data, err = GetInt64("key", func() (int64, error) { - return 0, fmt.Errorf("some error") + return 0, errors.New("some error") }) require.NoError(t, err) assert.EqualValues(t, 100, data) diff --git a/modules/charset/ambiguous/ambiguous.json b/modules/charset/ambiguous/ambiguous.json index d0f69f6ae2..82bbc7da1c 100644 --- a/modules/charset/ambiguous/ambiguous.json +++ b/modules/charset/ambiguous/ambiguous.json @@ -1 +1 @@ -"{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,8218,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,8242,96,1370,96,1523,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71922,67,71913,67,65315,67,8557,67,8450,67,8493,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71919,87,71910,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,66293,90,71909,90,65338,90,8484,90,8488,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65297,49,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125],\"_default\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"cs\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"es\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"fr\":[65374,126,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ja\":[8211,45,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65292,44,65307,59],\"ko\":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"pt-BR\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"ru\":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],\"zh-hans\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41],\"zh-hant\":[8211,45,65374,126,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65307,59]}" \ No newline at end of file +"{\"_common\":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,1523,96,8242,96,1370,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,118002,50,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,118003,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,118004,52,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,118005,53,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,118006,54,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,118007,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,118008,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,118009,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,117974,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,117975,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71913,67,71922,67,65315,67,8557,67,8450,67,8493,67,117976,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,117977,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,117978,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,117979,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,117980,71,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,117981,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,117983,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,117984,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,118001,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,117982,108,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,117985,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,117986,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,117987,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,118000,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,117988,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,117989,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,117990,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,117991,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,117992,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,117993,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,117994,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,117995,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71910,87,71919,87,117996,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,117997,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,117998,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,71909,90,66293,90,65338,90,8484,90,8488,90,117999,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65283,35,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125,119846,109],\"_default\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"cs\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"de\":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"es\":[8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"fr\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"it\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"ja\":[8211,45,8218,44,65281,33,8216,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65292,44,65297,49,65307,59],\"ko\":[8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"pl\":[65374,126,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"pt-BR\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"qps-ploc\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"ru\":[65374,126,8218,44,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"tr\":[160,32,8211,45,65374,126,8218,44,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41,65292,44,65297,49,65307,59,65311,63],\"zh-hans\":[160,32,65374,126,8218,44,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65297,49],\"zh-hant\":[8211,45,65374,126,8218,44,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89]}" \ No newline at end of file diff --git a/modules/charset/ambiguous_gen.go b/modules/charset/ambiguous_gen.go index c88ffd5aa5..b0fa6099a7 100644 --- a/modules/charset/ambiguous_gen.go +++ b/modules/charset/ambiguous_gen.go @@ -19,8 +19,8 @@ type AmbiguousTable struct { // AmbiguousCharacters provides a map by locale name to the confusable characters in that locale var AmbiguousCharacters = map[string]*AmbiguousTable{ "_common": { - Confusable: []rune{184, 383, 388, 397, 422, 423, 439, 444, 445, 448, 451, 540, 546, 547, 577, 593, 609, 611, 617, 618, 623, 651, 655, 660, 697, 699, 700, 701, 702, 706, 707, 708, 710, 712, 714, 715, 720, 727, 731, 732, 756, 760, 884, 890, 894, 895, 900, 913, 914, 917, 918, 919, 922, 924, 925, 927, 929, 932, 933, 935, 945, 947, 953, 957, 959, 961, 963, 965, 978, 988, 1000, 1010, 1011, 1017, 1018, 1029, 1030, 1032, 1109, 1110, 1112, 1121, 1140, 1141, 1198, 1199, 1211, 1213, 1216, 1231, 1248, 1281, 1292, 1307, 1308, 1309, 1357, 1359, 1365, 1370, 1373, 1377, 1379, 1382, 1392, 1400, 1404, 1405, 1409, 1412, 1413, 1417, 1472, 1475, 1493, 1496, 1497, 1503, 1505, 1523, 1549, 1575, 1607, 1632, 1633, 1637, 1639, 1643, 1645, 1726, 1729, 1748, 1749, 1776, 1777, 1781, 1783, 1793, 1794, 1795, 1796, 1984, 1994, 2036, 2037, 2042, 2307, 2406, 2429, 2534, 2538, 2541, 2662, 2663, 2666, 2691, 2790, 2819, 2848, 2918, 2920, 3046, 3074, 3174, 3202, 3302, 3330, 3360, 3430, 3437, 3458, 3664, 3792, 4125, 4160, 4327, 4351, 4608, 4816, 5024, 5025, 5026, 5029, 5033, 5034, 5035, 5036, 5038, 5043, 5047, 5051, 5053, 5056, 5058, 5059, 5070, 5071, 5074, 5076, 5077, 5081, 5082, 5086, 5087, 5090, 5094, 5095, 5102, 5107, 5108, 5120, 5167, 5171, 5176, 5194, 5196, 5229, 5231, 5234, 5261, 5290, 5311, 5441, 5500, 5501, 5511, 5551, 5556, 5573, 5598, 5610, 5616, 5623, 5741, 5742, 5760, 5810, 5815, 5825, 5836, 5845, 5846, 5868, 5869, 5941, 6147, 6153, 7428, 7439, 7441, 7452, 7456, 7457, 7458, 7462, 7555, 7564, 7837, 7935, 8125, 8126, 8127, 8128, 8175, 8189, 8190, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8208, 8209, 8210, 8218, 8219, 8228, 8232, 8233, 8239, 8242, 8249, 8250, 8257, 8259, 8260, 8270, 8275, 8282, 8287, 8450, 8458, 8459, 8460, 8461, 8462, 8464, 8465, 8466, 8467, 8469, 8473, 8474, 8475, 8476, 8477, 8484, 8488, 8490, 8492, 8493, 8494, 8495, 8496, 8497, 8499, 8500, 8505, 8509, 8517, 8518, 8519, 8520, 8521, 8544, 8548, 8553, 8556, 8557, 8558, 8559, 8560, 8564, 8569, 8572, 8573, 8574, 8722, 8725, 8726, 8727, 8739, 8744, 8746, 8758, 8764, 8868, 8897, 8899, 8959, 9075, 9076, 9082, 9213, 9585, 9587, 10088, 10089, 10094, 10095, 10098, 10099, 10100, 10101, 10133, 10134, 10187, 10189, 10201, 10539, 10540, 10741, 10744, 10745, 10799, 11397, 11406, 11410, 11412, 11416, 11418, 11422, 11423, 11426, 11427, 11428, 11429, 11430, 11432, 11436, 11450, 11462, 11466, 11468, 11472, 11474, 11576, 11577, 11599, 11601, 11604, 11605, 11613, 11840, 12034, 12035, 12295, 12308, 12309, 12339, 12448, 12755, 12756, 20022, 20031, 42192, 42193, 42194, 42195, 42196, 42198, 42199, 42201, 42202, 42204, 42205, 42207, 42208, 42209, 42210, 42211, 42214, 42215, 42218, 42219, 42220, 42222, 42224, 42226, 42227, 42228, 42232, 42233, 42237, 42239, 42510, 42564, 42567, 42719, 42731, 42735, 42801, 42842, 42858, 42862, 42872, 42889, 42892, 42904, 42905, 42911, 42923, 42930, 42931, 42932, 43826, 43829, 43837, 43847, 43848, 43854, 43858, 43866, 43893, 43905, 43907, 43923, 43945, 43946, 43951, 64422, 64423, 64424, 64425, 64426, 64427, 64428, 64429, 64830, 64831, 65072, 65101, 65102, 65103, 65112, 65128, 65165, 65166, 65257, 65258, 65259, 65260, 65282, 65284, 65285, 65286, 65287, 65290, 65291, 65293, 65294, 65295, 65296, 65297, 65298, 65299, 65300, 65301, 65302, 65303, 65304, 65305, 65308, 65309, 65310, 65312, 65313, 65314, 65315, 65316, 65317, 65318, 65319, 65320, 65321, 65322, 65323, 65324, 65325, 65326, 65327, 65328, 65329, 65330, 65331, 65332, 65333, 65334, 65335, 65336, 65337, 65338, 65339, 65340, 65341, 65342, 65343, 65344, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 65371, 65372, 65373, 65512, 66178, 66182, 66183, 66186, 66192, 66194, 66197, 66198, 66199, 66203, 66208, 66209, 66210, 66213, 66219, 66224, 66225, 66226, 66228, 66255, 66293, 66305, 66306, 66313, 66321, 66325, 66327, 66330, 66335, 66336, 66338, 66564, 66581, 66587, 66592, 66604, 66621, 66632, 66740, 66754, 66766, 66770, 66794, 66806, 66835, 66838, 66840, 66844, 66845, 66853, 66854, 66855, 68176, 70864, 71430, 71434, 71438, 71439, 71840, 71842, 71843, 71844, 71846, 71849, 71852, 71854, 71855, 71858, 71861, 71864, 71867, 71868, 71872, 71873, 71874, 71875, 71876, 71878, 71880, 71882, 71884, 71893, 71894, 71895, 71896, 71900, 71904, 71909, 71910, 71913, 71916, 71919, 71922, 93960, 93962, 93974, 93992, 94005, 94010, 94011, 94015, 94016, 94018, 94019, 94033, 94034, 119060, 119149, 119302, 119309, 119311, 119314, 119315, 119318, 119338, 119350, 119351, 119354, 119355, 119808, 119809, 119810, 119811, 119812, 119813, 119814, 119815, 119816, 119817, 119818, 119819, 119820, 119821, 119822, 119823, 119824, 119825, 119826, 119827, 119828, 119829, 119830, 119831, 119832, 119833, 119834, 119835, 119836, 119837, 119838, 119839, 119840, 119841, 119842, 119843, 119844, 119845, 119847, 119848, 119849, 119850, 119851, 119852, 119853, 119854, 119855, 119856, 119857, 119858, 119859, 119860, 119861, 119862, 119863, 119864, 119865, 119866, 119867, 119868, 119869, 119870, 119871, 119872, 119873, 119874, 119875, 119876, 119877, 119878, 119879, 119880, 119881, 119882, 119883, 119884, 119885, 119886, 119887, 119888, 119889, 119890, 119891, 119892, 119894, 119895, 119896, 119897, 119899, 119900, 119901, 119902, 119903, 119904, 119905, 119906, 119907, 119908, 119909, 119910, 119911, 119912, 119913, 119914, 119915, 119916, 119917, 119918, 119919, 119920, 119921, 119922, 119923, 119924, 119925, 119926, 119927, 119928, 119929, 119930, 119931, 119932, 119933, 119934, 119935, 119936, 119937, 119938, 119939, 119940, 119941, 119942, 119943, 119944, 119945, 119946, 119947, 119948, 119949, 119951, 119952, 119953, 119954, 119955, 119956, 119957, 119958, 119959, 119960, 119961, 119962, 119963, 119964, 119966, 119967, 119970, 119973, 119974, 119977, 119978, 119979, 119980, 119982, 119983, 119984, 119985, 119986, 119987, 119988, 119989, 119990, 119991, 119992, 119993, 119995, 119997, 119998, 119999, 120000, 120001, 120003, 120005, 120006, 120007, 120008, 120009, 120010, 120011, 120012, 120013, 120014, 120015, 120016, 120017, 120018, 120019, 120020, 120021, 120022, 120023, 120024, 120025, 120026, 120027, 120028, 120029, 120030, 120031, 120032, 120033, 120034, 120035, 120036, 120037, 120038, 120039, 120040, 120041, 120042, 120043, 120044, 120045, 120046, 120047, 120048, 120049, 120050, 120051, 120052, 120053, 120055, 120056, 120057, 120058, 120059, 120060, 120061, 120062, 120063, 120064, 120065, 120066, 120067, 120068, 120069, 120071, 120072, 120073, 120074, 120077, 120078, 120079, 120080, 120081, 120082, 120083, 120084, 120086, 120087, 120088, 120089, 120090, 120091, 120092, 120094, 120095, 120096, 120097, 120098, 120099, 120100, 120101, 120102, 120103, 120104, 120105, 120107, 120108, 120109, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120117, 120118, 120119, 120120, 120121, 120123, 120124, 120125, 120126, 120128, 120129, 120130, 120131, 120132, 120134, 120138, 120139, 120140, 120141, 120142, 120143, 120144, 120146, 120147, 120148, 120149, 120150, 120151, 120152, 120153, 120154, 120155, 120156, 120157, 120159, 120160, 120161, 120162, 120163, 120164, 120165, 120166, 120167, 120168, 120169, 120170, 120171, 120172, 120173, 120174, 120175, 120176, 120177, 120178, 120179, 120180, 120181, 120182, 120183, 120184, 120185, 120186, 120187, 120188, 120189, 120190, 120191, 120192, 120193, 120194, 120195, 120196, 120197, 120198, 120199, 120200, 120201, 120202, 120203, 120204, 120205, 120206, 120207, 120208, 120209, 120211, 120212, 120213, 120214, 120215, 120216, 120217, 120218, 120219, 120220, 120221, 120222, 120223, 120224, 120225, 120226, 120227, 120228, 120229, 120230, 120231, 120232, 120233, 120234, 120235, 120236, 120237, 120238, 120239, 120240, 120241, 120242, 120243, 120244, 120245, 120246, 120247, 120248, 120249, 120250, 120251, 120252, 120253, 120254, 120255, 120256, 120257, 120258, 120259, 120260, 120261, 120263, 120264, 120265, 120266, 120267, 120268, 120269, 120270, 120271, 120272, 120273, 120274, 120275, 120276, 120277, 120278, 120279, 120280, 120281, 120282, 120283, 120284, 120285, 120286, 120287, 120288, 120289, 120290, 120291, 120292, 120293, 120294, 120295, 120296, 120297, 120298, 120299, 120300, 120301, 120302, 120303, 120304, 120305, 120306, 120307, 120308, 120309, 120310, 120311, 120312, 120313, 120315, 120316, 120317, 120318, 120319, 120320, 120321, 120322, 120323, 120324, 120325, 120326, 120327, 120328, 120329, 120330, 120331, 120332, 120333, 120334, 120335, 120336, 120337, 120338, 120339, 120340, 120341, 120342, 120343, 120344, 120345, 120346, 120347, 120348, 120349, 120350, 120351, 120352, 120353, 120354, 120355, 120356, 120357, 120358, 120359, 120360, 120361, 120362, 120363, 120364, 120365, 120367, 120368, 120369, 120370, 120371, 120372, 120373, 120374, 120375, 120376, 120377, 120378, 120379, 120380, 120381, 120382, 120383, 120384, 120385, 120386, 120387, 120388, 120389, 120390, 120391, 120392, 120393, 120394, 120395, 120396, 120397, 120398, 120399, 120400, 120401, 120402, 120403, 120404, 120405, 120406, 120407, 120408, 120409, 120410, 120411, 120412, 120413, 120414, 120415, 120416, 120417, 120419, 120420, 120421, 120422, 120423, 120424, 120425, 120426, 120427, 120428, 120429, 120430, 120431, 120432, 120433, 120434, 120435, 120436, 120437, 120438, 120439, 120440, 120441, 120442, 120443, 120444, 120445, 120446, 120447, 120448, 120449, 120450, 120451, 120452, 120453, 120454, 120455, 120456, 120457, 120458, 120459, 120460, 120461, 120462, 120463, 120464, 120465, 120466, 120467, 120468, 120469, 120471, 120472, 120473, 120474, 120475, 120476, 120477, 120478, 120479, 120480, 120481, 120482, 120483, 120484, 120488, 120489, 120492, 120493, 120494, 120496, 120497, 120499, 120500, 120502, 120504, 120507, 120508, 120510, 120514, 120516, 120522, 120526, 120528, 120530, 120532, 120534, 120544, 120546, 120547, 120550, 120551, 120552, 120554, 120555, 120557, 120558, 120560, 120562, 120565, 120566, 120568, 120572, 120574, 120580, 120584, 120586, 120588, 120590, 120592, 120602, 120604, 120605, 120608, 120609, 120610, 120612, 120613, 120615, 120616, 120618, 120620, 120623, 120624, 120626, 120630, 120632, 120638, 120642, 120644, 120646, 120648, 120650, 120660, 120662, 120663, 120666, 120667, 120668, 120670, 120671, 120673, 120674, 120676, 120678, 120681, 120682, 120684, 120688, 120690, 120696, 120700, 120702, 120704, 120706, 120708, 120718, 120720, 120721, 120724, 120725, 120726, 120728, 120729, 120731, 120732, 120734, 120736, 120739, 120740, 120742, 120746, 120748, 120754, 120758, 120760, 120762, 120764, 120766, 120776, 120778, 120782, 120783, 120784, 120785, 120786, 120787, 120788, 120789, 120790, 120791, 120792, 120793, 120794, 120795, 120796, 120797, 120798, 120799, 120800, 120801, 120802, 120803, 120804, 120805, 120806, 120807, 120808, 120809, 120810, 120811, 120812, 120813, 120814, 120815, 120816, 120817, 120818, 120819, 120820, 120821, 120822, 120823, 120824, 120825, 120826, 120827, 120828, 120829, 120830, 120831, 125127, 125131, 126464, 126500, 126564, 126592, 126596, 128844, 128872, 130032, 130033, 130034, 130035, 130036, 130037, 130038, 130039, 130040, 130041}, - With: []rune{44, 102, 98, 103, 82, 50, 51, 53, 115, 73, 33, 51, 56, 56, 63, 97, 103, 121, 105, 105, 119, 117, 121, 63, 96, 96, 96, 96, 96, 60, 62, 94, 94, 96, 96, 96, 58, 45, 105, 126, 96, 58, 96, 105, 59, 74, 96, 65, 66, 69, 90, 72, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 89, 70, 50, 99, 106, 67, 77, 83, 73, 74, 115, 105, 106, 119, 86, 118, 89, 121, 104, 101, 73, 105, 51, 100, 71, 113, 87, 119, 85, 83, 79, 96, 96, 119, 113, 113, 104, 110, 110, 117, 103, 102, 111, 58, 108, 58, 108, 118, 96, 108, 111, 96, 44, 108, 111, 46, 108, 111, 86, 44, 42, 111, 111, 45, 111, 46, 73, 111, 86, 46, 46, 58, 58, 79, 108, 96, 96, 95, 58, 111, 63, 79, 56, 57, 111, 57, 56, 58, 111, 56, 79, 79, 57, 111, 111, 111, 111, 111, 111, 111, 111, 57, 111, 111, 111, 111, 111, 121, 111, 85, 79, 68, 82, 84, 105, 89, 65, 74, 69, 63, 87, 77, 72, 89, 71, 104, 90, 52, 98, 82, 87, 83, 86, 83, 76, 67, 80, 75, 100, 54, 71, 66, 61, 86, 62, 60, 96, 85, 80, 100, 98, 74, 76, 50, 120, 72, 120, 82, 98, 70, 65, 68, 68, 77, 66, 88, 120, 32, 60, 88, 73, 96, 75, 77, 58, 43, 47, 58, 58, 99, 111, 111, 117, 118, 119, 122, 114, 103, 121, 102, 121, 96, 105, 96, 126, 96, 96, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 44, 96, 46, 32, 32, 32, 96, 60, 62, 47, 45, 47, 42, 126, 58, 32, 67, 103, 72, 72, 72, 104, 73, 73, 76, 108, 78, 80, 81, 82, 82, 82, 90, 90, 75, 66, 67, 101, 101, 69, 70, 77, 111, 105, 121, 68, 100, 101, 105, 106, 73, 86, 88, 76, 67, 68, 77, 105, 118, 120, 73, 99, 100, 45, 47, 92, 42, 73, 118, 85, 58, 126, 84, 118, 85, 69, 105, 112, 97, 73, 47, 88, 40, 41, 60, 62, 40, 41, 123, 125, 43, 45, 47, 92, 84, 120, 120, 92, 47, 92, 120, 114, 72, 73, 75, 77, 78, 79, 111, 80, 112, 67, 99, 84, 89, 88, 45, 47, 57, 51, 76, 54, 86, 69, 73, 33, 79, 81, 88, 61, 92, 47, 79, 40, 41, 47, 61, 47, 92, 92, 47, 66, 80, 100, 68, 84, 71, 75, 74, 67, 90, 70, 77, 78, 76, 83, 82, 86, 72, 87, 88, 89, 65, 69, 73, 79, 85, 46, 44, 58, 61, 46, 50, 105, 86, 63, 50, 115, 50, 51, 57, 38, 58, 96, 70, 102, 117, 51, 74, 88, 66, 101, 102, 111, 114, 114, 117, 117, 121, 105, 114, 119, 122, 118, 115, 99, 111, 111, 111, 111, 111, 111, 111, 111, 40, 41, 58, 95, 95, 95, 45, 92, 108, 108, 111, 111, 111, 111, 34, 36, 37, 38, 96, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 73, 66, 69, 70, 124, 88, 79, 80, 83, 84, 43, 65, 66, 67, 70, 79, 77, 84, 89, 88, 72, 90, 66, 67, 124, 77, 84, 88, 56, 42, 108, 88, 79, 67, 76, 83, 111, 99, 115, 82, 79, 85, 55, 111, 117, 78, 79, 75, 67, 86, 70, 76, 88, 46, 79, 118, 119, 119, 119, 86, 70, 76, 89, 69, 90, 57, 69, 52, 76, 79, 85, 53, 84, 118, 115, 70, 105, 122, 55, 111, 51, 57, 54, 57, 111, 117, 121, 79, 90, 87, 67, 88, 87, 67, 86, 84, 76, 73, 82, 83, 51, 62, 65, 85, 89, 96, 96, 123, 46, 51, 86, 92, 55, 70, 82, 76, 60, 62, 47, 92, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 67, 68, 71, 74, 75, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 102, 104, 105, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 77, 79, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 105, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 70, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 108, 56, 108, 111, 111, 108, 111, 67, 84, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57}, + Confusable: []rune{184, 383, 388, 397, 422, 423, 439, 444, 445, 448, 451, 540, 546, 547, 577, 593, 609, 611, 617, 618, 623, 651, 655, 660, 697, 699, 700, 701, 702, 706, 707, 708, 710, 712, 714, 715, 720, 727, 731, 732, 756, 760, 884, 890, 894, 895, 900, 913, 914, 917, 918, 919, 922, 924, 925, 927, 929, 932, 933, 935, 945, 947, 953, 957, 959, 961, 963, 965, 978, 988, 1000, 1010, 1011, 1017, 1018, 1029, 1030, 1032, 1109, 1110, 1112, 1121, 1140, 1141, 1198, 1199, 1211, 1213, 1216, 1231, 1248, 1281, 1292, 1307, 1308, 1309, 1357, 1359, 1365, 1370, 1373, 1377, 1379, 1382, 1392, 1400, 1404, 1405, 1409, 1412, 1413, 1417, 1472, 1475, 1493, 1496, 1497, 1503, 1505, 1523, 1549, 1575, 1607, 1632, 1633, 1637, 1639, 1643, 1645, 1726, 1729, 1748, 1749, 1776, 1777, 1781, 1783, 1793, 1794, 1795, 1796, 1984, 1994, 2036, 2037, 2042, 2307, 2406, 2429, 2534, 2538, 2541, 2662, 2663, 2666, 2691, 2790, 2819, 2848, 2918, 2920, 3046, 3074, 3174, 3202, 3302, 3330, 3360, 3430, 3437, 3458, 3664, 3792, 4125, 4160, 4327, 4351, 4608, 4816, 5024, 5025, 5026, 5029, 5033, 5034, 5035, 5036, 5038, 5043, 5047, 5051, 5053, 5056, 5058, 5059, 5070, 5071, 5074, 5076, 5077, 5081, 5082, 5086, 5087, 5090, 5094, 5095, 5102, 5107, 5108, 5120, 5167, 5171, 5176, 5194, 5196, 5229, 5231, 5234, 5261, 5290, 5311, 5441, 5500, 5501, 5511, 5551, 5556, 5573, 5598, 5610, 5616, 5623, 5741, 5742, 5760, 5810, 5815, 5825, 5836, 5845, 5846, 5868, 5869, 5941, 6147, 6153, 7428, 7439, 7441, 7452, 7456, 7457, 7458, 7462, 7555, 7564, 7837, 7935, 8125, 8126, 8127, 8128, 8175, 8189, 8190, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8208, 8209, 8210, 8219, 8228, 8232, 8233, 8239, 8242, 8249, 8250, 8257, 8259, 8260, 8270, 8275, 8282, 8287, 8450, 8458, 8459, 8460, 8461, 8462, 8464, 8465, 8466, 8467, 8469, 8473, 8474, 8475, 8476, 8477, 8484, 8488, 8490, 8492, 8493, 8494, 8495, 8496, 8497, 8499, 8500, 8505, 8509, 8517, 8518, 8519, 8520, 8521, 8544, 8548, 8553, 8556, 8557, 8558, 8559, 8560, 8564, 8569, 8572, 8573, 8574, 8722, 8725, 8726, 8727, 8739, 8744, 8746, 8758, 8764, 8868, 8897, 8899, 8959, 9075, 9076, 9082, 9213, 9585, 9587, 10088, 10089, 10094, 10095, 10098, 10099, 10100, 10101, 10133, 10134, 10187, 10189, 10201, 10539, 10540, 10741, 10744, 10745, 10799, 11397, 11406, 11410, 11412, 11416, 11418, 11422, 11423, 11426, 11427, 11428, 11429, 11430, 11432, 11436, 11450, 11462, 11466, 11468, 11472, 11474, 11576, 11577, 11599, 11601, 11604, 11605, 11613, 11840, 12034, 12035, 12295, 12308, 12309, 12339, 12448, 12755, 12756, 20022, 20031, 42192, 42193, 42194, 42195, 42196, 42198, 42199, 42201, 42202, 42204, 42205, 42207, 42208, 42209, 42210, 42211, 42214, 42215, 42218, 42219, 42220, 42222, 42224, 42226, 42227, 42228, 42232, 42233, 42237, 42239, 42510, 42564, 42567, 42719, 42731, 42735, 42801, 42842, 42858, 42862, 42872, 42889, 42892, 42904, 42905, 42911, 42923, 42930, 42931, 42932, 43826, 43829, 43837, 43847, 43848, 43854, 43858, 43866, 43893, 43905, 43907, 43923, 43945, 43946, 43951, 64422, 64423, 64424, 64425, 64426, 64427, 64428, 64429, 64830, 64831, 65072, 65101, 65102, 65103, 65112, 65128, 65165, 65166, 65257, 65258, 65259, 65260, 65282, 65283, 65284, 65285, 65286, 65287, 65290, 65291, 65293, 65294, 65295, 65296, 65298, 65299, 65300, 65301, 65302, 65303, 65304, 65305, 65308, 65309, 65310, 65312, 65313, 65314, 65315, 65316, 65317, 65318, 65319, 65320, 65321, 65322, 65323, 65324, 65325, 65326, 65327, 65328, 65329, 65330, 65331, 65332, 65333, 65334, 65335, 65336, 65337, 65338, 65339, 65340, 65341, 65342, 65343, 65344, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 65371, 65372, 65373, 65512, 66178, 66182, 66183, 66186, 66192, 66194, 66197, 66198, 66199, 66203, 66208, 66209, 66210, 66213, 66219, 66224, 66225, 66226, 66228, 66255, 66293, 66305, 66306, 66313, 66321, 66325, 66327, 66330, 66335, 66336, 66338, 66564, 66581, 66587, 66592, 66604, 66621, 66632, 66740, 66754, 66766, 66770, 66794, 66806, 66835, 66838, 66840, 66844, 66845, 66853, 66854, 66855, 68176, 70864, 71430, 71434, 71438, 71439, 71840, 71842, 71843, 71844, 71846, 71849, 71852, 71854, 71855, 71858, 71861, 71864, 71867, 71868, 71872, 71873, 71874, 71875, 71876, 71878, 71880, 71882, 71884, 71893, 71894, 71895, 71896, 71900, 71904, 71909, 71910, 71913, 71916, 71919, 71922, 93960, 93962, 93974, 93992, 94005, 94010, 94011, 94015, 94016, 94018, 94019, 94033, 94034, 117974, 117975, 117976, 117977, 117978, 117979, 117980, 117981, 117982, 117983, 117984, 117985, 117986, 117987, 117988, 117989, 117990, 117991, 117992, 117993, 117994, 117995, 117996, 117997, 117998, 117999, 118000, 118001, 118002, 118003, 118004, 118005, 118006, 118007, 118008, 118009, 119060, 119149, 119302, 119309, 119311, 119314, 119315, 119318, 119338, 119350, 119351, 119354, 119355, 119808, 119809, 119810, 119811, 119812, 119813, 119814, 119815, 119816, 119817, 119818, 119819, 119820, 119821, 119822, 119823, 119824, 119825, 119826, 119827, 119828, 119829, 119830, 119831, 119832, 119833, 119834, 119835, 119836, 119837, 119838, 119839, 119840, 119841, 119842, 119843, 119844, 119845, 119846, 119847, 119848, 119849, 119850, 119851, 119852, 119853, 119854, 119855, 119856, 119857, 119858, 119859, 119860, 119861, 119862, 119863, 119864, 119865, 119866, 119867, 119868, 119869, 119870, 119871, 119872, 119873, 119874, 119875, 119876, 119877, 119878, 119879, 119880, 119881, 119882, 119883, 119884, 119885, 119886, 119887, 119888, 119889, 119890, 119891, 119892, 119894, 119895, 119896, 119897, 119899, 119900, 119901, 119902, 119903, 119904, 119905, 119906, 119907, 119908, 119909, 119910, 119911, 119912, 119913, 119914, 119915, 119916, 119917, 119918, 119919, 119920, 119921, 119922, 119923, 119924, 119925, 119926, 119927, 119928, 119929, 119930, 119931, 119932, 119933, 119934, 119935, 119936, 119937, 119938, 119939, 119940, 119941, 119942, 119943, 119944, 119945, 119946, 119947, 119948, 119949, 119951, 119952, 119953, 119954, 119955, 119956, 119957, 119958, 119959, 119960, 119961, 119962, 119963, 119964, 119966, 119967, 119970, 119973, 119974, 119977, 119978, 119979, 119980, 119982, 119983, 119984, 119985, 119986, 119987, 119988, 119989, 119990, 119991, 119992, 119993, 119995, 119997, 119998, 119999, 120000, 120001, 120003, 120005, 120006, 120007, 120008, 120009, 120010, 120011, 120012, 120013, 120014, 120015, 120016, 120017, 120018, 120019, 120020, 120021, 120022, 120023, 120024, 120025, 120026, 120027, 120028, 120029, 120030, 120031, 120032, 120033, 120034, 120035, 120036, 120037, 120038, 120039, 120040, 120041, 120042, 120043, 120044, 120045, 120046, 120047, 120048, 120049, 120050, 120051, 120052, 120053, 120055, 120056, 120057, 120058, 120059, 120060, 120061, 120062, 120063, 120064, 120065, 120066, 120067, 120068, 120069, 120071, 120072, 120073, 120074, 120077, 120078, 120079, 120080, 120081, 120082, 120083, 120084, 120086, 120087, 120088, 120089, 120090, 120091, 120092, 120094, 120095, 120096, 120097, 120098, 120099, 120100, 120101, 120102, 120103, 120104, 120105, 120107, 120108, 120109, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120117, 120118, 120119, 120120, 120121, 120123, 120124, 120125, 120126, 120128, 120129, 120130, 120131, 120132, 120134, 120138, 120139, 120140, 120141, 120142, 120143, 120144, 120146, 120147, 120148, 120149, 120150, 120151, 120152, 120153, 120154, 120155, 120156, 120157, 120159, 120160, 120161, 120162, 120163, 120164, 120165, 120166, 120167, 120168, 120169, 120170, 120171, 120172, 120173, 120174, 120175, 120176, 120177, 120178, 120179, 120180, 120181, 120182, 120183, 120184, 120185, 120186, 120187, 120188, 120189, 120190, 120191, 120192, 120193, 120194, 120195, 120196, 120197, 120198, 120199, 120200, 120201, 120202, 120203, 120204, 120205, 120206, 120207, 120208, 120209, 120211, 120212, 120213, 120214, 120215, 120216, 120217, 120218, 120219, 120220, 120221, 120222, 120223, 120224, 120225, 120226, 120227, 120228, 120229, 120230, 120231, 120232, 120233, 120234, 120235, 120236, 120237, 120238, 120239, 120240, 120241, 120242, 120243, 120244, 120245, 120246, 120247, 120248, 120249, 120250, 120251, 120252, 120253, 120254, 120255, 120256, 120257, 120258, 120259, 120260, 120261, 120263, 120264, 120265, 120266, 120267, 120268, 120269, 120270, 120271, 120272, 120273, 120274, 120275, 120276, 120277, 120278, 120279, 120280, 120281, 120282, 120283, 120284, 120285, 120286, 120287, 120288, 120289, 120290, 120291, 120292, 120293, 120294, 120295, 120296, 120297, 120298, 120299, 120300, 120301, 120302, 120303, 120304, 120305, 120306, 120307, 120308, 120309, 120310, 120311, 120312, 120313, 120315, 120316, 120317, 120318, 120319, 120320, 120321, 120322, 120323, 120324, 120325, 120326, 120327, 120328, 120329, 120330, 120331, 120332, 120333, 120334, 120335, 120336, 120337, 120338, 120339, 120340, 120341, 120342, 120343, 120344, 120345, 120346, 120347, 120348, 120349, 120350, 120351, 120352, 120353, 120354, 120355, 120356, 120357, 120358, 120359, 120360, 120361, 120362, 120363, 120364, 120365, 120367, 120368, 120369, 120370, 120371, 120372, 120373, 120374, 120375, 120376, 120377, 120378, 120379, 120380, 120381, 120382, 120383, 120384, 120385, 120386, 120387, 120388, 120389, 120390, 120391, 120392, 120393, 120394, 120395, 120396, 120397, 120398, 120399, 120400, 120401, 120402, 120403, 120404, 120405, 120406, 120407, 120408, 120409, 120410, 120411, 120412, 120413, 120414, 120415, 120416, 120417, 120419, 120420, 120421, 120422, 120423, 120424, 120425, 120426, 120427, 120428, 120429, 120430, 120431, 120432, 120433, 120434, 120435, 120436, 120437, 120438, 120439, 120440, 120441, 120442, 120443, 120444, 120445, 120446, 120447, 120448, 120449, 120450, 120451, 120452, 120453, 120454, 120455, 120456, 120457, 120458, 120459, 120460, 120461, 120462, 120463, 120464, 120465, 120466, 120467, 120468, 120469, 120471, 120472, 120473, 120474, 120475, 120476, 120477, 120478, 120479, 120480, 120481, 120482, 120483, 120484, 120488, 120489, 120492, 120493, 120494, 120496, 120497, 120499, 120500, 120502, 120504, 120507, 120508, 120510, 120514, 120516, 120522, 120526, 120528, 120530, 120532, 120534, 120544, 120546, 120547, 120550, 120551, 120552, 120554, 120555, 120557, 120558, 120560, 120562, 120565, 120566, 120568, 120572, 120574, 120580, 120584, 120586, 120588, 120590, 120592, 120602, 120604, 120605, 120608, 120609, 120610, 120612, 120613, 120615, 120616, 120618, 120620, 120623, 120624, 120626, 120630, 120632, 120638, 120642, 120644, 120646, 120648, 120650, 120660, 120662, 120663, 120666, 120667, 120668, 120670, 120671, 120673, 120674, 120676, 120678, 120681, 120682, 120684, 120688, 120690, 120696, 120700, 120702, 120704, 120706, 120708, 120718, 120720, 120721, 120724, 120725, 120726, 120728, 120729, 120731, 120732, 120734, 120736, 120739, 120740, 120742, 120746, 120748, 120754, 120758, 120760, 120762, 120764, 120766, 120776, 120778, 120782, 120783, 120784, 120785, 120786, 120787, 120788, 120789, 120790, 120791, 120792, 120793, 120794, 120795, 120796, 120797, 120798, 120799, 120800, 120801, 120802, 120803, 120804, 120805, 120806, 120807, 120808, 120809, 120810, 120811, 120812, 120813, 120814, 120815, 120816, 120817, 120818, 120819, 120820, 120821, 120822, 120823, 120824, 120825, 120826, 120827, 120828, 120829, 120830, 120831, 125127, 125131, 126464, 126500, 126564, 126592, 126596, 128844, 128872, 130032, 130033, 130034, 130035, 130036, 130037, 130038, 130039, 130040, 130041}, + With: []rune{44, 102, 98, 103, 82, 50, 51, 53, 115, 73, 33, 51, 56, 56, 63, 97, 103, 121, 105, 105, 119, 117, 121, 63, 96, 96, 96, 96, 96, 60, 62, 94, 94, 96, 96, 96, 58, 45, 105, 126, 96, 58, 96, 105, 59, 74, 96, 65, 66, 69, 90, 72, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 89, 70, 50, 99, 106, 67, 77, 83, 73, 74, 115, 105, 106, 119, 86, 118, 89, 121, 104, 101, 73, 105, 51, 100, 71, 113, 87, 119, 85, 83, 79, 96, 96, 119, 113, 113, 104, 110, 110, 117, 103, 102, 111, 58, 108, 58, 108, 118, 96, 108, 111, 96, 44, 108, 111, 46, 108, 111, 86, 44, 42, 111, 111, 45, 111, 46, 73, 111, 86, 46, 46, 58, 58, 79, 108, 96, 96, 95, 58, 111, 63, 79, 56, 57, 111, 57, 56, 58, 111, 56, 79, 79, 57, 111, 111, 111, 111, 111, 111, 111, 111, 57, 111, 111, 111, 111, 111, 121, 111, 85, 79, 68, 82, 84, 105, 89, 65, 74, 69, 63, 87, 77, 72, 89, 71, 104, 90, 52, 98, 82, 87, 83, 86, 83, 76, 67, 80, 75, 100, 54, 71, 66, 61, 86, 62, 60, 96, 85, 80, 100, 98, 74, 76, 50, 120, 72, 120, 82, 98, 70, 65, 68, 68, 77, 66, 88, 120, 32, 60, 88, 73, 96, 75, 77, 58, 43, 47, 58, 58, 99, 111, 111, 117, 118, 119, 122, 114, 103, 121, 102, 121, 96, 105, 96, 126, 96, 96, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 96, 46, 32, 32, 32, 96, 60, 62, 47, 45, 47, 42, 126, 58, 32, 67, 103, 72, 72, 72, 104, 73, 73, 76, 108, 78, 80, 81, 82, 82, 82, 90, 90, 75, 66, 67, 101, 101, 69, 70, 77, 111, 105, 121, 68, 100, 101, 105, 106, 73, 86, 88, 76, 67, 68, 77, 105, 118, 120, 73, 99, 100, 45, 47, 92, 42, 73, 118, 85, 58, 126, 84, 118, 85, 69, 105, 112, 97, 73, 47, 88, 40, 41, 60, 62, 40, 41, 123, 125, 43, 45, 47, 92, 84, 120, 120, 92, 47, 92, 120, 114, 72, 73, 75, 77, 78, 79, 111, 80, 112, 67, 99, 84, 89, 88, 45, 47, 57, 51, 76, 54, 86, 69, 73, 33, 79, 81, 88, 61, 92, 47, 79, 40, 41, 47, 61, 47, 92, 92, 47, 66, 80, 100, 68, 84, 71, 75, 74, 67, 90, 70, 77, 78, 76, 83, 82, 86, 72, 87, 88, 89, 65, 69, 73, 79, 85, 46, 44, 58, 61, 46, 50, 105, 86, 63, 50, 115, 50, 51, 57, 38, 58, 96, 70, 102, 117, 51, 74, 88, 66, 101, 102, 111, 114, 114, 117, 117, 121, 105, 114, 119, 122, 118, 115, 99, 111, 111, 111, 111, 111, 111, 111, 111, 40, 41, 58, 95, 95, 95, 45, 92, 108, 108, 111, 111, 111, 111, 34, 35, 36, 37, 38, 96, 42, 43, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 73, 66, 69, 70, 124, 88, 79, 80, 83, 84, 43, 65, 66, 67, 70, 79, 77, 84, 89, 88, 72, 90, 66, 67, 124, 77, 84, 88, 56, 42, 108, 88, 79, 67, 76, 83, 111, 99, 115, 82, 79, 85, 55, 111, 117, 78, 79, 75, 67, 86, 70, 76, 88, 46, 79, 118, 119, 119, 119, 86, 70, 76, 89, 69, 90, 57, 69, 52, 76, 79, 85, 53, 84, 118, 115, 70, 105, 122, 55, 111, 51, 57, 54, 57, 111, 117, 121, 79, 90, 87, 67, 88, 87, 67, 86, 84, 76, 73, 82, 83, 51, 62, 65, 85, 89, 96, 96, 65, 66, 67, 68, 69, 70, 71, 72, 108, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 79, 108, 50, 51, 52, 53, 54, 55, 56, 57, 123, 46, 51, 86, 92, 55, 70, 82, 76, 60, 62, 47, 92, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 67, 68, 71, 74, 75, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 102, 104, 105, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 77, 79, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 105, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 70, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 108, 56, 108, 111, 111, 108, 111, 67, 84, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57}, Locale: "_common", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -152,16 +152,15 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 8190, Hi: 8192, Stride: 2}, {Lo: 8193, Hi: 8202, Stride: 1}, {Lo: 8208, Hi: 8210, Stride: 1}, - {Lo: 8218, Hi: 8219, Stride: 1}, - {Lo: 8228, Hi: 8232, Stride: 4}, - {Lo: 8233, Hi: 8239, Stride: 6}, - {Lo: 8242, Hi: 8249, Stride: 7}, - {Lo: 8250, Hi: 8257, Stride: 7}, - {Lo: 8259, Hi: 8260, Stride: 1}, - {Lo: 8270, Hi: 8275, Stride: 5}, - {Lo: 8282, Hi: 8287, Stride: 5}, - {Lo: 8450, Hi: 8458, Stride: 8}, - {Lo: 8459, Hi: 8462, Stride: 1}, + {Lo: 8219, Hi: 8228, Stride: 9}, + {Lo: 8232, Hi: 8233, Stride: 1}, + {Lo: 8239, Hi: 8242, Stride: 3}, + {Lo: 8249, Hi: 8250, Stride: 1}, + {Lo: 8257, Hi: 8259, Stride: 2}, + {Lo: 8260, Hi: 8270, Stride: 10}, + {Lo: 8275, Hi: 8282, Stride: 7}, + {Lo: 8287, Hi: 8450, Stride: 163}, + {Lo: 8458, Hi: 8462, Stride: 1}, {Lo: 8464, Hi: 8467, Stride: 1}, {Lo: 8469, Hi: 8473, Stride: 4}, {Lo: 8474, Hi: 8477, Stride: 1}, @@ -250,10 +249,10 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 65112, Hi: 65128, Stride: 16}, {Lo: 65165, Hi: 65166, Stride: 1}, {Lo: 65257, Hi: 65260, Stride: 1}, - {Lo: 65282, Hi: 65284, Stride: 2}, - {Lo: 65285, Hi: 65287, Stride: 1}, + {Lo: 65282, Hi: 65287, Stride: 1}, {Lo: 65290, Hi: 65291, Stride: 1}, - {Lo: 65293, Hi: 65305, Stride: 1}, + {Lo: 65293, Hi: 65296, Stride: 1}, + {Lo: 65298, Hi: 65305, Stride: 1}, {Lo: 65308, Hi: 65310, Stride: 1}, {Lo: 65312, Hi: 65373, Stride: 1}, {Lo: 65512, Hi: 65512, Stride: 1}, @@ -304,15 +303,16 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 94011, Hi: 94015, Stride: 4}, {Lo: 94016, Hi: 94018, Stride: 2}, {Lo: 94019, Hi: 94033, Stride: 14}, - {Lo: 94034, Hi: 119060, Stride: 25026}, - {Lo: 119149, Hi: 119302, Stride: 153}, - {Lo: 119309, Hi: 119311, Stride: 2}, - {Lo: 119314, Hi: 119315, Stride: 1}, - {Lo: 119318, Hi: 119338, Stride: 20}, - {Lo: 119350, Hi: 119351, Stride: 1}, - {Lo: 119354, Hi: 119355, Stride: 1}, - {Lo: 119808, Hi: 119845, Stride: 1}, - {Lo: 119847, Hi: 119892, Stride: 1}, + {Lo: 94034, Hi: 117974, Stride: 23940}, + {Lo: 117975, Hi: 118009, Stride: 1}, + {Lo: 119060, Hi: 119149, Stride: 89}, + {Lo: 119302, Hi: 119309, Stride: 7}, + {Lo: 119311, Hi: 119314, Stride: 3}, + {Lo: 119315, Hi: 119318, Stride: 3}, + {Lo: 119338, Hi: 119350, Stride: 12}, + {Lo: 119351, Hi: 119354, Stride: 3}, + {Lo: 119355, Hi: 119808, Stride: 453}, + {Lo: 119809, Hi: 119892, Stride: 1}, {Lo: 119894, Hi: 119897, Stride: 1}, {Lo: 119899, Hi: 119949, Stride: 1}, {Lo: 119951, Hi: 119964, Stride: 1}, @@ -405,8 +405,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "_default": { - Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "_default", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -424,20 +424,21 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 8217, Hi: 8218, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + {Lo: 65297, Hi: 65306, Stride: 9}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, }, R32: []unicode.Range32{}, LatinOffset: 1, }, }, "cs": { - Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "cs", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -453,11 +454,11 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8216, Hi: 8218, Stride: 2}, {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65281, Hi: 65288, Stride: 7}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65297, Hi: 65306, Stride: 9}, {Lo: 65307, Hi: 65311, Stride: 4}, {Lo: 65374, Hi: 65374, Stride: 1}, }, @@ -466,8 +467,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "de": { - Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "de", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -483,11 +484,10 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65281, Hi: 65288, Stride: 7}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65297, Hi: 65306, Stride: 9}, {Lo: 65307, Hi: 65311, Stride: 4}, {Lo: 65374, Hi: 65374, Stride: 1}, }, @@ -496,8 +496,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "es": { - Confusable: []rune{180, 215, 305, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 215, 305, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "es", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -513,20 +513,21 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 8211, Hi: 8218, Stride: 7}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + {Lo: 65297, Hi: 65306, Stride: 9}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, }, R32: []unicode.Range32{}, LatinOffset: 1, }, }, "fr": { - Confusable: []rune{215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "fr", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -542,20 +543,21 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8245, Stride: 29}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 8216, Hi: 8218, Stride: 2}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + {Lo: 65297, Hi: 65306, Stride: 9}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, }, R32: []unicode.Range32{}, LatinOffset: 0, }, }, "it": { - Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "it", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -572,11 +574,11 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8211, Hi: 8218, Stride: 7}, {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65281, Hi: 65288, Stride: 7}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65297, Hi: 65306, Stride: 9}, {Lo: 65307, Hi: 65311, Stride: 4}, {Lo: 65374, Hi: 65374, Stride: 1}, }, @@ -585,8 +587,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "ja": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 65281, 65283, 65292, 65306, 65307}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 33, 35, 44, 58, 59}, + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8218, 8245, 65281, 65292, 65297, 65307}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 44, 96, 33, 44, 49, 59}, Locale: "ja", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -603,18 +605,17 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65307, Stride: 1}, + {Lo: 8218, Hi: 8245, Stride: 27}, + {Lo: 65281, Hi: 65292, Stride: 11}, + {Lo: 65297, Hi: 65307, Stride: 10}, }, R32: []unicode.Range32{}, LatinOffset: 1, }, }, "ko": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "ko", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -630,20 +631,21 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 8211, Hi: 8218, Stride: 7}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + {Lo: 65297, Hi: 65306, Stride: 9}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, }, R32: []unicode.Range32{}, LatinOffset: 1, }, }, "pl": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "pl", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -659,21 +661,20 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 8216, Hi: 8245, Stride: 29}, + {Lo: 12494, Hi: 65281, Stride: 52787}, {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + {Lo: 65292, Hi: 65297, Stride: 5}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, }, R32: []unicode.Range32{}, LatinOffset: 1, }, }, "pt-BR": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "pt-BR", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -689,11 +690,11 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8216, Hi: 8218, Stride: 2}, {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65281, Hi: 65288, Stride: 7}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65297, Hi: 65306, Stride: 9}, {Lo: 65307, Hi: 65311, Stride: 4}, {Lo: 65374, Hi: 65374, Stride: 1}, }, @@ -702,8 +703,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "qps-ploc": { - Confusable: []rune{160, 180, 215, 305, 921, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{160, 180, 215, 305, 921, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "qps-ploc", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -720,10 +721,10 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 8218, Hi: 8245, Stride: 27}, {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65297, Stride: 5}, {Lo: 65306, Hi: 65307, Stride: 1}, {Lo: 65311, Hi: 65374, Stride: 63}, }, @@ -732,18 +733,18 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "ru": { - Confusable: []rune{180, 215, 305, 921, 1009, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{180, 215, 305, 921, 1009, 8216, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 96, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "ru", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ {Lo: 180, Hi: 215, Stride: 35}, {Lo: 305, Hi: 921, Stride: 616}, {Lo: 1009, Hi: 8216, Stride: 7207}, - {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 8218, Hi: 8245, Stride: 27}, {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65297, Stride: 5}, {Lo: 65306, Hi: 65307, Stride: 1}, {Lo: 65311, Hi: 65374, Stride: 63}, }, @@ -752,8 +753,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "tr": { - Confusable: []rune{160, 180, 215, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Confusable: []rune{160, 180, 215, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8218, 8245, 12494, 65281, 65288, 65289, 65292, 65297, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 44, 96, 47, 33, 40, 41, 44, 49, 58, 59, 63, 126}, Locale: "tr", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -769,38 +770,39 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 8211, Hi: 8218, Stride: 7}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + {Lo: 65297, Hi: 65306, Stride: 9}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, }, R32: []unicode.Range32{}, LatinOffset: 1, }, }, "zh-hans": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8245, 12494, 65281, 65288, 65289, 65306, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 47, 33, 40, 41, 58, 126}, + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8218, 8245, 12494, 65297, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 44, 96, 47, 49, 126}, Locale: "zh-hans", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, {Lo: 1056, Hi: 1059, Stride: 1}, {Lo: 1061, Hi: 1068, Stride: 7}, {Lo: 1072, Hi: 1073, Stride: 1}, {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65288, Stride: 7}, - {Lo: 65289, Hi: 65306, Stride: 17}, + {Lo: 8218, Hi: 8245, Stride: 27}, + {Lo: 12494, Hi: 65297, Stride: 52803}, {Lo: 65374, Hi: 65374, Stride: 1}, }, R32: []unicode.Range32{}, @@ -808,8 +810,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ }, }, "zh-hant": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 12494, 65283, 65307, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 47, 35, 59, 126}, + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8218, 12494, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 44, 47, 126}, Locale: "zh-hant", RangeTable: &unicode.RangeTable{ R16: []unicode.Range16{ @@ -825,9 +827,8 @@ var AmbiguousCharacters = map[string]*AmbiguousTable{ {Lo: 1075, Hi: 1077, Stride: 2}, {Lo: 1086, Hi: 1088, Stride: 2}, {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 12494, Stride: 4283}, - {Lo: 65283, Hi: 65307, Stride: 24}, - {Lo: 65374, Hi: 65374, Stride: 1}, + {Lo: 8211, Hi: 8218, Stride: 7}, + {Lo: 12494, Hi: 65374, Stride: 52880}, }, R32: []unicode.Range32{}, LatinOffset: 1, diff --git a/modules/emoji/emoji_test.go b/modules/emoji/emoji_test.go index 2526cd121e..ad3db10fb4 100644 --- a/modules/emoji/emoji_test.go +++ b/modules/emoji/emoji_test.go @@ -23,16 +23,16 @@ func TestLookup(t *testing.T) { d := FromAlias("beer") if !reflect.DeepEqual(a, b) { - t.Errorf("a and b should equal") + t.Error("a and b should equal") } if !reflect.DeepEqual(b, c) { - t.Errorf("b and c should equal") + t.Error("b and c should equal") } if !reflect.DeepEqual(c, d) { - t.Errorf("c and d should equal") + t.Error("c and d should equal") } if !reflect.DeepEqual(a, d) { - t.Errorf("a and d should equal") + t.Error("a and d should equal") } m := FromCode("\U0001f44d") @@ -40,13 +40,13 @@ func TestLookup(t *testing.T) { o := FromAlias("+1") if !reflect.DeepEqual(m, n) { - t.Errorf("m and n should equal") + t.Error("m and n should equal") } if !reflect.DeepEqual(n, o) { - t.Errorf("n and o should equal") + t.Error("n and o should equal") } if !reflect.DeepEqual(m, o) { - t.Errorf("m and o should equal") + t.Error("m and o should equal") } } diff --git a/modules/forgefed/activity_follow.go b/modules/forgefed/activity_follow.go new file mode 100644 index 0000000000..5cb45ca885 --- /dev/null +++ b/modules/forgefed/activity_follow.go @@ -0,0 +1,57 @@ +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" + "github.com/google/uuid" +) + +// ForgeFollow activity data type +// swagger:model +type ForgeFollow struct { + // swagger:ignore + ap.Activity +} + +func NewForgeFollowFromAp(activity ap.Activity) (ForgeFollow, error) { + result := ForgeFollow{} + result.Activity = activity + if valid, err := validation.IsValid(result); !valid { + return ForgeFollow{}, err + } + return result, nil +} + +func NewForgeFollow(actor, object string) (ForgeFollow, error) { + result := ForgeFollow{} + result.Type = ap.FollowType + result.ID = ap.IRI(actor + "/follows/" + uuid.New().String()) + result.Actor = ap.IRI(actor) + result.Object = ap.IRI(object) + if valid, err := validation.IsValid(result); !valid { + return ForgeFollow{}, err + } + return result, nil +} + +func (follow ForgeFollow) MarshalJSON() ([]byte, error) { + return follow.Activity.MarshalJSON() +} + +func (follow *ForgeFollow) UnmarshalJSON(data []byte) error { + return follow.Activity.UnmarshalJSON(data) +} + +func (follow ForgeFollow) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(string(follow.Type), "type")...) + result = append(result, validation.ValidateOneOf(string(follow.Type), []any{"Follow"}, "type")...) + result = append(result, validation.ValidateIDExists(follow.Actor, "actor")...) + result = append(result, validation.ValidateIDExists(follow.Object, "object")...) + + return result +} diff --git a/modules/forgefed/activity_follow_test.go b/modules/forgefed/activity_follow_test.go new file mode 100644 index 0000000000..bb0c1de2f7 --- /dev/null +++ b/modules/forgefed/activity_follow_test.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "testing" + + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +func Test_NewForgeFollowValidation(t *testing.T) { + sut := ForgeFollow{} + sut.Type = "Follow" + sut.Actor = ap.IRI("example.org/alice") + sut.Object = ap.IRI("example.org/bob") + + if err, _ := validation.IsValid(sut); !err { + t.Errorf("sut is invalid: %v\n", err) + } + + sut = ForgeFollow{} + sut.Actor = ap.IRI("example.org/alice") + sut.Object = ap.IRI("example.org/bob") + + if err, _ := validation.IsValid(sut); err { + t.Errorf("sut is valid: %v\n", err) + } +} diff --git a/modules/forgefed/activity_like_test.go b/modules/forgefed/activity_like_test.go index 815b0e02f3..6b252d5960 100644 --- a/modules/forgefed/activity_like_test.go +++ b/modules/forgefed/activity_like_test.go @@ -4,7 +4,7 @@ package forgefed import ( - "fmt" + "errors" "reflect" "strings" "testing" @@ -99,7 +99,7 @@ func Test_LikeUnmarshalJSON(t *testing.T) { "invalid": { item: []byte(`{"type":"Invalid","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"`), want: &ForgeLike{}, - wantErr: fmt.Errorf("cannot parse JSON"), + wantErr: errors.New("cannot parse JSON"), }, } @@ -143,7 +143,7 @@ func Test_ForgeLikeValidation(t *testing.T) { "actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", "object":"https://codeberg.org/api/activitypub/repository-id/1", "startTime": "2014-12-31T23:00:00-08:00"}`)) - if err := validateAndCheckError(sut, "Value bad-type is not contained in allowed values [Like]"); err != nil { + if err := validateAndCheckError(sut, "Field type contains the value bad-type, which is not in allowed subset [Like]"); err != nil { t.Error(err) } @@ -154,14 +154,6 @@ func Test_ForgeLikeValidation(t *testing.T) { if err := validateAndCheckError(sut, "StartTime was invalid."); err != nil { t.Error(err) } - - sut.UnmarshalJSON([]byte(`{"type":"Wrong", - "actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1", - "object":"https://codeberg.org/api/activitypub/repository-id/1", - "startTime": "2014-12-31T23:00:00-08:00"}`)) - if err := validateAndCheckError(sut, "Value Wrong is not contained in allowed values [Like]"); err != nil { - t.Error(err) - } } func TestActivityValidation_Attack(t *testing.T) { diff --git a/modules/forgefed/activity_undo_like_test.go b/modules/forgefed/activity_undo_like_test.go index 1b77369b67..5867a84e7b 100644 --- a/modules/forgefed/activity_undo_like_test.go +++ b/modules/forgefed/activity_undo_like_test.go @@ -4,7 +4,7 @@ package forgefed import ( - "fmt" + "errors" "reflect" "strings" "testing" @@ -125,7 +125,7 @@ func Test_UndoLikeUnmarshalJSON(t *testing.T) { "invalid": { item: []byte(`invalid JSON`), want: nil, - wantErr: fmt.Errorf("cannot parse JSON"), + wantErr: errors.New("cannot parse JSON"), }, } diff --git a/modules/forgefed/activity_user_activity.go b/modules/forgefed/activity_user_activity.go new file mode 100644 index 0000000000..0eb05cd6ec --- /dev/null +++ b/modules/forgefed/activity_user_activity.go @@ -0,0 +1,77 @@ +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "time" + + user_model "forgejo.org/models/user" + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +// ForgeFollow activity data type +// swagger:model +type ForgeUserActivity struct { + ap.Activity + Note ForgeUserActivityNote +} + +func NewForgeUserActivityFromAp(activity ap.Activity) (ForgeUserActivity, error) { + result := ForgeUserActivity{} + result.Activity = activity + note, err := NewForgeUserActivityNoteFromAp(activity.Object) + if err != nil { + return ForgeUserActivity{}, err + } + result.Note = note + if valid, err := validation.IsValid(result); !valid { + return ForgeUserActivity{}, err + } + return result, nil +} + +func NewForgeUserActivity(doer *user_model.User, actionID int64, content string) (ForgeUserActivity, error) { + id := fmt.Sprintf("%s/activities/%d", doer.APActorID(), actionID) + published := time.Now() + + result := ForgeUserActivity{} + result.ID = ap.IRI(id + "/activity") + result.Type = ap.CreateType + result.Actor = ap.IRI(doer.APActorID()) + result.Published = published + result.To = ap.ItemCollection{ + ap.IRI("https://www.w3.org/ns/activitystreams#Public"), + } + result.CC = ap.ItemCollection{ + ap.IRI(doer.APActorID() + "/followers"), + } + note, err := newNote(doer, content, id, published) + if err != nil { + return ForgeUserActivity{}, err + } + result.Object = note + + return result, nil +} + +func (userActivity ForgeUserActivity) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(string(userActivity.Type), "type")...) + result = append(result, validation.ValidateOneOf(string(userActivity.Type), []any{"Create"}, "type")...) + result = append(result, validation.ValidateIDExists(userActivity.Actor, "actor")...) + + if len(userActivity.To) == 0 { + result = append(result, "Missing to") + } + if len(userActivity.CC) == 0 { + result = append(result, "Missing cc") + } + + result = append(result, userActivity.Note.Validate()...) + + return result +} diff --git a/modules/forgefed/activity_user_activity_test.go b/modules/forgefed/activity_user_activity_test.go new file mode 100644 index 0000000000..9cb9f133b9 --- /dev/null +++ b/modules/forgefed/activity_user_activity_test.go @@ -0,0 +1,40 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "testing" + + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +func Test_ForgeUserActivityValidation(t *testing.T) { + note := ForgeUserActivityNote{} + note.Type = "Note" + note.Content = ap.NaturalLanguageValues{ + { + Ref: ap.NilLangRef, + Value: ap.Content("Any Content!"), + }, + } + note.URL = ap.IRI("example.org/user-id/57") + + sut := ForgeUserActivity{} + sut.Type = "Create" + sut.Actor = ap.IRI("example.org/user-id/23") + sut.CC = ap.ItemCollection{ + ap.IRI("example.org/registration/public#2nd"), + } + sut.To = ap.ItemCollection{ + ap.IRI("example.org/registration/public"), + } + + sut.Note = note + + if res, _ := validation.IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } +} diff --git a/modules/forgefed/actor.go b/modules/forgefed/actor.go index 6fde2babde..5383d5adaf 100644 --- a/modules/forgefed/actor.go +++ b/modules/forgefed/actor.go @@ -10,8 +10,6 @@ import ( "strings" "forgejo.org/modules/validation" - - ap "github.com/go-ap/activitypub" ) // ----------------------------- ActorID -------------------------------------------- @@ -41,12 +39,18 @@ func NewActorID(uri string) (ActorID, error) { } func (id ActorID) AsURI() string { - var result string + var result, path string + + if id.Path == "" { + path = id.ID + } else { + path = fmt.Sprintf("%s/%s", id.Path, id.ID) + } if id.IsPortSupplemented { - result = fmt.Sprintf("%s://%s/%s/%s", id.HostSchema, id.Host, id.Path, id.ID) + result = fmt.Sprintf("%s://%s/%s", id.HostSchema, id.Host, path) } else { - result = fmt.Sprintf("%s://%s:%d/%s/%s", id.HostSchema, id.Host, id.HostPort, id.Path, id.ID) + result = fmt.Sprintf("%s://%s:%d/%s", id.HostSchema, id.Host, id.HostPort, path) } return result @@ -54,8 +58,7 @@ func (id ActorID) AsURI() string { func (id ActorID) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(id.ID, "userId")...) - result = append(result, validation.ValidateNotEmpty(id.Path, "path")...) + result = append(result, validation.ValidateNotEmpty(id.ID, "ID")...) result = append(result, validation.ValidateNotEmpty(id.Host, "host")...) result = append(result, validation.ValidateNotEmpty(id.HostPort, "hostPort")...) result = append(result, validation.ValidateNotEmpty(id.HostSchema, "hostSchema")...) @@ -68,115 +71,6 @@ func (id ActorID) Validate() []string { return result } -// ----------------------------- PersonID -------------------------------------------- -type PersonID struct { - ActorID -} - -// Factory function for PersonID. Created struct is asserted to be valid -func NewPersonID(uri, source string) (PersonID, error) { - result, err := newActorID(uri) - if err != nil { - return PersonID{}, err - } - result.Source = source - - // validate Person specific path - personID := PersonID{result} - if valid, err := validation.IsValid(personID); !valid { - return PersonID{}, err - } - - return personID, nil -} - -func (id PersonID) AsWebfinger() string { - result := fmt.Sprintf("@%s@%s", strings.ToLower(id.ID), strings.ToLower(id.Host)) - return result -} - -func (id PersonID) AsLoginName() string { - result := fmt.Sprintf("%s%s", strings.ToLower(id.ID), id.HostSuffix()) - return result -} - -func (id PersonID) HostSuffix() string { - result := fmt.Sprintf("-%s", strings.ToLower(id.Host)) - return result -} - -func (id PersonID) Validate() []string { - result := id.ActorID.Validate() - result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) - result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea"}, "Source")...) - - switch id.Source { - case "forgejo", "gitea": - if strings.ToLower(id.Path) != "api/v1/activitypub/user-id" && strings.ToLower(id.Path) != "api/activitypub/user-id" { - result = append(result, fmt.Sprintf("path: %q has to be a person specific api path", id.Path)) - } - } - - return result -} - -// ----------------------------- RepositoryID -------------------------------------------- - -type RepositoryID struct { - ActorID -} - -// Factory function for RepositoryID. Created struct is asserted to be valid. -func NewRepositoryID(uri, source string) (RepositoryID, error) { - result, err := newActorID(uri) - if err != nil { - return RepositoryID{}, err - } - result.Source = source - - // validate Person specific - repoID := RepositoryID{result} - if valid, err := validation.IsValid(repoID); !valid { - return RepositoryID{}, err - } - - return repoID, nil -} - -func (id RepositoryID) Validate() []string { - result := id.ActorID.Validate() - result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) - result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea"}, "Source")...) - switch id.Source { - case "forgejo", "gitea": - if strings.ToLower(id.Path) != "api/v1/activitypub/repository-id" && strings.ToLower(id.Path) != "api/activitypub/repository-id" { - result = append(result, fmt.Sprintf("path: %q has to be a repo specific api path", id.Path)) - } - } - return result -} - -func containsEmptyString(ar []string) bool { - for _, elem := range ar { - if elem == "" { - return true - } - } - return false -} - -func removeEmptyStrings(ls []string) []string { - var rs []string - for _, str := range ls { - if str != "" { - rs = append(rs, str) - } - } - return rs -} - -// ----------------------------- newActorID -------------------------------------------- - func newActorID(uri string) (ActorID, error) { validatedURI, err := url.ParseRequestURI(uri) if err != nil { @@ -212,28 +106,21 @@ func newActorID(uri string) (ActorID, error) { return result, nil } -// ----------------------------- ForgePerson ------------------------------------- - -// ForgePerson activity data type -// swagger:model -type ForgePerson struct { - // swagger:ignore - ap.Actor +func containsEmptyString(ar []string) bool { + for _, elem := range ar { + if elem == "" { + return true + } + } + return false } -func (s ForgePerson) MarshalJSON() ([]byte, error) { - return s.Actor.MarshalJSON() -} - -func (s *ForgePerson) UnmarshalJSON(data []byte) error { - return s.Actor.UnmarshalJSON(data) -} - -func (s ForgePerson) Validate() []string { - var result []string - result = append(result, validation.ValidateNotEmpty(string(s.Type), "Type")...) - result = append(result, validation.ValidateOneOf(string(s.Type), []any{string(ap.PersonType)}, "Type")...) - result = append(result, validation.ValidateNotEmpty(s.PreferredUsername.String(), "PreferredUsername")...) - - return result +func removeEmptyStrings(ls []string) []string { + var rs []string + for _, str := range ls { + if str != "" { + rs = append(rs, str) + } + } + return rs } diff --git a/modules/forgefed/actor_person.go b/modules/forgefed/actor_person.go new file mode 100644 index 0000000000..7c43b0d7ce --- /dev/null +++ b/modules/forgefed/actor_person.go @@ -0,0 +1,122 @@ +// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "strings" + + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +// ----------------------------- PersonID -------------------------------------------- +type PersonID struct { + ActorID +} + +const ( + personIDapiPathV1 = "api/v1/activitypub/user-id" + personIDapiPathV1Latest = "api/activitypub/user-id" +) + +// Factory function for PersonID. Created struct is asserted to be valid +func NewPersonID(uri, source string) (PersonID, error) { + result, err := newActorID(uri) + if err != nil { + return PersonID{}, err + } + result.Source = source + + // validate Person specific path + personID := PersonID{result} + if valid, err := validation.IsValid(personID); !valid { + return PersonID{}, err + } + + return personID, nil +} + +func NewPersonIDFromModel(host, schema string, port uint16, softwareName, id string) (PersonID, error) { + result := PersonID{} + result.ID = id + result.Source = softwareName + result.Host = host + result.HostSchema = schema + result.HostPort = port + result.IsPortSupplemented = false + + if softwareName == "forgejo" { + result.Path = personIDapiPathV1 + } + result.UnvalidatedInput = result.AsURI() + + // validate Person specific path + if valid, err := validation.IsValid(result); !valid { + return PersonID{}, err + } + + return result, nil +} + +func (id PersonID) AsWebfinger() string { + result := fmt.Sprintf("@%s@%s", strings.ToLower(id.ID), strings.ToLower(id.Host)) + return result +} + +func (id PersonID) AsLoginName() string { + result := fmt.Sprintf("%s%s", strings.ToLower(id.ID), id.HostSuffix()) + return result +} + +func (id PersonID) HostSuffix() string { + var result string + if !id.IsPortSupplemented { + result = fmt.Sprintf("-%s-%d", strings.ToLower(id.Host), id.HostPort) + } else { + result = fmt.Sprintf("-%s", strings.ToLower(id.Host)) + } + return result +} + +func (id PersonID) Validate() []string { + result := id.ActorID.Validate() + result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) + result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea", "mastodon", "gotosocial"}, "Source")...) + if id.Source == "forgejo" { + result = append(result, validation.ValidateNotEmpty(id.Path, "path")...) + if strings.ToLower(id.Path) != personIDapiPathV1 && strings.ToLower(id.Path) != personIDapiPathV1Latest { + result = append(result, fmt.Sprintf("path: %q has to be a person specific api path", id.Path)) + } + } + + return result +} + +// ----------------------------- ForgePerson ------------------------------------- + +// ForgePerson activity data type +// swagger:model +type ForgePerson struct { + // swagger:ignore + ap.Actor +} + +func (s ForgePerson) MarshalJSON() ([]byte, error) { + return s.Actor.MarshalJSON() +} + +func (s *ForgePerson) UnmarshalJSON(data []byte) error { + return s.Actor.UnmarshalJSON(data) +} + +func (s ForgePerson) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(string(s.Type), "Type")...) + result = append(result, validation.ValidateOneOf(string(s.Type), []any{string(ap.PersonType)}, "Type")...) + result = append(result, validation.ValidateNotEmpty(s.PreferredUsername.String(), "PreferredUsername")...) + + return result +} diff --git a/modules/forgefed/actor_person_test.go b/modules/forgefed/actor_person_test.go new file mode 100644 index 0000000000..f466ddb964 --- /dev/null +++ b/modules/forgefed/actor_person_test.go @@ -0,0 +1,268 @@ +// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "reflect" + "strings" + "testing" + + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewPersonIdFromModel(t *testing.T) { + expected := PersonID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "https" + expected.Path = "api/v1/activitypub/user-id" + expected.Host = "an.other.host" + expected.HostPort = 443 + expected.IsPortSupplemented = false + expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1" + + sut, _ := NewPersonIDFromModel("an.other.host", "https", 443, "forgejo", "1") + assert.Equal(t, expected, sut) +} + +func TestNewPersonId(t *testing.T) { + var sut, expected PersonID + var err error + + expected = PersonID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "https" + expected.Path = "api/v1/activitypub/user-id" + expected.Host = "an.other.host" + expected.HostPort = 443 + expected.IsPortSupplemented = true + expected.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1" + + sut, err = NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") + require.NoError(t, err) + assert.Equal(t, expected, sut) + + expected = PersonID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "https" + expected.Path = "api/v1/activitypub/user-id" + expected.Host = "an.other.host" + expected.HostPort = 443 + expected.IsPortSupplemented = false + expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1" + + sut, _ = NewPersonID("https://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo") + assert.Equal(t, expected, sut) + + expected = PersonID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "http" + expected.Path = "api/v1/activitypub/user-id" + expected.Host = "an.other.host" + expected.HostPort = 80 + expected.IsPortSupplemented = false + expected.UnvalidatedInput = "http://an.other.host:80/api/v1/activitypub/user-id/1" + + sut, _ = NewPersonID("http://an.other.host:80/api/v1/activitypub/user-id/1", "forgejo") + assert.Equal(t, expected, sut) + + expected = PersonID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "https" + expected.Path = "api/v1/activitypub/user-id" + expected.Host = "an.other.host" + expected.HostPort = 443 + expected.IsPortSupplemented = false + expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1" + + sut, _ = NewPersonID("HTTPS://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo") + assert.Equal(t, expected, sut) + + expected = PersonID{} + expected.ID = "@me" + expected.Source = "gotosocial" + expected.HostSchema = "https" + expected.Path = "" + expected.Host = "an.other.host" + expected.HostPort = 443 + expected.IsPortSupplemented = true + expected.UnvalidatedInput = "https://an.other.host/@me" + + sut, err = NewPersonID("https://an.other.host/@me", "gotosocial") + require.NoError(t, err) + assert.Equal(t, expected, sut) +} + +func TestPersonIdValidation(t *testing.T) { + sut := PersonID{} + sut.ID = "1" + sut.Source = "forgejo" + sut.HostSchema = "https" + sut.Path = "" + sut.Host = "an.other.host" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://an.other.host/1" + + result, err := validation.IsValid(sut) + assert.False(t, result) + require.EqualError(t, err, "Validation Error: forgefed.PersonID: path should not be empty\npath: \"\" has to be a person specific api path") + + sut = PersonID{} + sut.ID = "1" + sut.Source = "mastodon" + sut.HostSchema = "https" + sut.Path = "" + sut.Host = "an.other.host" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://an.other.host/1" + + result, err = validation.IsValid(sut) + assert.True(t, result) + require.NoError(t, err) + + sut = PersonID{} + sut.ID = "1" + sut.Source = "forgejo" + sut.HostSchema = "https" + sut.Path = "path" + sut.Host = "an.other.host" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://an.other.host/path/1" + + result, err = validation.IsValid(sut) + assert.False(t, result) + require.EqualError(t, err, "Validation Error: forgefed.PersonID: path: \"path\" has to be a person specific api path") + + sut = PersonID{} + sut.ID = "1" + sut.Source = "forgejox" + sut.HostSchema = "https" + sut.Path = "api/v1/activitypub/user-id" + sut.Host = "an.other.host" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1" + + result, err = validation.IsValid(sut) + assert.False(t, result) + require.EqualError(t, err, "Validation Error: forgefed.PersonID: Field Source contains the value forgejox, which is not in allowed subset [forgejo gitea mastodon gotosocial]") +} + +func TestWebfingerId(t *testing.T) { + sut, _ := NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") + assert.Equal(t, "@12345@codeberg.org", sut.AsWebfinger()) +} + +func TestShouldThrowErrorOnInvalidInput(t *testing.T) { + var err any + _, err = NewPersonID("", "forgejo") + if err == nil { + t.Errorf("empty input should be invalid.") + } + _, err = NewPersonID("http://localhost:3000/api/v1/something", "forgejo") + if err == nil { + t.Errorf("localhost uris are not external") + } + _, err = NewPersonID("./api/v1/something", "forgejo") + if err == nil { + t.Errorf("relative uris are not allowed") + } + _, err = NewPersonID("http://1.2.3.4/api/v1/something", "forgejo") + if err == nil { + t.Errorf("uri may not be ip-4 based") + } + _, err = NewPersonID("http:///[fe80::1ff:fe23:4567:890a%25eth0]/api/v1/something", "forgejo") + if err == nil { + t.Errorf("uri may not be ip-6 based") + } + _, err = NewPersonID("https://codeberg.org/api/v1/activitypub/../activitypub/user-id/12345", "forgejo") + if err == nil { + t.Errorf("uri may not contain relative path elements") + } + _, err = NewPersonID("https://myuser@an.other.host/api/v1/activitypub/user-id/1", "forgejo") + if err == nil { + t.Errorf("uri may not contain unparsed elements") + } + _, err = NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") + if err != nil { + t.Errorf("this uri should be valid but was: %v", err) + } +} + +func Test_PersonMarshalJSON(t *testing.T) { + sut := ForgePerson{} + sut.Type = "Person" + sut.PreferredUsername = ap.NaturalLanguageValuesNew() + sut.PreferredUsername.Set("en", ap.Content("MaxMuster")) + result, _ := sut.MarshalJSON() + assert.JSONEq(t, `{"type":"Person","preferredUsername":"MaxMuster"}`, string(result), "Expected string is not equal") +} + +func Test_PersonUnmarshalJSON(t *testing.T) { + expected := &ForgePerson{ + Actor: ap.Actor{ + Type: "Person", + PreferredUsername: ap.NaturalLanguageValues{ + ap.LangRefValue{Ref: "en", Value: []byte("MaxMuster")}, + }, + }, + } + sut := new(ForgePerson) + err := sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`)) + if err != nil { + t.Errorf("UnmarshalJSON() unexpected error: %v", err) + } + x, _ := expected.MarshalJSON() + y, _ := sut.MarshalJSON() + if !reflect.DeepEqual(x, y) { + t.Errorf("UnmarshalJSON() expected: %q got: %q", x, y) + } + + expectedStr := strings.ReplaceAll(strings.ReplaceAll(`{ + "id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10", + "type":"Person", + "icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatar/fa7f9c4af2a64f41b1bef292bf872614"}, + "url":"https://federated-repo.prod.meissa.de/stargoose9", + "inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10/inbox", + "outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10/outbox", + "preferredUsername":"stargoose9", + "publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10#main-key", + "owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10", + "publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBoj...XAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`, + "\n", ""), + "\t", "") + err = sut.UnmarshalJSON([]byte(expectedStr)) + if err != nil { + t.Errorf("UnmarshalJSON() unexpected error: %v", err) + } + result, _ := sut.MarshalJSON() + assert.JSONEq(t, expectedStr, string(result), "Expected string is not equal") +} + +func TestForgePersonValidation(t *testing.T) { + sut := new(ForgePerson) + sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`)) + if res, _ := validation.IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } +} + +func TestAsloginName(t *testing.T) { + sut, _ := NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") + assert.Equal(t, "12345-codeberg.org", sut.AsLoginName()) + + sut, _ = NewPersonID("https://codeberg.org:443/api/v1/activitypub/user-id/12345", "forgejo") + assert.Equal(t, "12345-codeberg.org-443", sut.AsLoginName()) +} diff --git a/modules/forgefed/actor_repository.go b/modules/forgefed/actor_repository.go new file mode 100644 index 0000000000..e73107caef --- /dev/null +++ b/modules/forgefed/actor_repository.go @@ -0,0 +1,52 @@ +// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "strings" + + "forgejo.org/modules/validation" +) + +// ----------------------------- RepositoryID -------------------------------------------- + +type RepositoryID struct { + ActorID +} + +const ( + repositoryIDapiPathV1 = "api/v1/activitypub/repository-id" + repositoryIDapiPathV1Latest = "api/activitypub/repository-id" +) + +// Factory function for RepositoryID. Created struct is asserted to be valid. +func NewRepositoryID(uri, source string) (RepositoryID, error) { + result, err := newActorID(uri) + if err != nil { + return RepositoryID{}, err + } + result.Source = source + + // validate Person specific + repoID := RepositoryID{result} + if valid, err := validation.IsValid(repoID); !valid { + return RepositoryID{}, err + } + + return repoID, nil +} + +func (id RepositoryID) Validate() []string { + result := id.ActorID.Validate() + result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) + result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea"}, "Source")...) + if id.Source == "forgejo" { + result = append(result, validation.ValidateNotEmpty(id.Path, "path")...) + if strings.ToLower(id.Path) != repositoryIDapiPathV1 && strings.ToLower(id.Path) != repositoryIDapiPathV1Latest { + result = append(result, fmt.Sprintf("path: %q has to be a repo specific api path", id.Path)) + } + } + return result +} diff --git a/modules/forgefed/actor_repository_test.go b/modules/forgefed/actor_repository_test.go new file mode 100644 index 0000000000..382706f387 --- /dev/null +++ b/modules/forgefed/actor_repository_test.go @@ -0,0 +1,45 @@ +// Copyright 2023, 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "testing" + + "forgejo.org/modules/setting" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewRepositoryId(t *testing.T) { + var sut, expected RepositoryID + var err error + setting.AppURL = "http://localhost:3000/" + + expected = RepositoryID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "http" + expected.Path = "" + expected.Host = "localhost" + expected.HostPort = 3000 + expected.IsPortSupplemented = false + expected.UnvalidatedInput = "http://localhost:3000/1" + + _, err = NewRepositoryID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") + require.EqualError(t, err, "Validation Error: forgefed.RepositoryID: path: \"api/v1/activitypub/user-id\" has to be a repo specific api path") + + expected = RepositoryID{} + expected.ID = "1" + expected.Source = "forgejo" + expected.HostSchema = "http" + expected.Path = "api/activitypub/repository-id" + expected.Host = "localhost" + expected.HostPort = 3000 + expected.IsPortSupplemented = false + expected.UnvalidatedInput = "http://localhost:3000/api/activitypub/repository-id/1" + sut, err = NewRepositoryID("http://localhost:3000/api/activitypub/repository-id/1", "forgejo") + require.NoError(t, err) + assert.Equal(t, expected, sut) +} diff --git a/modules/forgefed/actor_test.go b/modules/forgefed/actor_test.go index 5315d0b4de..48d773c5b9 100644 --- a/modules/forgefed/actor_test.go +++ b/modules/forgefed/actor_test.go @@ -4,258 +4,71 @@ package forgefed import ( - "reflect" - "strings" "testing" - "forgejo.org/modules/setting" - "forgejo.org/modules/validation" - - ap "github.com/go-ap/activitypub" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestNewPersonId(t *testing.T) { - expected := PersonID{} - expected.ID = "1" - expected.Source = "forgejo" - expected.HostSchema = "https" - expected.Path = "api/v1/activitypub/user-id" - expected.Host = "an.other.host" - expected.HostPort = 443 - expected.IsPortSupplemented = true - expected.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1" +func TestActorNew(t *testing.T) { + sut, err := NewActorID("https://an.other.forgejo.host/api/v1/activitypub/user-id/5") + require.NoError(t, err) + assert.Equal(t, ActorID{ + ID: "5", + HostSchema: "https", + Path: "api/v1/activitypub/user-id", + Host: "an.other.forgejo.host", + HostPort: 443, + UnvalidatedInput: "https://an.other.forgejo.host/api/v1/activitypub/user-id/5", + IsPortSupplemented: true, + }, sut) - sut, _ := NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") - if sut != expected { - t.Errorf("expected: %v\n but was: %v\n", expected, sut) - } + sut, err = NewActorID("https://an.other.forgejo.host/api/v1/activitypub/actor") + require.NoError(t, err) + assert.Equal(t, ActorID{ + ID: "actor", + HostSchema: "https", + Path: "api/v1/activitypub", + Host: "an.other.forgejo.host", + HostPort: 443, + UnvalidatedInput: "https://an.other.forgejo.host/api/v1/activitypub/actor", + IsPortSupplemented: true, + }, sut) - expected = PersonID{} - expected.ID = "1" - expected.Source = "forgejo" - expected.HostSchema = "https" - expected.Path = "api/v1/activitypub/user-id" - expected.Host = "an.other.host" - expected.HostPort = 443 - expected.IsPortSupplemented = false - expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1" - - sut, _ = NewPersonID("https://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo") - if sut != expected { - t.Errorf("expected: %v\n but was: %v\n", expected, sut) - } - - expected = PersonID{} - expected.ID = "1" - expected.Source = "forgejo" - expected.HostSchema = "http" - expected.Path = "api/v1/activitypub/user-id" - expected.Host = "an.other.host" - expected.HostPort = 80 - expected.IsPortSupplemented = false - expected.UnvalidatedInput = "http://an.other.host:80/api/v1/activitypub/user-id/1" - - sut, _ = NewPersonID("http://an.other.host:80/api/v1/activitypub/user-id/1", "forgejo") - if sut != expected { - t.Errorf("expected: %v\n but was: %v\n", expected, sut) - } - - expected = PersonID{} - expected.ID = "1" - expected.Source = "forgejo" - expected.HostSchema = "https" - expected.Path = "api/v1/activitypub/user-id" - expected.Host = "an.other.host" - expected.HostPort = 443 - expected.IsPortSupplemented = false - expected.UnvalidatedInput = "https://an.other.host:443/api/v1/activitypub/user-id/1" - - sut, _ = NewPersonID("HTTPS://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo") - if sut != expected { - t.Errorf("expected: %v\n but was: %v\n", expected, sut) - } -} - -func TestNewRepositoryId(t *testing.T) { - setting.AppURL = "http://localhost:3000/" - expected := RepositoryID{} - expected.ID = "1" - expected.Source = "forgejo" - expected.HostSchema = "http" - expected.Path = "api/activitypub/repository-id" - expected.Host = "localhost" - expected.HostPort = 3000 - expected.IsPortSupplemented = false - expected.UnvalidatedInput = "http://localhost:3000/api/activitypub/repository-id/1" - sut, _ := NewRepositoryID("http://localhost:3000/api/activitypub/repository-id/1", "forgejo") - if sut != expected { - t.Errorf("expected: %v\n but was: %v\n", expected, sut) - } + sut, err = NewActorID("https://an.other.gts.host/users/me") + require.NoError(t, err) + assert.Equal(t, ActorID{ + ID: "me", + HostSchema: "https", + Path: "users", + Host: "an.other.gts.host", + HostPort: 443, + UnvalidatedInput: "https://an.other.gts.host/users/me", + IsPortSupplemented: true, + }, sut) } func TestActorIdValidation(t *testing.T) { sut := ActorID{} - sut.Source = "forgejo" sut.HostSchema = "https" sut.Path = "api/v1/activitypub/user-id" sut.Host = "an.other.host" sut.HostPort = 443 sut.IsPortSupplemented = true sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/" - if sut.Validate()[0] != "userId should not be empty" { - t.Errorf("validation error expected but was: %v\n", sut.Validate()) - } + result := sut.Validate() + assert.Len(t, result, 1) + assert.Equal(t, "ID should not be empty", result[0]) sut = ActorID{} sut.ID = "1" - sut.Source = "forgejo" sut.HostSchema = "https" sut.Path = "api/v1/activitypub/user-id" sut.Host = "an.other.host" sut.HostPort = 443 sut.IsPortSupplemented = true sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1?illegal=action" - if sut.Validate()[0] != "not all input was parsed, \nUnvalidated Input:\"https://an.other.host/api/v1/activitypub/user-id/1?illegal=action\" \nParsed URI: \"https://an.other.host/api/v1/activitypub/user-id/1\"" { - t.Errorf("validation error expected but was: %v\n", sut.Validate()[0]) - } -} - -func TestPersonIdValidation(t *testing.T) { - sut := PersonID{} - sut.ID = "1" - sut.Source = "forgejo" - sut.HostSchema = "https" - sut.Path = "path" - sut.Host = "an.other.host" - sut.HostPort = 443 - sut.IsPortSupplemented = true - sut.UnvalidatedInput = "https://an.other.host/path/1" - - _, err := validation.IsValid(sut) - if validation.IsErrNotValid(err) && strings.Contains(err.Error(), "path: \"path\" has to be a person specific api path\n") { - t.Errorf("validation error expected but was: %v\n", err) - } - - sut = PersonID{} - sut.ID = "1" - sut.Source = "forgejox" - sut.HostSchema = "https" - sut.Path = "api/v1/activitypub/user-id" - sut.Host = "an.other.host" - sut.HostPort = 443 - sut.IsPortSupplemented = true - sut.UnvalidatedInput = "https://an.other.host/api/v1/activitypub/user-id/1" - if sut.Validate()[0] != "Value forgejox is not contained in allowed values [forgejo gitea]" { - t.Errorf("validation error expected but was: %v\n", sut.Validate()[0]) - } -} - -func TestWebfingerId(t *testing.T) { - sut, _ := NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") - if sut.AsWebfinger() != "@12345@codeberg.org" { - t.Errorf("wrong webfinger: %v", sut.AsWebfinger()) - } - - sut, _ = NewPersonID("https://Codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") - if sut.AsWebfinger() != "@12345@codeberg.org" { - t.Errorf("wrong webfinger: %v", sut.AsWebfinger()) - } -} - -func TestShouldThrowErrorOnInvalidInput(t *testing.T) { - var err any - _, err = NewPersonID("", "forgejo") - if err == nil { - t.Errorf("empty input should be invalid.") - } - _, err = NewPersonID("http://localhost:3000/api/v1/something", "forgejo") - if err == nil { - t.Errorf("localhost uris are not external") - } - _, err = NewPersonID("./api/v1/something", "forgejo") - if err == nil { - t.Errorf("relative uris are not allowed") - } - _, err = NewPersonID("http://1.2.3.4/api/v1/something", "forgejo") - if err == nil { - t.Errorf("uri may not be ip-4 based") - } - _, err = NewPersonID("http:///[fe80::1ff:fe23:4567:890a%25eth0]/api/v1/something", "forgejo") - if err == nil { - t.Errorf("uri may not be ip-6 based") - } - _, err = NewPersonID("https://codeberg.org/api/v1/activitypub/../activitypub/user-id/12345", "forgejo") - if err == nil { - t.Errorf("uri may not contain relative path elements") - } - _, err = NewPersonID("https://myuser@an.other.host/api/v1/activitypub/user-id/1", "forgejo") - if err == nil { - t.Errorf("uri may not contain unparsed elements") - } - _, err = NewPersonID("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") - if err != nil { - t.Errorf("this uri should be valid but was: %v", err) - } -} - -func Test_PersonMarshalJSON(t *testing.T) { - sut := ForgePerson{} - sut.Type = "Person" - sut.PreferredUsername = ap.NaturalLanguageValuesNew() - sut.PreferredUsername.Set("en", ap.Content("MaxMuster")) - result, _ := sut.MarshalJSON() - if string(result) != "{\"type\":\"Person\",\"preferredUsername\":\"MaxMuster\"}" { - t.Errorf("MarshalJSON() was = %q", result) - } -} - -func Test_PersonUnmarshalJSON(t *testing.T) { - expected := &ForgePerson{ - Actor: ap.Actor{ - Type: "Person", - PreferredUsername: ap.NaturalLanguageValues{ - ap.LangRefValue{Ref: "en", Value: []byte("MaxMuster")}, - }, - }, - } - sut := new(ForgePerson) - err := sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`)) - if err != nil { - t.Errorf("UnmarshalJSON() unexpected error: %v", err) - } - x, _ := expected.MarshalJSON() - y, _ := sut.MarshalJSON() - if !reflect.DeepEqual(x, y) { - t.Errorf("UnmarshalJSON() expected: %q got: %q", x, y) - } - - expectedStr := strings.ReplaceAll(strings.ReplaceAll(`{ - "id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10", - "type":"Person", - "icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatar/fa7f9c4af2a64f41b1bef292bf872614"}, - "url":"https://federated-repo.prod.meissa.de/stargoose9", - "inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10/inbox", - "outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10/outbox", - "preferredUsername":"stargoose9", - "publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10#main-key", - "owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/10", - "publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBoj...XAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`, - "\n", ""), - "\t", "") - err = sut.UnmarshalJSON([]byte(expectedStr)) - if err != nil { - t.Errorf("UnmarshalJSON() unexpected error: %v", err) - } - result, _ := sut.MarshalJSON() - if expectedStr != string(result) { - t.Errorf("UnmarshalJSON() expected: %q got: %q", expectedStr, result) - } -} - -func TestForgePersonValidation(t *testing.T) { - sut := new(ForgePerson) - sut.UnmarshalJSON([]byte(`{"type":"Person","preferredUsername":"MaxMuster"}`)) - if res, _ := validation.IsValid(sut); !res { - t.Errorf("sut expected to be valid: %v\n", sut.Validate()) - } + result = sut.Validate() + assert.Len(t, result, 1) + assert.Equal(t, "not all input was parsed, \nUnvalidated Input:\"https://an.other.host/api/v1/activitypub/user-id/1?illegal=action\" \nParsed URI: \"https://an.other.host/api/v1/activitypub/user-id/1\"", result[0]) } diff --git a/modules/forgefed/forgefed.go b/modules/forgefed/forgefed.go index 2344dc7a8b..8fca66cb53 100644 --- a/modules/forgefed/forgefed.go +++ b/modules/forgefed/forgefed.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Forgejo Authors. All rights reserved. +// Copyright 2023, 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package forgefed diff --git a/modules/forgefed/object_user_activity_note.go b/modules/forgefed/object_user_activity_note.go new file mode 100644 index 0000000000..758c25aef8 --- /dev/null +++ b/modules/forgefed/object_user_activity_note.go @@ -0,0 +1,68 @@ +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "time" + + user_model "forgejo.org/models/user" + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +// ForgeFollow activity data type +// swagger:model +type ForgeUserActivityNote struct { + // swagger.ignore + ap.Object +} + +func NewForgeUserActivityNoteFromAp(item ap.Item) (ForgeUserActivityNote, error) { + result := ForgeUserActivityNote{} + object := item.(*ap.Object) + result.Object = *object + if valid, err := validation.IsValid(result); !valid { + return ForgeUserActivityNote{}, err + } + return result, nil +} + +// TODO: Unused - might be removed +func newNote(doer *user_model.User, content, id string, published time.Time) (ForgeUserActivityNote, error) { + note := ForgeUserActivityNote{} + note.Type = ap.NoteType + note.AttributedTo = ap.IRI(doer.APActorID()) + note.Content = ap.NaturalLanguageValues{ + { + Ref: ap.NilLangRef, + Value: ap.Content(content), + }, + } + note.ID = ap.IRI(id) + note.Published = published + note.URL = ap.IRI(id) + note.To = ap.ItemCollection{ + ap.IRI("https://www.w3.org/ns/activitystreams#Public"), + } + note.CC = ap.ItemCollection{ + ap.IRI(doer.APActorID() + "/followers"), + } + + if valid, err := validation.IsValid(note); !valid { + return ForgeUserActivityNote{}, err + } + + return note, nil +} + +func (note ForgeUserActivityNote) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(string(note.Type), "type")...) + result = append(result, validation.ValidateOneOf(string(note.Type), []any{"Note"}, "type")...) + result = append(result, validation.ValidateNotEmpty(note.Content.String(), "content")...) + result = append(result, validation.ValidateIDExists(note.URL, "url")...) + + return result +} diff --git a/modules/forgefed/object_user_activity_note_test.go b/modules/forgefed/object_user_activity_note_test.go new file mode 100644 index 0000000000..20c3666bb1 --- /dev/null +++ b/modules/forgefed/object_user_activity_note_test.go @@ -0,0 +1,28 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "testing" + + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +func Test_UserActivityNoteValidation(t *testing.T) { + sut := ForgeUserActivityNote{} + sut.Type = "Note" + sut.Content = ap.NaturalLanguageValues{ + { + Ref: ap.NilLangRef, + Value: ap.Content("Any Content!"), + }, + } + sut.URL = ap.IRI("example.org/user-id/57") + + if res, _ := validation.IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } +} diff --git a/modules/git/blob.go b/modules/git/blob.go index 3fda358938..30615afe32 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -12,7 +12,6 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/typesniffer" - "forgejo.org/modules/util" ) // Blob represents a Git object. @@ -25,42 +24,25 @@ type Blob struct { repo *Repository } -// DataAsync gets a ReadCloser for the contents of a blob without reading it all. -// Calling the Close function on the result will discard all unread output. -func (b *Blob) DataAsync() (io.ReadCloser, error) { +func (b *Blob) newReader() (*bufio.Reader, int64, func(), error) { wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) if err != nil { - return nil, err + return nil, 0, nil, err } _, err = wr.Write([]byte(b.ID.String() + "\n")) if err != nil { cancel() - return nil, err + return nil, 0, nil, err } _, _, size, err := ReadBatchLine(rd) if err != nil { cancel() - return nil, err + return nil, 0, nil, err } b.gotSize = true b.size = size - - if size < 4096 { - bs, err := io.ReadAll(io.LimitReader(rd, size)) - defer cancel() - if err != nil { - return nil, err - } - _, err = rd.Discard(1) - return io.NopCloser(bytes.NewReader(bs)), err - } - - return &blobReader{ - rd: rd, - n: size, - cancel: cancel, - }, nil + return rd, size, cancel, err } // Size returns the uncompressed size of the blob @@ -91,10 +73,36 @@ func (b *Blob) Size() int64 { return b.size } +// DataAsync gets a ReadCloser for the contents of a blob without reading it all. +// Calling the Close function on the result will discard all unread output. +func (b *Blob) DataAsync() (io.ReadCloser, error) { + rd, size, cancel, err := b.newReader() + if err != nil { + return nil, err + } + + if size < 4096 { + bs, err := io.ReadAll(io.LimitReader(rd, size)) + defer cancel() + if err != nil { + return nil, err + } + _, err = rd.Discard(1) + return io.NopCloser(bytes.NewReader(bs)), err + } + + return &blobReader{ + rd: rd, + n: size, + cancel: cancel, + }, nil +} + type blobReader struct { - rd *bufio.Reader - n int64 - cancel func() + rd *bufio.Reader + n int64 // number of bytes to read + additionalDiscard int64 // additional number of bytes to discard + cancel func() } func (b *blobReader) Read(p []byte) (n int, err error) { @@ -117,7 +125,8 @@ func (b *blobReader) Close() error { defer b.cancel() - if err := DiscardFull(b.rd, b.n+1); err != nil { + // discard the unread bytes, the truncated bytes and the trailing newline + if err := DiscardFull(b.rd, b.n+b.additionalDiscard+1); err != nil { return err } @@ -131,47 +140,38 @@ func (b *Blob) Name() string { return b.name } -// GetBlobContent Gets the limited content of the blob as raw text +// NewTruncatedReader return a blob-reader which silently truncates when the limit is reached (io.EOF will be returned) +func (b *Blob) NewTruncatedReader(limit int64) (rc io.ReadCloser, fullSize int64, err error) { + r, fullSize, cancel, err := b.newReader() + if err != nil { + return nil, fullSize, err + } + + limit = min(limit, fullSize) + return &blobReader{ + rd: r, + n: limit, + additionalDiscard: fullSize - limit, + cancel: cancel, + }, fullSize, nil +} + +// GetBlobContent Gets the truncated content of the blob as raw text func (b *Blob) GetBlobContent(limit int64) (string, error) { if limit <= 0 { return "", nil } - dataRc, err := b.DataAsync() + rc, fullSize, err := b.NewTruncatedReader(limit) if err != nil { return "", err } - defer dataRc.Close() - buf, err := util.ReadWithLimit(dataRc, int(limit)) + defer rc.Close() + + buf := make([]byte, min(fullSize, limit)) + _, err = io.ReadFull(rc, buf) return string(buf), err } -// GetBlobLineCount gets line count of the blob -func (b *Blob) GetBlobLineCount() (int, error) { - reader, err := b.DataAsync() - if err != nil { - return 0, err - } - defer reader.Close() - buf := make([]byte, 32*1024) - count := 1 - lineSep := []byte{'\n'} - - c, err := reader.Read(buf) - if c == 0 && err == io.EOF { - return 0, nil - } - for { - count += bytes.Count(buf[:c], lineSep) - switch { - case err == io.EOF: - return count, nil - case err != nil: - return count, err - } - c, err = reader.Read(buf) - } -} - // GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string func (b *Blob) GetBlobContentBase64() (string, error) { dataRc, err := b.DataAsync() diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index 810964b33d..54115013d3 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -35,6 +35,106 @@ func TestBlob_Data(t *testing.T) { assert.Equal(t, output, string(data)) } +func TestBlob(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + repo, err := openRepositoryWithDefaultContext(bareRepo1Path) + require.NoError(t, err) + + defer repo.Close() + + testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375") + require.NoError(t, err) + + t.Run("GetBlobContent", func(t *testing.T) { + r, err := testBlob.GetBlobContent(100) + require.NoError(t, err) + require.Equal(t, "file2\n", r) + + r, err = testBlob.GetBlobContent(-1) + require.NoError(t, err) + require.Empty(t, r) + + r, err = testBlob.GetBlobContent(4) + require.NoError(t, err) + require.Equal(t, "file", r) + + r, err = testBlob.GetBlobContent(6) + require.NoError(t, err) + require.Equal(t, "file2\n", r) + }) + + t.Run("NewTruncatedReader", func(t *testing.T) { + // read fewer than available + rc, size, err := testBlob.NewTruncatedReader(100) + require.NoError(t, err) + require.Equal(t, int64(6), size) + + buf := make([]byte, 1) + n, err := rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Equal(t, "f", string(buf)) + n, err = rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Equal(t, "i", string(buf)) + + require.NoError(t, rc.Close()) + + // read more than available + rc, size, err = testBlob.NewTruncatedReader(100) + require.NoError(t, err) + require.Equal(t, int64(6), size) + + buf = make([]byte, 100) + n, err = rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 6, n) + require.Equal(t, "file2\n", string(buf[:n])) + + n, err = rc.Read(buf) + require.Error(t, err) + require.Equal(t, io.EOF, err) + require.Equal(t, 0, n) + + require.NoError(t, rc.Close()) + + // read more than truncated + rc, size, err = testBlob.NewTruncatedReader(4) + require.NoError(t, err) + require.Equal(t, int64(6), size) + + buf = make([]byte, 10) + n, err = rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 4, n) + require.Equal(t, "file", string(buf[:n])) + + n, err = rc.Read(buf) + require.Error(t, err) + require.Equal(t, io.EOF, err) + require.Equal(t, 0, n) + + require.NoError(t, rc.Close()) + }) + + t.Run("NonExisting", func(t *testing.T) { + nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000") + require.NoError(t, err) + + r, err := nonExistingBlob.GetBlobContent(100) + require.Error(t, err) + require.IsType(t, ErrNotExist{}, err) + require.Empty(t, r) + + rc, size, err := nonExistingBlob.NewTruncatedReader(100) + require.Error(t, err) + require.IsType(t, ErrNotExist{}, err) + require.Empty(t, rc) + require.Empty(t, size) + }) +} + func Benchmark_Blob_Data(b *testing.B) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := openRepositoryWithDefaultContext(bareRepo1Path) diff --git a/modules/git/commit.go b/modules/git/commit.go index a8ebed4968..96831e3ae4 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -9,7 +9,6 @@ import ( "bytes" "context" "errors" - "fmt" "io" "os/exec" "strconv" @@ -386,7 +385,7 @@ func parseSubmoduleContent(bs []byte) (*ObjectCache, error) { } submoduleCache := newObjectCache() if len(cfg.Submodules) == 0 { - return nil, fmt.Errorf("no submodules found") + return nil, errors.New("no submodules found") } for _, subModule := range cfg.Submodules { submoduleCache.Set(subModule.Path, subModule.URL) diff --git a/modules/git/git.go b/modules/git/git.go index 743ff59ddd..1dfd0b5134 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -38,6 +38,7 @@ var ( InvertedGitFlushEnv bool // 2.43.1 SupportCheckAttrOnBare bool // >= 2.40 SupportGitMergeTree bool // >= 2.38 + SupportGrepMaxCount bool // >= 2.38 HasSSHExecutable bool @@ -191,6 +192,7 @@ func InitFull(ctx context.Context) (err error) { InvertedGitFlushEnv = CheckGitVersionEqual("2.43.1") == nil SupportGitMergeTree = CheckGitVersionAtLeast("2.38") == nil + SupportGrepMaxCount = CheckGitVersionAtLeast("2.38") == nil if setting.LFS.StartServer { if CheckGitVersionAtLeast("2.1.2") != nil { diff --git a/modules/git/git_test.go b/modules/git/git_test.go index 6ad5ce7b84..01200dba68 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -86,10 +86,7 @@ func TestGitConfig(t *testing.T) { } func TestSyncConfig(t *testing.T) { - oldGitConfig := setting.GitConfig - defer func() { - setting.GitConfig = oldGitConfig - }() + defer test.MockProtect(&setting.GitConfig)() setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA" require.NoError(t, syncGitConfig()) @@ -108,6 +105,10 @@ func TestSyncConfigGPGFormat(t *testing.T) { }) t.Run("SSH format", func(t *testing.T) { + if CheckGitVersionAtLeast("2.34.0") != nil { + t.SkipNow() + } + r, err := os.OpenRoot(t.TempDir()) require.NoError(t, err) f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700) diff --git a/modules/git/grep.go b/modules/git/grep.go index 117b09fc83..3703b13660 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -98,8 +98,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) - // --max-count requires at least git 2.38 - if CheckGitVersionAtLeast("2.38.0") == nil { + if SupportGrepMaxCount { cmd.AddOptionValues("--max-count", fmt.Sprint(opts.MatchesPerFile)) } else { log.Warn("git-grep: --max-count requires at least git 2.38") diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 534468e268..83ddb766af 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -59,48 +59,55 @@ func TestGrepSearch(t *testing.T) { }, }, res) - res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{MatchesPerFile: 1}) - require.NoError(t, err) - assert.Equal(t, []*GrepResult{ - { - Filename: "i-am-a-python.p", - LineNumbers: []int{1}, - LineCodes: []string{"## This is a simple file to do a hello world"}, - HighlightedRanges: [][3]int{{0, 39, 44}}, - }, - { - Filename: "java-hello/main.java", - LineNumbers: []int{1}, - LineCodes: []string{"public class HelloWorld"}, - HighlightedRanges: [][3]int{{0, 18, 23}}, - }, - { - Filename: "main.vendor.java", - LineNumbers: []int{1}, - LineCodes: []string{"public class HelloWorld"}, - HighlightedRanges: [][3]int{{0, 18, 23}}, - }, - { - Filename: "python-hello/hello.py", - LineNumbers: []int{1}, - LineCodes: []string{"## This is a simple file to do a hello world"}, - HighlightedRanges: [][3]int{{0, 39, 44}}, - }, - }, res) + t.Run("Max count", func(t *testing.T) { + if !SupportGrepMaxCount { + t.Skip("Skipping, git grep --max-count is not supported") + return + } - res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{ - MatchesPerFile: 1, - Filename: "java-hello/", + res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{MatchesPerFile: 1}) + require.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "i-am-a-python.p", + LineNumbers: []int{1}, + LineCodes: []string{"## This is a simple file to do a hello world"}, + HighlightedRanges: [][3]int{{0, 39, 44}}, + }, + { + Filename: "java-hello/main.java", + LineNumbers: []int{1}, + LineCodes: []string{"public class HelloWorld"}, + HighlightedRanges: [][3]int{{0, 18, 23}}, + }, + { + Filename: "main.vendor.java", + LineNumbers: []int{1}, + LineCodes: []string{"public class HelloWorld"}, + HighlightedRanges: [][3]int{{0, 18, 23}}, + }, + { + Filename: "python-hello/hello.py", + LineNumbers: []int{1}, + LineCodes: []string{"## This is a simple file to do a hello world"}, + HighlightedRanges: [][3]int{{0, 39, 44}}, + }, + }, res) + + res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{ + MatchesPerFile: 1, + Filename: "java-hello/", + }) + require.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "java-hello/main.java", + LineNumbers: []int{1}, + LineCodes: []string{"public class HelloWorld"}, + HighlightedRanges: [][3]int{{0, 18, 23}}, + }, + }, res) }) - require.NoError(t, err) - assert.Equal(t, []*GrepResult{ - { - Filename: "java-hello/main.java", - LineNumbers: []int{1}, - LineCodes: []string{"public class HelloWorld"}, - HighlightedRanges: [][3]int{{0, 18, 23}}, - }, - }, res) res, err = GrepSearch(t.Context(), repo, "no-such-content", GrepOptions{}) require.NoError(t, err) diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 2154467332..2b07513162 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" "io" "os" @@ -116,7 +117,7 @@ func (ca GitAttribute) Bool() optional.Option[bool] { // instantiation. func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string) (*Command, *RunOpts, context.CancelFunc, error) { if len(attributes) == 0 { - return nil, nil, nil, fmt.Errorf("no provided attributes to check-attr") + return nil, nil, nil, errors.New("no provided attributes to check-attr") } env := os.Environ() diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go index 0006559709..c69382e245 100644 --- a/modules/git/repo_attribute_test.go +++ b/modules/git/repo_attribute_test.go @@ -251,10 +251,14 @@ func TestGitAttributeCheckerError(t *testing.T) { cancel() ac, err := gitRepo.GitAttributeChecker("8fee858da5796dfb37704761701bb8e800ad9ef3", "linguist-language") - require.NoError(t, err) + if SupportCheckAttrOnBare { + require.NoError(t, err) - _, err = ac.CheckPath("i-am-a-python.p") - require.Error(t, err) + _, err = ac.CheckPath("i-am-a-python.p") + require.Error(t, err) + } else { + require.Error(t, err) + } }) t.Run("Cancelled/DuringRun", func(t *testing.T) { diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 1992060351..3a9aa3e4e6 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -41,7 +41,7 @@ type Branch struct { // GetHEADBranch returns corresponding branch of HEAD. func (repo *Repository) GetHEADBranch() (*Branch, error) { if repo == nil { - return nil, fmt.Errorf("nil repo") + return nil, errors.New("nil repo") } stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index cd35367bc9..c5f0658d4e 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -35,6 +35,8 @@ type ServeHeaderOptions struct { Filename string CacheDuration time.Duration // defaults to 5 minutes LastModified time.Time + AdditionalHeaders http.Header + RedirectStatusCode int } // ServeSetHeaders sets necessary content serve headers @@ -82,6 +84,12 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) } + + if opts.AdditionalHeaders != nil { + for k, v := range opts.AdditionalHeaders { + header[k] = v + } + } } // ServeData download file from io.Reader diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index eb003baec7..c53b7a2e6d 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -260,11 +260,11 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int if opts.Mode == internal.CodeSearchModeUnion { query := bleve.NewDisjunctionQuery() for _, field := range strings.Fields(opts.Keyword) { - query.AddQuery(inner_bleve.MatchPhraseQuery(field, "Content", repoIndexerAnalyzer, false)) + query.AddQuery(inner_bleve.MatchPhraseQuery(field, "Content", repoIndexerAnalyzer, false, 1.0)) } keywordQuery = query } else { - keywordQuery = inner_bleve.MatchPhraseQuery(opts.Keyword, "Content", repoIndexerAnalyzer, false) + keywordQuery = inner_bleve.MatchPhraseQuery(opts.Keyword, "Content", repoIndexerAnalyzer, false, 1.0) } if len(opts.RepoIDs) > 0 { diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go index cc2c2aaf06..73662b1dda 100644 --- a/modules/indexer/code/internal/indexer.go +++ b/modules/indexer/code/internal/indexer.go @@ -5,7 +5,7 @@ package internal import ( "context" - "fmt" + "errors" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -57,13 +57,13 @@ type dummyIndexer struct { } func (d *dummyIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error { - return fmt.Errorf("indexer is not ready") + return errors.New("indexer is not ready") } func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error { - return fmt.Errorf("indexer is not ready") + return errors.New("indexer is not ready") } func (d *dummyIndexer) Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error) { - return 0, nil, nil, fmt.Errorf("indexer is not ready") + return 0, nil, nil, errors.New("indexer is not ready") } diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go index 4a61a80765..669417bb4b 100644 --- a/modules/indexer/internal/bleve/indexer.go +++ b/modules/indexer/internal/bleve/indexer.go @@ -5,7 +5,7 @@ package bleve import ( "context" - "fmt" + "errors" "forgejo.org/modules/indexer/internal" "forgejo.org/modules/log" @@ -38,11 +38,11 @@ func NewIndexer(indexDir string, version int, mappingGetter func() (mapping.Inde // Init initializes the indexer func (i *Indexer) Init(_ context.Context) (bool, error) { if i == nil { - return false, fmt.Errorf("cannot init nil indexer") + return false, errors.New("cannot init nil indexer") } if i.Indexer != nil { - return false, fmt.Errorf("indexer is already initialized") + return false, errors.New("indexer is already initialized") } indexer, version, err := openIndexer(i.indexDir, i.version) @@ -82,10 +82,10 @@ func (i *Indexer) Init(_ context.Context) (bool, error) { // Ping checks if the indexer is available func (i *Indexer) Ping(_ context.Context) error { if i == nil { - return fmt.Errorf("cannot ping nil indexer") + return errors.New("cannot ping nil indexer") } if i.Indexer == nil { - return fmt.Errorf("indexer is not initialized") + return errors.New("indexer is not initialized") } return nil } diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go index 7f411b516b..e043023671 100644 --- a/modules/indexer/internal/bleve/query.go +++ b/modules/indexer/internal/bleve/query.go @@ -29,11 +29,12 @@ func MatchQuery(matchTerm, field, analyzer string, fuzziness int) *query.MatchQu } // MatchPhraseQuery generates a match phrase query for the given phrase, field and analyzer -func MatchPhraseQuery(matchPhrase, field, analyzer string, autoFuzzy bool) *query.MatchPhraseQuery { +func MatchPhraseQuery(matchPhrase, field, analyzer string, autoFuzzy bool, boost float64) *query.MatchPhraseQuery { q := bleve.NewMatchPhraseQuery(matchPhrase) q.FieldVal = field q.Analyzer = analyzer q.SetAutoFuzziness(autoFuzzy) + q.SetBoost(boost) return q } diff --git a/modules/indexer/internal/elasticsearch/indexer.go b/modules/indexer/internal/elasticsearch/indexer.go index 9cd29f3e49..a7a710588c 100644 --- a/modules/indexer/internal/elasticsearch/indexer.go +++ b/modules/indexer/internal/elasticsearch/indexer.go @@ -5,6 +5,7 @@ package elasticsearch import ( "context" + "errors" "fmt" "forgejo.org/modules/indexer/internal" @@ -36,10 +37,10 @@ func NewIndexer(url, indexName string, version int, mapping string) *Indexer { // Init initializes the indexer func (i *Indexer) Init(ctx context.Context) (bool, error) { if i == nil { - return false, fmt.Errorf("cannot init nil indexer") + return false, errors.New("cannot init nil indexer") } if i.Client != nil { - return false, fmt.Errorf("indexer is already initialized") + return false, errors.New("indexer is already initialized") } client, err := i.initClient() @@ -66,10 +67,10 @@ func (i *Indexer) Init(ctx context.Context) (bool, error) { // Ping checks if the indexer is available func (i *Indexer) Ping(ctx context.Context) error { if i == nil { - return fmt.Errorf("cannot ping nil indexer") + return errors.New("cannot ping nil indexer") } if i.Client == nil { - return fmt.Errorf("indexer is not initialized") + return errors.New("indexer is not initialized") } resp, err := i.Client.ClusterHealth().Do(ctx) diff --git a/modules/indexer/internal/indexer.go b/modules/indexer/internal/indexer.go index c7f356da1e..3442bbaff2 100644 --- a/modules/indexer/internal/indexer.go +++ b/modules/indexer/internal/indexer.go @@ -5,7 +5,7 @@ package internal import ( "context" - "fmt" + "errors" ) // Indexer defines an basic indexer interface @@ -27,11 +27,11 @@ func NewDummyIndexer() Indexer { type dummyIndexer struct{} func (d *dummyIndexer) Init(ctx context.Context) (bool, error) { - return false, fmt.Errorf("indexer is not ready") + return false, errors.New("indexer is not ready") } func (d *dummyIndexer) Ping(ctx context.Context) error { - return fmt.Errorf("indexer is not ready") + return errors.New("indexer is not ready") } func (d *dummyIndexer) Close() {} diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go index feac1d052c..b21605772c 100644 --- a/modules/indexer/internal/meilisearch/indexer.go +++ b/modules/indexer/internal/meilisearch/indexer.go @@ -5,6 +5,7 @@ package meilisearch import ( "context" + "errors" "fmt" "github.com/meilisearch/meilisearch-go" @@ -33,11 +34,11 @@ func NewIndexer(url, apiKey, indexName string, version int, settings *meilisearc // Init initializes the indexer func (i *Indexer) Init(_ context.Context) (bool, error) { if i == nil { - return false, fmt.Errorf("cannot init nil indexer") + return false, errors.New("cannot init nil indexer") } if i.Client != nil { - return false, fmt.Errorf("indexer is already initialized") + return false, errors.New("indexer is already initialized") } i.Client = meilisearch.New(i.url, meilisearch.WithAPIKey(i.apiKey)) @@ -63,10 +64,10 @@ func (i *Indexer) Init(_ context.Context) (bool, error) { // Ping checks if the indexer is available func (i *Indexer) Ping(ctx context.Context) error { if i == nil { - return fmt.Errorf("cannot ping nil indexer") + return errors.New("cannot ping nil indexer") } if i.Client == nil { - return fmt.Errorf("indexer is not initialized") + return errors.New("indexer is not initialized") } resp, err := i.Client.Health() if err != nil { diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 64d3c8122e..8549ba8dfc 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -23,7 +23,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 4 + issueIndexerLatestVersion = 5 ) const unicodeNormalizeName = "unicodeNormalize" @@ -69,6 +69,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("is_public", boolFieldMapping) + docMapping.AddFieldMappingsAt("index", numberFieldMapping) docMapping.AddFieldMappingsAt("title", textFieldMapping) docMapping.AddFieldMappingsAt("content", textFieldMapping) docMapping.AddFieldMappingsAt("comments", textFieldMapping) @@ -163,9 +164,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( q := bleve.NewBooleanQuery() for _, token := range tokens { innerQ := bleve.NewDisjunctionQuery( - inner_bleve.MatchPhraseQuery(token.Term, "title", issueIndexerAnalyzer, token.Fuzzy), - inner_bleve.MatchPhraseQuery(token.Term, "content", issueIndexerAnalyzer, token.Fuzzy), - inner_bleve.MatchPhraseQuery(token.Term, "comments", issueIndexerAnalyzer, token.Fuzzy)) + inner_bleve.MatchPhraseQuery(token.Term, "title", issueIndexerAnalyzer, token.Fuzzy, 2.0), + inner_bleve.MatchPhraseQuery(token.Term, "content", issueIndexerAnalyzer, token.Fuzzy, 1.0), + inner_bleve.MatchPhraseQuery(token.Term, "comments", issueIndexerAnalyzer, token.Fuzzy, 1.0)) + + if issueID, err := token.ParseIssueReference(); err == nil { + idQuery := inner_bleve.NumericEqualityQuery(issueID, "index") + idQuery.SetBoost(20.0) + innerQ.AddQuery(idQuery) + } switch token.Kind { case internal.BoolOptMust: @@ -190,6 +197,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...)) } + if options.PriorityRepoID.Has() { + eq := inner_bleve.NumericEqualityQuery(options.PriorityRepoID.Value(), "repo_id") + eq.SetBoost(10.0) + meh := bleve.NewMatchAllQuery() + meh.SetBoost(0) + should := bleve.NewDisjunctionQuery(eq, meh) + queries = append(queries, should) + } + if options.IsPull.Has() { queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull")) } diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go index 9dd026e74f..5f42bce9a1 100644 --- a/modules/indexer/issues/db/db.go +++ b/modules/indexer/issues/db/db.go @@ -5,6 +5,7 @@ package db import ( "context" + "strconv" "forgejo.org/models/db" issue_model "forgejo.org/models/issues" @@ -52,6 +53,7 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( cond := builder.NewCond() + var priorityIssueIndex int64 if options.Keyword != "" { repoCond := builder.In("repo_id", options.RepoIDs) if len(options.RepoIDs) == 1 { @@ -71,12 +73,25 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( )), ), ) + + term := options.Keyword + if term[0] == '#' || term[0] == '!' { + term = term[1:] + } + if issueID, err := strconv.ParseInt(term, 10, 64); err == nil { + cond = builder.Or( + builder.Eq{"`index`": issueID}, + cond, + ) + priorityIssueIndex = issueID + } } opt, err := ToDBOptions(ctx, options) if err != nil { return nil, err } + opt.PriorityIssueIndex = priorityIssueIndex // If pagesize == 0, return total count only. It's a special case for search count. if options.Paginator != nil && options.Paginator.PageSize == 0 { diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 4411cc1c37..55a471fc8e 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -78,6 +78,11 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m User: nil, } + if options.PriorityRepoID.Has() { + opts.SortType = "priorityrepo" + opts.PriorityRepoID = options.PriorityRepoID.Value() + } + if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 { opts.MilestoneIDs = []int64{db.NoConditionID} } else { diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 1bf0145796..d632a22b2a 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 1 + issueIndexerLatestVersion = 2 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -56,7 +56,8 @@ const ( "repo_id": { "type": "long", "index": true }, "is_public": { "type": "boolean", "index": true }, - "title": { "type": "text", "index": true }, + "index": { "type": "long", "index": true }, + "title": { "type": "text", "index": true }, "content": { "type": "text", "index": true }, "comments": { "type" : "text", "index": true }, @@ -155,21 +156,25 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( return nil, err } for _, token := range tokens { - innerQ := elastic.NewMultiMatchQuery(token.Term, "title", "content", "comments") + innerQ := elastic.NewMultiMatchQuery(token.Term, "content", "comments").FieldWithBoost("title", 2.0).TieBreaker(0.5) if token.Fuzzy { // If the term is not a phrase use fuzziness set to AUTO innerQ = innerQ.Type(esMultiMatchTypeBestFields).Fuzziness(esFuzzyAuto) } else { innerQ = innerQ.Type(esMultiMatchTypePhrasePrefix) } - + var eitherQ elastic.Query = innerQ + if issueID, err := token.ParseIssueReference(); err == nil { + indexQ := elastic.NewTermQuery("index", issueID).Boost(20) + eitherQ = elastic.NewDisMaxQuery().Query(indexQ).Query(innerQ).TieBreaker(0.5) + } switch token.Kind { case internal.BoolOptMust: - q.Must(innerQ) + q.Must(eitherQ) case internal.BoolOptShould: - q.Should(innerQ) + q.Should(eitherQ) case internal.BoolOptNot: - q.MustNot(innerQ) + q.MustNot(eitherQ) } } query.Must(q) @@ -183,6 +188,10 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( } query.Must(q) } + if options.PriorityRepoID.Has() { + q := elastic.NewTermQuery("repo_id", options.PriorityRepoID.Value()).Boost(10) + query.Should(q) + } if options.IsPull.Has() { query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value())) diff --git a/modules/indexer/issues/elasticsearch/elasticsearch_test.go b/modules/indexer/issues/elasticsearch/elasticsearch_test.go index f8cd4e02f6..1ad01352d3 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch_test.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch_test.go @@ -11,36 +11,22 @@ import ( "time" "forgejo.org/modules/indexer/issues/internal/tests" + + "github.com/stretchr/testify/require" ) func TestElasticsearchIndexer(t *testing.T) { - // The elasticsearch instance started by testing.yml > test-unit > services > elasticsearch - url := "http://elastic:changeme@elasticsearch:9200" - - if os.Getenv("CI") == "" { - // Make it possible to run tests against a local elasticsearch instance - url = os.Getenv("TEST_ELASTICSEARCH_URL") - if url == "" { - t.Skip("TEST_ELASTICSEARCH_URL not set and not running in CI") - return - } - } - - ok := false - for i := 0; i < 60; i++ { - resp, err := http.Get(url) - if err == nil && resp.StatusCode == http.StatusOK { - ok = true - break - } - t.Logf("Waiting for elasticsearch to be up: %v", err) - time.Sleep(time.Second) - } - if !ok { - t.Fatalf("Failed to wait for elasticsearch to be up") + url := os.Getenv("TEST_ELASTICSEARCH_URL") + if url == "" { + t.Skip("TEST_ELASTICSEARCH_URL not set") return } + require.Eventually(t, func() bool { + resp, err := http.Get(url) + return err == nil && resp.StatusCode == http.StatusOK + }, time.Minute, time.Microsecond*100, "Failed to wait for elasticsearch to be up") + indexer := NewIndexer(url, fmt.Sprintf("test_elasticsearch_indexer_%d", time.Now().Unix())) defer indexer.Close() diff --git a/modules/indexer/issues/internal/indexer.go b/modules/indexer/issues/internal/indexer.go index 2f3b4029dc..1b9428b889 100644 --- a/modules/indexer/issues/internal/indexer.go +++ b/modules/indexer/issues/internal/indexer.go @@ -5,7 +5,7 @@ package internal import ( "context" - "fmt" + "errors" "forgejo.org/modules/indexer/internal" ) @@ -30,13 +30,13 @@ type dummyIndexer struct { } func (d *dummyIndexer) Index(_ context.Context, _ ...*IndexerData) error { - return fmt.Errorf("indexer is not ready") + return errors.New("indexer is not ready") } func (d *dummyIndexer) Delete(_ context.Context, _ ...int64) error { - return fmt.Errorf("indexer is not ready") + return errors.New("indexer is not ready") } func (d *dummyIndexer) Search(_ context.Context, _ *SearchOptions) (*SearchResult, error) { - return nil, fmt.Errorf("indexer is not ready") + return nil, errors.New("indexer is not ready") } diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 03f5595a5b..cdd113212d 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -14,6 +14,7 @@ type IndexerData struct { ID int64 `json:"id"` RepoID int64 `json:"repo_id"` IsPublic bool `json:"is_public"` // If the repo is public + Index int64 `json:"index"` // Fields used for keyword searching Title string `json:"title"` @@ -74,8 +75,9 @@ type SearchResult struct { type SearchOptions struct { Keyword string // keyword to search - RepoIDs []int64 // repository IDs which the issues belong to - AllPublic bool // if include all public repositories + RepoIDs []int64 // repository IDs which the issues belong to + AllPublic bool // if include all public repositories + PriorityRepoID optional.Option[int64] // issues from this repository will be prioritized when SortByScore IsPull optional.Option[bool] // if the issues is a pull request IsClosed optional.Option[bool] // if the issues is closed diff --git a/modules/indexer/issues/internal/qstring.go b/modules/indexer/issues/internal/qstring.go index fdb89b09e9..6b60b4c5f6 100644 --- a/modules/indexer/issues/internal/qstring.go +++ b/modules/indexer/issues/internal/qstring.go @@ -5,6 +5,7 @@ package internal import ( "io" + "strconv" "strings" ) @@ -22,6 +23,14 @@ type Token struct { Fuzzy bool } +func (tk *Token) ParseIssueReference() (int64, error) { + term := tk.Term + if len(term) > 1 && (term[0] == '#' || term[0] == '!') { + term = term[1:] + } + return strconv.ParseInt(term, 10, 64) +} + type Tokenizer struct { in *strings.Reader } diff --git a/modules/indexer/issues/internal/qstring_test.go b/modules/indexer/issues/internal/qstring_test.go index a911b86e2f..835491707c 100644 --- a/modules/indexer/issues/internal/qstring_test.go +++ b/modules/indexer/issues/internal/qstring_test.go @@ -169,3 +169,35 @@ func TestIssueQueryString(t *testing.T) { }) } } + +func TestToken_ParseIssueReference(t *testing.T) { + var tk Token + { + tk.Term = "123" + id, err := tk.ParseIssueReference() + require.NoError(t, err) + assert.Equal(t, int64(123), id) + } + { + tk.Term = "#123" + id, err := tk.ParseIssueReference() + require.NoError(t, err) + assert.Equal(t, int64(123), id) + } + { + tk.Term = "!123" + id, err := tk.ParseIssueReference() + require.NoError(t, err) + assert.Equal(t, int64(123), id) + } + { + tk.Term = "text" + _, err := tk.ParseIssueReference() + require.Error(t, err) + } + { + tk.Term = "" + _, err := tk.ParseIssueReference() + require.Error(t, err) + } +} diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index f0cbbfef46..b63957ff84 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -8,7 +8,6 @@ package tests import ( - "context" "fmt" "slices" "testing" @@ -40,7 +39,7 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) { data[v.ID] = v } require.NoError(t, indexer.Index(t.Context(), d...)) - require.NoError(t, waitData(indexer, int64(len(data)))) + waitData(t, indexer, int64(len(data))) } defer func() { @@ -54,13 +53,13 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) { for _, v := range c.ExtraData { data[v.ID] = v } - require.NoError(t, waitData(indexer, int64(len(data)))) + waitData(t, indexer, int64(len(data))) defer func() { for _, v := range c.ExtraData { require.NoError(t, indexer.Delete(t.Context(), v.ID)) delete(data, v.ID) } - require.NoError(t, waitData(indexer, int64(len(data)))) + waitData(t, indexer, int64(len(data))) }() } @@ -550,6 +549,55 @@ var cases = []*testIndexerCase{ }), result.Total) }, }, + { + Name: "Index", + SearchOptions: &internal.SearchOptions{ + Keyword: "13", + SortBy: internal.SortByScore, + RepoIDs: []int64{5}, + }, + ExpectedIDs: []int64{93}, // 93 = #13 in repo 5 + ExpectedTotal: 1, + }, + { + Name: "Index with prefix", + SearchOptions: &internal.SearchOptions{ + Keyword: "#13", + SortBy: internal.SortByScore, + RepoIDs: []int64{5}, + }, + ExpectedIDs: []int64{93}, + ExpectedTotal: 1, + }, + { + Name: "Index and title boost", + ExtraData: []*internal.IndexerData{ + {ID: 1001, Title: "re #13", RepoID: 5}, + {ID: 1002, Title: "re #1001", Content: "leave 13 alone. - 13", RepoID: 5}, + }, + SearchOptions: &internal.SearchOptions{ + Keyword: "!13", + SortBy: internal.SortByScore, + RepoIDs: []int64{5}, + }, + ExpectedIDs: []int64{93, 1001, 1002}, + ExpectedTotal: 3, + }, + { + Name: "Index exclude", + ExtraData: []*internal.IndexerData{ + {ID: 1001, Index: 101, Title: "Brrr", RepoID: 5}, + {ID: 1002, Index: 102, Title: "Brrr", Content: "Brrr", RepoID: 5}, + {ID: 1003, Index: 103, Title: "Brrr", RepoID: 5}, + {ID: 1004, Index: 104, Title: "Brrr", RepoID: 5}, + }, + SearchOptions: &internal.SearchOptions{ + Keyword: "Brrr -101 -103", + SortBy: internal.SortByScore, + }, + ExpectedIDs: []int64{1002, 1004}, + ExpectedTotal: 2, + }, { Name: "SortByCreatedDesc", SearchOptions: &internal.SearchOptions{ @@ -694,6 +742,25 @@ var cases = []*testIndexerCase{ } }, }, + { + Name: "PriorityRepoID", + SearchOptions: &internal.SearchOptions{ + IsPull: optional.Some(false), + IsClosed: optional.Some(false), + PriorityRepoID: optional.Some(int64(3)), + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByScore, + }, + Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { + for i, v := range result.Hits { + if i < 7 { + assert.Equal(t, int64(3), data[v.ID].RepoID) + } else { + assert.NotEqual(t, int64(3), data[v.ID].RepoID) + } + } + }, + }, } type testIndexerCase struct { @@ -742,6 +809,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { data = append(data, &internal.IndexerData{ ID: id, + Index: issueIndex, RepoID: repoID, IsPublic: repoID%2 == 0, Title: fmt.Sprintf("issue%d of repo%d", issueIndex, repoID), @@ -783,22 +851,17 @@ func countIndexerData(data map[int64]*internal.IndexerData, f func(v *internal.I // waitData waits for the indexer to index all data. // Some engines like Elasticsearch index data asynchronously, so we need to wait for a while. -func waitData(indexer internal.Indexer, total int64) error { +func waitData(t testing.TB, indexer internal.Indexer, total int64) { var actual int64 - for i := 0; i < 100; i++ { - result, err := indexer.Search(context.Background(), &internal.SearchOptions{ + assert.Eventually(t, func() bool { + result, err := indexer.Search(t.Context(), &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 0, }, }) - if err != nil { - return err - } + require.NoError(t, err) + actual = result.Total - if actual == total { - return nil - } - time.Sleep(100 * time.Millisecond) - } - return fmt.Errorf("waitData: expected %d, actual %d", total, actual) + return actual == total + }, time.Second*10, time.Millisecond*100, "expected %d but got %d", total, actual) } diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go index 55677e031b..7637e8d6b4 100644 --- a/modules/indexer/issues/meilisearch/meilisearch_test.go +++ b/modules/indexer/issues/meilisearch/meilisearch_test.go @@ -34,20 +34,10 @@ func TestMeilisearchIndexer(t *testing.T) { key = os.Getenv("TEST_MEILISEARCH_KEY") } - ok := false - for i := 0; i < 60; i++ { + require.Eventually(t, func() bool { resp, err := http.Get(url) - if err == nil && resp.StatusCode == http.StatusOK { - ok = true - break - } - t.Logf("Waiting for meilisearch to be up: %v", err) - time.Sleep(time.Second) - } - if !ok { - t.Fatalf("Failed to wait for meilisearch to be up") - return - } + return err == nil && resp.StatusCode == http.StatusOK + }, time.Minute, time.Microsecond*100, "Failed to wait for meilisearch to be up") indexer := NewIndexer(url, key, fmt.Sprintf("test_meilisearch_indexer_%d", time.Now().Unix())) defer indexer.Close() diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 3e6c8babe4..909e840ae5 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -95,6 +95,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD return &internal.IndexerData{ ID: issue.ID, RepoID: issue.RepoID, + Index: issue.Index, IsPublic: !issue.Repo.IsPrivate, Title: issue.Title, Content: issue.Content, diff --git a/modules/indexer/stats/queue.go b/modules/indexer/stats/queue.go index 2403eb8dca..e4eac70d95 100644 --- a/modules/indexer/stats/queue.go +++ b/modules/indexer/stats/queue.go @@ -4,7 +4,7 @@ package stats import ( - "fmt" + "errors" repo_model "forgejo.org/models/repo" "forgejo.org/modules/graceful" @@ -31,7 +31,7 @@ func handler(items ...int64) []int64 { func initStatsQueue() error { statsQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "repo_stats_update", handler) if statsQueue == nil { - return fmt.Errorf("unable to create repo_stats_update queue") + return errors.New("unable to create repo_stats_update queue") } go graceful.GetManager().RunWithCancel(statsQueue) return nil diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 859d27cda6..08c1b21c26 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -4,6 +4,7 @@ package template import ( + "errors" "fmt" "net/url" "regexp" @@ -31,17 +32,17 @@ func Validate(template *api.IssueTemplate) error { func validateMetadata(template *api.IssueTemplate) error { if strings.TrimSpace(template.Name) == "" { - return fmt.Errorf("'name' is required") + return errors.New("'name' is required") } if strings.TrimSpace(template.About) == "" { - return fmt.Errorf("'about' is required") + return errors.New("'about' is required") } return nil } func validateYaml(template *api.IssueTemplate) error { if len(template.Fields) == 0 { - return fmt.Errorf("'body' is required") + return errors.New("'body' is required") } ids := make(container.Set[string]) for idx, field := range template.Fields { diff --git a/modules/markup/html.go b/modules/markup/html.go index c13ebab98a..7961c5c930 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -69,6 +69,10 @@ var ( // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") + // Fediverse handle regex (same as emailRegex but with additonal @ or ! + // at start) + fediRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([@!]([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+)@([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+))(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") + // blackfriday extensions create IDs like fn:user-content-footnote blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) @@ -153,6 +157,7 @@ var defaultProcessors = []processor{ issueIndexPatternProcessor, commitCrossReferencePatternProcessor, hashCurrentPatternProcessor, + fediAddressProcessor, emailAddressProcessor, emojiProcessor, emojiShortCodeProcessor, @@ -1237,6 +1242,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { } } +// fediAddressProcessor replaces raw fediverse handles with toolforge links +func fediAddressProcessor(ctx *RenderContext, node *html.Node) { + next := node.NextSibling + for node != nil && node != next { + m := fediRegex.FindStringSubmatchIndex(node.Data) + if m == nil { + return + } + + fedihandle := node.Data[m[2]:m[3]] + replaceContent(node, m[2], m[3], createLink("https://fedirect.toolforge.org/?id="+url.QueryEscape(fedihandle), fedihandle, "fedihandle")) + node = node.NextSibling.NextSibling + } +} + // emailAddressProcessor replaces raw email addresses with a mailto: link. func emailAddressProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 9d0c40c9e8..2bc929bb04 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -307,6 +307,19 @@ func TestRender_email(t *testing.T) { test( "email@domain..com", `

email@domain..com

`) + + // Test fediverse handle + test( + "@forgejo@floss.social", + `

@forgejo@floss.social

`) + + test( + "!forgejo@programming.dev", + `

!forgejo@programming.dev

`) + + test( + "@#&@forgejo.org", + `

@#&@forgejo.org

`) } func TestRender_emoji(t *testing.T) { diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index db92631acc..e811d29994 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -5,7 +5,7 @@ package markdown import ( - "fmt" + "errors" "html/template" "io" "strings" @@ -54,7 +54,7 @@ func (l *limitWriter) Write(data []byte) (int, error) { if err != nil { return n, err } - return n, fmt.Errorf("rendered content too large - truncating render") + return n, errors.New("rendered content too large - truncating render") } n, err := l.w.Write(data) l.sum += int64(n) @@ -267,8 +267,13 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error // RenderString renders Markdown string to HTML with all specific handling stuff and return string func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) { + return RenderReader(ctx, strings.NewReader(content)) +} + +// RenderReader renders Markdown io.Reader to HTML with all specific handling stuff and return string +func RenderReader(ctx *markup.RenderContext, input io.Reader) (template.HTML, error) { var buf strings.Builder - if err := Render(ctx, strings.NewReader(content), &buf); err != nil { + if err := Render(ctx, input, &buf); err != nil { return "", err } return template.HTML(buf.String()), nil diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index e229ee4c65..f7955115e0 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -104,7 +104,7 @@ func TestRender_Images(t *testing.T) { test( "!["+title+"]("+url+")", - `

`+title+`

`) + `

`+title+`

`) test( "[["+title+"|"+url+"]]", @@ -115,7 +115,7 @@ func TestRender_Images(t *testing.T) { test( "!["+title+"]("+url+")", - `

`+title+`

`) + `

`+title+`

`) test( "[["+title+"|"+url+"]]", @@ -412,8 +412,8 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) { testcase := `![image1](/image1) ![image2](/image2) ` - expected := `

image1
-image2

+ expected := `

image1
+image2

` res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) require.NoError(t, err) @@ -845,10 +845,10 @@ mail@domain.com remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -872,10 +872,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -901,10 +901,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -930,10 +930,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -959,10 +959,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -988,10 +988,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -1018,10 +1018,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -1048,10 +1048,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -1078,10 +1078,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -1108,10 +1108,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -1139,10 +1139,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
@@ -1170,10 +1170,10 @@ space

remote link
local link
remote link
-local image
-local image
-local image
-remote image
+local image
+local image
+local image
+remote image


https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go index 0f9c69cae6..b86c9e3d41 100644 --- a/modules/markup/markdown/transform_image.go +++ b/modules/markup/markdown/transform_image.go @@ -44,6 +44,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image) for _, attr := range v.Attributes() { image.SetAttribute(attr.Name, attr.Value) } + image.SetAttributeString("loading", []byte("lazy")) for child := v.FirstChild(); child != nil; { next := child.NextSibling() image.AppendChild(image, child) diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 384dd1fe94..aacc2536bf 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -108,6 +108,9 @@ func createDefaultPolicy() *bluemonday.Policy { // Allow classes for emojis policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img") + // Allow attributes for images + policy.AllowAttrs("loading").Matching(regexp.MustCompile(`^lazy$`)).OnElements("img") + // Allow icons, emojis, chroma syntax and keyword markup on span policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") policy.AllowAttrs("data-alias").Matching(regexp.MustCompile(`^[a-zA-Z0-9-_+]+$`)).OnElements("span") diff --git a/modules/markup/sanitizer_test.go b/modules/markup/sanitizer_test.go index 9805a34910..a0faff0494 100644 --- a/modules/markup/sanitizer_test.go +++ b/modules/markup/sanitizer_test.go @@ -75,6 +75,10 @@ func Test_Sanitizer(t *testing.T) { // Emoji `THUMBS UP`, `THUMBS UP`, `THUMBS UP`, `THUMBS UP`, + + // Images lazy loading + `image1`, `image1`, + `image1`, `image1`, } for i := 0; i < len(testCases); i += 2 { diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index ec9d834357..6cac77b7ff 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -84,6 +84,13 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) { func parseOCIImageConfig(r io.Reader) (*Metadata, error) { var image oci.Image if err := json.NewDecoder(r).Decode(&image); err != nil { + // Handle empty config blobs (common in OCI artifacts) + if err == io.EOF { + return &Metadata{ + Type: TypeOCI, + Platform: DefaultPlatform, + }, nil + } return nil, err } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index 6c8c6ea5b9..5596c95751 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -4,6 +4,7 @@ package container import ( + "io" "strings" "testing" @@ -60,3 +61,49 @@ func TestParseImageConfig(t *testing.T) { assert.Equal(t, projectURL, metadata.ProjectURL) assert.Equal(t, repositoryURL, metadata.RepositoryURL) } + +func TestParseImageConfigEmptyBlob(t *testing.T) { + t.Run("Empty config blob (EOF)", func(t *testing.T) { + // Test empty reader (simulates empty config blob common in OCI artifacts) + metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("")) + require.NoError(t, err) + + assert.Equal(t, TypeOCI, metadata.Type) + assert.Equal(t, DefaultPlatform, metadata.Platform) + assert.Empty(t, metadata.Description) + assert.Empty(t, metadata.Authors) + assert.Empty(t, metadata.Labels) + assert.Empty(t, metadata.Manifests) + }) + + t.Run("Empty JSON object", func(t *testing.T) { + // Test minimal valid JSON config + metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("{}")) + require.NoError(t, err) + + assert.Equal(t, TypeOCI, metadata.Type) + assert.Equal(t, DefaultPlatform, metadata.Platform) + assert.Empty(t, metadata.Description) + assert.Empty(t, metadata.Authors) + }) + + t.Run("Invalid JSON still returns error", func(t *testing.T) { + // Test that actual JSON errors (not EOF) are still returned + _, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("{invalid json")) + require.Error(t, err) + assert.NotEqual(t, io.EOF, err) + }) + + t.Run("OCI artifact with empty config", func(t *testing.T) { + // Test OCI artifact scenario with minimal config + configOCI := `{"config": {}}` + metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) + require.NoError(t, err) + + assert.Equal(t, TypeOCI, metadata.Type) + assert.Equal(t, DefaultPlatform, metadata.Platform) + assert.Empty(t, metadata.Description) + assert.Empty(t, metadata.Authors) + assert.Empty(t, metadata.ImageLayers) + }) +} diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go index 30c91115e7..4af9af620f 100644 --- a/modules/packages/rpm/metadata.go +++ b/modules/packages/rpm/metadata.go @@ -11,7 +11,7 @@ import ( "forgejo.org/modules/timeutil" "forgejo.org/modules/validation" - "github.com/sassoftware/go-rpmutils" + "code.forgejo.org/forgejo/go-rpmutils" ) const ( diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index 32bdf3bfa2..87ee2854ae 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -23,6 +23,11 @@ var wellKnownMimeTypesLower = map[string]string{ ".wasm": "application/wasm", ".webp": "image/webp", ".xml": "text/xml; charset=utf-8", + ".glb": "model/gltf-binary", + ".gltf": "model/gltf+json", + ".obj": "model/obj", + ".stl": "model/stl", + ".3mf": "model/3mf", // well, there are some types missing from the builtin list ".txt": "text/plain; charset=utf-8", diff --git a/modules/public/public.go b/modules/public/public.go index 174936fd4a..a7db5b62e9 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -6,6 +6,7 @@ package public import ( "bytes" "io" + "io/fs" "net/http" "os" "path" @@ -59,7 +60,7 @@ func setWellKnownContentType(w http.ResponseWriter, file string) { } } -func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) { +func handleRequest(w http.ResponseWriter, req *http.Request, fs fs.FS, file string) { // actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here f, err := fs.Open(util.PathJoinRelX(file)) if err != nil { @@ -86,33 +87,31 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, return } - serveContent(w, req, fi, fi.ModTime(), f) + serveContent(w, req, fi.Name(), fi.ModTime(), f.(io.ReadSeeker)) } -type GzipBytesProvider interface { - GzipBytes() []byte +type ZstdBytesProvider interface { + ZstdBytes() []byte } // serveContent serve http content -func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) { - setWellKnownContentType(w, fi.Name()) +func serveContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) { + setWellKnownContentType(w, name) encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding")) - if encodings.Contains("gzip") { - // try to provide gzip content directly from bindata (provided by vfsgen۰CompressedFileInfo) - if compressed, ok := fi.(GzipBytesProvider); ok { - rdGzip := bytes.NewReader(compressed.GzipBytes()) - // all gzipped static files (from bindata) are managed by Gitea, so we can make sure every file has the correct ext name - // then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data + if encodings.Contains("zstd") { + // If the file was compressed, use the bytes directly. + if compressed, ok := content.(ZstdBytesProvider); ok { + rdZstd := bytes.NewReader(compressed.ZstdBytes()) if w.Header().Get("Content-Type") == "" { w.Header().Set("Content-Type", "application/octet-stream") } - w.Header().Set("Content-Encoding", "gzip") - httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip) + w.Header().Set("Content-Encoding", "zstd") + httpcache.ServeContentWithCacheControl(w, req, name, modtime, rdZstd) return } } - httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content) + httpcache.ServeContentWithCacheControl(w, req, name, modtime, content) return } diff --git a/modules/public/serve_static.go b/modules/public/serve_static.go index e19bd976eb..148d789bba 100644 --- a/modules/public/serve_static.go +++ b/modules/public/serve_static.go @@ -12,8 +12,6 @@ import ( "forgejo.org/modules/timeutil" ) -var _ GzipBytesProvider = (*vfsgen۰CompressedFileInfo)(nil) - // GlobalModTime provide a global mod time for embedded asset files func GlobalModTime(filename string) time.Time { return timeutil.GetExecutableModTime() diff --git a/modules/queue/manager_test.go b/modules/queue/manager_test.go index f02d66dff4..fd5d21570c 100644 --- a/modules/queue/manager_test.go +++ b/modules/queue/manager_test.go @@ -8,17 +8,14 @@ import ( "testing" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestManager(t *testing.T) { - oldAppDataPath := setting.AppDataPath - setting.AppDataPath = t.TempDir() - defer func() { - setting.AppDataPath = oldAppDataPath - }() + defer test.MockVariableValue(&setting.AppDataPath, t.TempDir())() newQueueFromConfig := func(name, cfg string) (*WorkerPoolQueue[int], error) { cfgProvider, err := setting.NewConfigProviderFromData(cfg) diff --git a/modules/references/references.go b/modules/references/references.go index 81267f0065..7df5119393 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -460,7 +460,8 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference } parts := strings.Split(u.EscapedPath(), "/") // /user/repo/issues/3 - if len(parts) != 5 || parts[0] != "" { + // /user/repo/pulls/7/files/... + if len(parts) < 5 || parts[0] != "" { continue } var sep string diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 77f6bfbae3..bb22c0bd59 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -132,6 +132,30 @@ func TestFindAllIssueReferences(t *testing.T) { {203, "user4", "repo5", "203", true, XRefActionNone, nil, nil, ""}, }, }, + { + "This http://gitea.com:3000/user4/repo5/pulls/202#x yes.", + []testResult{ + {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""}, + }, + }, + { + "This http://gitea.com:3000/user4/repo5/pulls/202/commits yes.", + []testResult{ + {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""}, + }, + }, + { + "This http://gitea.com:3000/user4/repo5/pulls/202/files yes.", + []testResult{ + {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""}, + }, + }, + { + "This http://gitea.com:3000/user4/repo5/pulls/202/files#diff- yes.", + []testResult{ + {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""}, + }, + }, { "This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.", []testResult{ diff --git a/modules/repository/create.go b/modules/repository/create.go index 060b995bc5..becfed0370 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -69,8 +69,11 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re // insert units for repo defaultUnits := unit.DefaultRepoUnits - if isFork { + switch { + case isFork: defaultUnits = unit.DefaultForkRepoUnits + case repo.IsMirror: + defaultUnits = unit.DefaultMirrorRepoUnits } units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) for _, tp := range defaultUnits { diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index e93b21abda..19f3b9008a 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -262,7 +262,7 @@ func (p *iniConfigProvider) Save() error { } filename := p.file if filename == "" { - return fmt.Errorf("config file path must not be empty") + return errors.New("config file path must not be empty") } if p.loadedFromEmpty { if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { diff --git a/modules/setting/git_test.go b/modules/setting/git_test.go index 582f5d403e..5604151907 100644 --- a/modules/setting/git_test.go +++ b/modules/setting/git_test.go @@ -6,17 +6,15 @@ package setting import ( "testing" + "forgejo.org/modules/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGitConfig(t *testing.T) { - oldGit := Git - oldGitConfig := GitConfig - defer func() { - Git = oldGit - GitConfig = oldGitConfig - }() + defer test.MockProtect(&Git)() + defer test.MockProtect(&GitConfig)() cfg, err := NewConfigProviderFromData(` [git.config] @@ -37,12 +35,8 @@ diff.algorithm = other } func TestGitReflog(t *testing.T) { - oldGit := Git - oldGitConfig := GitConfig - defer func() { - Git = oldGit - GitConfig = oldGitConfig - }() + defer test.MockProtect(&Git)() + defer test.MockProtect(&GitConfig)() // default reflog config without legacy options cfg, err := NewConfigProviderFromData(``) diff --git a/modules/setting/incoming_email.go b/modules/setting/incoming_email.go index 502be159a1..e592220de6 100644 --- a/modules/setting/incoming_email.go +++ b/modules/setting/incoming_email.go @@ -4,6 +4,7 @@ package setting import ( + "errors" "fmt" "net/mail" "strings" @@ -68,7 +69,7 @@ func checkReplyToAddress() error { } if parsed.Name != "" { - return fmt.Errorf("name must not be set") + return errors.New("name must not be set") } c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder) diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index b7f69c3492..9c004c6ce0 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -215,6 +215,11 @@ func loadMailerFrom(rootCfg ConfigProvider) { if err != nil { log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err) } + + if len(MailService.SendmailArgs) == 0 || MailService.SendmailArgs[len(MailService.SendmailArgs)-1] != "--" { + log.Warn("SENDMAIL_ARGS setting does not end in \"--\", appending it to prevent argument injection") + MailService.SendmailArgs = append(MailService.SendmailArgs, "--") + } case "smtp", "smtps", "smtp+starttls", "smtp+unix": ips := tryResolveAddr(MailService.SMTPAddr) if MailService.Protocol == "smtp" { diff --git a/modules/setting/mailer_test.go b/modules/setting/mailer_test.go index 685586d2c7..4523cc91dd 100644 --- a/modules/setting/mailer_test.go +++ b/modules/setting/mailer_test.go @@ -51,4 +51,28 @@ func Test_loadMailerFrom(t *testing.T) { assert.Equal(t, "jane.doe@example.com", MailService.User) assert.Equal(t, "y0u'll n3v3r gUess th1S!!1", MailService.Passwd) }) + + t.Run("sendmail argument sanitization", func(t *testing.T) { + cfg, _ := NewConfigProviderFromData("") + sec := cfg.Section("mailer") + sec.NewKey("ENABLED", "true") + sec.NewKey("PROTOCOL", "sendmail") + sec.NewKey("SENDMAIL_ARGS", "-B 8BITMIME") + + loadMailerFrom(cfg) + + assert.Equal(t, []string{"-B", "8BITMIME", "--"}, MailService.SendmailArgs) + }) + + t.Run("empty sendmail args", func(t *testing.T) { + cfg, _ := NewConfigProviderFromData("") + sec := cfg.Section("mailer") + sec.NewKey("ENABLED", "true") + sec.NewKey("PROTOCOL", "sendmail") + sec.NewKey("SENDMAIL_ARGS", "") + + loadMailerFrom(cfg) + + assert.Equal(t, []string{"--"}, MailService.SendmailArgs) + }) } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index c9e70560d0..7e774f0139 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -1,4 +1,5 @@ // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package setting @@ -52,6 +53,7 @@ var ( DisabledRepoUnits []string DefaultRepoUnits []string DefaultForkRepoUnits []string + DefaultMirrorRepoUnits []string PrefixArchiveFiles bool DisableMigrations bool DisableStars bool @@ -175,6 +177,7 @@ var ( DisabledRepoUnits: []string{}, DefaultRepoUnits: []string{}, DefaultForkRepoUnits: []string{}, + DefaultMirrorRepoUnits: []string{}, PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, diff --git a/modules/setting/security.go b/modules/setting/security.go index f3480d1056..c38d8dae79 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -35,7 +35,6 @@ var ( PasswordHashAlgo string PasswordCheckPwn bool SuccessfulTokensCacheSize int - DisableQueryAuthToken bool CSRFCookieName = "_csrf" CSRFCookieHTTPOnly = true ) @@ -160,14 +159,4 @@ func loadSecurityFrom(rootCfg ConfigProvider) { PasswordComplexity = append(PasswordComplexity, name) } } - - sectionHasDisableQueryAuthToken := sec.HasKey("DISABLE_QUERY_AUTH_TOKEN") - - // TODO: default value should be true in future releases - DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false) - - // warn if the setting is set to false explicitly - if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken { - log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.") - } } diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index 86193bddb9..1ad826a17a 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -56,7 +56,7 @@ var SSH = struct { Domain: "", Port: 22, ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"}, - ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, + ServerKeyExchanges: []string{"mlkem768x25519-sha256", "curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"}, KeygenPath: "", MinimumKeySizeCheck: true, diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go index e168a2efbb..ec1b2fc77a 100644 --- a/modules/storage/minio_test.go +++ b/modules/storage/minio_test.go @@ -18,13 +18,14 @@ import ( ) func TestMinioStorageIterator(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("minioStorage not present outside of CI") + endpoint := os.Getenv("TEST_MINIO_ENDPOINT") + if endpoint == "" { + t.Skip("TEST_MINIO_ENDPOINT not set") return } testStorageIterator(t, setting.MinioStorageType, &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: "minio:9000", + Endpoint: endpoint, AccessKeyID: "123456", SecretAccessKey: "12345678", Bucket: "gitea", @@ -34,13 +35,14 @@ func TestMinioStorageIterator(t *testing.T) { } func TestVirtualHostMinioStorage(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("minioStorage not present outside of CI") + endpoint := os.Getenv("TEST_MINIO_ENDPOINT") + if endpoint == "" { + t.Skip("TEST_MINIO_ENDPOINT not set") return } testStorageIterator(t, setting.MinioStorageType, &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: "minio:9000", + Endpoint: endpoint, AccessKeyID: "123456", SecretAccessKey: "12345678", Bucket: "gitea", @@ -85,13 +87,14 @@ func TestMinioStoragePath(t *testing.T) { } func TestS3StorageBadRequest(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("S3Storage not present outside of CI") + endpoint := os.Getenv("TEST_MINIO_ENDPOINT") + if endpoint == "" { + t.Skip("TEST_MINIO_ENDPOINT not set") return } cfg := &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: "minio:9000", + Endpoint: endpoint, AccessKeyID: "123456", SecretAccessKey: "12345678", Bucket: "bucket", diff --git a/modules/structs/action.go b/modules/structs/action.go index df9f845adc..f47b228d75 100644 --- a/modules/structs/action.go +++ b/modules/structs/action.go @@ -3,6 +3,10 @@ package structs +import ( + "time" +) + // ActionRunJob represents a job of a run // swagger:model type ActionRunJob struct { @@ -23,3 +27,60 @@ type ActionRunJob struct { // the action run job status Status string `json:"status"` } + +// ActionRun represents an action run +// swagger:model +type ActionRun struct { + // the action run id + ID int64 `json:"id"` + // the action run's title + Title string `json:"title"` + // the repo this action is part of + Repo *Repository `json:"repository"` + // the name of workflow file + WorkflowID string `json:"workflow_id"` + // a unique number for each run of a repository + Index int64 `json:"index_in_repo"` + // the user that triggered this action run + TriggerUser *User `json:"trigger_user"` + // the cron id for the schedule trigger + ScheduleID int64 + // the commit/tag/… the action run ran on + PrettyRef string `json:"prettyref"` + // has the commit/tag/… the action run ran on been deleted + IsRefDeleted bool `json:"is_ref_deleted"` + // the commit sha the action run ran on + CommitSHA string `json:"commit_sha"` + // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. + IsForkPullRequest bool `json:"is_fork_pull_request"` + // may need approval if it's a fork pull request + NeedApproval bool `json:"need_approval"` + // who approved this action run + ApprovedBy int64 `json:"approved_by"` + // the webhook event that causes the workflow to run + Event string `json:"event"` + // the payload of the webhook event that causes the workflow to run + EventPayload string `json:"event_payload"` + // the trigger event defined in the `on` configuration of the triggered workflow + TriggerEvent string `json:"trigger_event"` + // the current status of this run + Status string `json:"status"` + // when the action run was started + Started time.Time `json:"started,omitempty"` + // when the action run was stopped + Stopped time.Time `json:"stopped,omitempty"` + // when the action run was created + Created time.Time `json:"created,omitempty"` + // when the action run was last updated + Updated time.Time `json:"updated,omitempty"` + // how long the action run ran for + Duration time.Duration `json:"duration,omitempty"` + // the url of this action run + HTMLURL string `json:"html_url"` +} + +// ListActionRunResponse return a list of ActionRun +type ListActionRunResponse struct { + Entries []*ActionRun `json:"workflow_runs"` + TotalCount int64 `json:"total_count"` +} diff --git a/modules/structs/git_blob.go b/modules/structs/git_blob.go index 96c7a271a9..ef06693905 100644 --- a/modules/structs/git_blob.go +++ b/modules/structs/git_blob.go @@ -3,8 +3,8 @@ package structs -// GitBlobResponse represents a git blob -type GitBlobResponse struct { +// GitBlob represents a git blob +type GitBlob struct { Content string `json:"content"` Encoding string `json:"encoding"` URL string `json:"url"` diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 28c2e00588..11372ca6e1 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -53,8 +53,7 @@ type CreateHookOption struct { BranchFilter string `json:"branch_filter" binding:"GlobPattern"` AuthorizationHeader string `json:"authorization_header"` // default: false - Active bool `json:"active"` - IsSystemWebhook bool `json:"is_system_webhook"` + Active bool `json:"active"` } // EditHookOption options when modify one hook @@ -119,6 +118,7 @@ var ( _ Payloader = &RepositoryPayload{} _ Payloader = &ReleasePayload{} _ Payloader = &PackagePayload{} + _ Payloader = &ActionPayload{} ) // _________ __ @@ -484,3 +484,36 @@ type PackagePayload struct { func (p *PackagePayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } + +// _ _ _ +// / \ ___| |_(_) ___ _ __ +// / _ \ / __| __| |/ _ \| '_ \ +// / ___ \ (__| |_| | (_) | | | | +// /_/ \_\___|\__|_|\___/|_| |_| + +// this name is ridiculous, yes +// it's the sub-type of hook that has something to do with Forgejo Actions +type HookActionAction string + +const ( + HookActionFailure HookActionAction = "failure" + HookActionRecover HookActionAction = "recover" + HookActionSuccess HookActionAction = "success" +) + +// ActionPayload payload for action webhooks +type ActionPayload struct { + Action HookActionAction `json:"action"` + Run *ActionRun `json:"run"` + // the status of this run before it completed + // this must be a not done status + PriorStatus string `json:"prior_status"` + // the last run for the same workflow + // could be nil when Run is the first for it's workflow + LastRun *ActionRun `json:"last_run,omitempty"` +} + +// JSONPayload return payload information +func (p *ActionPayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} diff --git a/modules/templates/util_dict.go b/modules/templates/util_dict.go index 9d9af77fad..16f722e61b 100644 --- a/modules/templates/util_dict.go +++ b/modules/templates/util_dict.go @@ -4,6 +4,7 @@ package templates import ( + "errors" "fmt" "html" "html/template" @@ -33,7 +34,7 @@ func dictMerge(base map[string]any, arg any) bool { // The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys. func dict(args ...any) (map[string]any, error) { if len(args)%2 != 0 { - return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs") + return nil, errors.New("invalid dict constructor syntax: must have key-value pairs") } m := make(map[string]any, len(args)/2) for i := 0; i < len(args); i += 2 { diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index b75b061218..62e063213c 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -192,8 +192,8 @@ func TestRenderMarkdownToHtml(t *testing.T) { remote link local link remote link -local image -remote image +local image +remote image 88fc37a3c0...12fc37a3c0 (hash) diff --git a/modules/test/utils.go b/modules/test/utils.go index f60bad022e..db131f19d0 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "strings" + "time" "forgejo.org/modules/json" ) @@ -46,3 +47,8 @@ func MockProtect[T any](p *T) (reset func()) { old := *p return func() { *p = old } } + +// When this is called, sleep until the unix time was increased by one. +func SleepTillNextSecond() { + time.Sleep(time.Second - time.Since(time.Now().Truncate(time.Second))) +} diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index a8fc70e54c..262feb2b05 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -24,6 +24,16 @@ const ( AvifMimeType = "image/avif" // ApplicationOctetStream MIME type of binary files. ApplicationOctetStream = "application/octet-stream" + // GLTFMimeType MIME type of GLTF files. + GLTFMimeType = "model/gltf+json" + // GLBMimeType MIME type of GLB files. + GLBMimeType = "model/gltf-binary" + // OBJMimeType MIME type of OBJ files. + OBJMimeType = "model/obj" + // STLMimeType MIME type of STL files. + STLMimeType = "model/stl" + // 3MFMimeType MIME type of 3MF files. + ThreeMFMimeType = "model/3mf" ) var ( @@ -67,6 +77,36 @@ func (ct SniffedType) IsAudio() bool { return strings.Contains(ct.contentType, "audio/") } +// Is3DModel detects if data is a 3D format +func (ct SniffedType) Is3DModel() bool { + return strings.Contains(ct.contentType, "model/") +} + +// IsGLTFFile detects if data is an SVG image format +func (ct SniffedType) IsGLTF() bool { + return strings.Contains(ct.contentType, GLTFMimeType) +} + +// IsGLBFile detects if data is an GLB image format +func (ct SniffedType) IsGLB() bool { + return strings.Contains(ct.contentType, GLBMimeType) +} + +// IsOBJFile detects if data is an OBJ image format +func (ct SniffedType) IsOBJ() bool { + return strings.Contains(ct.contentType, OBJMimeType) +} + +// IsSTLTextFile detects if data is an STL text format +func (ct SniffedType) IsSTL() bool { + return strings.Contains(ct.contentType, STLMimeType) +} + +// Is3MFFile detects if data is an 3MF image format +func (ct SniffedType) Is3MF() bool { + return strings.Contains(ct.contentType, ThreeMFMimeType) +} + // IsRepresentableAsText returns true if file content can be represented as // plain text or is empty. func (ct SniffedType) IsRepresentableAsText() bool { @@ -75,7 +115,7 @@ func (ct SniffedType) IsRepresentableAsText() bool { // IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser func (ct SniffedType) IsBrowsableBinaryType() bool { - return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() + return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() || ct.Is3DModel() } // GetMimeType returns the mime type @@ -135,6 +175,13 @@ func DetectContentType(data []byte) SniffedType { ct = "audio/ogg" // for most cases, it is used as an audio container } } + + // GLTF is unsupported by http.DetectContentType + // hexdump -n 4 -C glTF.glb + if bytes.HasPrefix(data, []byte("glTF")) { + ct = GLBMimeType + } + return SniffedType{ct} } diff --git a/modules/typesniffer/typesniffer_test.go b/modules/typesniffer/typesniffer_test.go index 8d80b4ddb4..176d3658bb 100644 --- a/modules/typesniffer/typesniffer_test.go +++ b/modules/typesniffer/typesniffer_test.go @@ -117,6 +117,14 @@ func TestIsAudio(t *testing.T) { assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char } +func TestIsGLB(t *testing.T) { + glb, _ := hex.DecodeString("676c5446") + assert.True(t, DetectContentType(glb).IsGLB()) + assert.True(t, DetectContentType(glb).Is3DModel()) + assert.False(t, DetectContentType([]byte("plain text")).IsGLB()) + assert.False(t, DetectContentType([]byte("plain text")).Is3DModel()) +} + func TestDetectContentTypeFromReader(t *testing.T) { mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") st, err := DetectContentTypeFromReader(bytes.NewReader(mp3)) @@ -145,3 +153,15 @@ func TestDetectContentTypeAvif(t *testing.T) { assert.True(t, st.IsImage()) } + +func TestDetectContentTypeModelGLB(t *testing.T) { + glb, err := hex.DecodeString("676c5446") + require.NoError(t, err) + + st, err := DetectContentTypeFromReader(bytes.NewReader(glb)) + require.NoError(t, err) + + // print st for debugging + assert.Equal(t, "model/gltf-binary", st.GetMimeType()) + assert.True(t, st.IsGLB()) +} diff --git a/modules/util/io.go b/modules/util/io.go index 1559b019a0..4c99004c0c 100644 --- a/modules/util/io.go +++ b/modules/util/io.go @@ -4,7 +4,6 @@ package util import ( - "bytes" "errors" "io" ) @@ -20,42 +19,6 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { return n, err } -// ReadWithLimit reads at most "limit" bytes from r into buf. -// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil. -func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) { - return readWithLimit(r, 1024, n) -} - -func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) { - if limit <= batch { - buf := make([]byte, limit) - n, err := ReadAtMost(r, buf) - if err != nil { - return nil, err - } - return buf[:n], nil - } - res := bytes.NewBuffer(make([]byte, 0, batch)) - bufFix := make([]byte, batch) - eof := false - for res.Len() < limit && !eof { - bufTmp := bufFix - if res.Len()+batch > limit { - bufTmp = bufFix[:limit-res.Len()] - } - n, err := io.ReadFull(r, bufTmp) - if err == io.EOF || err == io.ErrUnexpectedEOF { - eof = true - } else if err != nil { - return nil, err - } - if _, err = res.Write(bufTmp[:n]); err != nil { - return nil, err - } - } - return res.Bytes(), nil -} - // ErrNotEmpty is an error reported when there is a non-empty reader var ErrNotEmpty = errors.New("not-empty") diff --git a/modules/util/io_test.go b/modules/util/io_test.go deleted file mode 100644 index 870e713646..0000000000 --- a/modules/util/io_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package util - -import ( - "bytes" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type readerWithError struct { - buf *bytes.Buffer -} - -func (r *readerWithError) Read(p []byte) (n int, err error) { - if r.buf.Len() < 2 { - return 0, errors.New("test error") - } - return r.buf.Read(p) -} - -func TestReadWithLimit(t *testing.T) { - bs := []byte("0123456789abcdef") - - // normal test - buf, err := readWithLimit(bytes.NewBuffer(bs), 5, 2) - require.NoError(t, err) - assert.Equal(t, []byte("01"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 5) - require.NoError(t, err) - assert.Equal(t, []byte("01234"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 6) - require.NoError(t, err) - assert.Equal(t, []byte("012345"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, len(bs)) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789abcdef"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 100) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789abcdef"), buf) - - // test with error - buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 10) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789"), buf) - - buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 100) - require.ErrorContains(t, err, "test error") - assert.Empty(t, buf) - - // test public function - buf, err = ReadWithLimit(bytes.NewBuffer(bs), 2) - require.NoError(t, err) - assert.Equal(t, []byte("01"), buf) - - buf, err = ReadWithLimit(bytes.NewBuffer(bs), 9999999) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789abcdef"), buf) -} diff --git a/modules/util/truncate.go b/modules/util/truncate.go index f2edbdc673..7207a89177 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -54,3 +54,12 @@ func SplitTrimSpace(input, sep string) []string { return stringList } + +// TruncateRunes returns a truncated string with given rune limit, +// it returns input string if its rune length doesn't exceed the limit. +func TruncateRunes(str string, limit int) string { + if utf8.RuneCountInString(str) < limit { + return str + } + return string([]rune(str)[:limit]) +} diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index dfe1230fd4..8187b13eb2 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -44,3 +44,18 @@ func TestSplitString(t *testing.T) { } test(tc, SplitStringAtByteN) } + +func TestTruncateRunes(t *testing.T) { + assert.Empty(t, TruncateRunes("", 0)) + assert.Empty(t, TruncateRunes("", 1)) + + assert.Empty(t, TruncateRunes("ab", 0)) + assert.Equal(t, "a", TruncateRunes("ab", 1)) + assert.Equal(t, "ab", TruncateRunes("ab", 2)) + assert.Equal(t, "ab", TruncateRunes("ab", 3)) + + assert.Empty(t, TruncateRunes("测试", 0)) + assert.Equal(t, "测", TruncateRunes("测试", 1)) + assert.Equal(t, "测试", TruncateRunes("测试", 2)) + assert.Equal(t, "测试", TruncateRunes("测试", 3)) +} diff --git a/modules/validation/email.go b/modules/validation/email.go index fb563c2b81..8e1ffc203d 100644 --- a/modules/validation/email.go +++ b/modules/validation/email.go @@ -8,7 +8,6 @@ package validation import ( "fmt" "net/mail" - "regexp" "strings" "forgejo.org/modules/setting" @@ -20,21 +19,6 @@ import ( // ErrEmailNotActivated e-mail address has not been activated error var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated") -// ErrEmailCharIsNotSupported e-mail address contains unsupported character -type ErrEmailCharIsNotSupported struct { - Email string -} - -// IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported -func IsErrEmailCharIsNotSupported(err error) bool { - _, ok := err.(ErrEmailCharIsNotSupported) - return ok -} - -func (err ErrEmailCharIsNotSupported) Error() string { - return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email) -} - // ErrEmailInvalid represents an error where the email address does not comply with RFC 5322 // or has a leading '-' character type ErrEmailInvalid struct { @@ -55,8 +39,6 @@ func (err ErrEmailInvalid) Unwrap() error { return util.ErrInvalidArgument } -var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - // check if email is a valid address with allowed domain func ValidateEmail(email string) error { if err := validateEmailBasic(email); err != nil { @@ -77,15 +59,12 @@ func validateEmailBasic(email string) error { return ErrEmailInvalid{email} } - if !emailRegexp.MatchString(email) { - return ErrEmailCharIsNotSupported{email} - } - - if email[0] == '-' { + parsedAddress, err := mail.ParseAddress(email) + if err != nil { return ErrEmailInvalid{email} } - if _, err := mail.ParseAddress(email); err != nil { + if parsedAddress.Name != "" { return ErrEmailInvalid{email} } diff --git a/modules/validation/email_test.go b/modules/validation/email_test.go index 68864b2450..b7ee766ddb 100644 --- a/modules/validation/email_test.go +++ b/modules/validation/email_test.go @@ -35,7 +35,7 @@ func TestEmailAddressValidate(t *testing.T) { `first|last@iana.org`: nil, `first}last@iana.org`: nil, `first~last@iana.org`: nil, - `first;last@iana.org`: ErrEmailCharIsNotSupported{`first;last@iana.org`}, + `first;last@iana.org`: ErrEmailInvalid{`first;last@iana.org`}, ".233@qq.com": ErrEmailInvalid{".233@qq.com"}, "!233@qq.com": nil, "#233@qq.com": nil, @@ -45,7 +45,7 @@ func TestEmailAddressValidate(t *testing.T) { "'233@qq.com": nil, "*233@qq.com": nil, "+233@qq.com": nil, - "-233@qq.com": ErrEmailInvalid{"-233@qq.com"}, + "-233@qq.com": nil, "/233@qq.com": nil, "=233@qq.com": nil, "?233@qq.com": nil, @@ -56,9 +56,10 @@ func TestEmailAddressValidate(t *testing.T) { "|233@qq.com": nil, "}233@qq.com": nil, "~233@qq.com": nil, - ";233@qq.com": ErrEmailCharIsNotSupported{";233@qq.com"}, - "Foo ": ErrEmailCharIsNotSupported{"Foo "}, - string([]byte{0xE2, 0x84, 0xAA}): ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})}, + "\"~@ \"@famfo.xyz": nil, + "Foo ": ErrEmailInvalid{"Foo "}, + ";233@qq.com": ErrEmailInvalid{";233@qq.com"}, + string([]byte{0xE2, 0x84, 0xAA}): ErrEmailInvalid{string([]byte{0xE2, 0x84, 0xAA})}, } for kase, err := range kases { t.Run(kase, func(t *testing.T) { diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 1f573564e6..4b28dead03 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -75,6 +75,11 @@ func IsValidExternalURL(uri string) bool { return true } +// IsValidReleaseAssetURL checks if the URL is valid for external release assets +func IsValidReleaseAssetURL(uri string) bool { + return IsValidURL(uri) +} + // IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers func IsValidExternalTrackerURLFormat(uri string) bool { if !IsValidExternalURL(uri) { diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 01a17f0d6b..7e32184691 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -7,6 +7,7 @@ import ( "testing" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" ) @@ -47,7 +48,7 @@ func Test_IsValidURL(t *testing.T) { } func Test_IsValidExternalURL(t *testing.T) { - setting.AppURL = "https://try.gitea.io/" + defer test.MockVariableValue(&setting.AppURL, "https://code.forgejo.org/")() cases := []struct { description string @@ -56,7 +57,7 @@ func Test_IsValidExternalURL(t *testing.T) { }{ { description: "Current instance URL", - url: "https://try.gitea.io/test", + url: "https://code.forgejo.org/test", valid: true, }, { @@ -66,7 +67,7 @@ func Test_IsValidExternalURL(t *testing.T) { }, { description: "Current instance API URL", - url: "https://try.gitea.io/api/v1/user/follow", + url: "https://code.forgejo.org/api/v1/user/follow", valid: false, }, { @@ -89,7 +90,7 @@ func Test_IsValidExternalURL(t *testing.T) { } func Test_IsValidExternalTrackerURLFormat(t *testing.T) { - setting.AppURL = "https://try.gitea.io/" + defer test.MockVariableValue(&setting.AppURL, "https://code.forgejo.org/")() cases := []struct { description string @@ -156,7 +157,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) { } func TestIsValidUsernameAllowDots(t *testing.T) { - setting.Service.AllowDotsInUsernames = true + defer test.MockVariableValue(&setting.Service.AllowDotsInUsernames, true)() + tests := []struct { arg string want bool @@ -188,10 +190,7 @@ func TestIsValidUsernameAllowDots(t *testing.T) { } func TestIsValidUsernameBanDots(t *testing.T) { - setting.Service.AllowDotsInUsernames = false - defer func() { - setting.Service.AllowDotsInUsernames = true - }() + defer test.MockVariableValue(&setting.Service.AllowDotsInUsernames, false)() tests := []struct { arg string diff --git a/modules/validation/validatable.go b/modules/validation/validatable.go index d2c5553259..4500f6e53d 100644 --- a/modules/validation/validatable.go +++ b/modules/validation/validatable.go @@ -10,6 +10,8 @@ import ( "unicode/utf8" "forgejo.org/modules/timeutil" + + ap "github.com/go-ap/activitypub" ) // ErrNotValid represents an validation error @@ -41,6 +43,13 @@ func IsValid(v Validateable) (bool, error) { return true, nil } +func ValidateIDExists(value ap.Item, name string) []string { + if value == nil { + return []string{fmt.Sprintf("%v should not be nil", name)} + } + return ValidateNotEmpty(value.GetID().String(), name) +} + func ValidateNotEmpty(value any, name string) []string { isValid := true switch v := value.(type) { @@ -83,5 +92,5 @@ func ValidateOneOf(value any, allowed []any, name string) []string { return []string{} } } - return []string{fmt.Sprintf("Value %v is not contained in allowed values %v", value, allowed)} + return []string{fmt.Sprintf("Field %s contains the value %v, which is not in allowed subset %v", name, value, allowed)} } diff --git a/modules/validation/validatable_test.go b/modules/validation/validatable_test.go index 0802d5cc92..1fe407b343 100644 --- a/modules/validation/validatable_test.go +++ b/modules/validation/validatable_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package validation @@ -7,6 +7,9 @@ import ( "testing" "forgejo.org/modules/timeutil" + + ap "github.com/go-ap/activitypub" + "github.com/stretchr/testify/assert" ) type Sut struct { @@ -37,33 +40,50 @@ func Test_IsValid(t *testing.T) { func Test_ValidateNotEmpty_ForString(t *testing.T) { sut := "" - if len(ValidateNotEmpty(sut, "dummyField")) == 0 { - t.Errorf("sut should be invalid") - } + res := ValidateNotEmpty(sut, "dummyField") + assert.Len(t, res, 1) + sut = "not empty" - if res := ValidateNotEmpty(sut, "dummyField"); len(res) > 0 { - t.Errorf("sut should be valid but was %q", res) - } + res = ValidateNotEmpty(sut, "dummyField") + assert.Empty(t, res, 0) } func Test_ValidateNotEmpty_ForTimestamp(t *testing.T) { sut := timeutil.TimeStamp(0) - if res := ValidateNotEmpty(sut, "dummyField"); len(res) == 0 { - t.Errorf("sut should be invalid") - } + res := ValidateNotEmpty(sut, "dummyField") + assert.Len(t, res, 1) + sut = timeutil.TimeStampNow() - if res := ValidateNotEmpty(sut, "dummyField"); len(res) > 0 { - t.Errorf("sut should be valid but was %q", res) + res = ValidateNotEmpty(sut, "dummyField") + assert.Empty(t, res, 0) +} + +func Test_ValidateIDExists_ForItem(t *testing.T) { + sut := ap.Activity{ + Object: nil, } + res := ValidateIDExists(sut.Object, "dummyField") + assert.Len(t, res, 1) + + sut = ap.Activity{ + Object: ap.IRI(""), + } + res = ValidateIDExists(sut.Object, "dummyField") + assert.Len(t, res, 1) + + sut = ap.Activity{ + Object: ap.IRI("https://dummy.link/id"), + } + res = ValidateIDExists(sut.Object, "dummyField") + assert.Empty(t, res, 0) } func Test_ValidateMaxLen(t *testing.T) { sut := "0123456789" - if len(ValidateMaxLen(sut, 9, "dummyField")) == 0 { - t.Errorf("sut should be invalid") - } + res := ValidateMaxLen(sut, 9, "dummyField") + assert.Len(t, res, 1) + sut = "0123456789" - if res := ValidateMaxLen(sut, 11, "dummyField"); len(res) > 0 { - t.Errorf("sut should be valid but was %q", res) - } + res = ValidateMaxLen(sut, 11, "dummyField") + assert.Empty(t, res, 0) } diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go index 760e092914..8fd24c9733 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -90,7 +90,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { status = v.WrittenStatus() } logf := logger.Info - if strings.HasPrefix(req.RequestURI, "/assets/") { + if strings.HasPrefix(req.RequestURI, "/assets/") || req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" || req.RequestURI == "/api/actions/runner.v1.RunnerService/UpdateLog" { logf = logger.Trace } message := completedMessage diff --git a/modules/webhook/structs.go b/modules/webhook/structs.go index 927a91a74c..6c0161bfc2 100644 --- a/modules/webhook/structs.go +++ b/modules/webhook/structs.go @@ -4,6 +4,7 @@ package webhook // HookEvents is a set of web hook events +// update TestCreateWebhook in models/webhook/webhook_test.go when adding or changing values here type HookEvents struct { Create bool `json:"create"` Delete bool `json:"delete"` @@ -26,9 +27,12 @@ type HookEvents struct { Repository bool `json:"repository"` Release bool `json:"release"` Package bool `json:"package"` + ActionRunFailure bool `json:"action_run_failure"` + ActionRunRecover bool `json:"action_run_recover"` + ActionRunSuccess bool `json:"action_run_success"` } -// HookEvent represents events that will delivery hook. +// HookEvent represents events that will deliver a hook. type HookEvent struct { PushOnly bool `json:"push_only"` SendEverything bool `json:"send_everything"` diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 244dc423c1..e833f90f58 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -7,6 +7,7 @@ package webhook type HookEventType string // Types of hook events +// update TestCreateWebhook in models/webhook/webhook_test.go when adding or changing values here const ( HookEventCreate HookEventType = "create" HookEventDelete HookEventType = "delete" @@ -33,6 +34,9 @@ const ( HookEventPackage HookEventType = "package" HookEventSchedule HookEventType = "schedule" HookEventWorkflowDispatch HookEventType = "workflow_dispatch" + HookEventActionRunFailure HookEventType = "action_run_failure" + HookEventActionRunRecover HookEventType = "action_run_recover" + HookEventActionRunSuccess HookEventType = "action_run_success" ) // Event returns the HookEventType as an event string @@ -65,6 +69,12 @@ func (h HookEventType) Event() string { return "repository" case HookEventRelease: return "release" + case HookEventActionRunFailure: + return "action_run_failure" + case HookEventActionRunRecover: + return "action_run_recover" + case HookEventActionRunSuccess: + return "action_run_success" } return "" } diff --git a/options/locale/locale_ar.ini b/options/locale/locale_ar.ini index f4ac1a0e3d..15d614e8bc 100644 --- a/options/locale/locale_ar.ini +++ b/options/locale/locale_ar.ini @@ -699,7 +699,7 @@ issues.filter_milestone_all = كل الأهداف issues.unlock.notice_2 = - يمكنك دوما إقفال هذه المسألة من جديد في المستقبل. issues.num_participants_few = %d متحاور release.title = عنوان الإصدار -issues.closed_at = `أغلق هذه المسألة %[2]s` +issues.closed_at = `أغلق هذه المسألة %s` issues.lock.title = إقفال التحاور في هذه المسألة. issues.new.no_label = بلا تصنيف issues.filter_sort.mostforks = الأعلى اشتقاقا @@ -759,7 +759,7 @@ branch.renamed = غُيّر اسم الفرع %s إلى %s. delete_preexisting = احذف الملفات الموجودة سابقا branch.included_desc = هذا الفرع جزء من الفرع المبدئي trust_model_helper_collaborator_committer = مشترك+مودع: ثق بتوقيعات المشتركين التي تطابق المودع -issues.reopened_at = `أعاد فتح هذه المسألة %[2]s` +issues.reopened_at = `أعاد فتح هذه المسألة %s` issues.action_milestone = هدف issues.new.assignees = المكلَّفون release.tag_name_protected = اسم الوسم محمي. @@ -1166,7 +1166,7 @@ pulls.status_checking = في انتظار بعض الفحوص pulls.status_checks_failure = بعض الفحوص فشلت pulls.status_checks_success = جميع الفحوص ناجحة pulls.status_checks_warning = بعض الفحوص تعطي تحذيرات -pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع %[2]s` +pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع %s` pulls.cmd_instruction_hint = `أظهر شرح استخدام سطر الأوامر.` pulls.cmd_instruction_checkout_title = اسحب pulls.cmd_instruction_checkout_desc = من مستودع مشروعك، اسحب (check out) فرعا جديدا واختبر التغييرات. @@ -1257,8 +1257,8 @@ pulls.status_checks_details = تفاصيل pulls.status_checks_hide_all = أخفِ كل الفحوص pulls.status_checks_show_all = أظهر كل الفحوص pulls.close = أغلق طلب الدمج -pulls.closed_at = `أغلق طلب الدمج %[2]s` -pulls.reopened_at = `أعاد فتح طلب الدمج %[2]s` +pulls.closed_at = `أغلق طلب الدمج %s` +pulls.reopened_at = `أعاد فتح طلب الدمج %s` milestones.title = العنوان milestones.desc = الوصف milestones.edit = عدّل الهدف @@ -1302,11 +1302,11 @@ issues.closed_by_fake = من %[2]s أُغلقت %[1]s issues.num_comments_1 = %d تعليق issues.num_comments = %d تعليقا issues.commented_at = `علّق %s` -issues.commit_ref_at = `أشار إلى هذه المسألة من إيداع %[2]s` -issues.ref_issue_from = `أشار إلى هذه المسألة %[4]s %[2]s` -issues.ref_pull_from = `أشار إلى هذا الطلب %[4]s %[2]s` -issues.ref_closing_from = `أشار إلى طلب دمج %[4]s سيغلق هذه المسألة %[2]s` -issues.ref_reopening_from = `أشار إلى طلب دمج %[4]s سيعيد فتح هذه المسألة %[2]s` +issues.commit_ref_at = `أشار إلى هذه المسألة من إيداع %s` +issues.ref_issue_from = `أشار إلى هذه المسألة %[3]s %[1]s` +issues.ref_pull_from = `أشار إلى هذا الطلب %[3]s %[1]s` +issues.ref_closing_from = `أشار إلى طلب دمج %[3]s سيغلق هذه المسألة %[1]s` +issues.ref_reopening_from = `أشار إلى طلب دمج %[3]s سيعيد فتح هذه المسألة %[1]s` issues.ref_closed_from = `أغلق هذه المسألة %[4]s %[2]s` issues.ref_reopened_from = `أعاد فتح هذه المسألة %[4]s %[2]s` issues.reference_issue.body = المحتوى diff --git a/options/locale/locale_bg.ini b/options/locale/locale_bg.ini index 6fc4b55eae..1b9767f674 100644 --- a/options/locale/locale_bg.ini +++ b/options/locale/locale_bg.ini @@ -11,7 +11,7 @@ copy_content = Копиране на съдържанието user_profile_and_more = Профил и настройки… view = Преглед your_settings = Настройки -mirrors = Огледала +mirrors = Огледални explore = Разглеждане write = Писане twofa = Двуфакторно удостоверяване @@ -36,7 +36,7 @@ dashboard = Табло logo = Лого toc = Съдържание copy_url = Копиране на URL -new_mirror = Ново огледало +new_mirror = Ново огледално re_type = Потвърдете паролата copy = Копиране enabled = Включено @@ -61,7 +61,7 @@ ok = Добре manage_org = Управление на организациите new_repo = Ново хранилище register = Регистрация -mirror = Огледало +mirror = Огледално username = Потребителско име password = Парола template = Шаблон @@ -69,7 +69,7 @@ signed_in_as = Влезли сте като sign_up = Регистриране enable_javascript = Този сайт изисква JavaScript. home = Начало -email = Адрес на ел. поща +email = Адрес за ел. поща issues = Задачи retry = Повторен опит remove = Премахване @@ -93,8 +93,8 @@ filter.not_fork = Не разклонения filter.is_template = Шаблони filter.not_template = Не шаблони filter.private = Частни -filter.is_mirror = Огледала -filter.not_mirror = Не огледала +filter.is_mirror = Огледални +filter.not_mirror = Не огледални copy_hash = Копиране на контролната сума artifacts = Артефакти show_log_seconds = Показване на секундите @@ -102,7 +102,7 @@ remove_all = Премахване на всичко test = Проба remove_label_str = Премахване на елемента „%s“ copy_branch = Копиране на името на клона -error404 = Страницата, която се опитвате да отворите, или не съществува или не сте упълномощени да я видите. +error404 = Страницата, която се опитвате да отворите, или не съществува, или е премахната, или не сте упълномощени да я видите. new_repo.link = Ново хранилище new_migrate.title = Нова миграция new_repo.title = Ново хранилище @@ -112,6 +112,35 @@ new_org.link = Нова организация copy_generic = Копиране в клипборда copy_error = Неуспешно копиране copy_path = Копиране на пътя +toggle_menu = Превключване на менюто +confirm_delete_artifact = Сигурни ли сте, че искате да изтриете артефакта „%s“? +more_items = Още елементи +twofa_scratch = Резервен код за двуфакторно удостоверяване +webauthn_use_twofa = Използвайте двуфакторен код от телефона си +webauthn_error_insecure = WebAuthn поддържа само сигурни връзки. За тестване през HTTP можете да използвате произход „localhost“ или „127.0.0.1“ +error413 = Изчерпали сте квотата си. +go_back = Връщане +invalid_data = Невалидни данни: %v +archived = Архивирано +concept_system_global = Глобално +concept_user_individual = Индивидуално +show_full_screen = Показване на цял екран +show_timestamps = Показване на времеви отпечатъци +rerun = Повторно изпълнение +copy_type_unsupported = Този тип файл не може да бъде копиран +webauthn_error_unknown = Възникна неизвестна грешка. Моля, опитайте отново. +webauthn_error_unable_to_process = Сървърът не можа да обработи заявката ви. +webauthn_error_empty = Трябва да зададете име за този ключ. +webauthn_error_timeout = Времето за изчакване изтече преди ключът ви да бъде прочетен. Моля, презаредете страницата и опитайте отново. +return_to_forgejo = Връщане към Forgejo +unknown = Неизвестно +confirm_delete_selected = Потвърждавате ли изтриването на всички избрани елементи? +webauthn_insert_key = Поставете вашия ключ за сигурност +webauthn_press_button = Моля, натиснете бутона на вашия ключ за сигурност… +webauthn_sign_in = Натиснете бутона на вашия ключ за сигурност. Ако ключът ви за сигурност няма бутон, поставете го отново. +webauthn_error = Неуспешно прочитане на вашия ключ за сигурност. +webauthn_unsupported_browser = Вашият браузър в момента не поддържа WebAuthn. +webauthn_error_duplicated = Ключът за сигурност не е разрешен за тази заявка. Моля, уверете се, че ключът не е вече регистриран. [settings] ui = Тема @@ -157,7 +186,7 @@ account = Акаунт update_avatar = Обновяване на профилната снимка ssh_gpg_keys = SSH / GPG ключове comment_type_group_milestone = Етап -manage_emails = Управление на адресите на ел. поща +manage_emails = Управление на адресите за ел. поща permission_read = Четене update_password = Обновяване на паролата biography_placeholder = Разкажете на другите малко за себе си! (Можете да използвате Маркдаун) @@ -183,7 +212,7 @@ user_block_success = Потребителят е блокиран успешно update_profile_success = Профилът ви е обновен. update_user_avatar_success = Профилната снимка на потребителя е обновена. remove_oauth2_application_success = Приложението е изтрито. -email_deletion_success = Адресът на ел. поща е премахнат. +email_deletion_success = Адресът за ел. поща е премахнат. update_avatar_success = Профилната ви снимка е обновена. change_username = Потребителското ви име е променено. comment_type_group_assignee = Изпълнител @@ -191,22 +220,22 @@ enable_custom_avatar = Използване на персонализирана requires_activation = Изисква активиране activated = Активиран primary = Основен -email_deletion = Премахване на адреса на ел. поща -add_new_email = Добавяне на нов адрес на ел. поща -add_email = Добавяне на адрес на ел. поща +email_deletion = Премахване на адреса за ел. поща +add_new_email = Добавяне на нов адрес за ел. поща +add_email = Добавяне на адрес за ел. поща key_content_gpg_placeholder = Започва с „-----BEGIN PGP PUBLIC KEY BLOCK-----“ comment_type_group_title = Заглавие comment_type_group_label = Етикет -change_username_prompt = Забележка: Промяната на потребителското ви име променя също URL на вашия акаунт. +change_username_prompt = Бележка: Промяната на потребителското ви име променя също URL на вашия акаунт. update_language_not_found = Езикът „%s“ не е наличен. keep_activity_private_popup = Вашата дейност ще бъде видима само за вас и администраторите на сайта uploaded_avatar_not_a_image = Каченият файл не е изображение. uploaded_avatar_is_too_big = Размерът на качения файл (%d KiB) надвишава максималния размер (%d KiB). -change_password_success = Паролата ви е обновена. Влизайте с новата си парола от сега нататък. +change_password_success = Паролата ви е обновена. Отсега нататък използвайте новата си парола, за да влезете. manage_themes = Тема по подразбиране manage_openid = OpenID адреси primary_email = Да е основен -keep_email_private = Скриване на адреса на ел. поща +keep_email_private = Скриване на адреса за ел. поща theme_update_error = Избраната тема не съществува. theme_update_success = Темата ви е обновена. key_content_ssh_placeholder = Започва с „ssh-ed25519“, „ssh-rsa“, „ecdsa-sha2-nistp256“, „ecdsa-sha2-nistp384“, „ecdsa-sha2-nistp521“, „sk-ecdsa-sha2-nistp256@openssh.com“, или „sk-ssh-ed25519@openssh.com“ @@ -227,7 +256,7 @@ saved_successfully = Настройките бяха запазени успеш no_activity = Няма скорошна дейност theme_desc = Тази тема ще се използва за уеб интерфейса, когато сте влезли. keep_activity_private = Скриване на дейността от профилната страница -lookup_avatar_by_mail = Търсене на профилна снимка по адреса на ел. поща +lookup_avatar_by_mail = Търсене на профилна снимка по адреса за ел. поща password_incorrect = Текущата парола е неправилна. change_username_redirect_prompt = Старото потребителско име ще се пренасочва, докато някой не го вземе. principal_content = Съдържание @@ -246,7 +275,7 @@ delete_prompt = Тази операция ще изтрие перманентн email_notifications.disable = Изключване на известията по ел. поща delete_account = Изтриване на акаунта ви confirm_delete_account = Потвърждаване на изтриването -email_notifications.onmention = Ел. поща само при споменаване +email_notifications.onmention = Ел. писмо само при споменаване pronouns_unspecified = Непосочени pronouns = Местоимения gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig @@ -254,8 +283,103 @@ language.title = Език по подразбиране language.localization_project = Помогнете ни да преведем Forgejo на вашия език! Научете повече. language.description = Този език ще бъде запазен във вашия акаунт и ще се използва като език по подразбиране, след като влезете. pronouns_custom = Персонализирани -visibility.limited_tooltip = Видимо само за влезли потребители +visibility.limited_tooltip = Видим само за влезли потребители pronouns_custom_label = Персонализирани местоимения +comment_type_group_review_request = Искане за рецензия +ssh_key_been_used = Този SSH ключ вече е добавен към сървъра. +create_oauth2_application = Създаване на ново OAuth2 приложение +update_oauth2_application_success = Успешно обновихте OAuth2 приложението. +authorized_oauth2_applications = Упълномощени OAuth2 приложения +manage_account_links = Свързани акаунти +revoke_oauth2_grant = Отнемане на достъпа +added_on = Добавен на %s +comment_type_group_dependency = Зависимост +update_hints_success = Подсказките са обновени. +manage_oauth2_applications = Управление на OAuth2 приложения +gpg_key_id_used = Вече съществува публичен GPG ключ със същото ID. +oauth2_applications_desc = OAuth2 приложенията позволяват на вашето приложение от трета страна да удостоверява сигурно потребители в тази инстанция на Forgejo. +blocked_since = Блокиран от %s +hidden_comment_types.ref_tooltip = Коментари, в които тази задача е спомената от друга задача/подаване/… +create_oauth2_application_success = Успешно създадохте ново OAuth2 приложение. +quota.applies_to_org = Следните правила за квота се прилагат за тази организация +keep_activity_private.description = Вашата публична дейност ще бъде видима само за вас и администраторите на инстанцията. +ssh_helper = Нуждаете се от помощ? Разгледайте ръководството за създаване на собствени SSH ключове или за решаване на често срещани проблеми, които може да срещнете при използване на SSH. +twofa_desc = За да защитите акаунта си от кражба на парола, можете да използвате смартфон или друго устройство за получаване на еднократни пароли, базирани на време („TOTP“). +scan_this_image = Сканирайте това изображение с вашето приложение за удостоверяване: +quota.rule.exceeded.helper = Общият размер на обектите за това правило надвиши квотата. +password_change_disabled = Нелокални потребители не могат да обновяват паролата си през уеб интерфейса на Forgejo. +twofa_disable_note = Можете да изключите двуфакторното удостоверяване, ако е необходимо. +hooks.desc = Добавете уеб-куки, които ще се задействат за всички хранилища, които притежавате. +delete_account_desc = Сигурни ли сте, че искате да изтриете перманентно този потребителски акаунт? +last_used = Последно използван на +revoke_oauth2_grant_description = Отнемането на достъпа за това приложение от трета страна ще му попречи да има достъп до вашите данни. Сигурни ли сте? +password_username_disabled = Нелокални потребители не могат да променят потребителското си име. Моля, свържете се с администратора на сайта за повече подробности. +change_username_redirect_prompt.with_cooldown.one = Старото потребителско име ще бъде достъпно за всички след период на изчакване от %[1]d ден. Все още можете да си върнете старото потребителско име по време на периода на изчакване. +change_username_redirect_prompt.with_cooldown.few = Старото потребителско име ще бъде достъпно за всички след период на изчакване от %[1]d дни. Все още можете да си върнете старото потребителско име по време на периода на изчакване. +generate_token_name_duplicate = %s вече е използвано като име на приложение. Моля, използвайте ново. +quota.rule.exceeded = Надвишена +repo_and_org_access = Достъп до хранилища и организации +permissions_public_only = Само публични +permissions_list = Разрешения: +edit_oauth2_application = Редактиране на OAuth2 приложение +remove_oauth2_application = Премахване на OAuth2 приложение +twofa_recovery_tip = Ако загубите устройството си, ще можете да използвате ключ за еднократно възстановяване, за да си върнете достъпа до акаунта. +visibility.private_tooltip = Видим само за членове на организации, в които участвате +quota.applies_to_user = Следните правила за квота се прилагат за вашия акаунт +quota.rule.no_limit = Неограничена +hints = Подсказки +comment_type_group_issue_ref = Препратка към задача +activate_email = Изпращане на активация +ssh_disabled = SSH е изключен +twofa_disable_desc = Изключването на двуфакторното удостоверяване ще направи акаунта ви по-малко сигурен. Продължаване? +keep_pronouns_private = Показване на местоименията само на удостоверени потребители +keep_pronouns_private.description = Това ще скрие вашите местоимения от посетители, които не са влезли в системата. +gpg_helper = Нуждаете се от помощ? Разгледайте ръководството относно GPG. +valid_until_date = Валиден до %s +ssh_externally_managed = Този SSH ключ се управлява външно за този потребител +regenerate_scratch_token_desc = Ако сте загубили ключа си за възстановяване или вече сте го използвали, за да влезете, можете да го нулирате тук. +create_oauth2_application_button = Създаване на приложение +revoke_oauth2_grant_success = Достъпът е отнет успешно. +comment_type_group_deadline = Краен срок +comment_type_group_time_tracking = Проследяване на времето +activations_pending = Чакащи активации +valid_forever = Валиден завинаги +key_state_desc = Този ключ е използван през последните 7 дни +revoke_key = Отнемане +delete_account_title = Изтриване на потребителския акаунт +update_hints = Обновяване на подсказките +permissions_access_all = Всички (публични, частни и ограничени) +oauth2_application_name = Име на приложението +visibility.public_tooltip = Видим за всички +user_block_yourself = Не можете да блокирате себе си. +hidden_comment_types.issue_ref_tooltip = Коментари, в които потребителят променя клона/маркера, свързан със задачата +comment_type_group_reference = Препратка +comment_type_group_branch = Клон +comment_type_group_pull_request_push = Добавени подавания +quota = Квота +webauthn_delete_key = Премахване на ключ за сигурност +webauthn_register_key = Добавяне на ключ за сигурност +webauthn_nickname = Прякор +webauthn_delete_key_desc = Ако премахнете ключ за сигурност, вече няма да можете да влизате с него. Продължаване? +additional_repo_units_hint = Предлагане за включване на допълнителни елементи на хранилището +twofa_is_enrolled = Вашият акаунт в момента е включен в двуфакторно удостоверяване. +twofa_not_enrolled = Вашият акаунт в момента не е включен в двуфакторно удостоверяване. +webauthn_key_loss_warning = Ако загубите ключовете си за сигурност, ще загубите достъп до акаунта си. +email_desc = Вашият основен адрес за ел. поща ще се използва за известия, възстановяване на парола и, при условие че не е скрит, за уеб-базирани Git операции. +email_preference_set_success = Предпочитанията за ел. поща са зададени успешно. +add_email_confirmation_sent = Изпратено е ел. писмо за потвърждение до „%s“. За да потвърдите адреса си за ел. поща, моля, проверете входящата си кутия и последвайте предоставената връзка в рамките на следващите %s. +additional_repo_units_hint_description = Показване на подсказка „Включване на повече“ за хранилища, които нямат включени всички налични елементи. +email_notifications.submit = Задаване на предпочит. за ел. поща +email_notifications.andyourown = И вашите собствени известия +email_deletion_desc = Адресът за ел. поща и свързаната информация ще бъдат премахнати от вашия акаунт. Git подаванията от този адрес за ел. поща ще останат непроменени. Продължаване? +add_email_success = Новият адрес за ел. поща е добавен. +remove_account_link = Премахване на свързан акаунт +webauthn_alternative_tip = Може да искате да конфигурирате допълнителен метод за удостоверяване. +hidden_comment_types_description = Типовете коментари, отметнати тук, няма да се показват в страниците на задачите. Например, отмятането на „Етикет“ премахва всички коментари от типа „<потребител> добави/премахна <етикет>“. +hidden_comment_types = Скрити типове коментари +comment_type_group_lock = Състояние на заключване +can_not_add_email_activations_pending = Има чакаща активация, опитайте отново след няколко минути, ако искате да добавите нова ел. поща. +storage_overview = Преглед на съхранението [packages] container.labels.value = Стойност @@ -287,6 +411,33 @@ generic.download = Изтеглете пакета от командния ре container.details.type = Тип образ alpine.repository = За хранилището container.images.title = Образи +arch.version.description = Описание +search_in_external_registry = Търсене в %s +filter.type = Тип +filter.container.untagged = Без маркер +filter.type.all = Всички +registry.documentation = За повече информация относно регистъра %s, вижте документацията. +filter.no_result = Вашият филтър не даде резултати. +filter.container.tagged = С маркер +arch.pacman.repo.multi = %s има същата версия в различни дистрибуции. +arch.pacman.helper.gpg = Добавете доверителен сертификат за pacman: +alpine.repository.architectures = Архитектури +arch.version.provides = Доставя +arch.version.groups = Група +details.project_site = Уебсайт на проекта +arch.pacman.conf = Добавете сървър със свързаната дистрибуция и архитектура към /etc/pacman.conf : +arch.pacman.sync = Синхронизирайте пакета с pacman: +details.repository_site = Уебсайт на хранилището +arch.version.depends = Зависимости +arch.version.optdepends = Допълнителни зависимости +arch.version.replaces = Заменя +go.install = Инсталирайте пакета от командния ред: +cargo.registry = Настройте този регистър в конфигурационния файл на Cargo (например ~/.cargo/config.toml): +cargo.install = За да инсталирате пакета с Cargo, изпълнете следната команда: +details.documentation_site = Уебсайт на документацията +arch.version.conflicts = В конфликт +alpine.repository.branches = Клонове +arch.pacman.repo.multi.item = Конфигурация за %s [tool] hours = %d часа @@ -447,7 +598,7 @@ projects.template.desc = Шаблон projects.card_type.text_only = Само текст projects.card_type.images_and_text = Изображения и текст wiki = Уики -wiki.welcome = Добре дошли в Уикито. +wiki.welcome = Добре дошли в уикито. wiki.create_first_page = Създаване на първата страница editor.upload_file = Качване на файл projects.column.color = Цвят @@ -598,7 +749,7 @@ settings.admin_settings = Администраторски настройки issues.role.owner = Притежател settings.transfer.title = Прехвърляне на притежанието issues.author = Автор -issues.closed_at = `затвори тази задача %[2]s` +issues.closed_at = `затвори тази задача %s` settings.collaborator_deletion_desc = Премахването на сътрудник ще отнеме достъпа му до това хранилище. Продължаване? commits.message = Съобщение issues.due_date_not_set = Няма зададен краен срок. @@ -622,9 +773,9 @@ issues.filter_type.all_issues = Всички задачи issues.filter_poster_no_select = Всички автори issues.opened_by = отворена %[1]s от %[3]s issues.action_open = Отваряне -pulls.closed_at = `затвори тази заявка за сливане %[2]s` -pulls.reopened_at = `отвори наново тази заявка за сливане %[2]s` -issues.reopened_at = `отвори наново тази задача %[2]s` +pulls.closed_at = `затвори тази заявка за сливане %s` +pulls.reopened_at = `отвори наново тази заявка за сливане %s` +issues.reopened_at = `отвори наново тази задача %s` projects.column.edit = Редактиране на колоната issues.close = Затваряне на задачата issues.ref_reopened_from = `отвори наново тази задача %[4]s %[2]s` @@ -644,7 +795,7 @@ milestones.filter_sort.latest_due_date = Най-далечен краен сро diff.view_file = Преглед на файла release.deletion_success = Изданието е изтрито. projects.column.delete = Изтриване на колоната -migrate.migrating = Мигриране от %s ... +migrate.migrating = Мигриране от %s … escape_control_characters = Екраниране issues.label_deletion_success = Етикетът е изтрит. pulls.is_closed = Заявката за сливане е затворена. @@ -1025,7 +1176,7 @@ issues.content_history.edited = редактирано pulls.title_desc_one = иска да слее %[1]d подаване от %[2]s в %[3]s pulls.showing_specified_commit_range = Показани са само промените между %[1]s..%[2]s pulls.merged_title_desc_one = сля %[1]d подаване от %[2]s в %[3]s %[4]s -pulls.no_merge_access = Не сте упълномощени за сливане на тази заявка за сливане. +pulls.no_merge_access = Не сте упълномощени да слеете тази заявка за сливане. activity.navbar.code_frequency = Честота на промените activity.git_stats_pushed_1 = е изтласкал activity.git_stats_push_to_branch = към %s и @@ -1054,7 +1205,7 @@ issues.dependency.cancel = Отказ issues.dependency.add_error_dep_exists = Зависимостта вече съществува. issues.dependency.add_error_dep_not_exist = Зависимостта не съществува. issues.remove_ref_at = `премахна препратката %s %s` -issues.ref_pull_from = `спомена тази заявка за сливане %[4]s %[2]s` +issues.ref_pull_from = `спомена тази заявка за сливане %[3]s %[1]s` issues.dependency.pr_no_dependencies = Няма зададени зависимости. issues.dependency.remove_info = Премахване на тази зависимост issues.dependency.removed_dependency = `премахна зависимостта %s` @@ -1079,11 +1230,11 @@ issues.dependency.title = Зависимости issues.dependency.issue_no_dependencies = Няма зададени зависимости. issues.dependency.pr_close_blocked = Трябва да затворите всички задачи, блокиращи тази заявка за сливане, преди да можете да я слеете. issues.dependency.pr_close_blocks = Тази заявка за сливане блокира затварянето на следните задачи -issues.ref_issue_from = `спомена тази задача %[4]s %[2]s` -issues.commit_ref_at = `спомена тази задача в подаване %[2]s` +issues.ref_issue_from = `спомена тази задача %[3]s %[1]s` +issues.commit_ref_at = `спомена тази задача в подаване %s` issues.add_ref_at = `добави препратка %s %s` pulls.merged_info_text = Клонът %s вече може да бъде изтрит. -pulls.commit_ref_at = `спомена тази заявка за сливане в подаване %[2]s` +pulls.commit_ref_at = `спомена тази заявка за сливане в подаване %s` issues.change_ref_at = `промени препратката от %s на %s %s` diff.review.reject = Поискване на промени diff.bin_not_shown = Двоичният файл не е показан. @@ -1139,7 +1290,7 @@ issues.review.review = Рецензия issues.review.comment = рецензира %s branch.deleted_by = Изтрит от %s branch.restore = Възстановяване на клона „%s“ -archive.title_date = Това хранилище е архивирано на %s. Можете да преглеждате файлове и да го клонирате, но не можете да изтласквате или отваряте задачи или заявки за сливане. +archive.title_date = Това хранилище е архивирано на %s. Можете да преглеждате файлове и да го клонирате, но не можете да правите промени в състоянието му, като изтласкване и създаване на нови задачи, заявки за сливане или коментари. release.download_count_one = %s изтегляне release.download_count_few = %s изтегляния branch.restore_success = Клонът „%s“ е възстановен. @@ -1148,15 +1299,15 @@ branch.create_new_branch = Създаване на клон от клон: pulls.status_checks_show_all = Показване на всички проверки size_format = %[1]s: %[2]s; %[3]s: %[4]s pulls.filter_changes_by_commit = Филтриране по подаване -issues.ref_closing_from = `спомена тази задача в заявка за сливане %[4]s, която ще я затвори, %[2]s` +issues.ref_closing_from = `спомена тази задача в заявка за сливане %[3]s, която ще я затвори, %[1]s` issues.ref_from = `от %[1]s` -issues.ref_reopening_from = `спомена тази задача в заявка за сливане %[4]s, която ще я отвори наново , %[2]s` +issues.ref_reopening_from = `спомена тази задача в заявка за сливане %[3]s, която ще я отвори наново , %[1]s` issues.draft_title = Чернова pulls.reopen_to_merge = Моля, отворете наново тази заявка за сливане, за да извършите сливане. pulls.cant_reopen_deleted_branch = Тази заявка за сливане не може да бъде отворена наново, защото клонът е изтрит. pulls.status_checks_hide_all = Скриване на всички проверки pulls.status_checks_failure = Някои проверки са неуспешни -issues.review.add_review_request = поиска рецензия от %s %s +issues.review.add_review_request = поиска рецензия от %[1]s %[2]s wiki.no_search_results = Няма резултати wiki.search = Търсене в уикито issues.author.tooltip.pr = Този потребител е авторът на тази заявка за сливане. @@ -1290,6 +1441,140 @@ issues.reaction.alt_few = %[1]s реагира с %[2]s. issues.reaction.alt_many = %[1]s и още %[2]d реагираха с %[3]s. issues.reaction.alt_add = Добавяне на реакция %[1]s към коментара. issues.reaction.alt_remove = Премахване на реакция %[1]s от коментара. +already_forked = Вече сте разклонили %s +generated_from = генерирано от +clear_ref = `Изчистване на текущата препратка` +file_follow = Последване на символната връзка +commitstatus.failure = Неуспех +issues.filter_label_exclude = `Използвайте alt + click/enter, за да изключите етикети` +migrate.migrating_failed = Мигрирането от %s е неуспешно. +migrate.migrating_issues = Мигриране на задачи +mirror_from = огледално на +fork_from_self = Не можете да разклоните хранилище, което притежавате. +commit_graph.hide_pr_refs = Скриване на заявките за сливане +generated = Генерирано +broken_message = Git данните, лежащи в основата на това хранилище, не могат да бъдат прочетени. Свържете се с администратора на тази инстанция или изтрийте това хранилище. +editor.file_is_a_symlink = `„%s“ е символна връзка. Символните връзки не могат да се редактират в уеб редактора` +commits.browse_further = Разглеждане нататък +commits.older = По-стари +form.reach_limit_of_creation_n = Притежателят вече е достигнал лимита от %d хранилища. +issues.edit.already_changed = Неуспешно запазване на промените в задачата. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени +transfer.accept_desc = Прехвърляне към „%s“ +archive.title = Това хранилище е архивирано. Можете да преглеждате файлове и да го клонирате, но не можете да правите промени в състоянието му, като изтласкване и създаване на нови задачи, заявки за сливане или коментари. +form.reach_limit_of_creation_1 = Притежателят вече е достигнал лимита от %d хранилище. +editor.patching = Прилагане на кръпка: +editor.fail_to_apply_patch = Неуспешно прилагане на кръпка „%s“ +commits.no_commits = Няма общи подавания. „%s“ и „%s“ имат напълно различни истории. +migrate.migrating_pulls = Мигриране на заявки за сливане +migrate.migrating_topics = Мигриране на теми +projects.desc = Управлявайте задачи и заявки за сливане в проектни табла. +issues.choose.invalid_templates = %v невалидни шаблона са намерени +pulls.edit.already_changed = Неуспешно запазване на промените в заявката за сливане. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени +migrate.gitbucket.description = Мигриране на данни от GitBucket инстанции. +migrate.migrating_git = Мигриране на Git данни +commits.newer = По-нови +issues.choose.blank_about = Създаване на задача от стандартен шаблон. +issues.filter_no_results = Няма резултати +issues.filter_no_results_placeholder = Опитайте да коригирате филтрите си за търсене. +archive.nocomment = Коментирането не е възможно, тъй като хранилището е архивирано. +migrate.gitlab.description = Мигриране на данни от gitlab.com или други GitLab инстанции. +transfer.no_permission_to_accept = Нямате разрешение да приемете това прехвърляне. +transfer.no_permission_to_reject = Нямате разрешение да отхвърлите това прехвърляне. +editor.file_changed_while_editing = Съдържанието на файла е променено, откакто сте го отворили. Щракнете тук, за да го видите, или Подайте промените отново, за да ги презапишете. +sync_fork.button = Синхронизиране +migrate.onedev.description = Мигриране на данни от code.onedev.io или други OneDev инстанции. +migrate.codebase.description = Мигриране на данни от codebasehq.com. +migrate.migrating_labels = Мигриране на етикети +migrate.migrating_releases = Мигриране на издания +editor.push_rejected_no_message = Промяната беше отхвърлена от сървъра без съобщение. Моля, проверете Git куките. +issues.choose.open_external_link = Отваряне +comments.edit.already_changed = Неуспешно запазване на промените в коментара. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени +commits.nothing_to_compare = Тези клонове са равни. +transfer.reject_desc = Отказ от прехвърляне към „%s“ +subscribe.pull.guest.tooltip = Влезте, за да се абонирате за тази заявка за сливане. +commit.contained_in_default_branch = Това подаване е част от стандартния клон +normal_view = Нормален изглед +issues.context.menu = Меню за коментара +form.name_reserved = Името на хранилището „%s“ е резервирано. +need_auth = Упълномощаване +subscribe.issue.guest.tooltip = Влезте, за да се абонирате за тази задача. +commitstatus.pending = В очакване +commitstatus.success = Успех +editor.cannot_commit_to_protected_branch = Не може да се подава в защитения клон „%s“. +editor.no_commit_to_branch = Не може да се подава директно в клона, защото: +editor.push_rejected = Промяната беше отхвърлена от сървъра. Моля, проверете Git куките. +cite_this_repo = Цитиране на това хранилище +migrate.gitea.description = Мигриране на данни от gitea.com или други Gitea инстанции. +editor.push_rejected_summary = Пълно съобщение на отхвърлянето: +sync_fork.branch_behind_one = Този клон е %[1]d подаване зад %[2]s +sync_fork.branch_behind_few = Този клон е %[1]d подавания зад %[2]s +form.string_too_long = Даденият низ е по-дълъг от %d знака. +editor.commit_id_not_matching = Файлът е променен, докато сте го редактирали. Подайте в нов клон и след това слейте. +editor.user_no_push_to_branch = Потребителят не може да изтласква в клона +archive.pull.noreview = Това хранилище е архивирано. Не можете да рецензирате заявки за сливане. +migrate.migrating_failed.error = Неуспешно мигриране: %s +migrate.github.description = Мигриране на данни от github.com или GitHub Enterprise сървър. +migrate.forgejo.description = Мигриране на данни от codeberg.org или други Forgejo инстанции. +migrate.gogs.description = Мигриране на данни от notabug.org или други Gogs инстанции. +migrate.migrating_milestones = Мигриране на етапи +migrate.failed = Мигрирането е неуспешно: %v +pulls.nothing_to_compare_and_allow_empty_pr = Тези клонове са равни. Тази заявка за сливане ще бъде празна. +pulls.has_pull_request = `Вече съществува заявка за сливане между тези клонове: %[2]s#%[3]d` +pulls.is_checking = Проверката за конфликти при сливане е в ход. Опитайте отново след няколко минути. +pulls.cannot_merge_work_in_progress = Тази заявка за сливане е отбелязана като в процес на работа. +pulls.blocked_by_approvals = Тази заявка за сливане все още няма достатъчно одобрения. Дадени са %d от %d одобрения. +pulls.blocked_by_rejection = Тази заявка за сливане има поискани промени от официален рецензент. +pulls.waiting_count_1 = %d чакаща рецензия +pulls.status_checks_requested = Задължително +pulls.update_branch_success = Обновяването на клона е успешно +pulls.cannot_auto_merge_helper = Слейте ръчно, за да разрешите конфликтите. +migrate.clone_address_desc = HTTP(S) или Git „clone“ URL на съществуващо хранилище +pulls.add_prefix = Добавете префикс %s +pulls.merge_pull_request = Създаване на подаване със сливане +pulls.waiting_count_n = %d чакащи рецензии +pulls.is_ancestor = Този клон вече е включен в целевия клон. Няма какво да се слива. +pulls.required_status_check_missing = Някои задължителни проверки липсват. +pulls.change_target_branch_at = `промени целевия клон от %s на %s %s` +issues.time_spent_total = Общо изразходвано време +issues.del_time_history = `изтри изразходваното време %s` +pulls.nothing_to_compare_have_tag = Избраните клон/маркер са равни. +pulls.cannot_auto_merge_desc = Тази заявка за сливане не може да бъде слята автоматично поради конфликти. +issues.tracker_auto_close = Таймерът ще бъде спрян автоматично, когато тази задача бъде затворена +issues.force_push_codes = `изтласка принудително %[1]s от %[2]s към %[4]s %[6]s` +pulls.blocked_by_official_review_requests = Тази заявка за сливане е блокирана, защото липсва одобрение от един или повече официални рецензенти. +issues.tracker = Проследяване на времето +issues.add_time_history = `добави изразходвано време %s` +migrate.repo_desc_helper = Оставете празно, за да внесете съществуващото описание +migrate.git.description = Мигриране само на хранилище от всяка Git услуга. +mirror_sync = синхронизирано +migrate_repo = Мигриране на хранилище +migrate_options = Опции за мигрирането +editor.fork_before_edit = Трябва да разклоните това хранилище, за да направите или предложите промени в този файл. +editor.must_have_write_access = Трябва да имате право на запис, за да правите или предлагате промени в този файл. +editor.new_branch_name = Дайте име на новия клон за това подаване +editor.invalid_commit_mail = Невалидна ел. поща за създаване на подаване. +pulls.required_status_check_failed = Някои задължителни проверки не са успешни. +issues.time_spent_from_all_authors = `Общо изразходвано време: %s` +issues.attachment.download = `Щракнете, за да изтеглите „%s“` +issues.attachment.open_tab = `Щракнете, за да видите „%s“ в нов раздел` +pulls.update_branch = Обновяване на клона чрез сливане +migrate_items = Елементи за мигриране +commit.load_referencing_branches_and_tags = Зареждане на клонове и маркери, препращащи към това подаване +pulls.files_conflicted = Тази заявка за сливане има промени, които са в конфликт с целевия клон. +pulls.still_in_progress = Все още е в процес на работа? +pulls.ready_for_review = Готово е за рецензиране? +pulls.is_empty = Промените в този клон вече са в целевия клон. Това ще бъде празно подаване. +issues.start_tracking = Започване на проследяване на времето +migrate_options_mirror_helper = Това хранилище ще бъде огледално +migrate_options_lfs = Мигриране на LFS файлове +editor.upload_file_is_locked = Файлът „%s“ е заключен от %s. +issues.tracking_already_started = `Вече сте започнали проследяване на времето по друга задача!` +pulls.remove_prefix = Премахнете префикса %s +author_search_tooltip = Показва максимум 30 потребители +migrate.migrating_failed_no_addr = Мигрирането е неуспешно. +issues.force_push_compare = Сравняване +pulls.status_checking = Някои проверки са в очакване +pulls.nothing_to_compare = Тези клонове са равни. Не е нужно да създавате заявка за сливане. [modal] confirm = Потвърждаване @@ -1319,6 +1604,11 @@ table_modal.placeholder.content = Съдържание table_modal.placeholder.header = Заглавка buttons.new_table.tooltip = Добавяне на таблица table_modal.header = Добавяне на таблица +link_modal.description = Описание +link_modal.header = Добавяне на връзка +buttons.indent.tooltip = Вмъкване на елементи с едно ниво +buttons.unindent.tooltip = Изваждане на елементи с едно ниво +link_modal.paste_reminder = Подсказка: С URL адрес в клипборда можете да поставите директно в редактора, за да създадете връзка. [org] teams.write_access = Писане @@ -1393,11 +1683,12 @@ members.private_helper = Да е видим teams.no_desc = Този екип няма описание settings.delete_org_desc = Тази организация ще бъде изтрита перманентно. Продължаване? open_dashboard = Отваряне на таблото +settings.change_orgname_prompt = Бележка: Промяната на името на организацията ще промени и URL адреса на вашата организация и ще освободи старото име. [install] admin_password = Парола user = Потребителско име -admin_email = Адрес на ел. поща +admin_email = Адрес за ел. поща path = Път password = Парола host = Хост @@ -1427,13 +1718,17 @@ admin_title = Настройки на администраторския ака err_empty_admin_password = Администраторската парола не може да бъде празна. docker_helper = Ако стартирате Forgejo в Docker, моля, прочетете документацията преди да промените настройки. sqlite_helper = Път на файла за SQLite3 базата данни.
Въведете абсолютен път, ако стартирате Forgejo като service. -err_empty_admin_email = Администраторският адрес на ел. поща не може да бъде празен. +err_empty_admin_email = Администраторският адрес за ел. поща не може да бъде празен. password_algorithm = Алгоритъм за хеш. на паролите -default_keep_email_private = Скриване на адресите на ел. поща по подразбиране +default_keep_email_private = Скриване на адресите за ел. поща по подразбиране invalid_password_algorithm = Невалиден алгоритъм за хеш. на паролите err_admin_name_is_reserved = Потребителското име на администратора е невалидно, потребителското име е резервирано err_admin_name_pattern_not_allowed = Потребителското име на администратора е невалидно, потребителското име съответства с резервиран шаблон err_admin_name_is_invalid = Потребителското име на администратора е невалидно +db_schema_helper = Оставете празно за схемата по подразбиране на базата данни („public“). +reinstall_error = Опитвате се да инсталирате върху съществуваща Forgejo база данни +reinstall_confirm_message = Преинсталирането със съществуваща Forgejo база данни може да причини множество проблеми. В повечето случаи трябва да използвате съществуващия си „app.ini“, за да стартирате Forgejo. Ако знаете какво правите, потвърдете следното: +app_slogan = Слоган на инстанцията [filter] string.asc = А - Я @@ -1462,8 +1757,8 @@ link_not_working_do_paste = Ако връзката не работи, опит activate_account = Моля, активирайте своя акаунт admin.new_user.subject = Нов потребител %s току-що се регистрира activate_account.text_1 = Здравейте, %[1]s, благодарим ви за регистрацията в %[2]s! -activate_email.text = Моля, щракнете върху следната връзка, за да потвърдите своя адрес на ел. поща в рамките на %s: -activate_email = Потвърдете своя адрес на ел. поща +activate_email.text = Моля, щракнете върху следната връзка, за да потвърдите своя адрес за ел. поща в рамките на %s: +activate_email = Потвърдете своя адрес за ел. поща activate_account.text_2 = Моля, щракнете върху следната връзка, за да активирате своя акаунт в рамките на %s: issue_assigned.issue = @%[1]s ви възложи задача %[2]s в хранилище %[3]s. issue.action.push_n = @%[1]s изтласка %[3]d подавания към %[2]s @@ -1473,6 +1768,28 @@ issue.action.merge = @%[1]s сля #%[2]d в %[3]s. issue_assigned.pull = @%[1]s ви възложи заявката за сливане %[2]s в хранилище %[3]s. issue.action.ready_for_review = @%[1]s отбеляза тази заявка за сливане като готова за рецензиране. repo.transfer.subject_to = %s иска да прехвърли хранилище "%s" към %s +password_change.subject = Вашата парола е променена +admin.new_user.text = Моля, щракнете тук, за да управлявате този потребител от администраторския панел. +password_change.text_1 = Паролата за вашия акаунт току-що беше променена. +reset_password = Възстановете своя акаунт +account_security_caution.text_1 = Ако това сте били вие, можете спокойно да игнорирате това ел. писмо. +issue.action.force_push = %[1]s изтласка принудително %[2]s от %[3]s към %[4]s. +team_invite.text_3 = Бележка: Тази покана е предназначена за %[1]s. Ако не сте очаквали тази покана, можете да игнорирате това ел. писмо. +view_it_on = Вижте го на %s +register_notify.text_1 = това е ел. писмо за потвърждение на вашата регистрация в %s! +register_notify.text_2 = Можете да влезете в акаунта си с потребителско име: %s +register_notify.text_3 = Ако някой друг е създал този акаунт за вас, първо ще трябва да зададете парола. +repo.collaborator.added.subject = %s ви добави към %s като сътрудник +primary_mail_change.text_1 = Основният адрес за ел. поща на вашия акаунт току-що беше променен на %[1]s. Това означава, че този адрес за ел. поща повече няма да получава известия по ел. поща за вашия акаунт. +team_invite.text_2 = Моля, щракнете върху следната връзка, за да се присъедините към екипа: +repo.transfer.body = За да го приемете или отхвърлите, посетете %s или просто го игнорирайте. +repo.collaborator.added.text = Бяхте добавени като сътрудник в хранилище: +team_invite.subject = %[1]s ви покани да се присъедините към организацията %[2]s +team_invite.text_1 = %[1]s ви покани да се присъедините към екип %[2]s в организация %[3]s. +reply = или отговорете директно на това ел. писмо +reset_password.text = Ако това сте вие, моля, щракнете върху следната връзка, за да възстановите акаунта си в рамките на %s: +primary_mail_change.subject = Основният ви адрес за ел. поща е променен +account_security_caution.text_2 = Ако това не сте били вие, акаунтът ви е компрометиран. Моля, свържете се с администраторите на този сайт. [user] joined_on = Присъединени на %s @@ -1493,7 +1810,7 @@ follow = Последване followers_few = %d последователи block_user = Блокиране на потребителя change_avatar = Променете профилната си снимка… -email_visibility.limited = Вашият адрес на ел. поща е видим за всички удостоверени потребители +email_visibility.limited = Вашият адрес за ел. поща е видим за всички удостоверени потребители disabled_public_activity = Този потребител е изключил публичната видимост на дейността. email_visibility.private = Вашият адрес на ел. поща е видим само за вас и администраторите show_on_map = Показване на това място на картата @@ -1507,6 +1824,15 @@ public_activity.visibility_hint.self_public = Вашата дейност е в form.name_pattern_not_allowed = Шаблонът "%s" не е разрешен в потребителско име. form.name_reserved = Потребителското име "%s" е резервирано. public_activity.visibility_hint.self_private_profile = Вашата дейност е видима само за вас и администраторите на инстанцията, тъй като вашият профил е частен. Конфигуриране. +block_user.detail = Моля, имайте предвид, че блокирането на потребител има и други ефекти, като например: +block_user.detail_2 = Този потребител няма да може да взаимодейства с хранилищата, които притежавате, или със задачите и коментарите, които сте създали. +block_user.detail_3 = Няма да можете да се добавяте един друг като сътрудници на хранилище. +public_activity.visibility_hint.self_private = Вашата дейност е видима само за вас и администраторите на инстанцията. Конфигуриране. +form.name_chars_not_allowed = Потребителското име „%s“ съдържа невалидни знаци. +public_activity.visibility_hint.admin_private = Тази дейност е видима за вас, защото сте администратор, но потребителят иска тя да остане частна. +public_activity.visibility_hint.admin_public = Тази дейност е видима за всички, но като администратор можете да виждате и взаимодействия в частни пространства. +follow_blocked_user = Не можете да последвате този потребител, защото сте го блокирали или той ви е блокирал. +block_user.detail_1 = Ще спрете да се следвате един друг и няма да можете да се последвате отново. [home] filter = Други филтри @@ -1530,6 +1856,7 @@ view_home = Преглед на %s collaborative_repos = Съвместни хранилища switch_dashboard_context = Превключване на контекста на таблото show_only_public = Показване само на публични +filter_by_team_repositories = Филтриране по хранилища на екипа [admin] packages.version = Версия @@ -1587,7 +1914,7 @@ config.server_config = Сървърна конфигурация packages.size = Размер settings = Админ. настройки users = Потребителски акаунти -emails.duplicate_active = Този адрес на ел. поща вече е активен за друг потребител. +emails.duplicate_active = Този адрес за ел. поща вече е активен за друг потребител. config.app_ver = Forgejo версия config.custom_conf = Път на конфигурационния файл config.git_version = Git версия @@ -1606,16 +1933,20 @@ users.details = Потребителски данни packages.total_size = Общ размер: %s dashboard.new_version_hint = Forgejo %s вече е наличен, вие изпълнявате %s. Проверете блога за повече подробности. total = Общо: %d +config.db_type = Тип +monitor.queue.type = Тип +notices.type = Тип [error] not_found = Целта не може да бъде намерена. report_message = Ако смятате, че това е грешка на Forgejo, моля, потърсете в задачите на Codeberg или отворете нова задача, ако е необходимо. network_error = Мрежова грешка occurred = Възникна грешка +server_internal = Вътрешна грешка на сървъра [form] UserName = Потребителско име -Email = Адрес на ел. поща +Email = Адрес за ел. поща Password = Парола RepoName = Име на хранилището username_been_taken = Потребителското име вече е заето. @@ -1633,8 +1964,8 @@ url_error = `„%s“ не е валиден URL.` Content = Съдържание team_not_exist = Екипът не съществува. TeamName = Име на екипа -email_error = ` не е валиден адрес на ел. поща.` -email_invalid = Адресът на ел. поща е невалиден. +email_error = ` не е валиден адрес за ел. поща.` +email_invalid = Адресът за ел. поща е невалиден. SSHTitle = Име на SSH ключ repo_name_been_taken = Името на хранилището вече е използвано. team_name_been_taken = Името на екипа вече е заето. @@ -1647,6 +1978,44 @@ Pronouns = Местоимения Biography = Биография Website = Уебсайт Location = Местоположение +cannot_add_org_to_team = Организация не може да бъде добавена като член на екип. +auth_failed = Неуспешно удостоверяване: %v +team_no_units_error = Разрешете достъп до поне една секция на хранилището. +password_uppercase_one = Поне един голям знак +CommitSummary = Обобщение на подаването +username_error = ` може да съдържа само буквено-цифрови знаци („0-9“, „a-z“, „A-Z“), тире („-“), долна черта („_“) и точка („.“). Не може да започва или завършва с не-буквено-цифрови знаци, като също така са забранени и последователни не-буквено-цифрови знаци.` +username_error_no_dots = ` може да съдържа само буквено-цифрови знаци („0-9“, „a-z“, „A-Z“), тире („-“) и долна черта („_“). Не може да започва или завършва с не-буквено-цифрови знаци, като също така са забранени и последователни не-буквено-цифрови знаци.` +duplicate_invite_to_team = Потребителят вече е поканен като член на екипа. +must_use_public_key = Ключът, който предоставихте, е частен ключ. Моля, не качвайте частния си ключ никъде. Вместо това използвайте публичния си ключ. +org_still_own_packages = Тази организация все още притежава един или повече пакети, първо ги изтрийте. +admin_cannot_delete_self = Не можете да изтриете себе си, когато сте администратор. Моля, първо премахнете администраторските си привилегии. +To = Име на клон +CommitMessage = Съобщение на подаването +include_error = ` трябва да съдържа подниз „%s“.` +alpha_dash_error = ` трябва да съдържа само буквено-цифрови знаци, тире („-“) и долна черта („_“).` +alpha_dash_dot_error = ` трябва да съдържа само буквено-цифрови знаци, тире („-“), долна черта („_“) и точка („.“).` +size_error = ` трябва да е с размер %s.` +min_size_error = ` трябва да съдържа поне %s знака.` +max_size_error = ` трябва да съдържа най-много %s знака.` +invalid_group_team_map_error = ` съпоставянето е невалидно: %s` +password_complexity = Паролата не отговаря на изискванията за сложност: +password_lowercase_one = Поне един малък знак +password_digit_one = Поне една цифра +password_special_one = Поне един специален знак (препинателни знаци, скоби, кавички и др.) +enterred_invalid_repo_name = Името на хранилището, което въведохте, е неправилно. +enterred_invalid_org_name = Името на организацията, което въведохте, е неправилно. +enterred_invalid_password = Паролата, която въведохте, е неправилна. +organization_leave_success = Успешно напуснахте организацията %s. +still_has_org = Вашият акаунт е член на една или повече организации, първо ги напуснете. +org_still_own_repo = Тази организация все още притежава едно или повече хранилища, първо ги изтрийте или прехвърлете. +target_branch_not_exist = Целевият клон не съществува. +glob_pattern_error = ` glob шаблонът е невалиден: %s.` +openid_been_used = OpenID адресът „%s“ вече е използван. +unknown_error = Неизвестна грешка: +TreeName = Път до файла +AdminEmail = Администраторски адрес за ел. поща +email_domain_is_not_allowed = Домейнът на адреса за ел. поща на потребителя %s е в конфликт с EMAIL_DOMAIN_ALLOWLIST или EMAIL_DOMAIN_BLOCKLIST. Уверете се, че сте въвели правилно адреса за ел. поща. +email_been_used = Адресът за ел. поща вече се използва. [action] close_issue = `затвори задача %[3]s#%[2]s` @@ -1709,11 +2078,38 @@ sign_up_button = Регистрирайте се. back_to_sign_in = Назад към Вход sign_in_openid = Продължаване с OpenID send_reset_mail = Изпращане на ел. писмо за възстановяване +authorize_application = Упълномощаване на приложение +password_pwned_err = Неуспешно завършване на заявката към HaveIBeenPwned +last_admin = Не можете да премахнете последния администратор. Трябва да има поне един администратор. +allow_password_change = Изискване потребителят да смени паролата си (препоръчително) +authorize_title = Упълномощавате ли „%s“ да има достъп до вашия акаунт? +reset_password_mail_sent_prompt = Изпратено е ел. писмо за потвърждение до %s. За да завършите процеса по възстановяване на акаунта, моля, проверете входящата си поща и последвайте предоставената връзка в рамките на следващите %s. +reset_password_wrong_user = Вие сте влезли като %s, но връзката за възстановяване на акаунта е предназначена за %s +authorize_redirect_notice = Ще бъдете пренасочени към %s, ако упълномощите това приложение. +authorize_application_description = Ако предоставите достъп, то ще може да осъществява достъп и да записва цялата информация за вашия акаунт, включително частни хранилища и организации. +twofa_scratch_used = Използвали сте своя резервен код. Пренасочени сте към страницата с настройки за двуфакторно удостоверяване, за да можете да премахнете регистрацията на устройството си или да генерирате нов резервен код. +reset_password_helper = Възстановяване на акаунт +invalid_password = Вашата парола не съвпада с паролата, използвана за създаване на акаунта. +invalid_code = Вашият код за потвърждение е невалиден или е изтекъл. +invalid_code_forgot_password = Вашият код за потвърждение е невалиден или е изтекъл. Щракнете тук, за да започнете нова сесия. +scratch_code = Резервен код +use_scratch_code = Използвайте резервен код +use_onetime_code = Използвайте еднократен код +twofa_scratch_token_incorrect = Вашият резервен код е неправилен. +authorize_application_created_by = Това приложение е създадено от %s. +authorization_failed = Неуспешно упълномощаване +resent_limit_prompt = Вече сте поискали ел. писмо за активация наскоро. Моля, изчакайте 3 минути и опитайте отново. +has_unconfirmed_mail = Здравейте, %s, имате непотвърден адрес за ел. поща (%s). Ако не сте получили ел. писмо за потвърждение или трябва да изпратите ново, моля, щракнете върху бутона по-долу. +change_unconfirmed_email_error = Неуспешна промяна на адреса за ел. поща: %v +resend_mail = Щракнете тук, за повторно изпращане на ел. писмо за активация +change_unconfirmed_email_summary = Промяна на адреса, на който се изпраща ел. писмо за активация. +change_unconfirmed_email = Ако сте въвели грешен адрес за ел. поща по време на регистрацията, можете да го промените по-долу и потвърждение ще бъде изпратено на новия адрес. [aria] footer.software = Относно този софтуер footer.links = Връзки footer = Долен колонтитул +navbar = Навигационна лента [startpage] install = Лесен за инсталиране @@ -1780,6 +2176,7 @@ runs.no_workflows.help_no_write_access = За да научите повече variables.management = Управление на променливи variables.not_found = Променливата не е открита. variables.id_not_exist = Променлива с идентификатор %d не съществува. +runners.owner_type = Тип [heatmap] less = По-малко @@ -1809,9 +2206,10 @@ invalid_input_type = Не можете да качвате файлове от component_loading_failed = Неуспешно зареждане на %s contributors.what = приноси recent_commits.what = скорошни подавания -component_loading = Зареждане на %s... +component_loading = Зареждане на %s… component_loading_info = Това може да отнеме известно време… code_frequency.what = честота на промените +component_failed_to_load = Възникна неочаквана грешка. [projects] type-1.display_name = Индивидуален проект @@ -1820,20 +2218,29 @@ deleted.display_name = Изтрит проект [search] no_results = Няма намерени съответстващи резултати. -team_kind = Търсене на екипи... -repo_kind = Търсене на хранилища... -org_kind = Търсене на организации... -user_kind = Търсене на потребители... -code_kind = Търсене на код... -commit_kind = Търсене на подавания... -project_kind = Търсене на проекти... -package_kind = Търсене на пакети... -search = Търсене... -branch_kind = Търсене на клонове... -pull_kind = Търсене на заявки за сливане... -issue_kind = Търсене на задачи... +team_kind = Търсене на екипи… +repo_kind = Търсене на хранилища… +org_kind = Търсене на организации… +user_kind = Търсене на потребители… +code_kind = Търсене на код… +commit_kind = Търсене на подавания… +project_kind = Търсене на проекти… +package_kind = Търсене на пакети… +search = Търсене… +branch_kind = Търсене на клонове… +pull_kind = Търсене на заявки за сливане… +issue_kind = Търсене на задачи… fuzzy = Приблизително exact = Прецизно +regexp = Регекс +regexp_tooltip = Третиране на термина за търсене като регулярен израз +fuzzy_tooltip = Включване на резултати, които също съвпадат приблизително с термина за търсене +exact_tooltip = Включване само на резултати, които съвпадат точно с термина за търсене +code_search_unavailable = Търсенето на код в момента не е достъпно. Моля, свържете се с администратора на сайта. +keyword_search_unavailable = Търсенето по ключова дума в момента не е достъпно. Моля, свържете се с администратора на сайта. +union_tooltip = Включване на резултати, които съвпадат с някоя от ключовите думи, разделени с интервал +union = Обединение +type_tooltip = Тип търсене [markup] filepreview.lines = Редове от %[1]d до %[2]d в %[3]s diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 04c4da2f17..830065fb64 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -118,7 +118,7 @@ go_back=Zpět never=Nikdy unknown=Neznámý -rss_feed=RSS kanál +rss_feed=Kanál RSS pin=Připnout unpin=Odepnout @@ -493,11 +493,11 @@ use_onetime_code = Použít jednorázový kód view_it_on=Zobrazit na %s reply=nebo přímo odpovědět na tento e-mail link_not_working_do_paste=Odkaz nefunguje? Zkuste jej zkopírovat a vložit do adresního řádku svého prohlížeče. -hi_user_x=Ahoj %s, +hi_user_x=Dobrý den, uživateli %s, activate_account=Prosíme, aktivujte si váš účet activate_account.title=%s, prosím aktivujte si váš účet -activate_account.text_1=Ahoj %[1]s, děkujeme za registraci na %[2]s! +activate_account.text_1=Dobrý den, uživateli %[1]s, děkujeme za registraci ve službě %[2]s! activate_account.text_2=Pro aktivaci vašeho účtu klikněte %s na následující odkaz : activate_email=Ověřte vaši e-mailovou adresu @@ -932,7 +932,7 @@ generate_new_token=Vygenerovat nový token tokens_desc=Tyto tokeny umožňují přístup k vašemu účtu pomocí Forgejo API. token_name=Název tokenu generate_token=Vygenerovat token -generate_token_success=Nový token byl vygenerován. Zkopírujte jej nyní, jelikož již nebude znovu zobrazen. +generate_token_success=Nový token byl vygenerován. Zkopírujte si jej nyní, jelikož již nebude znovu zobrazen. generate_token_name_duplicate=%s byl již použit jako název aplikace. Použijte prosím nový. delete_token=Smazat access_token_deletion=Odstranit přístupový token @@ -1633,13 +1633,13 @@ issues.opened_by_fake=otevřeno %[1]s uživatelem %[2]s issues.closed_by_fake=od %[2]s byl uzavřen %[1]s issues.previous=Předchozí issues.next=Další -issues.open_title=Otevřeno -issues.closed_title=Uzavřeno +issues.open_title=Otevřené +issues.closed_title=Uzavřené issues.draft_title=Koncept issues.num_comments_1=%d komentář issues.num_comments=%d komentářů issues.commented_at=`okomentoval/a %s` -issues.delete_comment_confirm=Jste si jist, že chcete smazat tento komentář? +issues.delete_comment_confirm=Opravdu chcete smazat tento komentář? issues.context.copy_link=Kopírovat odkaz issues.context.quote_reply=Citovat odpověď issues.context.reference_issue=Odkázat v novém problému @@ -1653,13 +1653,13 @@ issues.close_comment_issue=Zavřít s komentářem issues.reopen_issue=Znovu otevřít issues.reopen_comment_issue=Znovu otevřít s komentářem issues.create_comment=Komentovat -issues.closed_at=`uzavřel/a tento problém %[2]s` -issues.reopened_at=`znovu otevřel/a tento problém %[2]s` -issues.commit_ref_at=`odkázal/a na tento problém z revize %[2]s` -issues.ref_issue_from=`odkázal/a na tento problém %[4]s %[2]s` -issues.ref_pull_from=`odkázal/a na tuto žádost o sloučení %[4]s %[2]s` -issues.ref_closing_from=`odkazoval/a na tento problém ze žádosti o sloučení %[4]s, která jej uzavře, %[2]s` -issues.ref_reopening_from=`odkazoval/a na tento problém ze žádosti o sloučení %[4]s, která jej znovu otevře, %[2]s` +issues.closed_at=`uzavřel/a tento problém %s` +issues.reopened_at=`znovu otevřel/a tento problém %s` +issues.commit_ref_at=`odkázal/a na tento problém z revize %s` +issues.ref_issue_from=`odkázal/a na tento problém %[3]s %[1]s` +issues.ref_pull_from=`odkázal/a na tuto žádost o sloučení %[3]s %[1]s` +issues.ref_closing_from=`odkázal/a na tento problém ze žádosti o sloučení %[3]s, která jej uzavře, %[1]s` +issues.ref_reopening_from=`odkázal/a na tento problém ze žádosti o sloučení %[3]s, která jej znovu otevře, %[1]s` issues.ref_closed_from=`uzavřel/a tento problém %[4]s %[2]s` issues.ref_reopened_from=`znovu otevřel/a tento problém %[4]s %[2]s` issues.ref_from=`z %[1]s` @@ -1966,8 +1966,8 @@ pulls.update_branch_success=Aktualizace větve byla úspěšná pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi pulls.close=Zavřít žádost o sloučení -pulls.closed_at=`uzavřel/a tuto žádost o sloučení %[2]s` -pulls.reopened_at=`znovu otevřel/a tuto žádost o sloučení %[2]s` +pulls.closed_at=`uzavřel/a tuto žádost o sloučení %s` +pulls.reopened_at=`znovu otevřel/a tuto žádost o sloučení %s` pulls.cmd_instruction_hint=Zobrazit instrukce příkazové řádky pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny. pulls.cmd_instruction_merge_title=Sloučit @@ -2758,7 +2758,7 @@ settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning = Tuto ak settings.new_owner_blocked_doer = Nový majitel vás zablokoval. settings.mirror_settings.pushed_repository = Odeslaný repozitář settings.add_collaborator_blocked_our = Nepodařilo se přidat spolupracovníka, jelikož byl zablokován majitelem repozitáře. -pulls.commit_ref_at = `se odkázal/a na tuto žádost o sloučení z revize %[2]s` +pulls.commit_ref_at = `odkázal/a na tuto žádost o sloučení z revize %s` settings.wiki_rename_branch_main = Normalizovat název větve wiki settings.wiki_rename_branch_main_desc = Přejmenovat větev interně používanou pro wiki na „%s“. Tato změna je trvalá a nelze ji vrátit. pulls.fast_forward_only_merge_pull_request = Pouze zrychlené @@ -2915,6 +2915,14 @@ comment.blocked_by_user = Komentování není možné, protože jste byli zablok sync_fork.branch_behind_few = Tato větev je %[1]d revizí pozadu za %[2]s sync_fork.button = Synchronizovat sync_fork.branch_behind_one = Tato větev je %[1]d revizi pozadu za %[2]s +settings.event_action_failure = Selhání +settings.event_action_failure_desc = Běh akce selhal. +settings.event_action_recover = Obnovit +settings.event_action_success = Úspěch +settings.event_action_success_desc = Běh akce byl úspěšný. +settings.event_header_action = Události běhu akce +settings.event_action_recover_desc = Běh akce byl úspěšný, předchozí běh akce ve stejném workflow selhal. +issues.filter_type.all_pull_requests = Všechny žádosti o sloučení [graphs] component_loading_info = Tohle může chvíli trvat… @@ -3568,7 +3576,7 @@ notices.type=Typ notices.type_1=Repozitář notices.type_2=Úloha notices.desc=Popis -notices.op=Akce +notices.op=Op. notices.delete_success=Systémové upozornění bylo smazáno. dashboard.sync_repo_branches = Synchronizovat vynechané větve z dat Gitu do databáze monitor.queue.activeworkers = Aktivní workery diff --git a/options/locale/locale_da.ini b/options/locale/locale_da.ini index dd5a404b5e..c82779ab60 100644 --- a/options/locale/locale_da.ini +++ b/options/locale/locale_da.ini @@ -10,7 +10,7 @@ logo = Logo sign_in = Login sign_in_with_provider = Login med %s sign_in_or = eller -sign_out = Logud +sign_out = Log ud sign_up = Register return_to_forgejo = Vend tilbage til Forgejo new_repo.title = Ny repository @@ -1432,7 +1432,7 @@ issues.new.no_items = Ingen elementer issues.new.milestone = Milepæl issues.new.no_milestone = Ingen milepæl issues.filter_assignees = Filter tildelt -issues.filter_milestones = Filter Milepæl +issues.filter_milestones = Filter milepæl issues.filter_projects = Filter projekt issues.filter_labels = Filter etiket issues.filter_reviewers = Filter anmelder @@ -1520,15 +1520,15 @@ issues.add_labels = tilføjede %s etiketterne %s issues.add_remove_labels = tilføjede %s og fjernede %s etiketter %s issues.add_milestone_at = `føjede dette til %s milepælen %s` issues.add_project_at = `føjede dette til %s- projektet %s` -issues.ref_reopening_from = `henviste til dette problem fra en pull-anmodning %[4]s, der vil genåbne den, %[2]s` +issues.ref_reopening_from = `henviste til dette problem fra en pull-anmodning %[3]s, der vil genåbne den, %[1]s` issues.ref_closed_from = `lukkede dette problem %[4]s %[2 ]s` issues.ref_reopened_from = `genåbnede dette problem %[4]s %[2 ]s` issues.ref_from = `fra %[1]s` issues.author = Forfatter -issues.commit_ref_at = `henviste til dette problem fra en commit %[2]s` -issues.ref_issue_from = `henviste til dette problem %[4]s %[2 ]s` -issues.ref_pull_from = `henviste til denne pull-anmodning %[4]s %[ 2]s` -issues.ref_closing_from = `henviste til dette problem fra en pull-anmodning %[4]s, der vil lukke det, %[2]s` +issues.commit_ref_at = `henviste til dette problem fra en commit %s` +issues.ref_issue_from = `henviste til dette problem %[3]s %[2 ]s` +issues.ref_pull_from = `henviste til denne pull-anmodning %[3]s %[1]s` +issues.ref_closing_from = `henviste til dette problem fra en pull-anmodning %[3]s, der vil lukke det, %[1]s` issues.author.tooltip.issue = Denne bruger er forfatteren til dette problem. issues.author.tooltip.pr = Denne bruger er forfatteren af denne pull-anmodning. issues.role.owner = Ejer @@ -1564,8 +1564,8 @@ issues.reaction.alt_add = Tilføj %[1]s reaktion til kommentar. issues.context.menu = Kommentar menu issues.reopen_comment_issue = Genåbner med kommentar issues.create_comment = Kommentar -issues.closed_at = `lukkede dette problem %[2]s` -issues.reopened_at = `genåbnede dette problem %[2]s` +issues.closed_at = `lukkede dette problem %s` +issues.reopened_at = `genåbnede dette problem %s` issues.remove_label = fjernede %s etiketten %s issues.remove_labels = fjernede %s etiketterne %s issues.change_project_at = `modificerede projektet fra %s til %s %s` @@ -1911,10 +1911,10 @@ pulls.editable_explanation = Denne pull-anmodning tillader redigeringer fra vedl pulls.auto_merge_button_when_succeed = (Når kontroller lykkes) pulls.status_checks_requested = Påkrævet pulls.close = Luk pull anmodning -pulls.commit_ref_at = `henviste til denne pull-anmodning fra en commit %[2]s` +pulls.commit_ref_at = `henviste til denne pull-anmodning fra en commit %s` pulls.cmd_instruction_hint = Se instruktionerne på kommandolinjen -pulls.reopened_at = `genåbnede denne pull-anmodning %[2]s` -pulls.closed_at = `lukkede denne pull-anmodning %[2]s` +pulls.reopened_at = `genåbnede denne pull-anmodning %s` +pulls.closed_at = `lukkede denne pull-anmodning %s` pulls.cmd_instruction_checkout_desc = Fra dit projektdepot, tjek en ny gren og test ændringerne. pulls.editable = Redigerbar pulls.made_using_agit = AGit @@ -2018,7 +2018,7 @@ settings.lfs_pointers.inRepo = i depot settings.lfs_pointers.exists = Eksisterer i lager settings.lfs_pointers.accessible = Tilgængeligt for bruger signing.wont_sign.not_signed_in = Du er ikke logget ind. -wiki.welcome = Velkommen til Wikien +wiki.welcome = Velkommen til wikien milestones.modify = Opdater milepæl milestones.edit_success = Milepæl "%s" er blevet opdateret. milestones.filter_sort.least_issues = Mindst problemer @@ -2262,7 +2262,7 @@ settings.wiki_delete_notices_1 = - Dette vil permanent slette og deaktivere depo settings.wiki_branch_rename_failure = Det lykkedes ikke at normalisere depotwikiens filialnavn. settings.add_collaborator_duplicate = Samarbejdspartneren er allerede føjet til dette depot. settings.add_collaborator_owner = Kan ikke tilføje en ejer som samarbejdspartner. -settings.collaborator_deletion = Fjern Samarbejdspartner +settings.collaborator_deletion = Fjern samarbejdspartner settings.collaborator_deletion_desc = Fjernelse af en samarbejdspartner vil tilbagekalde deres adgang til dette depot. Vil du fortsætte? settings.add_team_duplicate = Teamet har allerede depotet settings.add_collaborator_blocked_our = Samarbejdspartneren kan ikke tilføjes, fordi depots ejer har blokeret dem. @@ -2728,6 +2728,14 @@ comment.blocked_by_user = Det er ikke muligt at kommentere, fordi du er blokeret sync_fork.branch_behind_few = Denne gren er %[1]d commits bag %[2]s sync_fork.button = Sync sync_fork.branch_behind_one = Denne gren er %[1]d commit bag %[2]s +settings.event_header_action = Handling Run-begivenheder +settings.event_action_failure = Mislykket +settings.event_action_success_desc = Handlingen blev udført. +settings.event_action_success = Success +settings.event_action_recover_desc = Handlingskørsel lykkedes efter at den sidste handlingskørsel i samme arbejdsgang mislykkedes. +settings.event_action_failure_desc = Handlingskørsel sluttede som en fejl. +settings.event_action_recover = Gendan +issues.filter_type.all_pull_requests = Alle pull-anmodninger [notification] watching = Overvåger @@ -3529,8 +3537,8 @@ composer.install = For at installere pakken ved hjælp af Composer skal du køre container.multi_arch = OS / Arch rubygems.required.ruby = Kræver Ruby version swift.install = Tilføj pakken i din Package.swift-fil: -settings.link.select = Vælg Depot -settings.link.button = Opdater Depot Link +settings.link.select = Vælg depot +settings.link.button = Opdater depot link settings.link.error = Kunne ikke opdatere depotlinket. owner.settings.cargo.initialize.success = Cargo-indekset blev oprettet. owner.settings.cargo.rebuild.description = Genopbygning kan være nyttig, hvis indekset ikke er synkroniseret med de lagrede Cargo-pakker. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index ed27d53014..f8bfc9258a 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -745,7 +745,7 @@ social=Soziale Konten applications=Anwendungen orgs=Organisationen repos=Repositorys -delete=Konto löschen +delete=Account löschen twofa=Zwei-Faktor-Authentifizierung (TOTP) account_link=Verknüpfte Benutzerkonten organization=Organisationen @@ -1511,7 +1511,7 @@ projects.card_type.images_and_text=Bilder und Text projects.card_type.text_only=Nur Text issues.desc=Verwalte Bug-Reports, Aufgaben und Meilensteine. -issues.filter_assignees=Filter +issues.filter_assignees=Verantwortliche filtern issues.filter_milestones=Meilenstein filtern issues.filter_projects=Projekt filtern issues.filter_labels=Label filtern @@ -1577,7 +1577,7 @@ issues.remove_ref_at=`hat die Referenz %s %s entfernt` issues.add_ref_at=`hat die Referenz %s %s hinzugefügt` issues.delete_branch_at=`löschte den Branch %s %s` issues.filter_label=Label -issues.filter_label_exclude=`Alt + Klick/Enter verwenden, um Labels auszuschließen` +issues.filter_label_exclude=`Verwende Alt + Klick/Enter, um Labels auszuschließen` issues.filter_label_no_select=Alle Labels issues.filter_label_select_no_label=Kein Label issues.filter_milestone=Meilenstein @@ -1651,13 +1651,13 @@ issues.close_comment_issue=Mit Kommentar schließen issues.reopen_issue=Wieder öffnen issues.reopen_comment_issue=Mit Kommentar wieder öffnen issues.create_comment=Kommentieren -issues.closed_at=`hat diesen Issue %[2]s geschlossen` -issues.reopened_at=`hat dieses Issue %[2]s wieder geöffnet` -issues.commit_ref_at=`hat dieses Issue %[2]s aus einem Commit referenziert` -issues.ref_issue_from=`hat %[2]s auf dieses Issue verwiesen %[4]s` -issues.ref_pull_from=`hat %[2]s auf diesen Pull-Request verwiesen %[4]s` -issues.ref_closing_from=`hat %[2]s in einem Pull-Request %[4]s auf dieses Issue verwiesen, welcher es schließen wird` -issues.ref_reopening_from=`hat %[2]s in einem Pull-Request %[4]s auf dieses Issue verwiesen, welcher es erneut öffnen wird` +issues.closed_at=`hat dieses Issue %s geschlossen` +issues.reopened_at=`hat dieses Issue %s wieder geöffnet` +issues.commit_ref_at=`hat dieses Issue %s aus einem Commit referenziert` +issues.ref_issue_from=`hat %[1]s auf dieses Issue verwiesen %[3]s` +issues.ref_pull_from=`referenzierte diesen Pull-Request %[3]s %[1]s` +issues.ref_closing_from=`referenzierte dieses Issue aus einem Pull-Request %[3]s der es schließen wird, %[1]s` +issues.ref_reopening_from=`referenzierte dieses Issue aus einem Pull-Request %[3]s der es wieder öffnen wird, %[1]s` issues.ref_closed_from=`hat dieses Issue %[4]s geschlossen %[2]s` issues.ref_reopened_from=`hat dieses Issue %[4]s %[2]s wieder geöffnet` issues.ref_from=`von %[1]s` @@ -1962,8 +1962,8 @@ pulls.update_branch_success=Branch-Aktualisierung erfolgreich pulls.update_not_allowed=Du hast keine Berechtigung, den Branch zu updaten pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits des Basis-Branches pulls.close=Pull-Request schließen -pulls.closed_at=`hat diesen Pull-Request %[2]s geschlossen` -pulls.reopened_at=`hat diesen Pull-Request %[2]s wieder geöffnet` +pulls.closed_at=`hat diesen Pull-Request %s geschlossen` +pulls.reopened_at=`hat diesen Pull-Request %s wieder geöffnet` pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie „Co-Authored-By …“ erhalten. @@ -2767,7 +2767,7 @@ settings.wiki_globally_editable = Allen erlauben, das Wiki zu bearbeiten settings.protect_branch_name_pattern_desc = Geschützte Branch-Namens-Patterns. Siehe die Dokumentation für Pattern-Syntax. Beispiele: main, release/** settings.ignore_stale_approvals = Abgestandene Genehmigungen ignorieren settings.ignore_stale_approvals_desc = Genehmigungen, welche für ältere Commits gemacht wurden (abgestandene Reviews), nicht in die Gesamtzahl der Genehmigung des PRs mitzählen. Irrelevant, falls abgestandene Reviews bereits verworfen werden. -pulls.commit_ref_at = `hat sich auf diesen Pull-Request von einem Commit %[2]s bezogen` +pulls.commit_ref_at = `referenzierte diesen Pull-Request aus einem Commit %s` pulls.fast_forward_only_merge_pull_request = Nur Fast-forward pulls.cmd_instruction_checkout_desc = Checke einen neuen Branch aus deinem Projekt-Repository aus und teste die Änderungen. pulls.cmd_instruction_merge_title = Zusammenführen @@ -2917,6 +2917,14 @@ comment.blocked_by_user = Kommentieren ist nicht möglich, da du vom Repository- sync_fork.branch_behind_one = Dieser Branch ist %[1]d Commit hinter %[2]s sync_fork.branch_behind_few = Dieser Branch ist %[1]d Commits hinter %[2]s sync_fork.button = Sync +settings.event_action_failure_desc = Action-Run endete im Fehlschlag. +settings.event_action_success_desc = Action-Run war erfolgreich. +settings.event_action_failure = Fehlschlag +settings.event_action_success = Erfolg +settings.event_header_action = Action-Run-Ereignisse +settings.event_action_recover_desc = Action-Run war erfolgreich, nachdem der letzte Action-Run im selben Arbeitsablauf fehlgeschlagen ist. +settings.event_action_recover = Wiederherstellen +issues.filter_type.all_pull_requests = Alle Pull-Requests [graphs] component_loading_failed = Konnte %s nicht laden @@ -3053,8 +3061,8 @@ teams.invite.by=Von %s eingeladen teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. follow_blocked_user = Du kannst dieser Organisation nicht folgen, weil diese Organisation dich blockiert hat. open_dashboard = Übersicht öffnen -settings.change_orgname_redirect_prompt.with_cooldown.one = Der alte Organisationsname ist nach einer Abkühldauer von einem Tag wieder für alle verfügbar. Du kannst den alten Namen während dieser Abkühldauer erneut beanspruchen. -settings.change_orgname_redirect_prompt.with_cooldown.few = Der alte Organisationsname ist nach einer Abkühldauer von %[1]d Tagen wieder für alle verfügbar. Du kannst den alten Namen während dieser Abkühldauer erneut beanspruchen. +settings.change_orgname_redirect_prompt.with_cooldown.one = Der alte Organisationsname ist nach einer Schutzzeit von einem Tag wieder für alle verfügbar. Du kannst den alten Namen während dieser Schutzzeit erneut beanspruchen. +settings.change_orgname_redirect_prompt.with_cooldown.few = Der alte Organisationsname ist nach einer Schutzzeit von %[1]d Tagen wieder für alle verfügbar. Du kannst den alten Namen während dieser Schutzzeit erneut beanspruchen. [admin] dashboard=Übersicht @@ -4031,7 +4039,7 @@ repo_kind = Repos suchen … user_kind = Benutzer suchen … org_kind = Orgs suchen … team_kind = Teams suchen … -code_kind = Code suchen… +code_kind = Code durchsuchen … package_kind = Pakete suchen … project_kind = Projekte suchen … branch_kind = Branches suchen … diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 4b1f19e295..398a0d9ce4 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -112,7 +112,7 @@ preview=Προεπισκόπηση loading=Φόρτωση… error=Σφάλμα -error404=Η σελίδα που προσπαθείτε να φτάσετε είτε δεν υπάρχει είτε δεν είστε εξουσιοδοτημένοι για να την δείτε. +error404=Η σελίδα που προσπαθείτε να φτάσετε είτε δεν υπάρχει, έχει αφαιρεθεί είτε δεν είστε εξουσιοδοτημένοι για να την δείτε. go_back=Επιστροφή never=Ποτέ @@ -1626,13 +1626,13 @@ issues.close_comment_issue=Αποστολή σχολίου και κλείσιμ issues.reopen_issue=Ανοίξτε ξανά issues.reopen_comment_issue=Αποστολή σχολίου και επανάνοιγμα ζητήματος issues.create_comment=Προσθήκη Σχολίου -issues.closed_at=`αυτό το ζήτημα έκλεισε %[2]s` -issues.reopened_at=`ξανά άνοιξε αυτό το ζήτημα %[2]s` -issues.commit_ref_at=`αναφορά σε αυτό το ζήτημα από την παραπομπή %[2]s` -issues.ref_issue_from=`αναφέρθηκε σε αυτό το ζήτημα %[4]s %[2]s` -issues.ref_pull_from=`αναφέρθηκε σε αυτό το pull request %[4]s %[2]s` -issues.ref_closing_from=`ανέφερε αυτό το ζήτημα σε ένα pull request %[4]s που στοχεύει να κλείσει το ζήτημα %[2]s` -issues.ref_reopening_from=`αναφέρθηκε σε αυτό το ζήτημα σε ένα pull request %[4]s που θα ξαναανοίξει αυτό το ζήτημα %[2]s` +issues.closed_at=`αυτό το ζήτημα έκλεισε %s` +issues.reopened_at=`ξανά άνοιξε αυτό το ζήτημα %s` +issues.commit_ref_at=`αναφορά σε αυτό το ζήτημα από την παραπομπή %s` +issues.ref_issue_from=`αναφέρθηκε σε αυτό το ζήτημα %[3]s %[1]s` +issues.ref_pull_from=`αναφέρθηκε σε αυτό το pull request %[3]s %[1]s` +issues.ref_closing_from=`ανέφερε αυτό το ζήτημα σε ένα pull request %[3]s που στοχεύει να κλείσει το ζήτημα %[1]s` +issues.ref_reopening_from=`αναφέρθηκε σε αυτό το ζήτημα σε ένα pull request %[3]s που θα ξαναανοίξει αυτό το ζήτημα %[1]s` issues.ref_closed_from=`έκλεισε αυτό το ζήτημα %[4]s %[2]s` issues.ref_reopened_from=`άνοιξε ξανά αυτό το ζήτημα %[4]s %[2]s` issues.ref_from=`από %[1]s` @@ -1939,8 +1939,8 @@ pulls.update_branch_success=Η ενημέρωση του κλάδου ήταν pulls.update_not_allowed=Δεν επιτρέπεται να ενημερώσετε τον κλάδο pulls.outdated_with_base_branch=Αυτός ο κλάδος δεν είναι ενημερωμένος με τον βασικό κλάδο pulls.close=Κλείσιμο pull request -pulls.closed_at=`έκλεισε αυτό το pull request %[2]s` -pulls.reopened_at=`άνοιξε ξανά αυτό το pull request %[2]s` +pulls.closed_at=`έκλεισε αυτό το pull request %s` +pulls.reopened_at=`άνοιξε ξανά αυτό το pull request %s` pulls.cmd_instruction_hint=Προβολή οδηγιών γραμμής εντολών pulls.cmd_instruction_checkout_title=Έλεγχος pulls.cmd_instruction_checkout_desc=Από το repository του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές. @@ -2720,7 +2720,7 @@ settings.new_owner_blocked_doer = Ο νέος κάτοχος του αποθετ settings.enter_repo_name = Γράψτε το όνομα του κατόχου και του αποθετηρίου ακριβώς όπως το βλέπετε: settings.confirmation_string = Κείμενο επιβεβαίωσης settings.units.overview = Επισκόπηση -pulls.commit_ref_at = `ανέφερε το pull request στο commit %[2]s` +pulls.commit_ref_at = `ανέφερε το pull request στο commit %s` contributors.contribution_type.filter_label = Είδος συνεισφοράς: settings.wiki_rename_branch_main_notices_1 = Αυτή η ενέργεια ΔΕΝ αναιρείται. activity.navbar.contributors = Συνεισφέροντες @@ -3941,29 +3941,29 @@ code_search_unavailable = Η αναζήτηση κώδικα δεν είναι keyword_search_unavailable = Η αναζήτηση με την χρήση λέξεων-κλειδιών δεν είναι επί του παρόντος διαθέσιμη. Παρακαλώ επικοινωνήστε με τον διαχειριστή σας. runner_kind = Αναζήτηση runner... code_search_by_git_grep = Για την αναζήτηση κώδικα, χρησιμοποιείται η εντολή «git grep». Ίσως να παρουσιαστούν καλύτερα αποτελέσματα, αν ο διαχειριστής σας ενεργοποιήσει ένα ευρετήριο για αποθετήρια («Repository Indexer»). -package_kind = Αναζήτηση πακέτων... +package_kind = Αναζήτηση πακέτων… project_kind = Αναζήτηση έργων... -branch_kind = Αναζήτηση κλάδων... +branch_kind = Αναζήτηση κλάδων… commit_kind = Αναζήτηση commit... no_results = Δεν βρέθηκαν κατάλληλα αποτελέσματα. -search = Αναζήτηση... +search = Αναζήτηση… type_tooltip = Είδος αναζήτησης fuzzy = Στο περίπου fuzzy_tooltip = Να συμπεριληφθούν αποτελέσματα που μοιάζουν με τον όρο αναζήτησης match = Ακριβής match_tooltip = Να συμπεριληφθούν αποτελέσματα που ταιριάζουν με τον όρο αναζήτησης -repo_kind = Αναζήτηση αποθετηρίων... -user_kind = Αναζήτηση χρηστών... -org_kind = Αναζήτηση οργανισμών... -team_kind = Αναζήτηση ομαδών... -code_kind = Αναζήτηση κώδικα... +repo_kind = Αναζήτηση αποθετηρίων… +user_kind = Αναζήτηση χρηστών… +org_kind = Αναζήτηση οργανισμών… +team_kind = Αναζήτηση ομαδών… +code_kind = Αναζήτηση κώδικα… exact_tooltip = Να συμπεριληφθούν μόνο αποτελέσματα που ταιριάζουν με τον όρο αναζήτησης issue_kind = Αναζήτηση ζητημάτων... pull_kind = Αναζήτηση pull... exact = Ακριβής milestone_kind = Αναζήτηση ορόσημων... union = Ένωση -union_tooltip = Να συμπεριληφθούν αποτελέσματα που περιέχουν οποιαδήποτε από τις λέξεις που έχουν εισαχθεί και διαχωριστεί με κενό +union_tooltip = Να συμπεριληφθούν αποτελέσματα που περιέχουν οποιαδήποτε από τις λέξεις χωρισμένες με κενό regexp = Κανονική Έκφραση regexp_tooltip = Ερμηνεία του όρου αναζήτησης ως κανονική έκφραση diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 856e070efb..5fd2ebd163 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -7,9 +7,9 @@ logo = Logo sign_in = Sign in sign_in_with_provider = Sign in with %s sign_in_or = or -sign_out = Sign Out +sign_out = Sign out sign_up = Register -link_account = Link Account +link_account = Link account register = Register version = Version powered_by = Powered by %s @@ -740,7 +740,7 @@ ssh_gpg_keys = SSH / GPG keys applications = Applications orgs = Organizations repos = Repositories -delete = Delete Account +delete = Delete account twofa = Two-factor authentication (TOTP) organization = Organizations uid = UID @@ -768,8 +768,8 @@ update_profile_success = Your profile has been updated. change_username = Your username has been changed. change_username_prompt = Note: Changing your username also changes your account URL. change_username_redirect_prompt = The old username will redirect until someone claims it. -change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old username during the cooldown period. -change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old username during the cooldown period. +change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old username during the cooldown period. +change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old username during the cooldown period. continue = Continue cancel = Cancel language = Language @@ -1540,11 +1540,11 @@ projects.card_type.images_and_text = Images and text projects.card_type.text_only = Text only issues.desc = Organize bug reports, tasks and milestones. -issues.filter_assignees = Filter Assignee -issues.filter_milestones = Filter Milestone -issues.filter_projects = Filter Project -issues.filter_labels = Filter Label -issues.filter_reviewers = Filter Reviewer +issues.filter_assignees = Filter assignee +issues.filter_milestones = Filter milestone +issues.filter_projects = Filter project +issues.filter_labels = Filter label +issues.filter_reviewers = Filter reviewer issues.filter_no_results = No results issues.filter_no_results_placeholder = Try adjusting your search filters. issues.new = New issue @@ -1610,7 +1610,7 @@ issues.remove_ref_at = `removed reference %s %s` issues.add_ref_at = `added reference %s %s` issues.delete_branch_at = `deleted branch %s %s` issues.filter_label = Label -issues.filter_label_exclude = `Use alt + click/enter to exclude labels` +issues.filter_label_exclude = Use Alt + Click to exclude labels issues.filter_label_no_select = All labels issues.filter_label_select_no_label = No label issues.filter_milestone = Milestone @@ -1628,6 +1628,7 @@ issues.filter_poster = Author issues.filter_poster_no_select = All authors issues.filter_type = Type issues.filter_type.all_issues = All issues +issues.filter_type.all_pull_requests = All pull requests issues.filter_type.assigned_to_you = Assigned to you issues.filter_type.created_by_you = Created by you issues.filter_type.mentioning_you = Mentioning you @@ -1693,15 +1694,13 @@ issues.close_comment_issue = Close with comment issues.reopen_issue = Reopen issues.reopen_comment_issue = Reopen with comment issues.create_comment = Comment -issues.closed_at = `closed this issue %[2]s` -issues.reopened_at = `reopened this issue %[2]s` -issues.commit_ref_at = `referenced this issue from a commit %[2]s` -issues.ref_issue_from = `referenced this issue %[4]s %[2]s` -issues.ref_pull_from = `referenced this pull request %[4]s %[2]s` -issues.ref_closing_from = `referenced this issue from a pull request %[4]s that will close it, %[2]s` -issues.ref_reopening_from = `referenced this issue from a pull request %[4]s that will reopen it, %[2]s` -issues.ref_closed_from = `closed this issue %[4]s %[2]s` -issues.ref_reopened_from = `reopened this issue %[4]s %[2]s` +issues.closed_at = `closed this issue %s` +issues.reopened_at = `reopened this issue %s` +issues.commit_ref_at = `referenced this issue from a commit %s` +issues.ref_issue_from = `referenced this issue %[3]s %[1]s` +issues.ref_pull_from = `referenced this pull request %[3]s %[1]s` +issues.ref_closing_from = `referenced this issue from a pull request %[3]s that will close it, %[1]s` +issues.ref_reopening_from = `referenced this issue from a pull request %[3]s that will reopen it, %[1]s` issues.ref_from = `from %[1]s` issues.author = Author issues.author.tooltip.issue = This user is the author of this issue. @@ -2013,9 +2012,9 @@ pulls.update_branch_success = Branch update was successful pulls.update_not_allowed = You are not allowed to update branch pulls.outdated_with_base_branch = This branch is out-of-date with the base branch pulls.close = Close pull request -pulls.closed_at = `closed this pull request %[2]s` -pulls.reopened_at = `reopened this pull request %[2]s` -pulls.commit_ref_at = `referenced this pull request from a commit %[2]s` +pulls.closed_at = `closed this pull request %s` +pulls.reopened_at = `reopened this pull request %s` +pulls.commit_ref_at = `referenced this pull request from a commit %s` pulls.cmd_instruction_hint = View command line instructions pulls.cmd_instruction_checkout_title = Checkout pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. @@ -2106,7 +2105,7 @@ signing.wont_sign.not_signed_in = You are not signed in. ext_wiki = External Wiki wiki = Wiki -wiki.welcome = Welcome to the Wiki. +wiki.welcome = Welcome to the wiki. wiki.welcome_desc = The wiki lets you write and share documentation with collaborators. wiki.desc = Write and share documentation with collaborators. wiki.create_first_page = Create the first page @@ -2389,7 +2388,7 @@ settings.add_collaborator_duplicate = The collaborator is already added to this settings.add_collaborator_blocked_our = Cannot add the collaborator, because the repository owner has blocked them. settings.add_collaborator_blocked_them = Cannot add the collaborator, because they have blocked the repository owner. settings.delete_collaborator = Remove -settings.collaborator_deletion = Remove Collaborator +settings.collaborator_deletion = Remove collaborator settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue? settings.remove_collaborator_success = The collaborator has been removed. settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator. @@ -2486,6 +2485,13 @@ settings.event_pull_request_review_request_desc = Pull request review requested settings.event_pull_request_approvals = Pull request approvals settings.event_pull_request_merge = Pull request merge settings.event_pull_request_enforcement = Enforcement +settings.event_header_action = Action Run events +settings.event_action_failure = Failure +settings.event_action_failure_desc = Action Run ended as failure. +settings.event_action_recover = Recover +settings.event_action_recover_desc = Action Run succeeded after last Action Run in the same workflow failed. +settings.event_action_success = Success +settings.event_action_success_desc = Action Run succeeded. settings.event_package = Package settings.event_package_desc = Package created or deleted in a repository. settings.branch_filter = Branch filter @@ -2645,36 +2651,36 @@ settings.unarchive.text = Unarchiving the repo will restore its ability to recei settings.unarchive.success = The repo was successfully unarchived. settings.unarchive.error = An error occurred while trying to unarchive the repo. See the log for more details. settings.update_avatar_success = The repository avatar has been updated. -settings.lfs=LFS -settings.lfs_filelist=LFS files stored in this repository -settings.lfs_no_lfs_files=No LFS files stored in this repository -settings.lfs_findcommits=Find commits -settings.lfs_lfs_file_no_commits=No commits found for this LFS file -settings.lfs_noattribute=This path does not have the lockable attribute in the default branch -settings.lfs_delete=Delete LFS file with OID %s -settings.lfs_delete_warning=Deleting an LFS file may cause "object does not exist" errors on checkout. Are you sure? -settings.lfs_findpointerfiles=Find pointer files -settings.lfs_locks=Locks -settings.lfs_invalid_locking_path=Invalid path: %s -settings.lfs_invalid_lock_directory=Cannot lock directory: %s -settings.lfs_lock_already_exists=Lock already exists: %s -settings.lfs_lock=Lock -settings.lfs_lock_path=Filepath to lock… -settings.lfs_locks_no_locks=No locks -settings.lfs_lock_file_no_exist=Locked file does not exist in default branch -settings.lfs_force_unlock=Force unlock -settings.lfs_pointers.found=Found %d blob pointer(s) - %d associated, %d unassociated (%d missing from store) -settings.lfs_pointers.sha=Blob hash -settings.lfs_pointers.oid=OID -settings.lfs_pointers.inRepo=In repo -settings.lfs_pointers.exists=Exists in store -settings.lfs_pointers.accessible=Accessible to user -settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs -settings.rename_branch_failed_protected=Cannot rename branch %s because it is a protected branch. -settings.rename_branch_failed_exist=Cannot rename branch because target branch %s exists. -settings.rename_branch_failed_not_exist=Cannot rename branch %s because it does not exist. -settings.rename_branch_success =Branch %s was successfully renamed to %s. -settings.rename_branch=Rename branch +settings.lfs = LFS +settings.lfs_filelist = LFS files stored in this repository +settings.lfs_no_lfs_files = No LFS files stored in this repository +settings.lfs_findcommits = Find commits +settings.lfs_lfs_file_no_commits = No commits found for this LFS file +settings.lfs_noattribute = This path does not have the lockable attribute in the default branch +settings.lfs_delete = Delete LFS file with OID %s +settings.lfs_delete_warning = Deleting an LFS file may cause "object does not exist" errors on checkout. Are you sure? +settings.lfs_findpointerfiles = Find pointer files +settings.lfs_locks = Locks +settings.lfs_invalid_locking_path = Invalid path: %s +settings.lfs_invalid_lock_directory = Cannot lock directory: %s +settings.lfs_lock_already_exists = Lock already exists: %s +settings.lfs_lock = Lock +settings.lfs_lock_path = Filepath to lock… +settings.lfs_locks_no_locks = No locks +settings.lfs_lock_file_no_exist = Locked file does not exist in default branch +settings.lfs_force_unlock = Force unlock +settings.lfs_pointers.found = Found %d blob pointer(s) - %d associated, %d unassociated (%d missing from store) +settings.lfs_pointers.sha = Blob hash +settings.lfs_pointers.oid = OID +settings.lfs_pointers.inRepo = In repo +settings.lfs_pointers.exists = Exists in store +settings.lfs_pointers.accessible = Accessible to user +settings.lfs_pointers.associateAccessible = Associate accessible %d OIDs +settings.rename_branch_failed_protected = Cannot rename branch %s because it is a protected branch. +settings.rename_branch_failed_exist = Cannot rename branch because target branch %s exists. +settings.rename_branch_failed_not_exist = Cannot rename branch %s because it does not exist. +settings.rename_branch_success = Branch %s was successfully renamed to %s. +settings.rename_branch = Rename branch diff.browse_source = Browse source diff.parent = parent @@ -2925,8 +2931,8 @@ settings.update_settings = Update settings settings.update_setting_success = Organization settings have been updated. settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name. settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. -settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old name during the cooldown period. -settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old name during the cooldown period. +settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old name during the cooldown period. +settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old name during the cooldown period. settings.update_avatar_success = The organization's avatar has been updated. settings.delete = Delete organization settings.delete_account = Delete this organization @@ -2978,8 +2984,6 @@ teams.invite_team_member.list = Pending invitations teams.delete_team_title = Delete team teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue? teams.delete_team_success = The team has been deleted. -teams.read_permission_desc = This team grants Read access: members can view and clone team repositories. -teams.write_permission_desc = This team grants Write access: members can read from and push to team repositories. teams.admin_permission_desc = This team grants Administrator access: members can read from, push to and add collaborators to team repositories. teams.create_repo_permission_desc = Additionally, this team grants Create repository permission: members can create new repositories in organization. teams.repositories = Team repositories @@ -3777,8 +3781,8 @@ swift.install2 = and run the following command: vagrant.install = To add a Vagrant box, run the following command: settings.link = Link this package to a repository settings.link.description = If you link a package with a repository, the package is listed in the repository's package list. -settings.link.select = Select Repository -settings.link.button = Update Repository Link +settings.link.select = Select repository +settings.link.button = Update repository link settings.link.success = Repository link was successfully updated. settings.link.error = Failed to update repository link. settings.delete = Delete package @@ -3824,7 +3828,7 @@ owner.settings.chef.keypair.description = Requests sent to the Chef registry mus secrets = Secrets description = Secrets will be passed to certain actions and cannot be read otherwise. none = There are no secrets yet. -creation = Add Secret +creation = Add secret creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. creation.success = The secret "%s" has been added. @@ -3866,7 +3870,7 @@ runners.task_list.run = Run runners.task_list.status = Status runners.task_list.repository = Repository runners.task_list.commit = Commit -runners.task_list.done_at = Done At +runners.task_list.done_at = Done at runners.edit_runner = Edit Runner runners.update_runner = Update changes runners.update_runner_success = Runner updated successfully @@ -3939,7 +3943,7 @@ variables.update.failed = Failed to edit variable. variables.update.success = The variable has been edited. [projects] -deleted.display_name = Deleted Project +deleted.display_name = Deleted project type-1.display_name = Individual project type-2.display_name = Repository project type-3.display_name = Organization project diff --git a/options/locale/locale_eo.ini b/options/locale/locale_eo.ini index e4fb8cdbc7..6393765d63 100644 --- a/options/locale/locale_eo.ini +++ b/options/locale/locale_eo.ini @@ -103,7 +103,7 @@ ok = Bone download_logs = Elsuti protokolojn unknown = Nekonata issues = Eraroj -error404 = Aŭ tiu ĉi paĝo ne ekzistas aŭ vi ne rajtas vidi ĝin. +error404 = Aŭ tiu ĉi paĝo ne ekzistas, estis forigita aŭ vi ne rajtas vidi ĝin. retry = Reprovi activities = Aktivecoj confirm_delete_selected = Konfirmi forigon de ĉiu elektito? @@ -171,6 +171,7 @@ table_modal.placeholder.header = Kapo table_modal.placeholder.content = Enhavo table_modal.label.rows = Horizontaloj table_modal.label.columns = Vertikaloj +link_modal.description = Priskribo [aria] navbar = Esplora breto @@ -523,6 +524,7 @@ totp_enrolled.text_1.has_webauthn = Vi ĵus aktivigis TOTP-n por via konto. Tio totp_enrolled.text_1.no_webauthn = Vi ĵus aktivigis TOTP-n por via konto. Tio volas diri ke por ĉiuj venontaj salutoj al via konto, vi devos uzi TOTP-n kiel 2FA metodo. removed_security_key.no_2fa = Ne estas aliaj 2FA agorditaj metodoj, tio estas ke ne plus necesas uzi 2FA-n por saluti. totp_disabled.no_2fa = Ne estas plu aliaj 2FA agorditaj metodoj, tio estas ke ne plus necesas uzi 2FA-n por saluti. +account_security_caution.text_1 = Se tio estis vi, vi povas sekure ignori ĉi tiun retmesaĝon. [form] TeamName = Gruponomo @@ -861,30 +863,30 @@ npm.details.tag = Etikedo [search] -search = Serĉi... +search = Serĉi… regexp = RegEsp milestone_kind = Serĉi celojn... code_search_by_git_grep = Nunaj rezultoj de kodoserĉo estas provizitaj de "git grep". Eble estas plibonaj rezultoj se la retejestro aktivigas la indeksilon de kodo. code_search_unavailable = Kodoserĉo ne haveblas nune. Bonvolu kontakti la retejestron. -package_kind = Serĉi pakojn... +package_kind = Serĉi pakojn… type_tooltip = Serĉotipo -user_kind = Serĉi uzantojn... +user_kind = Serĉi uzantojn… fuzzy_tooltip = Inkluzivas rezultojn proksime kongruantajn kun la serĉoterminoj -repo_kind = Serĉi deponejojn... -org_kind = Serĉi organizaĵojn... -code_kind = Serĉi kodon... -project_kind = Serĉi projektojn... -team_kind = Serĉi teamojn... +repo_kind = Serĉi deponejojn… +org_kind = Serĉi organizaĵojn… +code_kind = Serĉi kodon… +project_kind = Serĉi projektojn… +team_kind = Serĉi teamojn… keyword_search_unavailable = Serĉo per ŝlosilvortoj ne haveblas nune. Bonvolu kontakti la retejestron. union = Ŝlosilvortoj union_tooltip = Inkluzivas rezultojn kongruantajn kun la ajnaj blankaspacitaj ŝlosilvortoj -commit_kind = Serĉi enmetojn... +commit_kind = Serĉi enmetojn… no_results = Ne trovis kongruantajn rezultojn. exact = Ĝusta exact_tooltip = Inkluzivas nur rezultojn kongruantajn kun la ĝustaj serĉoterminoj issue_kind = Serĉi erarojn... regexp_tooltip = Interpretas la serĉoterminoj kiel regulesprimo fuzzy = Svaga -branch_kind = Serĉi disbranĉigojn... +branch_kind = Serĉi disbranĉigojn… runner_kind = Serĉi rulantojn... pull_kind = Serĉi tirpetojn... \ No newline at end of file diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 63be50f3ce..bdafba93b4 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1648,13 +1648,13 @@ issues.close_comment_issue=Cerrar con comentario issues.reopen_issue=Reabrir issues.reopen_comment_issue=Reabrir con comentario issues.create_comment=Comentar -issues.closed_at=`cerró esta incidencia %[2]s` -issues.reopened_at=`reabrió esta incidencia %[2]s` -issues.commit_ref_at=`referenció esta incidencia en un commit %[2]s` -issues.ref_issue_from=`referenció esta incidencia %[4]s %[2]s` -issues.ref_pull_from=`referenció este pull request %[4]s %[2]s` -issues.ref_closing_from=`hizo referencia a esta incidencia desde un pull request %[4]s que lo cerrará , %[2]s` -issues.ref_reopening_from=`hizo referencia a esta incidencia desde un pull request %[4]s que lo reabrirá, %[2]s` +issues.closed_at=`cerró esta incidencia %s` +issues.reopened_at=`reabrió esta incidencia %s` +issues.commit_ref_at=`referenció esta incidencia en un commit %s` +issues.ref_issue_from=`referenció esta incidencia %[3]s %[1]s` +issues.ref_pull_from=`referenció este pull request %[3]s %[1]s` +issues.ref_closing_from=`hizo referencia a esta incidencia desde un pull request %[3]s que lo cerrará , %[1]s` +issues.ref_reopening_from=`hizo referencia a esta incidencia desde un pull request %[3]s que lo reabrirá, %[1]s` issues.ref_closed_from=`cerró esta incidencia %[4]s %[2]s` issues.ref_reopened_from=`reabrió esta incidencia %[4]s %[2]s` issues.ref_from=`de %[1]s` @@ -1959,8 +1959,8 @@ pulls.update_branch_success=La actualización de la rama ha finalizado correctam pulls.update_not_allowed=No tiene permisos para actualizar esta rama pulls.outdated_with_base_branch=Esta rama está desactualizada con la rama base pulls.close=Cerrar pull request -pulls.closed_at=`cerró este pull request %[2]s` -pulls.reopened_at=`reabrió este pull request %[2]s` +pulls.closed_at=`cerró este pull request %s` +pulls.reopened_at=`reabrió este pull request %s` pulls.clear_merge_message=Borrar mensaje de fusión pulls.clear_merge_message_hint=Limpiar el mensaje de fusión solo eliminará el contenido del mensaje de commit y mantendrá frases generadas como "Co-Autorizado por …". @@ -2789,7 +2789,7 @@ pulls.status_checks_hide_all = Ocultar todas las verificaciones settings.federation_not_enabled = La federación no está habilitada en tu instancia. wiki.search = Buscar en wiki pulls.status_checks_show_all = Mostrar todas las verificaciones -pulls.commit_ref_at = `hizo referencia a este pull request desde un commit %[2]s` +pulls.commit_ref_at = `hizo referencia a este pull request desde un commit %s` pulls.cmd_instruction_merge_title = Fusionar contributors.contribution_type.deletions = Eliminaciones contributors.contribution_type.filter_label = Tipo de contribución: @@ -2886,6 +2886,7 @@ issues.filter_no_results_placeholder = Intenta ajustar tus filtros de búsqueda. pulls.delete_after_merge.head_branch.is_default = La rama actual que desea eliminar es la rama por defecto y no se puede eliminar. summary_card_alt = Tarjeta de resumen del repositorio %s settings.pull_mirror_sync_quota_exceeded = Cuota excedida, no se empujan los cambios. +archive.nocomment = No es posible hacer comentarios porque el repositorio está archivado. [graphs] component_loading = Cargando %s… @@ -3945,6 +3946,8 @@ runs.no_workflows = Aún no hay flujos de trabajo. workflow.dispatch.success = La ejecución del flujo de trabajo se ha solicitado correctamente. variables.not_found = No se ha encontrado la variable. workflow.dispatch.input_required = Se requiere valor para la entrada "%s". +workflow.dispatch.trigger_found = Este flujo de trabajo tiene un disparador de eventos workflow_dispatch. +workflow.dispatch.warn_input_limit = Sólo se muestran las primeras %d entradas. [projects] type-1.display_name=Proyecto individual diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 804b48b2b2..dae0695495 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1250,13 +1250,13 @@ issues.close_comment_issue=ثبت دیدگاه و بستن issues.reopen_issue=بازگشایی issues.reopen_comment_issue=ثبت دیدگاه و بازگشایی issues.create_comment=دیدگاه -issues.closed_at=`%[2]s این موضوع را بست` -issues.reopened_at=`%[2]s این موضوع را دوباره باز کرد` -issues.commit_ref_at=`ارجاع این مسئله به کامیت %[2]s` -issues.ref_issue_from=` ارجاعات این مسائله %[4] %[2]s` -issues.ref_pull_from=` ارجاعات این تقاضای ادغام %[4] %[2]s` -issues.ref_closing_from=` ارجاعات این تقاضای واکشی %[4] %[2]s` -issues.ref_reopening_from=` تقاضای واکشی ارجاع شده %[4] که مسائله بازگشایی خواهد کرد %[2] ` +issues.closed_at=`%s این موضوع را بست` +issues.reopened_at=`%s این موضوع را دوباره باز کرد` +issues.commit_ref_at=`ارجاع این مسئله به کامیت %s` +issues.ref_issue_from=` ارجاعات این مسائله %[3] %[1]s` +issues.ref_pull_from=` ارجاعات این تقاضای ادغام %[4] %[1]s` +issues.ref_closing_from=` ارجاعات این تقاضای واکشی %[4] %[1]s` +issues.ref_reopening_from=` تقاضای واکشی ارجاع شده %[3]sکه مسائله بازگشایی خواهد کرد %[2] ` issues.ref_closed_from=` بسته شده این مسائله %[4] %[2]s` issues.ref_reopened_from=` بازگشایی این مسائله %[4] %[2]s` issues.ref_from=`از %[1]` @@ -1493,8 +1493,8 @@ pulls.update_branch_rebase=بروزآوری شاخه با بازسازی مجد pulls.update_branch_success=شاخه به موفقیت بروز شد pulls.update_not_allowed=شما اجازه بروزرسانی شاخه را ندارید pulls.outdated_with_base_branch=این شاخه با شاخه پایه منسوخ شده است -pulls.closed_at=`این درخواست pull بسته شده %[2]s` -pulls.reopened_at=`این درخواست pull را بازگشایی کرد %[2]s` +pulls.closed_at=`این درخواست pull بسته شده %s` +pulls.reopened_at=`این درخواست pull را بازگشایی کرد %s` diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 7fb667f6e2..164a60cc8d 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -8,7 +8,7 @@ sign_in=Kirjaudu sisään sign_in_or=tai sign_out=Kirjaudu ulos sign_up=Rekisteröidy -link_account=Yhdistä tili +link_account=Linkitä tili register=Rekisteröidy version=Versio powered_by=Voimanlähteenä %s @@ -23,7 +23,7 @@ toc=Sisällysluettelo licenses=Lisenssit return_to_forgejo=Palaa Forgejohon -username=Käyttäjätunnus +username=Käyttäjänimi email=Sähköpostiosoite password=Salasana access_token=Pääsypoletti @@ -31,29 +31,29 @@ re_type=Vahvista salasana captcha=CAPTCHA twofa=Kaksivaiheinen todennus twofa_scratch=Kaksivaiheinen kertakäyttöinen koodi -passcode=Tunnuskoodi +passcode=Pääsykoodi webauthn_insert_key=Aseta turva-avaimesi -webauthn_sign_in=Paina turva-avaimesi painiketta. Jos turva-avaimessasi ei ole painiketta, irroita se ja aseta uudelleen. +webauthn_sign_in=Paina turva-avaimesi painiketta. Jos turva-avaimessasi ei ole painiketta, irrota se ja aseta uudelleen. webauthn_press_button=Paina turva-avaimesi painiketta… -webauthn_use_twofa=Käytä kaksivaihesta vahvistusta puhelimestasi +webauthn_use_twofa=Käytä kaksivaihesta todennusta puhelimestasi webauthn_error=Turva-avainta ei voitu lukea. webauthn_unsupported_browser=Selaimesi ei tällä hetkellä tue WebAuthnia. webauthn_error_unknown=Tuntematon virhe. Yritä uudelleen. -webauthn_error_insecure=`WebAuthn tukee vain suojattuja yhteyksiä. Testaukseen HTTP:n yli, voit käyttää osoitetta "localhost" tai "127.0.0.1"` +webauthn_error_insecure=WebAuthn tukee vain suojattuja yhteyksiä. Testatessa HTTP-yhteydellä voit käyttää osoitetta "localhost" tai "127.0.0.1" webauthn_error_unable_to_process=Palvelin ei pystynyt käsittelemään pyyntöä. webauthn_error_duplicated=Turva-avainta ei ole sallittu tässä pyynnössä. Varmista, ettei avainta ole jo rekisteröity. webauthn_error_empty=Sinun täytyy asettaa nimi tälle avaimelle. -webauthn_error_timeout=Aikakatkaisu saavutettu ennenkuin avaintasi on voitu lukea. Lataa tämä sivu uudelleen ja yritä uudelleen. +webauthn_error_timeout=Aikakatkaisu ennen kuin avaintasi voitiin lukea. Lataa tämä sivu uudelleen ja yritä uudelleen. webauthn_reload=Päivitä -repository=Repo +repository=Tietovarasto organization=Organisaatio mirror=Peili new_repo=Uusi repo new_migrate=Uusi migraatio new_mirror=Uusi peili -new_fork=Uusi tietovarastohaarukka +new_fork=Uusi tietovarastoforkki new_org=Uusi organisaatio new_project=Uusi projekti manage_org=Ylläpidä organisaatioita @@ -68,7 +68,7 @@ all=Kaikki sources=Lähteet mirrors=Peilit collaborative=Yhteistyössä -forks=Haarat +forks=Forkit activities=Toimet pull_requests=Vetopyynnöt @@ -98,7 +98,7 @@ preview=Esikatselu loading=Ladataan… error=Virhe -error404=Sivu, jolle yrität päästä, joko ei ole olemassa, on poistettu tai sinulla ei ole oikeutta tarkastella sitä. +error404=Sivu, jolle yrität päästä, ei joko ole olemassa, on poistettu tai sinulla ei ole oikeutta nähdä sitä. never=Ei koskaan @@ -108,13 +108,13 @@ rss_feed=RSS-syöte archived=Arkistoidut -concept_code_repository=Repo +concept_code_repository=Tietovarasto concept_user_organization=Organisaatio name=Nimi -enable_javascript = Tämä sivu vaatii Javascriptin. +enable_javascript = Tämä sivu vaatii JavaScriptin. new_project_column = Uusi sarake retry = Yritä uudelleen copy_type_unsupported = Tätä tiedostotyyppiä ei voi kopioida @@ -152,19 +152,19 @@ new_org.title = Uusi organisaatio new_org.link = Uusi organisaatio new_repo.link = Uusi tietovarasto new_migrate.link = Uusi migraatio -rerun_all = Uudelleensuorita kaikki työt -artifacts = Artifaktit -confirm_delete_artifact = Haluatko varmasti poistaa artifaktin "%s"? +rerun_all = Suorita uudelleen kaikki työt +artifacts = Artefaktit +confirm_delete_artifact = Haluatko varmasti poistaa artefaktin "%s"? new_migrate.title = Uusi migraatio test = Testi concept_system_global = Yleisesti pätevä sign_in_with_provider = Kirjaudu %s-tilillä filter.is_fork = Forkit -filter.is_mirror = Peilattu +filter.is_mirror = Peilit filter.is_template = Mallipohjat -filter.not_fork = Ei forkkeja +filter.not_fork = Ei forkit filter.not_template = Ei mallipohjat -filter.not_mirror = Ei peilattu +filter.not_mirror = Ei peilit copy_path = Kopioi polku concept_user_individual = Yksittäinen käyttäjä @@ -180,7 +180,7 @@ more = Enemmän number_of_contributions_in_the_last_12_months = %s kontribuutiota viimeisen vuoden aikana contributions_zero = Ei kontribuutioita contributions_one = kontribuutio -contributions_few = panokset +contributions_few = kontribuutiota contributions_format = {contributions} {day}. {month} {year} [editor] @@ -197,7 +197,7 @@ buttons.list.ordered.tooltip = Lisää numeroitu lista buttons.switch_to_legacy.tooltip = Käytä vanhentunutta tekstieditoria buttons.indent.tooltip = Sisennä yhden tason verran buttons.quote.tooltip = Aseta lainaustekstiksi -buttons.enable_monospace_font = Ota tasalevyinen kirjasin käyttöön +buttons.enable_monospace_font = Ota tasalevyinen fontti käyttöön buttons.ref.tooltip = Viittaa ongelmaa tai vetopyyntöä buttons.new_table.tooltip = Lisää taulukko table_modal.header = Lisää taulukko @@ -205,7 +205,7 @@ table_modal.placeholder.header = Otsikko table_modal.placeholder.content = Sisältö table_modal.label.rows = Rivit table_modal.label.columns = Sarakkeet -buttons.unindent.tooltip = Kumoa sisäkkäisyyden yhden tason verran +buttons.unindent.tooltip = Vähennä sisennystä yhden tason verran link_modal.header = Lisää linkki link_modal.url = Osoite link_modal.description = Kuvaus @@ -228,22 +228,22 @@ report_message = Jos uskot tämän olevan Forgejon virhe, etsi ongelmia Forgejo! Liity mukaan tekemään projektista entistäkin parempi. Älä ujostele avustamista! -install_desc = Yksinkertaisesti suorita alustasi binääritiedosto, lähetä se Dockerin kanssa, tai pakkaa se. +license_desc=Mene ja lataa Forgejo! Liity tekemään projektista entistäkin parempi. Älä ujostele avustamista! +install_desc = Suorita alustallesi tarkoitettu binääritiedosto, kontita se, tai hanki se paketoituna. [install] install=Asennus title=Aloitusasetukset -docker_helper=Jos ajat Forgejoa Dockerin sisällä, lue ohjeet ennen minkään asetuksen muuttamista. +docker_helper=Jos suoritat Forgejoa kontitettuna, lue ohjeet, ennen kuin muutat yhtäkään asetusta. require_db_desc=Forgejo tarvitsee toimiakseen MySQL-, PostgreSQL-, SQLite3- tai TiDB- (MySQL-protokolla) tietokannan. db_title=Tietokannan asetukset db_type=Tietokannan tyyppi host=Isäntä -user=Käyttäjätunnus +user=Käyttäjänimi password=Salasana db_name=Tietokannan nimi db_schema=Skeema @@ -251,22 +251,22 @@ ssl_mode=SSL path=Polku sqlite_helper=SQLite3-tietokannan tiedostopolku.
Syötä absoluuttinen polku, jos ajat Forgejoa palveluna. reinstall_error=Yrität asentaa olemassa olevaan Forgejo-tietokantaan -reinstall_confirm_message=Asentaminen uudelleen olemassa olevalla Forgejo-tietokannalla voi aiheuttaa useita ongelmia. Useimmissa tapauksissa sinun pitäisi käyttää olemassa olevia "app.ini" asetuksia Forgejon käyttöön. Jos tiedät mitä teet, vahvista seuraavat seikat: -reinstall_confirm_check_1=Tiedot, jotka on salattu SECRET_KEY:llä app.ini:ssä saatetaan menettää: käyttäjät eivät ehkä voi kirjautua sisään 2FA/OTP:lla ja peilit eivät välttämättä toimi oikein. Ruksaamalla tämän vahvistat, että nykyinen app.ini -tiedosto sisältää oikean SECRET_KEY:n. -reinstall_confirm_check_2=Repot ja asetukset saattaa olla tarpeen uudelleensynkronoida. Valitsemalla tämän vahvistat, että uudelleensynkronoit repojen koukut ja authorized_keys -tiedoston manuaalisesti. Varmistat, että repon ja peilin asetukset ovat oikeat. -reinstall_confirm_check_3=Vahvistat, että olet täysin varma siitä, että tämä Forgejo toimii oikealla app.ini sijainnilla ja että olet varma, että sinun täytyy asentaa uudelleen. Vahvistat, että tunnustat edellä mainitut riskit. +reinstall_confirm_message=Asentaminen uudelleen olemassa olevalla Forgejo-tietokannalla voi aiheuttaa useita ongelmia. Useimmissa tapauksissa sinun pitäisi käyttää olemassa olevia "app.ini"-asetuksia Forgejon suorittamiseksi. Jos tiedät mitä teet, vahvista seuraavat seikat: +reinstall_confirm_check_1=Tiedot, jotka on salattu SECRET_KEY:llä app.ini:ssä saatetaan menettää: käyttäjät eivät ehkä voi kirjautua sisään 2FA/OTP:lla ja peilit eivät välttämättä toimi oikein. Ruksaamalla tämän vahvistat, että nykyinen app.ini-tiedosto sisältää oikean SECRET_KEY:n. +reinstall_confirm_check_2=Tietovarastot ja asetukset saattaa olla tarpeen synkronoida uudelleen. Valitsemalla tämän vahvistat, että synkronoit uudelleen tietovarastojen koukut ja authorized_keys-tiedoston manuaalisesti. Varmistat, että tietovaraston ja peilin asetukset ovat oikeat. +reinstall_confirm_check_3=Vahvistat, että olet täysin varma siitä, että tämä Forgejo toimii oikealla app.ini-sijainnilla ja että olet varma, että sinun täytyy asentaa uudelleen. Vahvistat, että tunnustat edellä mainitut riskit. err_empty_db_path=SQLite3-tietokannan polku ei voi olla tyhjä. no_admin_and_disable_registration=Et voi kytkeä rekisteröintiä pois luomatta sitä ennen ylläpitotiliä. err_empty_admin_password=Ylläpitäjän salasana ei voi olla tyhjä. err_empty_admin_email=Ylläpitäjän sähköpostiosoite ei voi olla tyhjä. -err_admin_name_is_reserved=`Ylläpitäjän käyttäjätunnus on virheellinen; käyttäjätunnus on varattu` -err_admin_name_is_invalid=Ylläpitäjän käyttäjätunnus on virheellinen +err_admin_name_is_reserved=Ylläpitäjän käyttäjänimi on virheellinen; käyttäjänimi on varattu +err_admin_name_is_invalid=Ylläpitäjän käyttäjänimi on virheellinen general_title=Yleiset asetukset -app_name=Ilmentymän otsikko -app_name_helper=Syötä ilmentymän nimi tähän. Se näytetään kaikilla sivuilla. +app_name=Instanssin otsikko +app_name_helper=Syötä instanssin nimi tähän. Se näytetään kaikilla sivuilla. repo_path=Tietovaraston juuren polku -repo_path_helper=Muualla olevat git-repositoriot tullaan tallentamaan tähän kansioon. +repo_path_helper=Muualla olevat git-tietovarastot tullaan tallentamaan tähän kansioon. lfs_path=Git LFS -juuripolku lfs_path_helper=Git LFS:n ylläpitämät tiedostot tullaan tallentamaan tähän hakemistoon. Jätä tyhjäksi kytkeäksesi toiminnon pois. run_user=Aja käyttäjänä @@ -276,7 +276,7 @@ ssh_port_helper=Porttinumero, jossa SSH-palvelimesi kuuntelee. Jätä tyhjäksi http_port=HTTP-kuunteluportti http_port_helper=Forgejo-verkkopalvelimen käyttämä porttinumero. app_url=Juuriosoite -app_url_helper=Juuriosoite HTTP(S)-klooniosoitteille ja sähköpostimuistutuksille. +app_url_helper=Juuriosoite HTTP(S)-klooniosoitteille ja sähköposti-ilmoituksille. log_root_path=Lokitiedostojen polku log_root_path_helper=Lokitiedostot kirjoitetaan tähän kansioon. @@ -285,8 +285,8 @@ email_title=Sähköpostiasetukset smtp_addr=SMTP-isäntä smtp_port=SMTP-portti smtp_from=Lähetä sähköpostit osoitteella -smtp_from_helper=Sähköpostiosoite, jota Forgejo käyttää. Kirjoita osoite ”nimi” -muodossa. -mailer_user=SMTP-käyttäjätunnus +smtp_from_helper=Sähköpostiosoite, jota Forgejo käyttää. Kirjoita pelkkä sähköpostiosoite tai "Nimi” -muodossa. +mailer_user=SMTP-käyttäjänimi mailer_password=SMTP-salasana register_confirm=Vaadi sähköpostinvahvistus rekisteröinnin edellytykseksi mail_notify=Ota sähköposti-ilmoitukset käyttöön @@ -294,12 +294,12 @@ server_service_title=Palvelimen ja kolmansien osapuolten palveluiden asetukset offline_mode=Ota paikallinen tila käyttöön offline_mode.description=Poista kolmannen osapuolen sisällönjakeluverkot ja tarjoa kaikki resurssit paikallisesti. disable_gravatar=Poista Gravatar käytöstä -disable_gravatar.description=Poista Gravatar- tai muiden kolmansien osapuolien avatar-lähteet käytöstä. Oletuskuvia käytetään käyttäjien avatareissa, elleivät he uloslataa omaa avatariaan ilmentymään. +disable_gravatar.description=Poista Gravatar- tai muiden kolmansien osapuolien avatar-lähteet käytöstä. Oletuskuvia käytetään käyttäjien avatareissa, elleivät käyttäjät lähetä omaa avatariaan Forgejo-instanssiin. federated_avatar_lookup=Käytä federoituja profiilikuvia federated_avatar_lookup.description=Käytä Libravatar-palvelua profiilikuvien hakemiseen. disable_registration=Poista itserekisteröinti käytöstä -disable_registration.description=Vain järjestelmänvalvojat voivat luoda uusia käyttäjiä. On suositeltavaa pitää rekisteröinti suljettuna mikäli kyseessä ei ole julkinen instanssi jota varten tarvitsee hallinnoida suuria määriä roskapostikäyttäjiä. -allow_only_external_registration.description=Käyttäjät voivat luoda uusia käyttäjiä vain erikseen konfiguroituja ulkoisia palveluja käyttäen. +disable_registration.description=Vain ylläpitäjät voivat luoda uusia käyttäjiä. On suositeltavaa pitää rekisteröinti suljettuna, jos kyseessä ei ole julkinen instanssi, ja et olet valmis hallinnoimaan suuria määriä mahdollisia bottikäyttäjiä. +allow_only_external_registration.description=Käyttäjät voivat luoda uusia tilejä vain erikseen määritettyjä ulkoisia palveluja käyttäen. openid_signin=Ota OpenID-kirjautuminen käyttöön openid_signin.description=Salli OpenID:n kautta kirjautuminen. openid_signup=Ota OpenID-itserekisteröinti käyttöön @@ -307,9 +307,9 @@ openid_signup.description=Salli OpenID:n kautta rekisteröinti mikäli itserekis enable_captcha=Ota käyttöön CAPTCHA rekisteröityessä enable_captcha.description=Vaadi CAPTCHA rekisteröinnin yhteydessä. require_sign_in_view=Vaadi sisäänkirjautuminen sisällön näkemiseksi -admin_setting.description=Järjestelmänvalvojan tilin luominen on valinnaista. Ensimmäisestä rekisteröityneestä käyttäjästä tulee automaattisesti järjestelmänvalvoja. -admin_title=Järjestelmänvalvojan tilin asetukset -admin_name=Järjestelmänvalvojan käyttäjänimi +admin_setting.description=Ylläpitotilin luominen on valinnaista. Ensimmäisestä rekisteröityneestä käyttäjästä tulee automaattisesti ylläpitäjä. +admin_title=Ylläpitotilin asetukset +admin_name=Ylläpitäjän käyttäjänimi admin_password=Salasana confirm_password=Varmista salasana admin_email=Sähköpostiosoite @@ -326,46 +326,46 @@ default_keep_email_private.description=Piilota oletusarvoisesti uusien käyttäj default_enable_timetracking=Ota ajanseuranta oletusarvoisesti käyttöön default_enable_timetracking.description=Salli ajanseuranta-ominaisuuden käyttöönotto oletuksena uusille tietovarastoille. no_reply_address=Piilotetun sähköpostin toimialue -no_reply_address_helper=Verkkotunnuksen nimi käyttäjille, joilla on piilotettu sähköpostiosoite. Esimerkiksi käyttäjätunnus 'joe' kirjataan Git-palveluun nimellä 'joe@noreply.example.org' jos piilotetun sähköpostiosoitteen arvoksi on asetettu 'noreply.example.org'. +no_reply_address_helper=Verkkotunnuksen nimi käyttäjille, joilla on piilotettu sähköpostiosoite. Esimerkiksi käyttäjänimi 'joe' kirjataan Git-palveluun nimellä 'joe@noreply.example.org' jos piilotetun sähköpostiosoitteen arvoksi on asetettu 'noreply.example.org'. password_algorithm=Salasanan hajautusalgoritmi -enable_update_checker_helper_forgejo = Se tarkistaa tietyin väliajoin uusia Forgejo-versioita tutkimalla sen TXT DNS record -tietoja osoitteesta release.forgejo.org . -invalid_admin_setting = Järjestelmänvalvojatilin asetukset eivät kelpaa: %v +enable_update_checker_helper_forgejo = Se tarkistaa väliajoin uusia Forgejo-versioita tutkimalla TXT DNS -tietueen osoitteesta release.forgejo.org . +invalid_admin_setting = Ylläpitotilin asetukset eivät kelpaa: %v env_config_keys = Ympäristökonfiguraatio run_user_helper = Käyttöjärjestelmätason käyttäjänimi, jona Forgejo ajetaan. Huomaa, että kyseinen käyttäjän on oltava pääsy tietovaraston juuripolkuun. -env_config_keys_prompt = Seuraavat ympäristömuuttujat sisällytetään myös asetustiedostoonne: +env_config_keys_prompt = Seuraavat ympäristömuuttujat sisällytetään myös asetustiedostoon: secret_key_failed = Salausavaimen generointi epäonnistui: %v default_allow_create_organization.description = Salli organisaatioiden luonti uusille käyttäjille oletuksena. Järjestelmänvalvojan tarvitsee antaa lupa luoda organisaatioita mikäli tämä asetus on pois päältä. config_location_hint = Tallennettujen asetusten sijainti: invalid_db_table = Tietokantataulu "%s" ei kelpaa: %v -invalid_password_algorithm = Salasananhajautusalgoritmi ei kelpaa -password_algorithm_helper = Aseta salasananhajautusalgoritmi. Eri algoritmeilla on erilaisia vaatimuksia ja vahvuuksia - argon2-algoritmi on erityisen turvallinen mutta vaatii paljon muistia ja voi näin ollen olla pienille järjestelmille soveltumaton. +invalid_password_algorithm = Salasanan hajautusalgoritmi on virheellinen +password_algorithm_helper = Aseta salasanan hajautusalgoritmi. Eri algoritmeilla on erilaisia vaatimuksia ja vahvuuksia. Argon2-algoritmi on erityisen turvallinen, mutta vaatii paljon muistia ja voi siten olla pienille järjestelmille soveltumaton. db_schema_helper = Jätä tyhjäksi käyttääksesi oletusarvoa ("public"). run_user_not_match = Tämänhetkinen käyttäjänimi ei täsmää tiettynä käyttäjänä ajettavan käyttäjänimen kanssa: %s -> %s invalid_log_root_path = Lokitiedoston polku ei kelpaa: %v require_sign_in_view.description = Rajoita sisältö vain kirjautuneille. Vieraat pääsevät vain autentikaatiosivuille. allow_only_external_registration = Salli rekisteröinti vain ulkoisia palveluja käyttäen default_allow_create_organization = Salli organisaatioiden luonti oletuksena -allow_dots_in_usernames = Salli pisteiden käyttö käyttäjänimissä. Ei vaikuta olemassaoleviin käyttäjiin. +allow_dots_in_usernames = Salli pisteiden käyttö käyttäjänimissä. Ei vaikuta olemassa oleviin käyttäjiin. enable_update_checker = Ota päivitystentarkistus käyttöön -app_slogan = Ilmentymän tunnuslause -app_slogan_helper = Syötä ilmentymän tunnuslause tähän. Jätä tyhjäksi poistaaksesi käytöstä. +app_slogan = Instanssin tunnuslause +app_slogan_helper = Syötä instanssin tunnuslause tähän. Jätä tyhjäksi poistaaksesi käytöstä. domain_helper = Palvelimen toimialue tai isäntänimi. -smtp_from_invalid = "Lähetä sähköpostit osoitteella" -osoite on epäkelvollinen -err_admin_name_pattern_not_allowed = Järjestelmänvalvojan käyttäjänimi on epäkelvollinen, se vastaa varattua kaavaa +smtp_from_invalid = "Lähetä sähköpostit osoitteella"-osoite on epäkelvollinen +err_admin_name_pattern_not_allowed = Ylläpitäjän käyttäjänimi on epäkelpo, se vastaa varattua kaavaa [home] -uname_holder=Käyttäjätunnus tai sähköpostiosoite +uname_holder=käyttäjänimi tai sähköpostiosoite password_holder=Salasana switch_dashboard_context=Vaihda kojelaudan kontekstia -my_repos=Repot +my_repos=Tietovarastot show_more_repos=Näytä lisää repoja… collaborative_repos=Yhteistyö repot -my_orgs=Organisaationi +my_orgs=Organisaatiot my_mirrors=Peilini view_home=Näytä %s search_repos=Etsi repo… filter=Muut suodattimet -filter_by_team_repositories=Suodata tiimin repojen mukaan +filter_by_team_repositories=Suodata tiimin tietovarastojen mukaan feed_of=`Syöte "%s"` show_archived=Arkistoidut @@ -378,10 +378,10 @@ show_both_private_public=Näytetään sekä julkiset että yksityiset show_only_private=Näytetään vain yksityiset show_only_public=Näytetään vain julkiset -issues.in_your_repos=Repoissasi +issues.in_your_repos=Tietovarastoissasi [explore] -repos=Repot +repos=Tietovarastot users=Käyttäjät organizations=Organisaatiot search=Hae @@ -395,43 +395,43 @@ code_last_indexed_at=Viimeksi indeksoitu %s stars_one = %d tähti stars_few = %d tähteä relevant_repositories = Vain asiaankuuluvat tietovarastot näytetään, näytä suodattamattomat tulokset. -forks_one = %d haarukka -forks_few = %d haarukkaa +forks_one = %d forkki +forks_few = %d forkkia go_to = Siirry -relevant_repositories_tooltip = Tietovarastot, jotka ovat haarukoita tai joilla ei ole aihetta, kuvaketta tai kuvausta, piilotetaan. +relevant_repositories_tooltip = Tietovarastot, jotka ovat forkkeja tai joilla ei ole aihetta, kuvaketta tai kuvausta, piilotetaan. [auth] create_new_account=Rekisteröi tili register_helper_msg=On jo tili? Kirjaudu sisään nyt! social_register_helper_msg=Onko sinulla jo tili? Linkitä se nyt! -disable_register_prompt=Rekisteröinti on estetty. Ota yhteys ylläpitäjääsi. +disable_register_prompt=Rekisteröinti on estetty. Ota yhteys sivuston ylläpitäjään. disable_register_mail=Sähköpostivahvistus rekisteröinnille on estetty. remember_me=Muista tämä laite forgot_password_title=Unohtuiko salasana forgot_password=Unohtuiko salasana? sign_up_now=Tarvitsetko tilin? Rekisteröidy nyt. -confirmation_mail_sent_prompt=Uusi varmistussähköposti on lähetetty osoitteeseen %s. Tarkista sähköpostisi ja seuraa saamaasi linkkiä seuraavan %s aikana saadaksesi rekisteröintiprosessin valmiiksi. Mikäli annettu sähköpostiosoite on väärin, voit kirjautua sisään ja pyytää uutta varmistussähköpostia toiseen osoitteeseen. +confirmation_mail_sent_prompt=Uusi vahvistussähköposti on lähetetty osoitteeseen %s. Tarkista sähköpostisi ja seuraa saamaasi linkkiä seuraavan %s aikana viimeistelläksesi rekisteröinnin. Mikäli annettu sähköpostiosoite on väärin, voit kirjautua sisään ja pyytää uutta vahvistussähköpostia toiseen osoitteeseen. must_change_password=Vaihda salasanasi allow_password_change=Vaadi käyttäjää vaihtamaan salasanansa (suositeltava) -reset_password_mail_sent_prompt=Varmistussähköposti on lähetetty osoitteeseen %s. Tarkista sähköpostisi ja seuraa annettua linkkiä seuraavan %s aikana saadaksesi tilin palauttamisen valmiiksi. +reset_password_mail_sent_prompt=Vahvistussähköposti on lähetetty osoitteeseen %s. Tarkista sähköpostisi ja seuraa annettua linkkiä seuraavan %s aikana saadaksesi tilin palauttamisen valmiiksi. active_your_account=Aktivoi tilisi account_activated=Tili on aktivoitu prohibit_login=Tili on jäädytetty -resent_limit_prompt=Olet jo tilannut aktivointisähköpostin hetki sitten. Ole hyvä ja odota 3 minuuttia ja yritä sitten uudelleen. -has_unconfirmed_mail=Hei %s, sinulla on varmistamaton sähköposti osoite (%s). Jos et ole saanut varmistus sähköpostia tai tarvitset uudelleenlähetyksen, ole hyvä ja klikkaa allaolevaa painiketta. -resend_mail=Klikkaa tästä uudelleenlähettääksesi aktivointi sähköpostisi +resent_limit_prompt=Olet jo tilannut aktivointisähköpostin äskettäin. Odota kolme minuuttia ja yritä sitten uudelleen. +has_unconfirmed_mail=Hei %s, sinulla on vahvistamaton sähköpostiosoite (%s). Jos et ole saanut vahvistussähköpostia tai tarvitset uudelleenlähetyksen, ole hyvä ja napsauta alla olevaa painiketta. +resend_mail=Napsauta tästä lähettääksesi aktivointisähköpostin uudelleen email_not_associate=Tätä sähköpostiosoitetta ei ole liitetty mihinkään tiliin. send_reset_mail=Lähetä palautussähköposti reset_password=Tilin palautus -invalid_code=Vahvistusavain on virheellinen tai vanhentunut. +invalid_code=Vahvistuskoodi on virheellinen tai vanhentunut. reset_password_helper=Palauta käyttäjätili password_too_short=Salasanan pituus ei voi olla vähemmän kuin %d merkkiä. -non_local_account=Ei-lokaalit käyttäjät eivät voi päivittää salasanojaan Forgejon web-käyttöliittymän kautta. +non_local_account=Muut kuin lokaalit käyttäjät eivät voi päivittää salasanojaan Forgejo-selainkäyttöliittymän kautta. verify=Vahvista scratch_code=Kertakäyttökoodi use_scratch_code=Käytä kertakäyttökoodia twofa_scratch_used=Olet käyttänyt kertakäyttökoodisi. Sinut on uudelleenohjattu kaksivaiheisen kirjautumisen asetussivulle, jotta voit kytkeä sen pois tai luoda uuden kertakäyttökoodin. -twofa_passcode_incorrect=Salasanasi on väärä. Jos olet hukannut laitteesi, käytäthän kertakäyttökoodia sisäänkirjautumiseen. +twofa_passcode_incorrect=Pääsykoodi on väärä. Jos olet hukannut laitteesi, käytäthän kertakäyttökoodia sisäänkirjautumiseen. twofa_scratch_token_incorrect=Kertakäyttökoodisi on virheellinen. login_userpass=Kirjaudu sisään tab_openid=OpenID @@ -440,26 +440,26 @@ oauth_signup_title=Viimeistele uusi tili oauth_signup_submit=Viimeistele tili oauth_signin_tab=Linkitä olemassa olevaan tiliin oauth_signin_title=Kirjaudu sisään valtuuttaaksesi linkitetyn tilin -oauth_signin_submit=Yhdistä tiliin +oauth_signin_submit=Linkitä tili oauth.signin.error.access_denied=Valtuutuspyyntö on evätty. -openid_connect_submit=Connect -openid_connect_title=Yhdistä olemassaolevaan tiliin +openid_connect_submit=Yhdistä +openid_connect_title=Yhdistä olemassa olevaan tiliin openid_connect_desc=Valittu OpenID-osoite on tuntematon. Liitä se uuteen tiliin täällä. openid_register_title=Luo uusi tili openid_register_desc=Valittu OpenID-osoite on tuntematon. Liitä se uuteen tiliin täällä. email_domain_blacklisted=Et voi rekisteröityä sähköpostiosoittellasi. authorize_application=Valtuuta sovellus authorize_redirect_notice=Sinut uudelleen ohjataan osoitteeseen %s jos valtuutat tämän sovelluksen. -authorize_application_created_by=Tämän sovelluksen on luonnut %s. +authorize_application_created_by=Tämän sovelluksen on luonut %s. authorize_application_description=Jos myönnät valtuuden, se pystyy pääsemään kaikkiin tilitietoihisi ja kirjoittamaan niihin, mukaan lukien yksityiset tietovarastot ja organisaatiot. authorize_title=Valtuutatko "%s" pääsemään tilillesi? -authorization_failed=Käyttöoikeuden varmistus epäonnistui +authorization_failed=Valtuuttaminen epäonnistui sspi_auth_failed=SSPI todennus epäonnistui sign_up_successful = Käyttäjätili luotiin onnistuneesti. Tervetuloa! hint_login = Onko sinulla jo käyttäjätili? Kirjaudu sisään! hint_register = Tarvitsetko käyttäjätilin? Rekisteröidy nyt. sign_up_button = Rekisteröidy nyt. -manual_activation_only = Ota yhteyttä järjestelmänvalvojaanne viimeistelläksesi aktivoinnin. +manual_activation_only = Ota yhteys sivuston ylläpitoon viimeistelläksesi aktivoinnin. change_unconfirmed_email_error = Sähköpostiosoitteen vaihtaminen ei onnistu: %v invalid_password = Salasanasi ei vastaa tilin luomisen yhteydessä käytettyä salasanaa. back_to_sign_in = Takaisin kirjautumiseen @@ -471,19 +471,19 @@ change_unconfirmed_email_summary = Vaihda sähköpostiosoite, johon aktivointis reset_password_wrong_user = Olet kirjautuneena tilillä %s, mutta tilin palautuslinkki on tarkoitettu tilille %s last_admin = Et voi poistaa viimeistä ylläpitäjää. Ylläpitäjiä tulee olla vähintään yksi. password_pwned = Valitsemasi salasana on varastettujen salasanojen listalla, eli se on paljastanut jossain julkisessa tietovuodossa. Kokeile asettaa eri salasana, ja jos käytät samaa salasanaa muissa palveluissa, vaihda kyseinen salasana. -use_onetime_code = Käytä kertakäyttöiskoodia -unauthorized_credentials = Valtuustiedot ovat epäkelvolliset tai umpeutuneet. Yritä komentoa uudelleen tai katso lisätietoja kohdasta %s +use_onetime_code = Käytä kertakäyttökoodia +unauthorized_credentials = Kirjautumistiedot ovat virheelliset tai vanhentuneet. Yritä suorittaa komento uudelleen tai katso %s saadaksesi lisätietoja oauth.signin.error.temporarily_unavailable = Valtuus epäonnistui, koska todennuspalvelin ei ole tällä hetkellä käytettävissä. Yritä uudelleen myöhemmin. disable_forgot_password_mail = Tilin palautus ei ole käytössä, koska sähköpostia ei ole määritetty. Ota yhteys sivuston ylläpitoon. password_pwned_err = Pyyntöä HaveIBeenPwned-palveluun ei voitu suorittaa authorization_failed_desc = Valtuus epäonnistui, koska havaitsimme virheellisen pyynnön. Ota yhteys sen sovelluksen ylläpitäjään, jota yritit valtuuttaa. -oauth.signin.error = Valtuutuspyynnön käsittelyssä tapahtui virhe. Jos tämä virhe jatkuu, ota yhteyttä sivuston järjestelmänvalvojaan. +oauth.signin.error = Valtuuspyynnön käsittelyssä tapahtui virhe. Jos virhe toistuu, ota yhteys sivuston ylläpitoon. disable_forgot_password_mail_admin = Tilin palautus on käytössä vain, jos sähköposti on määritetty. Aseta sähköposti, jotta tilin palauttaminen on mahdollista ottaa käyttöön. -prohibit_login_desc = Tilisi käyttö ilmentymän kanssa on estetty. Ota yhteyttä ilmentymän järjestelmänvalvojaan saadaksesi pääsyn takaisin. +prohibit_login_desc = Tilisi käyttö instanssin kanssa on estetty. Ota yhteyttä instanssin ylläpitoon saadaksesi pääsyn takaisin. [mail] view_it_on=Näytä %s -link_not_working_do_paste=Eikö linkki toimi? Yritä kopioida ja liittää se selaimesi osoitepalkkiin. +link_not_working_do_paste=Eikö linkki toimi? Kopioi ja liitä se selaimesi osoiteriville. hi_user_x=Hei %s, activate_account=Ole hyvä ja aktivoi tilisi @@ -491,7 +491,7 @@ activate_account=Ole hyvä ja aktivoi tilisi activate_email=Vahvista sähköpostiosoitteesi register_notify=Tervetuloa %s-palveluun -register_notify.text_2=Voit nyt kirjautua tilillesi käyttäjätunnuksella: %s +register_notify.text_2=Voit nyt kirjautua tilillesi käyttäjänimellä: %s reset_password=Palauta käyttäjätili reset_password.title=%s, olet pyytänyt tilisi palauttamista @@ -500,8 +500,8 @@ register_success=Rekisteröinti onnistui issue.x_mentioned_you=@%s mainitsi sinut: -issue.action.push_1=@%[1]s työnsi %[3]d commitin kohteeseen %[2]s -issue.action.push_n=@%[1]s työnsi %[3]d committia kohteeseen %[2]s +issue.action.push_1=@%[1]s työnsi %[3]d kommitin kohteeseen %[2]s +issue.action.push_n=@%[1]s työnsi %[3]d kommittia kohteeseen %[2]s issue.action.reject=@%[1]s pyysi muutoksia tässä vetopyynnössä. release.title=Otsikko: %s @@ -517,7 +517,7 @@ removed_security_key.subject = Turva-avain on poistettu removed_security_key.text_1 = Turva-avain "%[1]s" on poistettu tililtäsi. team_invite.text_2 = Napsauta seuraavaa linkkiä liittyäksesi tiimiin: activate_account.text_1 = Hei %[1]s, kiitos kun rekisteröidyit palveluun %[2]s! -activate_account.text_2 = Aktivoidaksesi tilin, napsauta alla olevaa linkkiä aikaikkunan %s sisällä: +activate_account.text_2 = Aktivoi tilisi napsauttamalla alla olevaa linkkiä aikaikkunan %s sisällä: totp_disabled.subject = TOTP on poistettu käytöstä primary_mail_change.subject = Ensisijainen sähköpostiosoitteesi on vaihdettu admin.new_user.user_info = Käyttäjätiedot @@ -540,25 +540,25 @@ admin.new_user.text = Napsauta tästä hallitaksesi tätä käy repo.collaborator.added.text = Sinut on lisätty avustajaksi tietovarastoon: primary_mail_change.text_1 = Tilisi ensisijaiseksi sähköpostiosoitteeksi asetettiin %[1]s. Se tarkoittaa, että tämä sähköpostiosoite ei enää vastaanota tilisi ilmoituksia sähköpostitse. team_invite.text_1 = %[1]s on kutsunut sinut liittymään tiimiin %[2]s organisaatiossa %[3]s. -issue_assigned.pull = @%[1]s osoitti sinulle vetopyynnön %[2]stietovarastossa %[3]s. -issue_assigned.issue = @%[1]s osoitti sinulle vianlipun %[2]s tietovarastossa %[3]s. +issue_assigned.pull = @%[1]s osoitti sinulle vetopyynnön %[2]s tietovarastossa %[3]s. +issue_assigned.issue = @%[1]s osoitti sinulle ongelman %[2]s tietovarastossa %[3]s. register_notify.text_1 = tämä on %s:n rekistöröitymisen vahvistussähköposti! -reset_password.text = jos tämä oli sinun toimestasi, ole hyvä ja klikkaa oheista linkkiä palauttaaksesi tilisi %s sisällä: -totp_disabled.no_2fa = Muita kaksivaiheisen tunnistautumisen menetelmiä ei ole konfiguroituna, joten et tarvitse kaksivaiheista tunnistautumista kirjautuaaksesi tilillesi. +reset_password.text = jos se olit sinä, napsauta oheista linkkiä palauttaaksesi tilisi %s sisällä: +totp_disabled.no_2fa = Muita kaksivaiheisen todennuksen menetelmiä ei ole määritetty, joten et enää tarvitse kaksivaiheista todennusta kirjautuaksesi tilillesi. totp_enrolled.subject = Olet aktivoinut TOTP:in kaksivaiheisen todennuksen menetelmäksi -totp_enrolled.text_1.no_webauthn = Otit TOTP:n käyttöön tilillesi. Tämä tarkoittaa, että kirjauduttaessa sisään tilillesi sinun täytyy käyttää TOTP menetelmää kaksivaiheisena tunnistautumisena. -team_invite.text_3 = Huomaa: Tämä kutsu on tarkoitettu %[1]s:lle. Jos et odottanut tätä kutsua, voit jättää tämän sähköpostin huomiotta. -removed_security_key.no_2fa = Yhtään kaksivaiheisen tunnistautumisen menetelmää ei ole määritelty, joten tilillesi ei enää tarvitse kirjautua kaksivaiheisella tunnistautumisella. -totp_enrolled.text_1.has_webauthn = Otit TOTP:n käyttöön tilillesi. Tämä tarkoittaa, että kirjauduttaessa sisään tilillesi voit käyttää TOTP menetelmää kaksivaiheisena tunnistautumisena tai mitä tahansa turva-avaimiasi. -repo.collaborator.added.subject = %s lisäsi sinut %s yhteistyökumppaniksi -release.new.text = @%[1]s julkaistu %[2]s %[3]s:ssa -issue.action.review_dismissed = @%[1]s hylkäsi %[2]s:n viimeisimmän arvion tälle vetopyynnölle. +totp_enrolled.text_1.no_webauthn = Otit TOTP:n käyttöön tilillesi. Tämä tarkoittaa, että kirjautuessasi tilillesi sinun täytyy käyttää TOTP-menetelmää kaksivaiheisena todennuksena. +team_invite.text_3 = Huomaa: Tämä kutsu on tarkoitettu käyttäjälle %[1]s. Jos et odottanut tätä kutsua, voit jättää tämän sähköpostin huomiotta. +removed_security_key.no_2fa = Yhtäkään kaksivaiheisen todennuksen menetelmää ei ole määritelty, joten tilillesi ei enää tarvitse kirjautua kaksivaiheisella todennuksella. +totp_enrolled.text_1.has_webauthn = Otit TOTP:n käyttöön tilillesi. Tämä tarkoittaa, että kirjautuessasi tilillesi voit käyttää TOTP-menetelmää kaksivaiheisena tunnistautumisena tai mitä tahansa turva-avaintasi. +repo.collaborator.added.subject = %s lisäsi sinut tietovaraston %s avustajaksi +release.new.text = @%[1]s julkaisi %[2]s projektissa %[3]s +issue.action.review_dismissed = @%[1]s hylkäsi viimeisimmän katselmoinnin taholta %[2]s tälle vetopyynnölle. release.new.subject = %s %s:ssa julkaistu issue.in_tree_path = %s:ssa: -issue.action.force_push = %[1]s pakkotyönnetty %[2]s %[3]s:sta %[4]s:hen. -issue.action.merge = @%[1]s yhdistetty #%[2]d %[3]s:hen. -repo.transfer.subject_to = %s haluaa siirtää tietovaraston "%s" %s:hen -repo.transfer.body = Hyväksy tai hylkää se käymällä %s:lla tai jätä se huomiotta. +issue.action.force_push = %[1]s pakkotyönsi %[2]s lähteestä %[3]s kohteeseen %[4]s:hen. +issue.action.merge = @%[1]s yhdisti kohteen #%[2]d kohteeseen %[3]s. +repo.transfer.subject_to = %s haluaa siirtää tietovaraston "%s" taholle %s +repo.transfer.body = Hyväksy tai hylkää se käymällä %s tai jätä se huomiotta. @@ -566,13 +566,13 @@ repo.transfer.body = Hyväksy tai hylkää se käymällä %s:lla tai jätä se h yes=Kyllä no=Ei cancel=Peruuta -modify=Päivitys +modify=Päivitä confirm = Vahvista [form] -UserName=Käyttäjätunnus -RepoName=Repon nimi -Email=Sähköposti osoite +UserName=Käyttäjänimi +RepoName=Tietovaraston nimi +Email=Sähköpostiosoite Password=Salasana Retype=Vahvista salasana SSHTitle=SSH avain nimi @@ -582,9 +582,9 @@ AuthName=Luvan nimi AdminEmail=Ylläpitäjän sähköposti NewBranchName=Uuden haaran nimi -CommitSummary=Commitin yhteenveto -CommitMessage=Commitin viesti -CommitChoice=Commitin valinta +CommitSummary=Kommitin yhteenveto +CommitMessage=Kommitointiviesti +CommitChoice=Kommitin valinta TreeName=Tiedostopolku Content=Sisältö @@ -597,18 +597,18 @@ min_size_error=` täytyy sisältää vähintään %s merkkiä.` max_size_error=` täytyy sisältää enintään %s merkkiä.` email_error=` ei ole kelvollinen sähköpostiosoite.` unknown_error=Tuntematon virhe: -captcha_incorrect=CAPTCHA koodi on virheellinen. +captcha_incorrect=CAPTCHA-koodi on virheellinen. password_not_match=Salasanat eivät täsmää. lang_select_error=Valitse kieli listalta. -username_been_taken=Käyttäjätunnus on jo varattu. -repo_name_been_taken=Repon nimi on jo käytössä. -repository_force_private=Pakotettu yksityisyys käytössä: yksityisiä repoja ei voida muuttaa julkisiksi. +username_been_taken=Käyttäjänimi on jo varattu. +repo_name_been_taken=Tietovaraston nimi on jo käytössä. +repository_force_private=Pakotettu yksityisyys käytössä: yksityisiä tietovarastoja ei voida muuttaa julkisiksi. org_name_been_taken=Organisaation nimi on jo käytössä. team_name_been_taken=Tiimin nimi on jo varattu. email_been_used=Sähköpostiosoite on jo käytössä. email_invalid=Sähköpostiosoite on virheellinen. -username_password_incorrect=Käyttäjätunnus tai salasana on virheellinen. +username_password_incorrect=Käyttäjänimi tai salasana on virheellinen. password_lowercase_one=Ainakin yksi pieni kirjan password_uppercase_one=Ainakin yksi iso kirjain password_digit_one=Ainakin yksi numero @@ -624,7 +624,7 @@ invalid_gpg_key=GPG-avaintasi ei voi vahvistaa: %s auth_failed=Todennus epäonnistui: %v -target_branch_not_exist=Kohde branchia ei ole olemassa. +target_branch_not_exist=Kohdehaaraa ei ole olemassa. Pronouns = Pronomini FullName = Koko nimi Description = Kuvaus @@ -634,7 +634,7 @@ Location = Sijainti To = Haaran nimi still_own_repo = Tilisi omistaa yhden tai useamman tietovaraston; poista tai siirrä ne ensin. organization_leave_success = Olet poistunut organisaatiosta %s. -enterred_invalid_repo_name = Syöttämäsi tietovaraston nimi on epäkelvollinen. +enterred_invalid_repo_name = Syöttämäsi tietovaraston nimi on epäkelpo. openid_been_used = OpenID-osoite "%s" on jo käytetty. password_complexity = Salasana ei täytä monimutkaisuusvaatimuksia: still_has_org = Tilisi on jäsen yhdessä tai useamassa organisaatiossa, poistu niistä ensin. @@ -654,11 +654,24 @@ regex_pattern_error = `regex-kuvio on epäkelvollinen: %s.` username_error_no_dots = `voi sisältää vain aakkosnumeerisia merkkejä ("0–9", "a–z", "A–Z"), yhdysviivaa ("-") ja alaviivaa ("_"). Se ei voi alkaa tai päättyä muihin kuin aakkosnumeerisiin merkkeihin, ja peräkkäiset muut kuin aakkosnumeeriset merkit ovat myös kiellettyjä.` PayloadUrl = Hyötykuorman URL-osoite repository_files_already_exist.adopt = Tälle tietovarastolle on jo olemassa tiedostoja, ja ne voidaan vain omaksua. +repository_files_already_exist = Tässä tietovarastossa on jo tiedostoja. Ota yhteys järjestelmän ylläpitäjään. +last_org_owner = Et voi poistaa viimeistä käyttäjää "owners"-tiimistä. Organisaatiolla tulee olla vähintään yksi omistaja. +required_prefix = Syötteen tulee alkaa "%s" +repository_files_already_exist.delete = Tässä tietovarastossa on jo tiedostoja. Poista ne. +duplicate_invite_to_team = Käyttäjä oli jo kutsuttu tiimin jäseneksi. +org_still_own_repo = Organisaatio omistaa yhden tai useamman tietovaraston. Poista tai siirrä ne ensin. +org_still_own_packages = Organisaatio omistaa yhden tai useamman paketin. Poista ne ensin. +team_no_units_error = Salli pääsy vähintään yhteen tietovaraston osioon. +repository_files_already_exist.adopt_or_delete = Tässä tietovarastossa on jo tiedostoja. Omaksu ne itsellesi tai poista ne. +username_change_not_local_user = Ei-paikallisten käyttäjien ei sallita vaihtaa käyttäjänimeä. +admin_cannot_delete_self = Et voi poistaa itseäsi, kun olet ylläpitäjä. Poista ensin ylläpito-oikeudet itseltäsi. +username_claiming_cooldown = Käyttäjänimeä ei voi ottaa käyttöön, koska siihen kohdistuva suojaamisjakso ei ole vielä päättynyt. Käyttäjänimen voi ottaa käyttöön %[1]s. +email_domain_is_not_allowed = Käyttäjän sähköpostiosoitteen %s verkkotunnus on ristiriidassa EMAIL_DOMAIN_ALLOWLIST:in tai EMAIL_DOMAIN_BLOCKLIST:in kanssa. Varmista, että olen asettanut sähköpostiosoitteen oikein. [user] change_avatar=Vaihda profiilikuvasi… -repositories=Repot +repositories=Tietovarastot activity=Julkinen toiminta followers_few=%d seuraajaa starred=Tähdelliset tietovarastot @@ -685,13 +698,18 @@ unblock = Poista esto following_one = %d seurataan block_user.detail = Huomaa, että käyttäjän estämisellä on muita vaikutuksia, kuten: show_on_map = Näytä paikka kartalla -form.name_chars_not_allowed = Käyttäjätunnus "%s" sisältää virheellisiä merkkejä. +form.name_chars_not_allowed = Käyttäjänimi "%s" sisältää virheellisiä merkkejä. follow_blocked_user = Et voi seurata tätä käyttäjää, koska olet estänyt kyseisen käyttäjän tai kyseinen käyttäjä on estänyt sinut. disabled_public_activity = Käyttäjä on poistanut käytöstä toiminnan julkisen näkyvyyden. -form.name_reserved = Käyttäjätunnus "%s" on varattu. -form.name_pattern_not_allowed = Kaava "%s" ei ole sallittu käyttäjätunnuksessa. +form.name_reserved = Käyttäjänimi "%s" on varattu. +form.name_pattern_not_allowed = Kaava "%s" ei ole sallittu käyttäjänimessä. public_activity.visibility_hint.admin_private = Aktiivisuus on näkyvissä sinulle, koska olet ylläpitäjä, mutta käyttäjä haluaa pitää aktiivisuutensa yksityisenä. public_activity.visibility_hint.self_private_profile = Aktiivisuutesi on näkyvissä vain sinulle ja instanssin ylläpitäjille, koska profiilisi on yksityinen. Määritä. +watched = Tarkaillut tietovarastot +block_user.detail_2 = Tämä käyttäjä ei voi olla vuorovaikutuksessa omistamiesi tietovarastojen kanssa, tai luomiesi ongelmien ja kommenttien kanssa. +block_user.detail_3 = Ette voi lisätä toisianne tietovaraston avustajiksi. +block_user.detail_1 = Lopetatte toistenne seuraamisen, ettekä pysty enää seurata toisianne. +public_activity.visibility_hint.admin_public = Tämä aktiviteetti on näkyvissä kaikille, mutta ylläpitäjänä voit nähdä myös vuorovaikutukset yksityisissä tiloissa. [settings] @@ -705,7 +723,7 @@ ssh_gpg_keys=SSH-/GPG-avaimet social=Sosiaaliset tilit applications=Sovellukset orgs=Organisaatiot -repos=Repot +repos=Tietovarastot delete=Poista tili twofa=Kaksivaiheinen todennus (TOTP) account_link=Linkitetyt tilit @@ -713,7 +731,7 @@ organization=Organisaatiot webauthn=Kaksivaiheinen todennus (Turva-avaimet) public_profile=Julkinen profiili -password_username_disabled=Ei-paikalliset käyttäjät eivät voi muuttaa käyttäjätunnustaan. Ole hyvä ja ota yhteyttä sivuston ylläpitäjään saadaksesi lisätietoa. +password_username_disabled=Ei-paikalliset käyttäjät eivät voi muuttaa käyttäjänimeään. Ota yhteys sivuston ylläpitoon saadaksesi lisätietoa. full_name=Koko nimi website=Verkkosivusto location=Sijainti @@ -722,14 +740,14 @@ update_profile=Päivitä profiili update_language=Vaihda kieli update_language_success=Kieli on päivitetty. update_profile_success=Profiilisi on päivitetty. -change_username=Käyttäjätunnuksesi on muutettu. +change_username=Käyttäjänimesi on muutettu. continue=Jatka cancel=Peruuta language=Kieli ui=Teema hidden_comment_types=Piilotetut kommenttityypit comment_type_group_reference=Viittaus -comment_type_group_label=Tunniste +comment_type_group_label=Nimilappu comment_type_group_milestone=Merkkipaalu comment_type_group_assignee=Osoitettu henkilölle comment_type_group_title=Otsikko @@ -738,8 +756,8 @@ comment_type_group_time_tracking=Ajanseuranta comment_type_group_deadline=Määräaika comment_type_group_dependency=Riippuvuus comment_type_group_lock=Lukituksen tila -comment_type_group_review_request=Arviointipyyntö -comment_type_group_pull_request_push=Lisätyt commitit +comment_type_group_review_request=Katselmointipyyntö +comment_type_group_pull_request_push=Lisätyt kommitit comment_type_group_project=Projekti saved_successfully=Asetuksesi tallennettiin onnistuneesti. privacy=Yksityisyys @@ -757,7 +775,7 @@ update_avatar_success=Profiilikuva on päivitetty. update_password=Päivitä salasana old_password=Nykyinen salasana new_password=Uusi salasana -password_incorrect=Nykyinen salasanasi on virheellinen. +password_incorrect=Nykyinen salasana on virheellinen. password_change_disabled=Ei-lokaalit käyttäjät eivät voi päivittää salasanojaan Forgejon web-käyttöliittymän kautta. emails=Sähköposti osoitteet @@ -768,12 +786,12 @@ theme_desc=Tätä teemaa käytetään verkkosivuston käyttöliittymässä, kun primary=Ensisijainen activated=Aktivoitu requires_activation=Vaatii aktivoinnin -primary_email=Tee ensisijainen +primary_email=Aseta ensisijaiseksi activate_email=Lähetä aktivointi activations_pending=Odottaa aktivointia delete_email=Poista email_deletion=Poista sähköpostiosoite -email_deletion_desc=Sähköpostiosoite ja siihen liittyvät tiedot poistetaan tililtäsi. Kyseisen sähköpostiosoitteen sisältävät commitit pysyvät muuttumattomia. Jatketaanko? +email_deletion_desc=Sähköpostiosoite ja siihen liittyvät tiedot poistetaan tililtäsi. Kyseisen sähköpostiosoitteen sisältävät kommitit pysyvät muuttumattomia. Jatketaanko? email_deletion_success=Sähköpostiosoite on poistettu. theme_update_success=Teemasi on päivitetty. theme_update_error=Valittua teemaa ei löydy. @@ -787,48 +805,48 @@ add_email_success=Uusi sähköpostiosoite on lisätty. email_preference_set_success=Sähköpostin asetukset on asetettu onnistuneesti. add_openid_success=Uusi OpenID-osoite on lisätty. keep_email_private=Piilota sähköpostiosoite -openid_desc=OpenID mahdollistaa todentamisen delegoinnin ulkopuoliselle palvelun tarjoajalle. +openid_desc=OpenID mahdollistaa todentamisen delegoinnin ulkopuoliselle palveluntarjoajalle. manage_ssh_keys=Hallitse SSH-avaimia manage_gpg_keys=Hallitse GPG-avaimia add_key=Lisää avain -ssh_desc=Nämä julkiset SSH-avaimet on liitetty tiliisi. Vastaavat yksityiset avaimet antavat täyden pääsyn tietovarastoihisi. Vahvistettuja SSH-avaimia voidaan käyttää SSH-allekirjoitettujen Git-sitoumusten varmentamiseen. -gpg_desc=Nämä julkiset GPG-avaimet liitetään tiliisi ja niitä käytetään sitoumuksesi vahvistamiseen. Pidä yksityiset avaimesi turvassa, koska niiden avulla voit allekirjoittaa sitoumuksia henkilöllisyytesi kanssa. +ssh_desc=Nämä julkiset SSH-avaimet on liitetty tiliisi. Vastaavat yksityiset avaimet antavat täyden pääsyn tietovarastoihisi. Vahvistettuja SSH-avaimia voidaan käyttää SSH-allekirjoitettujen Git-kommittien varmentamiseen. +gpg_desc=Nämä julkiset GPG-avaimet liitetään tiliisi ja niitä käytetään tekemiesi kommittien vahvistamiseen. Pidä yksityiset avaimesi turvassa, koska niiden avulla voit allekirjoittaa kommitteja henkilöllisyytesi kanssa. ssh_helper=Tarvitsetko apua? Tutustu GitHubin oppaaseen omien SSH-avainten luonnista tai yleisistä ongelmista, joita voit kohdata SSH:n kanssa. gpg_helper=Tarvitsetko apua? Katso GitHubin opas GPG:stä. add_new_key=Lisää SSH avain add_new_gpg_key=Lisää GPG-avain key_content_ssh_placeholder=Alkaa sanoilla "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "sk-ecdsa-sha2-nistp256@openssh.com" tai "sk-ssh-ed25519@openssh.com" key_content_gpg_placeholder=Alkaa sanoilla "-----BEGIN PGP PUBLIC KEY BLOCK-----" -ssh_key_name_used=Samanniminen SSH avain on jo olemassa tililläsi. -gpg_key_id_used=Julkinen GPG-avain samalla tunnuksella on jo olemassa. -gpg_no_key_email_found=Tämä GPG-avain ei vastaa mitään tiliisi liitettyä aktivoitua sähköpostiosoitetta. Se voidaan silti lisätä, jos allekirjoitat annetun pääsymerkin. +ssh_key_name_used=SSH-avain samalla nimellä on jo olemassa tililläsi. +gpg_key_id_used=Julkinen GPG-avain samalla ID-tunnisteella on jo olemassa. +gpg_no_key_email_found=Tämä GPG-avain ei vastaa mitään tiliisi liitettyä aktivoitua sähköpostiosoitetta. Se voidaan silti lisätä, jos allekirjoitat annetun pääsypoletin. gpg_key_verified=Vahvistettu avain -gpg_key_verified_long=Avain on vahvistettu pääsymerkillä ja sitä voidaan käyttää todentamaan commitit, jotka vastaavat tämän käyttäjän aktivoituja sähköpostiosoitteita tämän avaimen kaikkien vastaavien identiteettien lisäksi. +gpg_key_verified_long=Avain on vahvistettu poletilla ja sitä voidaan käyttää vahvistamaan kommitit, jotka vastaavat tämän käyttäjän aktivoituja sähköpostiosoitteita tämän avaimen kaikkien vastaavien identiteettien lisäksi. gpg_key_verify=Vahvista -gpg_token_required=Sinun täytyy antaa allekirjoitus alla olevalle pääsymerkille -gpg_token=Pääsymerkki +gpg_token_required=Sinun täytyy antaa allekirjoitus alla olevalle poletille +gpg_token=Poletti gpg_token_help=Voit luoda allekirjoituksen käyttäen: gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Panssaroitu GPG-allekirjoitus key_signature_gpg_placeholder=Alkaa sanoilla "-----BEGIN PGP SIGNATURE-----" ssh_key_verified=Vahvistettu avain -ssh_key_verified_long=Avain on vahvistettu pääsymerkillä ja sitä voidaan käyttää todentamaan commitit, jotka vastaavat tämän käyttäjän aktivoituja sähköpostiosoitteita. +ssh_key_verified_long=Avain on vahvistettu poletilla ja sitä voidaan käyttää vahvistamaan kommitit, jotka vastaavat tämän käyttäjän aktivoituja sähköpostiosoitteita. ssh_key_verify=Vahvista -ssh_token_required=Sinun täytyy antaa allekirjoitus alla olevalle pääsymerkille -ssh_token=Pääsymerkki +ssh_token_required=Sinun täytyy antaa allekirjoitus alla olevalle poletille +ssh_token=Poletti ssh_token_help=Voit luoda allekirjoituksen käyttäen: ssh_token_signature=Panssaroitu SSH-allekirjoitus key_signature_ssh_placeholder=Alkaa sanoilla "-----BEGIN SSH SIGNATURE-----" subkeys=Aliavaimet -key_id=Avain ID +key_id=Avain-ID key_name=Avaimen nimi key_content=Sisältö principal_content=Sisältö delete_key=Poista ssh_key_deletion=Poista SSH-avain gpg_key_deletion=Poista GPG-avain -gpg_key_deletion_desc=GPG-avaimen poistaminen peruuttaa sillä allekirjoitettujen committien vahvistukset. Jatketaanko? +gpg_key_deletion_desc=GPG-avaimen poistaminen peruuttaa sillä allekirjoitettujen kommittien vahvistukset. Jatketaanko? gpg_key_deletion_success=GPG-avain on poistettu. valid_forever=Voimassa ikuisesti last_used=Käytetty viimeksi @@ -840,16 +858,16 @@ hide_openid=Piilota profiilista ssh_disabled=SSH on pois käytöstä manage_social=Hallitse liitettyjä sosiaalisia tilejä -manage_access_token=Hallitse pääsymerkkejä -generate_new_token=Luo uusi pääsymerkki -token_name=Pääsymerkin nimi -generate_token=Luo pääsymerkki -generate_token_success=Uusi pääsymerkkisi on nyt luotu. Kopioi se nyt, koska sitä ei näytetä enää uudelleen. +manage_access_token=Pääsypoletit +generate_new_token=Luo uusi poletti +token_name=Poletin nimi +generate_token=Luo pääsypoletti +generate_token_success=Uusi pääsypolettisi on nyt luotu. Kopioi se nyt, koska sitä ei näytetä enää uudelleen. delete_token=Poista -access_token_deletion=Poista pääsymerkki +access_token_deletion=Poista pääsypoletti access_token_deletion_cancel_action=Peruuta access_token_deletion_confirm_action=Poista -permission_read=Luettu +permission_read=Lue edit_oauth2_application=Muokkaa OAuth2-sovellusta remove_oauth2_application=Poista OAuth2-sovellus @@ -859,26 +877,26 @@ create_oauth2_application_button=Luo sovellus oauth2_application_name=Sovelluksen nimi save_application=Tallenna oauth2_regenerate_secret=Luo salaisuus uudelleen -oauth2_regenerate_secret_hint=Kadotitko secretin? +oauth2_regenerate_secret_hint=Kadotitko salaisuuden? oauth2_application_edit=Muokkaa twofa_desc=Kaksivaiheinen todennus parantaa tilisi turvallisuutta. -twofa_is_enrolled=Tilisi käyttää kaksivaiheista vahvistusta. -twofa_not_enrolled=Tilisi ei tällä hetkellä käytä kaksivaiheista vahvistusta. -twofa_enroll=Ilmoittaudu kaksivaiheiseen todennuksen käyttöön +twofa_is_enrolled=Tilisi käyttää kaksivaiheista todennusta. +twofa_not_enrolled=Tilisi ei tällä hetkellä käytä kaksivaiheista todennusta. +twofa_enroll=Ota kaksivaiheinen todennus käyttöön twofa_disabled=Kaksivaiheinen todennus on otettu pois käytöstä. -scan_this_image=Skannaa tämä kuva tunnistautumissovelluksellasi: +scan_this_image=Skannaa tämä kuva todennussovelluksellasi: or_enter_secret=Tai kirjoita salainen avain: %s -twofa_enrolled=Tiliisi on otettu käyttöön kaksivaiheinen vahvistus. Ota palautustunnus (%s) talteen turvalliseen paikkaan, sillä se näytetään vain kerran! +twofa_enrolled=Tiliisi on otettu käyttöön kaksivaiheinen todennus. Ota kertakäyttöinen palautusavain (%s) talteen turvalliseen paikkaan, sillä se näytetään vain kerran! webauthn_nickname=Nimimerkki -manage_account_links=Yhdistetyt tilit +manage_account_links=Linkitetyt tilit manage_account_links_desc=Nämä ulkoiset tilit on linkitetty Forgejo-tiliisi. -link_account=Yhdistä tili -remove_account_link=Poista yhdistetty tili -remove_account_link_desc=Linkitetyn tilin poistaminen peruuttaa pääsyn Forgejo-tiliisi linkitetyn tili kautta. Jatketaanko? +link_account=Linkitä tili +remove_account_link=Poista linkitetty tili +remove_account_link_desc=Linkitetyn tilin poistaminen peruuttaa pääsyn Forgejo-tiliisi linkitetyn tilin kautta. Jatketaanko? remove_account_link_success=Linkitetty tili on poistettu. @@ -929,7 +947,7 @@ webauthn_key_loss_warning = Jos menetät turva-avaimesi, menetät pääsyn tilil keep_activity_private.description = Julkinen toimintasi näkyy vain sinulle ja instanssin ylläpitäjille. email_desc = Ensisijaista sähköpostiosoitettasi käytetään ilmoituksiin, salasanan palautukseen ja jos sähköpostiosoite ei ole piilotettu, web-pohjaisiin Git-toimenpiteisiin. tokens_desc = Nämä poletit mahdollistavat pääsyn tilillesi Forgejon rajapintaa vasten. -keep_email_private_popup = Sähköpostiosoitettasi ei näytetä profiilissasi, eikä sitä käytetä oletuksena verkkokäyttöliittymän kautta tehtävissä sitoumuksissa, kuten tiedostojen ulospäinlatauksissa, muokkauksissa ja yhdistämissitoumuksissa. Sen sijaan voit käyttää erityistä osoitetta %s sitoumuksien linkittämiseen tiliisi. Tämä vaihtoehto ei vaikuta olemassa oleviin sitoumuksiin. +keep_email_private_popup = Sähköpostiosoitettasi ei näytetä profiilissasi, eikä sitä käytetä oletuksena verkkokäyttöliittymän kautta tehtävissä kommiteissa, kuten tiedostojen lähetyksissä, muokkauksissa ja yhdistämiskommiteissa. Sen sijaan voit käyttää erityistä osoitetta %s kommittien linkittämiseen tiliisi. Tämä vaihtoehto ei vaikuta olemassa oleviin kommitteihin. added_on = Lisätty %s additional_repo_units_hint = Ehdota tietovaraston lisäyksiköiden käyttöönottoa revoke_oauth2_grant_success = Pääsy mitätöity. @@ -942,7 +960,7 @@ twofa_recovery_tip = Jos menetät laitteesi, voit palauttaa pääsyn tiliisi kä hooks.desc = Lisää web-koukkuja, jotka aktivoituvat kaikissa omistamissasi tietovarastoissa. revoke_key = Mitätöi permissions_list = Käyttöoikeudet: -at_least_one_permission = Pääsymerkin luominen vaatii vähintään yhden käyttöoikeuden +at_least_one_permission = Pääsypoletin luominen vaatii vähintään yhden käyttöoikeuden select_permissions = Valitse käyttöoikeudet twofa_disable_note = Voit poistaa kaksivaiheisen todennuksen käytöstä, jos tarve. authorized_oauth2_applications_description = Olet myöntänyt pääsyn henkilökohtaiseen Forgejo-tiliisi näille kolmannen osapuolen sovelluksille. Jos et enää käytä sovellusta, mitätöi sen pääsy tilillesi. @@ -954,14 +972,14 @@ webauthn_alternative_tip = Saatat haluta määrittää lisätodennusmenetelmän. twofa_disable = Poista kaksivaiheinen todennus käytöstä twofa_disable_desc = Kaksivaiheisen todennuksen poistaminen asettaa tilisi aiempaa suurempaan uhkaan. Jatketaanko? update_language_not_found = Kieli "%s" ei ole käytettävissä. -change_username_prompt = Huomio: Käyttäjätunnuksen vaihtaminen muuttaa myös tilisi URL-osoitteen. +change_username_prompt = Huomio: Käyttäjänimen vaihtaminen muuttaa myös tilisi URL-osoitteen. oauth2_client_secret_hint = Tätä salaisuutta ei näytetä uudelleen, kun olet poistunut sivulta tai päivittänyt sivun. Varmista, että olet ottanut salaisuuden talteen. blocked_since = Estetty %s lähtien user_unblock_success = Käyttäjän esto on poistettu. oauth2_redirect_uris = Uudelleenohjaus-URI:t. Käytä uutta riviä (newline) jokaista URI:a kohden. oauth2_client_secret = Asiakkaan salaisuus verify_ssh_key_success = SSH-avain "%s" on vahvistettu. -change_username_redirect_prompt = Vanha käyttäjätunnus uudelleenohjaa, kunnes joku muu ottaa käyttäjätunnuksen käyttönsä. +change_username_redirect_prompt = Vanha käyttäjänimi uudelleenohjaa, kunnes joku muu ottaa käyttäjänimen käyttöönsä. uploaded_avatar_is_too_big = Lähetetyn tiedoston koko (%d KiB) ylittää enimmäiskoon (%d KiB). ssh_key_been_used = Tämä SSH-avain on jo lisätty palvelimelle. verify_gpg_key_success = GPG-avain "%s" on vahvistettu. @@ -981,7 +999,7 @@ add_email_confirmation_sent = Vahvistusviesti on lähetetty osoitteeseen "%s". V pronouns_custom_label = Mukautetut pronominit openid_deletion_desc = Tämän OpenID-osoitteen poistaminen tililtäsi estää kirjautumisen sitä käyttäen. Jatketaanko? generate_token_name_duplicate = Nimeä %s on jo käytetty sovelluksen nimenä. Käytä eri nimeä. -ssh_signonly = SSH on tällä hetkellä poistettu käytöstä, joten näitä avaimia käytetään vain sitoumusten allekirjoituksen vahvistamiseen. +ssh_signonly = SSH on tällä hetkellä poistettu käytöstä, joten näitä avaimia käytetään vain kommittien allekirjoituksen vahvistamiseen. oauth2_applications_desc = OAuth2-sovellukset mahdollistavat käyttämäsi kolmannen osapuolen sovelluksen todentaa turvallisesti käyttäjiä tähän Forgejo-instanssiin. quota.sizes.assets.attachments.all = Liitteet quota.applies_to_user = Seuraavia kiintiösääntöjä sovelletaan tiliisi @@ -998,30 +1016,51 @@ quota.sizes.repos.private = Yksityiset tietovarastot quota.sizes.git.all = Git-sisältö quota.sizes.assets.packages.all = Paketit quota.sizes.wiki = Wiki +update_hints_success = Vihjeet on päivitetty. +twofa_scratch_token_regenerate = Luo uudelleen kertakäyttöinen palautusavain +token_state_desc = Tätä polettia on käytetty viimeisen 7 päivän aikana +quota.sizes.assets.artifacts = Artefaktit +update_user_avatar_success = Käyttäjän profiilikuva on päivitetty. +access_token_regeneration = Luo uudelleen pääsypoletti +regenerate_token = Luo uudelleen +keep_pronouns_private = Näytä pronominit vain tunnistautuneille käyttäjille +keep_pronouns_private.description = Tämä piilottaa pronominisi käyttäjiltä, jotka eivät ole kirjautuneet sisään. +comment_type_group_issue_ref = Ongelmaviittaus +twofa_scratch_token_regenerated = Kertakäyttöinen palautusavaimesi on nyt %s. Talleta se turvalliseen sijaintiin, koska sitä ei näytetä uudelleen. +change_username_redirect_prompt.with_cooldown.few = Vanha käyttäjänimi on kenen tahansa saatavilla %[1]d päivän suojaamisjakson jälkeen. Voit palauttaa käyttäjänimen itsellesi suojaamisjakson aikana. +additional_repo_units_hint_description = Näytä "Ota lisää käyttöön"-vihje tietovarastoissa, missä kaikki saatavilla olevat yksiköt eivät ole käytössä. +change_username_redirect_prompt.with_cooldown.one = Vanha käyttäjänimi on kenen tahansa saatavilla %[1]d päivän suojaamisjakson jälkeen. Voit palauttaa käyttäjänimen itsellesi suojaamisjakson aikana. +gpg_key_matched_identities = Vastaavat identiteetit: +delete_token_success = Pääsypoletti on poistettu. Sitä käyttävillä sovelluksilla ei ole enää pääsyä tilillesi. +ssh_externally_managed = Tämän käyttäjän SSH-avainta hallitaan ulkoisesti +passcode_invalid = Virheellinen pääsykoodi. Yritä uudelleen. +then_enter_passcode = Kirjoita sovelluksessa näkyvä pääsykoodi: +gpg_key_matched_identities_long = Tähän avaimeen upotetut identiteetit vastaavat tämän käyttäjän seuraavia aktivoituja sähköpostiosoitteita. Kommitit, jotka vastaavat näitä sähköpostiosoitteita, voidaan vahvistaa tällä avaimella. +twofa_failed_get_secret = Salaisuuden saaminen epäonnistui. [repo] owner=Omistaja -owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska repojen maksimimäärää on rajoitettu. +owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska tietovarastojen enimmäismäärää on rajoitettu. repo_name=Tietovaraston nimi -repo_name_helper=Hyvä repon nimi on lyhyt, mieleenpainuva ja yksilöllinen. -repo_size=Repon koko -template=Malli -template_select=Valitse malli -template_helper=Tee reposta mallipohja +repo_name_helper=Hyvä tietovaraston nimi on lyhyt, mieleenpainuva ja yksilöllinen. +repo_size=Tietovaraston koko +template=Mallipohja +template_select=Valitse mallipohja +template_helper=Tee tietovarastosta mallipohja visibility=Näkyvyys visibility_description=Vain omistaja tai organisaation jäsenet, jos heillä on oikeudet, voivat nähdä sen. -visibility_helper_forced=Sivuston ylläpitäjä pakottaa uudet repot olemaan yksityisiä. -fork_repo=Luo tietovaraston haarukka +visibility_helper_forced=Sivuston ylläpitäjä pakottaa uudet tietovarastot olemaan yksityisiä. +fork_repo=Forkkaa tietovarasto fork_from=Forkkaa lähteestä -fork_visibility_helper=Forkatun repon näkyvyyttä ei voi muuttaa. +fork_visibility_helper=Forkatun tietovaraston näkyvyyttä ei voi muuttaa. clone_in_vsc=Kloonaa VS Codessa download_zip=Lataa ZIP download_tar=Lataa TAR.GZ repo_desc=Kuvaus repo_lang=Kieli repo_gitignore_helper=Valitse .gitignore-mallit -issue_labels=Tunnisteet -issue_labels_helper=Valitse nimiöjoukko +issue_labels=Nimilaput +issue_labels_helper=Valitse nimilappujoukko license=Lisenssi license_helper=Valitse lisenssitiedosto readme=README @@ -1031,14 +1070,14 @@ default_branch=Oletushaara mirror_prune=Karsi watchers=Tarkkailijat stargazers=Tähtiharrastajat -forks=Haarat +forks=Forkit delete_preexisting_label=Poista desc.private=Yksityinen desc.public=Julkinen -desc.template=Malli +desc.template=Mallipohja desc.internal=Sisäinen desc.archived=Arkistoidut @@ -1046,28 +1085,28 @@ template.git_hooks=Git-koukut template.webhooks=Webkoukut template.topics=Aiheet template.avatar=Profiilikuva -template.issue_labels=Viannimikkeet +template.issue_labels=Ongelmanimilaput -migrate_items=Siirrettävät kohteet +migrate_items=Migraation kohteet migrate_items_wiki=Wiki migrate_items_milestones=Merkkipaalut -migrate_items_labels=Tunnisteet +migrate_items_labels=Nimilaput migrate_items_issues=Ongelmat migrate_items_pullrequests=Vetopyynnöt migrate_items_releases=Julkaisut -migrate_repo=Siirrä tietovarasto +migrate_repo=Suorita tietovaraston migraatio migrate.clone_address=Migraatio/kloonaus URL-osoitteesta -migrate.github_token_desc=Voit laittaa yhden tai useamman pääsymerkin pilkulla erotellen tähän nopeuttaaksesi migraatiota GitHub APIn vauhtirajojen takia. VAROITUS: Tämän ominaisuuden väärinkäyttö voi rikkoa palveluntarjoajan ehtoja ja johtaa tilin estämiseen. -migrate.permission_denied=Sinun ei sallita tuovan paikallisia repoja. -migrate.failed=Siirto epäonnistui: %v -migrate.migrate_items_options=Lisäkohteiden siirtämistä varten vaaditaan pääsypoletti -migrate.migrating=Tuodaan kohteesta %s … -migrate.migrating_failed=Tuonti kohteesta %s epäonnistui. -migrate.migrating_git=Git-tietojen siirtäminen +migrate.github_token_desc=Voit laittaa yhden tai useamman pääsypoletin pilkulla erotellen tähän nopeuttaaksesi migraatiota GitHubin rajapinnan tahtirajojen takia. VAROITUS: Tämän ominaisuuden väärinkäyttö voi rikkoa palveluntarjoajan ehtoja ja johtaa tilin estämiseen. +migrate.permission_denied=Paikallisten tietovarastojen tuominen ei ole sallittua. +migrate.failed=Migraatio epäonnistui: %v +migrate.migrate_items_options=Lisäkohteiden migraatiota varten vaaditaan pääsypoletti +migrate.migrating=Suoritetaan migraatio lähteestä %s … +migrate.migrating_failed=Migraatio lähteestä %s epäonnistui. +migrate.migrating_git=Suoritetaan Git-datan migraatiota -mirror_from=peilaus alkaen +mirror_from=peili kohteelle forked_from=forkattu lähteestä unwatch=Lopeta tarkkailu watch=Tarkkaile @@ -1077,10 +1116,10 @@ download_archive=Lataa tietovarasto no_desc=Ei kuvausta quick_guide=Pikaopas -clone_this_repo=Kloonaa tämä repo +clone_this_repo=Kloonaa tämä tietovarasto code=Koodi -code.desc=Pääsy lähdekoodiin, tiedostoihin, committeihin ja haaroihin. +code.desc=Pääsy lähdekoodiin, tiedostoihin, kommitteihin ja haaroihin. branch=Haara tree=Puu filter_branch_and_tag=Suodata haara tai tagi @@ -1090,22 +1129,22 @@ issues=Ongelmat pulls=Vetopyynnöt project_board=Projektit packages=Paketit -labels=Tunnisteet +labels=Nimilaput milestones=Merkkipaalut -commits=Commitit -commit=Commit +commits=Kommitit +commit=Kommitti releases=Julkaisut tag=Tagi released_this=julkaisi tämän file_raw=Raaka file_history=Historia file_view_raw=Näytä raaka -file_permalink=Pysyvä linkki +file_permalink=Pysyväislinkki video_not_supported_in_browser=Selaimesi ei tue HTML5:n video-tagia. audio_not_supported_in_browser=Selaimesi ei tue HTML5:n audio-tagia. -blame=Selitys +blame=Blame download_file=Lataa tiedosto normal_view=Normaali näkymä line=rivi @@ -1121,20 +1160,20 @@ editor.delete_this_file=Poista tiedosto editor.name_your_file=Nimeä tiedostosi… editor.filename_help=Lisää hakemisto kirjoittamalla sen nimi ja perään kauttaviiva ("/"). Poista hakemisto kirjoittamalla askelpalautin syöttökentän alkuun. editor.or=tai -editor.cancel_lower=Peru -editor.commit_signed_changes=Sitoudu allekirjoitetut muutokset -editor.commit_changes=Sitoudu muutokset +editor.cancel_lower=Peruuta +editor.commit_signed_changes=Kommitoi allekirjoitetut muutokset +editor.commit_changes=Kommitoi muutokset editor.add_tmpl=Lisää "<%s>" -editor.commit_directly_to_this_branch=Sitoudu suoraan %[1]s -haaraan. -editor.create_new_branch=Luo uusi haara tälle commitille ja aloita vetopyyntö. -editor.create_new_branch_np=Luo uusi haara tälle commitille. +editor.commit_directly_to_this_branch=Kommitoi suoraan haaraan %[1]s. +editor.create_new_branch=Luo uusi haara tälle kommitille ja aloita vetopyyntö. +editor.create_new_branch_np=Luo uusi haara tälle kommitille. editor.cancel=Peruuta editor.filename_cannot_be_empty=Tiedostonimi ei voi olla tyhjä. editor.no_changes_to_show=Ei muutoksia näytettäväksi. editor.add_subdir=Lisää hakemisto… -editor.require_signed_commit=Haara vaatii vahvistetun commitin +editor.require_signed_commit=Haara vaatii allekirjoitetun kommitin -commits.commits=Commitit +commits.commits=Kommitit commits.nothing_to_compare=Nämä haarat vastaavat toisiaan. commits.find=Haku commits.search_all=Kaikki haarat @@ -1173,9 +1212,9 @@ issues.desc=Ongelmien, tehtävien ja merkkipaalujen hallinta. issues.filter_assignees=Suodata käyttäjiä issues.filter_milestones=Suodata merkkipaalu issues.new=Uusi ongelma -issues.new.labels=Tunnisteet -issues.new.no_label=Ei tunnisteita -issues.new.clear_labels=Tyhjennä tunnisteet +issues.new.labels=Nimilaput +issues.new.no_label=Ei nimilappuja +issues.new.clear_labels=Tyhjennä nimilaput issues.new.projects=Projektit issues.new.no_items=Ei kohteita issues.new.milestone=Merkkipaalu @@ -1188,13 +1227,13 @@ issues.new.clear_assignees=Tyhjennä käsittelijä issues.new.no_assignees=Ei käsittelijää issues.choose.open_external_link=Avaa issues.choose.blank=Oletus -issues.no_ref=Haaran/tagia ei määritelty +issues.no_ref=Haaraa/tagia ei määritelty issues.create=Luo ongelma -issues.new_label=Uusi tunniste -issues.new_label_placeholder=Tunnisteen nimi +issues.new_label=Uusi nimilappu +issues.new_label_placeholder=Nimilapun nimi issues.new_label_desc_placeholder=Kuvaus -issues.create_label=Luo tunniste -issues.label_templates.helper=Valitse tunnisteen esiasetus +issues.create_label=Luo nimilappu +issues.label_templates.helper=Valitse nimilapun esiasetus issues.add_milestone_at=`lisäsi tämän merkkipaaluun %s %s` issues.change_milestone_at=`vaihtoi merkkipaalun %s merkkipaaluun %s %s` issues.remove_milestone_at=`poisti tämän %s merkkipaalusta %s` @@ -1204,12 +1243,12 @@ issues.deleted_project=`(poistettu)` issues.self_assign_at=`itse otti tämän käsittelyyn %s` issues.change_title_at=`muutti otsikon %s otsikoksi %s %s` issues.delete_branch_at=`poisti haaran %s %s` -issues.filter_label=Tunniste -issues.filter_label_exclude=`Käytä alt + klikkaus/rivinvaihto poissulkeaksesi tunnisteita` -issues.filter_label_no_select=Kaikki tunnisteet +issues.filter_label=Nimilappu +issues.filter_label_exclude=`Käytä alt + napsautus/rivinvaihto poissulkeaksesi nimilappuja` +issues.filter_label_no_select=Kaikki nimilaput issues.filter_milestone=Merkkipaalu issues.filter_project=Projekti -issues.filter_assignee=Osoitettu +issues.filter_assignee=Käsittelijä issues.filter_poster=Tekijä issues.filter_type=Tyyppi issues.filter_type.all_issues=Kaikki ongelmat @@ -1224,15 +1263,15 @@ issues.filter_sort.recentupdate=Äskettäin päivitetty issues.filter_sort.leastupdate=Kauiten aikaa sitten päivitetty issues.filter_sort.mostcomment=Eniten kommentoidut issues.filter_sort.leastcomment=Vähiten kommentoidut -issues.filter_sort.nearduedate=Lähin määräpäivä -issues.filter_sort.farduedate=Kaukaisin määräpäivä +issues.filter_sort.nearduedate=Lähin eräpäivä +issues.filter_sort.farduedate=Kaukaisin eräpäivä issues.filter_sort.moststars=Eniten tähtiä issues.filter_sort.feweststars=Vähiten tähtiä issues.filter_sort.mostforks=Eniten forkattu issues.filter_sort.fewestforks=Vähiten forkattu issues.action_open=Avaa issues.action_close=Sulje -issues.action_label=Tunniste +issues.action_label=Nimilappu issues.action_milestone=Merkkipaalu issues.action_milestone_no_select=Ei merkkipaalua issues.action_assignee=Osoitettu henkilölle @@ -1241,7 +1280,7 @@ issues.previous=Edellinen issues.next=Seuraava issues.open_title=Avoinna issues.closed_title=Suljettu -issues.draft_title=Työversio +issues.draft_title=Luonnos issues.num_comments=%d kommenttia issues.commented_at=`kommentoi %s` issues.delete_comment_confirm=Haluatko varmasti poistaa tämän kommentin? @@ -1254,24 +1293,24 @@ issues.close_comment_issue=Kommentoi ja sulje issues.reopen_issue=Avaa uudelleen issues.reopen_comment_issue=Kommentoi ja avaa uudelleen issues.create_comment=Kommentoi -issues.closed_at=`sulki tämän ongelman %[2]s` -issues.reopened_at=`uudelleenavasi tämän ongelman %[2]s` -issues.commit_ref_at=`viittasi tähän ongelmaan commitissa %[2]s` +issues.closed_at=`sulki tämän ongelman %s` +issues.reopened_at=`uudelleenavasi tämän ongelman %s` +issues.commit_ref_at=`viittasi tähän ongelmaan kommitissa %s` issues.author=Tekijä issues.role.owner=Omistaja issues.role.member=Jäsen issues.edit=Muokkaa issues.cancel=Peruuta issues.save=Tallenna -issues.label_title=Tunnisteen nimi +issues.label_title=Nimi issues.label_description=Kuvaus -issues.label_color=Tunnisteen väri -issues.label_count=%d tunnistetta -issues.label_open_issues=%d avointa ongelmaa +issues.label_color=Väri +issues.label_count=%d nimilappua +issues.label_open_issues=%d avointa ongelmaa/vetopyyntöä issues.label_edit=Muokkaa issues.label_delete=Poista -issues.label_modify=Muokkaa tunnistetta -issues.label_deletion=Poista tunniste +issues.label_modify=Muokkaa nimilappua +issues.label_deletion=Poista nimilappu issues.label.filter_sort.alphabetically=Aakkosjärjestyksessä issues.label.filter_sort.reverse_alphabetically=Käänteisessä aakkosjärjestyksessä issues.label.filter_sort.by_size=Pienin koko @@ -1284,7 +1323,7 @@ issues.unlock=Avaa keskustelu issues.unlock_comment=avasi tämän keskustelun lukituksen %s issues.lock_confirm=Lukitse issues.unlock_confirm=Avaa -issues.lock.notice_1=- Muut käyttäjät eivät voi lisätä uusia kommentteja tähän vianlippuun. +issues.lock.notice_1=- Muut käyttäjät eivät voi lisätä uusia kommentteja tähän ongelmaan. issues.lock.notice_3=- Voit aina myöhemmin avata tämän ongelman lukituksesta. issues.unlock.notice_2=- Voit aina myöhemmin lukita tämän ongelman uudelleen. issues.lock.reason=Lukitsemisen syy @@ -1306,19 +1345,19 @@ issues.add_time_hours=Tuntia issues.add_time_minutes=Minuuttia issues.add_time_sum_to_small=Aikaa ei syötetty. issues.time_spent_from_all_authors=`Käytetty kokonaisaika: %s` -issues.due_date=Määräpäivä -issues.push_commit_1=lisäsi %d sitoumuksen %s -issues.push_commits_n=lisäsi %d sitoumusta %s +issues.due_date=Eräpäivä +issues.push_commit_1=lisäsi %d kommitin %s +issues.push_commits_n=lisäsi %d kommittia %s issues.due_date_form=vvvv-kk-pp issues.due_date_form_edit=Muokkaa issues.due_date_form_remove=Poista -issues.due_date_not_set=Määräpäivää ei ole asetettu. +issues.due_date_not_set=Eräpäivää ei ole asetettu. issues.due_date_overdue=Myöhässä issues.dependency.title=Riippuvuudet -issues.dependency.issue_no_dependencies=Riippuvuuksia ei asetettu. -issues.dependency.pr_no_dependencies=Riippuvuuksia ei asetettu. +issues.dependency.issue_no_dependencies=Riippuvuuksia ei ole asetettu. +issues.dependency.pr_no_dependencies=Riippuvuuksia ei ole asetettu. issues.dependency.add=Lisää riippuvuus… -issues.dependency.cancel=Peru +issues.dependency.cancel=Peruuta issues.dependency.remove=Poista issues.dependency.remove_info=Poistä tämä riippuvuus issues.review.self.approval=Et voi hyväksyä omia vetopyyntöjä. @@ -1335,11 +1374,11 @@ issues.content_history.created=luotu pulls.new=Uusi vetopyyntö -pulls.compare_changes=Uusi pull-pyyntö +pulls.compare_changes=Uusi vetopyyntö pulls.has_viewed_file=Katsottu pulls.viewed_files_label=%[1]d / %[2]d tiedostoa katsottu pulls.compare_compare=vedä kohteesta -pulls.filter_branch=Suodata branch +pulls.filter_branch=Suodata haara pulls.no_results=Tuloksia ei löytynyt. pulls.nothing_to_compare=Nämä haarat vastaavat toisiaan. Ei ole tarvetta luoda vetopyyntöä. pulls.nothing_to_compare_and_allow_empty_pr=Nämä haarat vastaavat toisiaan. Vetopyyntö tulee olemaan tyhjä. @@ -1348,13 +1387,13 @@ pulls.create=Luo vetopyyntö pulls.title_desc_few=haluaa yhdistää %[1]d committia lähteestä %[2]s kohteeseen %[3]s pulls.merged_title_desc_few=yhdistetty %[1]d committia lähteestä %[2]s kohteeseen %[3]s %[4]s pulls.tab_conversation=Keskustelu -pulls.tab_commits=Commitit +pulls.tab_commits=Kommitit pulls.tab_files=Muuttuneet tiedostot pulls.merged=Yhdistetty pulls.title_wip_desc=`Aloita otsikko sanalla %s estääksesi vetopyynnön yhdistämisen vahingossa.` -pulls.add_prefix=Lisää %s etuliite -pulls.remove_prefix=Poista %s etuliite -pulls.can_auto_merge_desc=Tämä pull-pyyntö voidaan yhdistää automaattisesti. +pulls.add_prefix=Lisää etuliite %s +pulls.remove_prefix=Poista etuliite %s +pulls.can_auto_merge_desc=Tämä vetopyyntö voidaan yhdistää automaattisesti. @@ -1366,13 +1405,13 @@ pulls.can_auto_merge_desc=Tämä pull-pyyntö voidaan yhdistää automaattisesti milestones.new=Uusi merkkipaalu milestones.closed=Suljettu %s -milestones.no_due_date=Ei määräpäivää +milestones.no_due_date=Ei eräpäivää milestones.open=Avaa uudelleen milestones.close=Sulje milestones.create=Luo merkkipaalu milestones.title=Otsikko milestones.desc=Kuvaus -milestones.due_date=Määräpäivä (valinnainen) +milestones.due_date=Eräpäivä (valinnainen) milestones.clear=Tyhjennä milestones.edit=Muokkaa merkkipaalua milestones.cancel=Peruuta @@ -1383,11 +1422,11 @@ milestones.filter_sort.least_issues=Vähiten ongelmia wiki=Wiki -wiki.welcome=Tervetuloa Wikiin. +wiki.welcome=Tervetuloa wikiin. wiki.welcome_desc=Wikissä voit kirjoittaa ja jakaa dokumentaatiota käyttäjien kesken. wiki.create_first_page=Luo ensimmäinen sivu wiki.page=Sivu -wiki.filter_page=Suodatin sivu +wiki.filter_page=Suodata sivu wiki.new_page=Sivu wiki.default_commit_message=Kirjoita muistiinpano tästä päivityksestä (valinnainen). wiki.save_page=Tallenna sivu @@ -1416,7 +1455,7 @@ activity.active_issues_count_1=%d aktiivinen ongelma activity.active_issues_count_n=%d aktiivista ongelmaa activity.closed_issues_count_1=suljettu ongelma activity.closed_issues_count_n=suljettua ongelmaa -activity.title.issues_created_by=%s luonnut %s +activity.title.issues_created_by=%s luonut %s activity.closed_issue_label=Suljettu activity.new_issues_count_1=Uusi ongelma activity.new_issues_count_n=uutta ongelmaa @@ -1432,14 +1471,14 @@ activity.git_stats_and_deletions=ja activity.git_stats_deletion_1=%d poisto activity.git_stats_deletion_n=%d poistoa -contributors.contribution_type.commits=Sitoumukset +contributors.contribution_type.commits=Kommitit search=Haku search.match=Osuma search.code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt. settings=Asetukset -settings.options=Repo +settings.options=Tietovarasto settings.collaboration.admin=Ylläpitäjä settings.collaboration.write=Kirjoita settings.collaboration.read=Lue @@ -1450,31 +1489,31 @@ settings.githooks=Git-koukut settings.basic_settings=Perusasetukset settings.mirror_settings=Peilauksen asetukset -settings.site=Nettisivu +settings.site=Verkkosivusto settings.update_settings=Tallenna asetukset settings.advanced_settings=Lisäasetukset settings.use_internal_wiki=Käytä sisäänrakennettua wikiä settings.use_external_wiki=Käytä ulkoista wikiä settings.external_wiki_url=Ulkoisen wikin URL-osoite -settings.external_wiki_url_desc=Wiki-välilehden klikkaus ohjaa vierailijat ulkoisen wiki-URL-osoitteeseen. -settings.tracker_url_format=Ulkoisen vianseurannan URL-muoto +settings.external_wiki_url_desc=Wiki-välilehden napsauttaminen ohjaa vierailijat ulkoiseen wiki-URL-osoitteeseen. +settings.tracker_url_format=Ulkoisen ongelmanseurannan URL-muoto settings.tracker_issue_style.numeric=Numeerinen settings.tracker_issue_style.alphanumeric=Aakkosnumeerinen -settings.enable_timetracker=Ota ajan seuranta käyttöön +settings.enable_timetracker=Ota ajanseuranta käyttöön settings.danger_zone=Vaaravyöhyke -settings.new_owner_has_same_repo=Uudella omistajalla on jo samanniminen repo. Ole hyvä ja valitse toinen nimi. +settings.new_owner_has_same_repo=Uudella omistajalla on jo samanniminen tietovarasto. Valitse toinen nimi. settings.transfer.title=Siirrä omistajuus settings.transfer_form_title=Syötä repon nimi vahvistuksena: -settings.transfer_notices_3=- Jos arkisto on yksityinen ja se siirretään yksittäiselle käyttäjälle, tämä toiminto varmistaa, että käyttäjällä on ainakin lukuoikeudet (ja muuttaa käyttöoikeuksia tarvittaessa). +settings.transfer_notices_3=- Jos tietovarasto on yksityinen ja se siirretään yksittäiselle käyttäjälle, tämä toiminto varmistaa, että käyttäjällä on ainakin lukuoikeudet (ja muuttaa käyttöoikeuksia tarvittaessa). settings.transfer_owner=Uusi omistaja settings.wiki_delete=Poista wikidata -settings.wiki_delete_desc=Repon wikin data poistaminen on pysyvä eikä voi peruuttaa. +settings.wiki_delete_desc=Tietovaraston wikin data poistaminen on pysyvä, eikä sitä voi perua. settings.confirm_wiki_delete=Poista wikidata -settings.wiki_deletion_success=Repon wiki data on poistettu. +settings.wiki_deletion_success=Tietovaraston wikidata on poistettu. settings.delete=Poista tämä tietovarasto -settings.delete_desc=Repon poistaminen on pysyvä eikä voi peruuttaa. -settings.delete_notices_1=- Tätä toimintoa EI VOI peruuttaa myöhemmin. -settings.update_settings_success=Repon asetukset on päivitetty. +settings.delete_desc=Tietovaraston poistaminen on pysyvä, eikä sitä voi perua. +settings.delete_notices_1=- Tätä toimintoa EI VOI perua myöhemmin. +settings.update_settings_success=Tietovaraston asetukset on päivitetty. settings.delete_collaborator=Poista settings.search_user_placeholder=Etsi käyttäjä… settings.teams=Tiimit @@ -1484,19 +1523,19 @@ settings.webhook_deletion=Poista webkoukku settings.webhook.test_delivery=Testitoimitus settings.webhook.request=Pyyntö settings.webhook.response=Vastaus -settings.webhook.headers=Otsikot +settings.webhook.headers=Otsakkeet settings.webhook.payload=Sisältö settings.webhook.body=Sisältö settings.githook_edit_desc=Jos koukku ei ole käytössä, esitellään esimerkkisisältö. Sisällön jättäminen tyhjäksi arvoksi poistaa tämän koukun käytöstä. settings.githook_name=Koukun nimi settings.githook_content=Koukun sisältö settings.update_githook=Päivitä koukku -settings.payload_url=Kohde URL +settings.payload_url=Kohde-URL settings.http_method=HTTP-menetelmä -settings.secret=Salaus -settings.slack_username=Käyttäjätunnus -settings.slack_icon_url=Kuvakkeen URL -settings.discord_username=Käyttäjätunnus +settings.secret=Salaisuus +settings.slack_username=Käyttäjänimi +settings.slack_icon_url=Kuvakkeen URL-osoite +settings.discord_username=Käyttäjänimi settings.event_desc=Laukaisu päällä: settings.event_send_everything=Kaikki tapahtumat settings.event_choose=Mukautetut tapahtumat… @@ -1506,29 +1545,29 @@ settings.event_create_desc=Haara tai tagi luotu. settings.event_delete=Poista settings.event_delete_desc=Haara tai tagi poistettu. settings.event_wiki=Wiki -settings.event_release_desc=Julkaisu julkaistu, päivitetty tai poistettu varastosta. +settings.event_release_desc=Julkaisu julkaistu, päivitetty tai poistettu tietovarastosta. settings.event_push=Työnnä -settings.event_push_desc=Git push repoon. -settings.event_repository=Repo -settings.event_repository_desc=Repo luotu tai poistettu. +settings.event_push_desc=Git-työntö tietovarastoon. +settings.event_repository=Tietovarasto +settings.event_repository_desc=Tietovarasto luotu tai poistettu. settings.event_header_issue=Ongelmien tapahtumat settings.event_issues=Muokkaus settings.event_issues_desc=Ongelma avattu, suljettu, avattu uudelleen tai muokattu. settings.event_issue_assign=Toimeksianto settings.event_issue_assign_desc=Ongelma osoitettu tai osoitus poistettu. -settings.event_issue_label_desc=Viannimikkeet lisätty tai poistettu. +settings.event_issue_label_desc=Ongelmanimilaput lisätty tai poistettu. settings.event_issue_milestone_desc=Merkkipaalu lisätty, poistettu tai muokattu. settings.event_issue_comment_desc=Ongelman kommentti luotu, muokattu tai poistettu. settings.event_header_pull_request=Vetopyyntöjen tapahtumat settings.event_pull_request=Muokkaus -settings.event_package_desc=Paketti on luotu tai poistettu repossa. +settings.event_package_desc=Paketti on luotu tai poistettu tietovarastossa. settings.active_helper=Tiedot käynnistetyistä tapahtumista lähetetään tähän webkoukun URL-osoitteeseen. settings.add_hook_success=Uusi webkoukku on lisätty. settings.update_webhook=Päivitä webkoukku settings.delete_webhook=Poista webkoukku settings.recent_deliveries=Viimeisimmät toimitukset settings.hook_type=Koukun tyyppi -settings.slack_token=Pääsymerkki +settings.slack_token=Poletti settings.slack_domain=Verkkotunnus settings.slack_channel=Kanava settings.add_web_hook_desc=Integroi %s repoon. @@ -1543,36 +1582,36 @@ settings.web_hook_name_matrix=Matrix settings.web_hook_name_feishu_only =Feishu settings.web_hook_name_larksuite_only =Lark Suite settings.web_hook_name_packagist=Packagist -settings.deploy_keys=Julkaisuavaimet -settings.add_deploy_key=Lisää julkaisuavain -settings.deploy_key_desc=Julkaisuavaimilla on vain-luku oikeudet repoon. -settings.is_writable_info=Salli tämän julkaisuavaimen puskea repoon. -settings.no_deploy_keys=Julkaisuavaimia ei ole käytössä vielä. +settings.deploy_keys=Toimitusavaimet +settings.add_deploy_key=Lisää toimitusavain +settings.deploy_key_desc=Toimitusavaimilla on pelkkä lukuoikeus tietovarastoon. +settings.is_writable_info=Salli tämän toimitusavaimen työntää tietovarastoon. +settings.no_deploy_keys=Toimitusavaimia ei ole käytössä vielä. settings.title=Otsikko settings.deploy_key_content=Sisältö -settings.key_been_used=Julkaisuavain identtisellä sisällöllä on jo käytössä. -settings.key_name_used=Julkaisuavain samalla nimellä on jo olemassa. -settings.deploy_key_deletion=Poista julkaisuavain -settings.deploy_key_deletion_desc=Julkaisuavaimen poistaminen kumoaa sen pääsyn tähän repoon. Jatketaanko? -settings.deploy_key_deletion_success=Julkaisuavain on poistettu. +settings.key_been_used=Toimitusavain identtisellä sisällöllä on jo käytössä. +settings.key_name_used=Toimitusavain samalla nimellä on jo olemassa. +settings.deploy_key_deletion=Poista toimitusavain +settings.deploy_key_deletion_desc=Toimitusavaimen poistaminen kumoaa sen pääsyn tähän tietovarastoon. Jatketaanko? +settings.deploy_key_deletion_success=Toimitusavain on poistettu. settings.branches=Haarat settings.protected_branch=Haaran suojaus settings.branch_protection=Haaran "%s" suojaussäännöt settings.protect_this_branch=Ota haaran suojaus käyttöön -settings.protect_whitelist_deploy_keys=Lisää julkaisuavaimet sallittujen listalle mahdollistaaksesi repohin kirjoituksen. -settings.protect_whitelist_users=Sallitut käyttäjät suhteessa työntämiseen +settings.protect_whitelist_deploy_keys=Lisää toimitusavaimet sallittujen listalle mahdollistaaksesi tietovarastoihin kirjoituksen. +settings.protect_whitelist_users=Työntämiseen oikeutettujen käyttäjien lista settings.protect_whitelist_search_users=Etsi käyttäjiä… settings.protect_merge_whitelist_committers_desc=Salli vain listaan merkittyjen käyttäjien ja tiimien yhdistää vetopyynnöt tähän haaraan. -settings.protect_merge_whitelist_users=Sallitut käyttäjät suhteessa yhdistämiseen +settings.protect_merge_whitelist_users=Yhdistämiseen oikeutettujen käyttäjien lista settings.protect_required_approvals=Vaadittavat hyväksynnät -settings.protect_approvals_whitelist_users=Sallittujen tarkastajien lista +settings.protect_approvals_whitelist_users=Sallittujen katselmoijien lista settings.choose_branch=Valitse haara… settings.no_protected_branch=Suojattuja haaroja ei ole. settings.edit_protected_branch=Muokkaa -settings.protected_branch_required_approvals_min=Vaadittavat hyväksynnät ei voi olla negatiivinen. +settings.protected_branch_required_approvals_min=Vaadittavat hyväksynnät eivät voi olla negatiivisia. settings.tags=Tagit -settings.tags.protection=Tagien suojaaminen -settings.tags.protection.pattern=Tagin kuvio +settings.tags.protection=Tagien suojaus +settings.tags.protection.pattern=Tagin kaava settings.tags.protection.allowed=Sallitut settings.tags.protection.allowed.users=Sallitut käyttäjät settings.tags.protection.allowed.teams=Sallitut tiimit @@ -1585,13 +1624,13 @@ settings.archive.button=Arkistoi tietovarasto settings.archive.header=Arkistoi tämä tietovarasto settings.archive.tagsettings_unavailable=Tagi-asetukset eivät ole käytettävissä arkistoiduissa tietovarastoissa. settings.lfs=LFS -settings.lfs_filelist=LFS-tiedostot tallennettu tähän repoon -settings.lfs_no_lfs_files=LFS-tiedostoja ei ole tallennettu tähän repoon. -settings.lfs_findcommits=Etsi commitit -settings.lfs_lfs_file_no_commits=Tälle LFS-tiedostolle ei löytynyt sitoumuksia +settings.lfs_filelist=Tähän tietovarastoon tallennetut LFS-tiedostot +settings.lfs_no_lfs_files=LFS-tiedostoja ei ole tallennettu tähän tietovarastoon. +settings.lfs_findcommits=Etsi kommitteja +settings.lfs_lfs_file_no_commits=Tälle LFS-tiedostolle ei löytynyt kommitteja settings.lfs_noattribute=Tällä polulla ei ole lukittavaa attribuuttia oletushaarassa settings.lfs_delete=Poista LFS-tiedosto OID:lla %s -settings.lfs_delete_warning=LFS-tiedoston poistaminen saattaa aiheuttaa "objektia ei ole olemassa" -virheitä uloskirjauksessa. Oletko varma? +settings.lfs_delete_warning=LFS-tiedoston poistaminen saattaa aiheuttaa "objektia ei ole olemassa"-virheitä uloskuittauksessa. Oletko varma? settings.lfs_findpointerfiles=Etsi osoitintiedostoja settings.lfs_locks=Lukot settings.lfs_invalid_locking_path=Virheellinen polku: %s @@ -1601,7 +1640,7 @@ settings.lfs_lock_path=Lukittavan tiedostopolku… settings.lfs_locks_no_locks=Ei lukkoja settings.lfs_lock_file_no_exist=Lukittua tiedostoa ei ole olemassa oletushaarassa settings.lfs_force_unlock=Pakota lukituksen avaus -settings.lfs_pointers.found=Löytyi %d blob osoitinta - %d yhdistettyö, %d yhdistämätöntä (%d puuttuu varastosta) +settings.lfs_pointers.found=Löytyi %d blob-osoitinta - %d assosioitu, %d assosioitumaton (%d puuttuu varastosta) settings.lfs_pointers.sha=Blob-tiiviste settings.lfs_pointers.oid=OID settings.lfs_pointers.inRepo=Tietovarastossa @@ -1610,8 +1649,8 @@ settings.lfs_pointers.accessible=Saatavilla käyttäjälle diff.browse_source=Selaa lähdekoodia diff.parent=vanhempi -diff.commit=commit -diff.git-notes=Muistiinpanot +diff.commit=kommitti +diff.git-notes=Huomautukset diff.options_button=Vertailun asetukset diff.show_split_view=Jaettu näkymä diff.show_unified_view=Yhdistetty näkymä @@ -1620,7 +1659,7 @@ diff.whitespace_show_everything=Näytä kaikki muutokset diff.whitespace_ignore_all_whitespace=Ohita tyhjämerkit rivejä verratessa diff.whitespace_ignore_amount_changes=Ohita tyhjämerkin määrän muutokset diff.whitespace_ignore_at_eol=Ohita muutokset rivin lopun tyhjämerkeissä -diff.stats_desc=%d muutettua tiedostoa jossa %d lisäystä ja %d poistoa +diff.stats_desc=%d muutettua tiedostoa joissa %d lisäystä ja %d poistoa diff.bin=BIN diff.view_file=Näytä tiedosto diff.file_image_width=Leveys @@ -1629,7 +1668,7 @@ diff.file_byte_size=Koko diff.comment.markdown_info=Muotoilu Markdownilla on tuettu. diff.comment.add_single_comment=Lisää yksittäinen kommentti diff.comment.add_review_comment=Lisää kommentti -diff.comment.start_review=Aloita tarkistus +diff.comment.start_review=Aloita katselmointi diff.comment.reply=Vastaa diff.review.header=Lähetä katselmointi diff.review.placeholder=Katselmoinnin kommentti @@ -1640,14 +1679,14 @@ diff.review.reject=Pyydä muutoksia release.releases=Julkaisut release.tags=Tagit release.new_release=Uusi julkaisu -release.draft=Työversio +release.draft=Luonnos release.prerelease=Esijulkaisu release.stable=Vakaa release.edit=Muokkaa release.source_code=Lähdekoodi release.new_subheader=Julkaisut organisoivat projektien versioita. release.edit_subheader=Julkaisut organisoivat projektien versioita. -release.tag_name=Taginimi +release.tag_name=Tagin nimi release.target=Kohde release.tag_helper=Valitse olemassa oleva tagi tai luo uusi tagi. release.prerelease_desc=Merkitse esijulkaisuksi @@ -1659,7 +1698,7 @@ release.edit_release=Päivitä julkaisu release.delete_release=Poista julkaisu release.delete_tag=Poista tagi release.deletion=Poista julkaisu -release.deletion_tag_desc=Poistetaanko tämä tagi reposta? Repon sisältö ja historia pysyvät muuttumattomina. Jatketaanko? +release.deletion_tag_desc=Poistetaan tämä tagi tietovarastosta. Tietovaraston sisältö ja historia pysyvät muuttumattomina. Jatketaanko? release.deletion_tag_success=Tagi on poistettu. release.tag_name_invalid=Tagin nimi ei ole kelvollinen. release.downloads=Lataukset @@ -1675,23 +1714,23 @@ topic.done=Valmis already_forked = Olet jo forkannut %s fork_to_different_account = Forkkaa toiselle tilille release.compare = Vertaa -release.ahead.commits = %d sitoumusta +release.ahead.commits = %d kommittia all_branches = Kaikki haarat n_tag_few = %s tagia -settings.event_fork_desc = Tietovaraston haarukka luotu. +settings.event_fork_desc = Tietovarasto forkattu. actions = Toiminnat -fork_guest_user = Kirjaudu sisään luodaksesi tämän tietovaraston haarukan. -fork_from_self = Et voi luoda omistamasi tietovaraston haarukkaa. +fork_guest_user = Kirjaudu sisään forkataksesi tämän tietovaraston. +fork_from_self = Et voi forkata omistamaasi tietovarastoa. visibility_fork_helper = (Tämän muuttaminen vaikuttaa kaikkien forkkien näkyvyyteen.) fork = Forkkaa -activity.git_stats_commit_n = %d sitoumusta +activity.git_stats_commit_n = %d kommittia commits.search_branch = Tämä haara n_branch_few = %s haaraa -pulls.show_all_commits = Näytä kaikki sitoumukset +pulls.show_all_commits = Näytä kaikki kommitit commit_graph.select = Valitse haarat -activity.navbar.recent_commits = Viimeaikaiset sitoumukset +activity.navbar.recent_commits = Viimeaikaiset kommitit settings.branches.add_new_rule = Lisää uusi sääntö -n_commit_few = %s sitoumusta +n_commit_few = %s kommittia issues.force_push_compare = Vertaa commits.desc = Selaa lähdekoodin muutoshistoriaa. clone_helper = Tarvitseko apua kloonauksen kanssa? Siirry tukisivulle. @@ -1716,7 +1755,7 @@ release.detail = Julkaisun tiedot diff.hide_file_tree = Piilota tiedostopuu issues.role.owner_helper = Tämä käyttäjä on tämän tietovaraston omistaja. issues.all_title = Kaikki -issues.label_archived_filter = Näytä arkistoidut tunnisteet +issues.label_archived_filter = Näytä arkistoidut nimilaput pulls.close = Sulje vetopyyntö branch.already_exists = Haara nimellä "%s" on jo olemassa. diff.show_file_tree = Näytä tiedostopuu @@ -1739,7 +1778,7 @@ settings.event_issue_comment = Kommentit diff.download_patch = Lataa patch-tiedosto issues.filter_milestone_none = Ei merkkipaaluja issues.filter_milestone_open = Avoimet merkkipaalut -new_repo_helper = Tietovarasto sisältää kaikki projektitiedostot, mukaan lukien tarkistushistoria. Järjestätkö jo sellaisen muualla? Siirrä tietovarasto. +new_repo_helper = Tietovarasto sisältää kaikki projektitiedostot, mukaan lukien versiohistorian. Onko sinulla tietovarasto jo muualla? Siirrä tietovarasto. use_template = Käytä tätä mallipohjaa star_guest_user = Kirjaudu sisään lisätäksesi tähden tähän tietovarastoon. watch_guest_user = Kirjaudu sisään tarkkaillaksesi tätä tietovarastoa. @@ -1765,12 +1804,12 @@ milestones.deletion_success = Merkkipaalu on poistettu. project = Projektit pulls.delete.title = Poistetaanko tämä vetopyyntö? activity.title.issues_1 = %d ongelma -contributors.contribution_type.filter_label = Avustuksen tyyppi: +contributors.contribution_type.filter_label = Kontribuution tyyppi: settings.protected_branch.delete_rule = Poista sääntö settings.archive.success = Tietovarasto arkistoitiin onnistuneesti. diff.comment.placeholder = Jätä kommentti release.message = Kuvaile tätä julkaisua -branch.delete_desc = Haaran poistaminen on pysyvä toimenpide. Vaikka poistettu haara voi jäädä olemaan lyhyeksi ajaksi, ennen kuin todellisesti poistetaan, poistoa EI VOI perua useimmiten. Jatketaanko? +branch.delete_desc = Haaran poistaminen on pysyvä toimenpide. Vaikka poistettu haara voi jäädä olemaan lyhyeksi ajaksi, ennen kuin se todellisesti poistetaan, poistoa EI VOI perua useimmiten. Jatketaanko? branch.protected_deletion_failed = Haara "%s" on suojattu. Sitä ei voi poistaa. open_with_editor = Avaa sovelluksella %s download_bundle = Lataa BUNDLE @@ -1785,7 +1824,7 @@ issues.new.closed_projects = Suljetut projektit settings.event_issue_milestone = Merkkipaalut branch.branch_already_exists = Haara "%s" on jo olemassa tässä tietovarastossa. projects.card_type.images_and_text = Kuvat ja teksti -default_branch_helper = Oletushaara on kantahaara vetopyyntöjä ja koodisitoumuksia varten. +default_branch_helper = Oletushaara on kantahaara vetopyyntöjä ja koodikommitteja varten. author_search_tooltip = Näyttää enintään 30 käyttäjää migrate_options_mirror_helper = Tästä tietovarastosta tulee peili commit_graph.color = Väri @@ -1804,7 +1843,7 @@ issues.dependency.pr_remove_text = Riippuvuus poistetaan tästä vetopyynnöstä release.download_count_few = %s latausta diff.data_not_available = Diff-sisältö ei ole saatavilla diff.image.side_by_side = Rinnakkain -release.ahead.target = projektiin %s tämän julkaisun jälkeen +release.ahead.target = haaraan %s tämän julkaisun jälkeen issues.close = Sulje ongelma issues.no_content = Ei kuvausta. pulls.reject_count_1 = %d muutospyyntö @@ -1818,7 +1857,7 @@ find_file.no_matching = Vastaavaa tiedostoa ei löytynyt editor.file_delete_success = Tiedosto "%s" on poistettu. settings.transfer.button = Siirrä omistajuus settings.slack_color = Väri -release.tag_name_already_exist = Julkaisu tällä taginimellä on jo olemassa. +release.tag_name_already_exist = Julkaisu tällä tagin nimellä on jo olemassa. pulls.allow_edits_from_maintainers_err = Päivittäminen epäonnistui stars = Tähdet editor.branch_already_exists = Haara "%s" on jo olemassa tässä tietovarastossa. @@ -1832,12 +1871,12 @@ wiki.back_to_wiki = Takaisin wikisivulle wiki.delete_page_notice_1 = Wikisivun "%s" poistamista ei voi perua. Jatketaanko? activity.merged_prs_count_1 = Yhdistetty vetopyyntö activity.merged_prs_count_n = Yhdistettyä vetopyyntöä -activity.opened_prs_count_1 = ehdotettu vetopyyntö -activity.opened_prs_count_n = ehdotettua vetopyyntöä +activity.opened_prs_count_1 = Ehdotettu vetopyyntö +activity.opened_prs_count_n = Ehdotettua vetopyyntöä activity.title.user_1 = %d käyttäjä activity.title.prs_n = %d vetopyyntöä settings.sourcehut_builds.secrets = Salaisuudet -commit_graph = Sitoumuskaavio +commit_graph = Kommittikaavio visibility_helper = Tee tietovarastosta yksityinen pulls.approve_count_1 = %d hyväksyntä settings.confirm_delete = Poista tietovarasto @@ -1852,7 +1891,7 @@ pull.deleted_branch = (poistettu):%s settings.transfer.rejected = Tietovaraston siirto hylättiin. settings.transfer.modal.title = Siirrä omistajuus settings.event_pull_request_sync = Synkronoitu -editor.commit_empty_file_text = Tiedosto, jonka olet aikeissa sitoa, on tyhjä. Edetäänkö? +editor.commit_empty_file_text = Tiedosto, jonka olet aikeissa kommitoida, on tyhjä. Jatketaanko? diff.load = Lataa diff branch.create_branch_operation = Luo haara activity.title.releases_n = %d julkaisua @@ -1869,11 +1908,11 @@ pulls.expand_files = Laajenna kaikki tiedostot issues.content_history.delete_from_history = Poista historiasta milestones.filter_sort.name = Nimi issues.filter_milestone_all = Kaikki merkkipaalut -issues.filter_label_select_no_label = Ei tunnistetta +issues.filter_label_select_no_label = Ei nimilappua projects.column.set_default = Aseta oletukseksi projects.edit_success = Projekti "%s" on päivitetty. desc.sha256 = SHA256 -n_commit_one = %s sitoumus +n_commit_one = %s kommitti transfer.accept = Hyväksy siirto transfer.reject = Hylkää siirto default_branch_label = oletus @@ -1886,7 +1925,7 @@ need_auth = Valtuutus migrate_options = Migraatioasetukset projects.create_success = Projekti "%s" on luotu. projects.description = Kuvaus (valinnainen) -editor.commit_empty_file_header = Sitoudu tyhjä tiedosto +editor.commit_empty_file_header = Kommitoi tyhjä tiedosto editor.branch_does_not_exist = Haaraa "%s" ei ole olemassa tässä tietovarastossa. editor.delete = Poista %s editor.patching = Paikkaus: @@ -1914,7 +1953,7 @@ settings.branches.update_default_branch = Päivitä oletushaara settings.transfer.success = Tietovaraston siirto onnistui. settings.transfer_abort = Peru siirto settings.sync_mirror = Synkronoi nyt -settings.mirror_settings.docs.doc_link_title = Kuinka peilaan tietovarastot? +settings.mirror_settings.docs.doc_link_title = Kuinka peilaan tietovarastoja? tag.create_tag_operation = Luo tagi branch.rename = Nimeä haara "%s" uudelleen branch.download = Lataa haara "%s" @@ -1954,18 +1993,18 @@ stored_lfs = Talletettu Git LFS:llä activity.git_stats_author_1 = %d tekijä issues.choose.blank_about = Luo ongelma oletusarvoisesta mallipohjasta. pulls.made_using_agit = AGit -editor.cannot_edit_lfs_files = LFS-tiedostoja ei voi muokata web-käyttöliittymässä. +editor.cannot_edit_lfs_files = LFS-tiedostoja ei voi muokata selainkäyttöliittymässä. pulls.cmd_instruction_hint = Näytä komentoriviohjeet settings.wiki_globally_editable = Salli kenen tahansa muokata wikiä pulls.rebase_conflict_summary = Virheviesti wiki.search = Etsi wikistä -activity.commit = Sitoutumistoiminta -editor.cannot_edit_non_text_files = Binääritiedostoja ei voi muokata web-käyttöliittymässä. +activity.commit = Kommittitoiminta +editor.cannot_edit_non_text_files = Binääritiedostoja ei voi muokata selainkäyttöliittymässä. projects.template.desc_helper = Valitse projektin mallipohja aloittaaksesi -commit.contained_in_default_branch = Tämä sitoumus on osa oletushaaraa +commit.contained_in_default_branch = Tämä kommitti on osa oletushaaraa activity.git_stats_exclude_merges = Poissulkien yhdistämiset -activity.no_git_activity = Tällä ajanjaksolla ei ole ollut sitoutumistoimintaa. -activity.git_stats_commit_1 = %d sitoumus +activity.no_git_activity = Tällä ajanjaksolla ei ole ollut kommitointitoimintaa. +activity.git_stats_commit_1 = %d kommitin activity.git_stats_push_to_all_branches = kaikkiin haaroihin. settings.graphql_url = GraphQL:n URL-osoite branch.create_new_branch = Luo haara haarasta: @@ -1984,45 +2023,45 @@ migrate.forgejo.description = Tee migraatio codeberg.orgista tai muista Forgejo- migrate.gitbucket.description = Tee migraatio GitBucket-instansseista. migrate.onedev.description = Tee migraatio code.onedev.io:sta tai muista OneDev-instansseista. migrate.codebase.description = Tee migraatio codebasehq.comista. -migrate.git.description = Siirrä tietovarasto mistä tahansa Git-palvelusta. +migrate.git.description = Suorita tietovaraston migraatio mistä tahansa Git-palvelusta. migrate.gitlab.description = Tee migraatio gitlab.comista tai muista GitLab-instansseista. migrate.gitea.description = Tee migraatio gitea.comista tai muista Gitea-instansseista. repo_gitignore_helper_desc = Valitse mitä tiedostoja ei seurata yleisimpien kielten mallipohjista. Tyypilliset artefaktit, joita eri kielten koostamistyökalut tuottavat, lisätään .gitignore-tiedostoon oletusarvoisesti. -milestones.filter_sort.latest_due_date = Kaukaisin määräpäivä +milestones.filter_sort.latest_due_date = Kaukaisin eräpäivä license_helper_desc = Lisenssi määrää, mitä muut voivat ja eivät voi tehdä koodillasi. Etkö ole varma, mikä lisenssi soveltuu projektillesi? Lue ohje lisenssin valinnasta. -milestones.filter_sort.earliest_due_data = Lähin määräpäivä +milestones.filter_sort.earliest_due_data = Lähin eräpäivä issues.filter_type.reviewed_by_you = Katselmoitu toimestasi settings.units.overview = Yleisnäkymä -settings.remove_team_success = Joukkueen pääsy tietovarastoon on poistettu. +settings.remove_team_success = Tiimin pääsy tietovarastoon on poistettu. migrate.cancel_migrating_confirm = Haluatko perua tämän migraation? settings.units.units = Yksiköt settings.update_settings_no_unit = Tietovaraston tulisi sallia edes jonkinlainen vuorovaikutus. settings.units.add_more = Ota lisää käyttöön -settings.add_team_success = Joukkueella on nyt pääsy tietovarastoon. -settings.use_external_issue_tracker = Käytä ulkoista ongelmienseurantaa -settings.transfer_started = Tämä tietovarasto on merkitty siirrettäväksi ja se odottaa vahvistusta "%s":lta -signing.wont_sign.pubkey = Sitoumusta ei allekirjoiteta, koska sinulla ei ole julkista avainta liitetty tiliisi. +settings.add_team_success = Tiimillä on nyt pääsy tietovarastoon. +settings.use_external_issue_tracker = Käytä ulkoista ongelmanseurantaa +settings.transfer_started = Tämä tietovarasto on merkitty siirrettäväksi ja se odottaa vahvistusta käyttäjältä "%s" +signing.wont_sign.pubkey = Kommittia ei allekirjoiteta, koska sinulla ei ole julkista avainta liitetty tiliisi. settings.transfer_succeed = Tietovarasto on siirretty. activity.git_stats_on_default_branch = Haarassa %s, settings.tracker_issue_style.regexp = Säännöllinen lauseke wiki.reserved_page = Wikisivun nimi "%s" on varattu. pulls.recently_pushed_new_branches = Työnsit haaraan %[1]s %[2]s -signing.will_sign = Tämä sitoumus allekirjoitetaan avaimella "%s". -signing.wont_sign.never = Sitoumukset eivät ole koskaan allekirjoitettuja. +signing.will_sign = Tämä kommitti allekirjoitetaan avaimella "%s". +signing.wont_sign.never = Kommitit eivät ole koskaan allekirjoitettuja. settings.mirror_settings.direction = Suunta settings.mirror_settings.push_mirror.remote_url = Git-etätietovaraston URL-osoite -settings.issues_desc = Ota tietovaraston vianseuranta käyttöön -settings.use_internal_issue_tracker = Käytä sisäänrakennettua ongelmienseurantaa -settings.external_tracker_url = Ulkoisen ongelmienseurannan URL-osoite -settings.transfer_abort_success = Tietovaraston siirto %s:hen peruttiin. +settings.issues_desc = Ota tietovaraston ongelmanseuranta käyttöön +settings.use_internal_issue_tracker = Käytä sisäänrakennettua ongelmanseurantaa +settings.external_tracker_url = Ulkoisen ongelmanseurannan URL-osoite +settings.transfer_abort_success = Tietovaraston siirto käyttäjälle %s peruttiin. settings.transfer_quota_exceeded = Uusi omistaja (%s) on ylittänyt kiintiön. Tietovarastoa ei ole siirretty. settings.projects_desc = Ota tietovarastoprojektit käyttöön settings.releases_desc = Ota tietovaraston julkaisut käyttöön settings.packages_desc = Ota tietovarastopakettien rekisteri käyttöön activity.git_stats_push_to_branch = haaraan %s ja wiki.wiki_page_revisions = Sivun versiot -settings.wiki_desc = Ota tietovaraston Wiki käyttöön -signing.wont_sign.always = Sitoumukset ovat aina allekirjoitettuja. +settings.wiki_desc = Ota tietovaraston wiki käyttöön +signing.wont_sign.always = Kommitit ovat aina allekirjoitettuja. milestones.edit_subheader = Merkkipaalut järjestävät ongelmia ja seuraavat edistymistä. view_git_blame = Näytä git blame editor.push_rejected = Tämä muutos hylättiin palvelimen toimesta. Tarkista Git-koukut. @@ -2034,26 +2073,26 @@ releases.desc = Seuraa projektin versioita ja latauksia. settings.protect_patterns = Kaavat branch.new_branch_from = Luo uusi haara kohteesta "%s" settings.matrix.message_type = Viestin tyyppi -diff.committed_by = Sitoumuksen toimijana +diff.committed_by = kommitoinut invisible_runes_line = `Tällä rivillä on näkymättömiä Unicode-merkkejä` -editor.fork_before_edit = Sinun on luotava tämän tietovaraston haarukka voidaksesi tehdä tai ehdottaa muutoksia tähän tiedostoon. +editor.fork_before_edit = Sinun täytyy forkata tämä tietovarasto tehdäksesi tai ehdottaaksesi muutoksia tähän tiedostoon. editor.file_deleting_no_longer_exists = Poistettavaa tiedostoa "%s" ei enää ole tässä tietovarastossa. editor.add_tmpl.filename = tiedostonimi editor.fail_to_apply_patch = Ei voitu toteuttaa paikkaa "%s" editor.propose_file_change = Ehdota tiedostomuutosta -editor.new_branch_name = Nimeä uusi haara tätä sitoumusta varten +editor.new_branch_name = Nimeä uusi haara tätä kommittia varten editor.new_branch_name_desc = Uuden haaran nimi… editor.file_editing_no_longer_exists = Muokattavaa tiedostoa "%s" ei enää ole tässä tietovarastossa. -editor.cannot_commit_to_protected_branch = Suojattuun haaraan "%s" ei voi sitoutua. +editor.cannot_commit_to_protected_branch = Suojattuun haaraan "%s" ei voi kommitoida. issues.remove_request_review = Poista katselmointipyyntö issues.remove_request_review_block = Katselmointipyyntöä ei voi poistaa -pulls.require_signed_wont_sign = Haara vaatii allekirjoitettuja sitoumuksia, mutta tätä yhdistämistä ei allekirjoiteta +pulls.require_signed_wont_sign = Haara vaatii allekirjoitettuja kommitteja, mutta tätä yhdistämistä ei allekirjoiteta pulls.push_rejected_summary = Koko hylkäysviesti settings.unarchive.button = Kumoa tietovaraston arkistointi release.type_attachment = Liite tag.create_tag_from = Luo uusi tagi kohteesta"%s" topic.count_prompt = Voit valita korkeintaan 25 aihetta -settings.require_signed_commits = Vaadi allekirjoitettuja sitoumuksia +settings.require_signed_commits = Vaadi allekirjoitetut kommitit editor.push_rejected_summary = Koko hylkäysviesti: release.title = Julkaisun nimi release.tag_helper_existing = Olemassa oleva tagi. @@ -2065,7 +2104,7 @@ editor.must_have_write_access = Sinulla täytyy olla kirjoitusoikeus tehdäksesi issues.re_request_review = Pyydä katselmointia uudelleen pulls.status_checks_details = Yksityiskohdat release.title_empty = Nimi ei voi olla tyhjä. -archive.title = Tämä tietovarasto on arkistoitu. Voit tarkastella sen tiedostoja ja kloonata sen, mutta et voi tehdä muutoksia sen tilaan, kuten lähettää tai luoda uusia vikalippuja, vetopyyntöjä tai kommentteja. +archive.title = Tämä tietovarasto on arkistoitu. Voit tarkastella sen tiedostoja ja kloonata sen, mutta et voi tehdä muutoksia sen tilaan, kuten tehdä työntöjä tai luoda uusia ongelmia, vetopyyntöjä tai kommentteja. reactions_more = ja %d lisää mirror_address = Kloonaa URL-osoitteesta migrate_items_merge_requests = Yhdistämispyynnöt @@ -2077,9 +2116,9 @@ settings.discord_icon_url.exceeds_max_length = Kuvakkeen URL-osoite voi sisält settings.event_wiki_desc = Wiki-sivu luotu, nimetty uudelleen, muokattu tai poistettu. settings.event_pull_request_desc = Vetopyyntö avattu, suljettu, avattu uudelleen tai muokattu. settings.protect_branch_name_pattern = Suojatun haaran nimen kaava -issues.dependency.add_error_dep_not_same_repo = Molempien vikojen tulee olla samassa tietovarastossa. +issues.dependency.add_error_dep_not_same_repo = Molempien ongelmien tulee olla samassa tietovarastossa. settings.event_release = Julkaisu -pulls.merge_pull_request = Luo yhdistämissitoumus +pulls.merge_pull_request = Luo yhdistämiskommitti settings.pull_mirror_sync_quota_exceeded = Kiintiö ylitetty, ei vedetä muutoksia. settings.wiki_rename_branch_main_notices_1 = Tätä toimintoa EI VOI perua. settings.webhook.test_delivery_desc_disabled = Aktivoi webkoukku testataksesi sitä tekaistulla tapahtumalla. @@ -2092,31 +2131,31 @@ pulls.switch_comparison_type = Vaihda vertailutyyppiä settings.hooks_desc = Webkoukut tekevät automaattisesti HTTP POST -pyyntöjä palvelimelle, kun jotkin Forgejo-tapahtumat käynnistyvät. Lue lisää webkoukkujen oppaasta. issues.num_participants_one = %d osallistuja issues.reference_link = Viittaus: %s -settings.transfer_desc = Siirrä tämä tietovarasto käyttäjälle tai organisaatiolle, johon sinulla on hallintaoikeudet. +settings.transfer_desc = Siirrä tämä tietovarasto käyttäjälle tai organisaatiolle, johon sinulla on ylläpito-oikeudet. settings.add_collaborator = Lisää avustaja -settings.mirror_settings.push_mirror.none = Push-peilejä ei ole määritetty -settings.collaborator_deletion_desc = Yhteistyöhenkilön poistaminen peruuttaa hänen pääsynsä tähän tietovarastoon. Jatketaanko? -settings.archive.text = Tietovaraston arkistointi tekee siitä kokonaan vain-lukuisen. Se piilotetaan kojelaudalta. Kukaan –et edes sinä– ei voi tehdä uusia sitoumuksia tai avata vianlippuja tai vetopyyntöjä. -settings.mirror_settings.docs = Määritä tietovarastosi synkronoimaan sitoumukset, tagit ja haarat automaattisesti toisen tietovaraston kanssa. -settings.add_collaborator_duplicate = Yhteistyöhenkilö on jo lisätty tähän tietovarastoon. -settings.add_collaborator_blocked_them = Yhteistyöhenkilöä ei voida lisätä, koska hän on estänyt tietovaraston omistajan. -settings.add_collaborator_blocked_our = Yhteistyöhenkilöä ei voi lisätä, koska tietovaraston omistaja on estänyt hänet. -settings.default_branch_desc = Valitse oletustietovaraston haara vetopyyntöjä ja koodin sitoumuksia varten: +settings.mirror_settings.push_mirror.none = Työntöpeilejä ei ole määritetty +settings.collaborator_deletion_desc = Avustajan poistaminen peruuttaa hänen pääsynsä tähän tietovarastoon. Jatketaanko? +settings.archive.text = Tietovaraston arkistointi asettaa sen pelkkään lukutilaan. Se piilotetaan kojelaudalta. Kukaan ei voi tehdä (et edes sinä) uusia kommitteja, tai avata ongelmia tai vetopyyntöjä. +settings.mirror_settings.docs = Määritä tietovarastosi synkronoimaan kommitit, tagit ja haarat automaattisesti toisen tietovaraston kanssa. +settings.add_collaborator_duplicate = Avustaja on jo lisätty tähän tietovarastoon. +settings.add_collaborator_blocked_them = Avustajaa ei voi lisätä, koska hän on estänyt tietovaraston omistajan. +settings.add_collaborator_blocked_our = Avustajaa ei voi lisätä, koska tietovaraston omistaja on estänyt hänet. +settings.default_branch_desc = Valitse tietovaraston oletushaara vetopyyntöjä ja koodin kommitteja varten: issues.role.collaborator = Avustaja -settings.trust_model.collaboratorcommitter.long = Yhteistyöhenkilö + Sitoumuksen toimija: Luota yhteistyöhenkilöiden allekirjoituksiin, jotka vastaavat sitoumuksen toimijaa +settings.trust_model.collaboratorcommitter.long = Avustaja+kommitoija: Luota avustajien allekirjoituksiin, jotka vastaavat kommitoijaa settings.collaborator_deletion = Poista avustaja wiki.desc = Kirjoita ja jaa dokumentaatiota avustajien kesken. settings.trust_model.collaborator = Avustaja -mirror_sync_on_commit = Synkronoi, kun sitoumuksia työnnetään -settings.mirror_settings.docs.disabled_pull_mirror.instructions = Määritä projektisi työntämään sitoumukset, tagit ja haarat automaattisesti toiseen tietovarastoon. Sivustosi järjestelmänvalvoja on poistanut vetopeilit käytöstä. -settings.mirror_settings.docs.more_information_if_disabled = Löydät lisätietoja push- ja pull-peileistä täältä: -settings.mirror_settings.push_mirror.add = Lisää push-peili +mirror_sync_on_commit = Synkronoi, kun kommitteja työnnetään +settings.mirror_settings.docs.disabled_pull_mirror.instructions = Määritä projektisi työntämään kommitit, tagit ja haarat automaattisesti toiseen tietovarastoon. Sivustosi järjestelmänvalvoja on poistanut vetopeilit käytöstä. +settings.mirror_settings.docs.more_information_if_disabled = Löydät lisätietoja työntö- ja vetopeileistä täältä: +settings.mirror_settings.push_mirror.add = Lisää työntöpeili settings.mirror_settings.push_mirror.edit_sync_time = Muokkaa peilin synkronoinnin aikaväliä -settings.trust_model.collaboratorcommitter = Yhteistyöhenkilö + Sitoumuksen toimija +settings.trust_model.collaboratorcommitter = Avustaja+kommitoija settings.trust_model.default = Oletusarvoinen luottamusmalli settings.admin_enable_health_check = Ota tietovaraston terveystarkastukset käyttöön (git fsck) settings.remove_collaborator_success = Avustaja on poistettu. -issues.role.collaborator_helper = Tämä käyttäjä on kutsuttu yhteistyöhön tietovaraston parissa. +issues.role.collaborator_helper = Tämä käyttäjä on kutsuttu avustajaksi tietovarastoon. settings.pulls_desc = Ota tietovaraston vetopyynnöt käyttöön mirror_interval_invalid = Peilauksen aikaväli ei ole kelvollinen. settings.collaboration = Avustajat @@ -2126,56 +2165,56 @@ mirror_interval = Peilauksen aikaväli (kelvolliset yksiköt ovat "h", "m", "s") settings.add_collaborator_success = Avustaja on lisätty. settings.add_collaborator_owner = Omistajaa ei voi lisätä avustajaksi. settings.signing_settings = Allekirjoituksen vahvistuksen asetukset -settings.mirror_settings.docs.disabled_push_mirror.instructions = Määritä projektisi automaattisesti vetämään sitoumukset, tagit ja haarat toisesta tietovarastosta. +settings.mirror_settings.docs.disabled_push_mirror.instructions = Määritä projektisi automaattisesti vetämään kommitit, tagit ja haarat toisesta tietovarastosta. settings.trust_model.collaborator.long = Avustaja: Luota avustajien allekirjoituksiin issues.dependency.setting = Käytä riippuvuuksia ongelmiin ja vetopyyntöihin settings.allow_only_contributors_to_track_time = Salli vain avustajien seurata aikaa settings.actions_desc = Käytä integroituja CI-/CD-putkia Forgejo Actionsia hyödyntäen -settings.admin_enable_close_issues_via_commit_in_any_branch = Sulje vianlippu muussa kuin oletushaarassa tehdyllä sitoumuksella +settings.admin_enable_close_issues_via_commit_in_any_branch = Sulje ongelma muussa kuin oletushaarassa tehdyllä kommitilla settings.mirror_settings.pushed_repository = Työnnetty tietovarasto pulls.compare_changes_desc = Valitse haara, johon yhdistetään, ja haara, josta vedetään. no_eol.text = Ei EOL:ää auto_init_description = Aloita Git-historia README-tiedostolla ja valinnaisesti License- ja .gitignore-tiedostoilla. new_from_template = Käytä mallipohjaa -new_from_template_description = Voit valita olemassa olevan tietovarastomallipohjan tässä ilmentymässä ja käyttää sen asetuksia. +new_from_template_description = Voit valita olemassa olevan tietovarastomallipohjan tässä instanssissa ja käyttää sen asetuksia. new_advanced = Lisäasetukset new_advanced_expand = Laajenna napsauttamalla template_description = Mallipohjaisten tietovarastojen avulla käyttäjät voivat luoda uusia tietovarastoja, joilla on sama hakemistorakenne, tiedostot ja valinnaiset asetukset. settings.enter_repo_name = Syötä omistajan ja tietovaraston nimi täsmälleen kuten esitetty: settings.confirmation_string = Vahvistusteksti -settings.delete_notices_2 = - Tämä toiminto poistaa pysyvästi %s tietovaraston, joka sisältää koodin, viat, kommentit, Wiki-tiedot ja yhteistyöhenkilön asetukset. +settings.delete_notices_2 = - Tämä toiminto poistaa pysyvästi tietovaraston %s mukaan lukien koodin, ongelmat, kommentit, wikidatan ja avustaja-asetukset. issues.filter_assginee_no_select = Kaikki käsittelijät issues.new.assign_to_me = Osoita itselle -pulls.closed_at = `sulki tämän vetopyynnön %[2]s` +pulls.closed_at = `sulki tämän vetopyynnön %s` tree_path_not_found_branch = Polkua %[1]s ei ole olemassa haarassa %[2]s transfer.no_permission_to_reject = Sinulla ei ole oikeutta hylätä tätä siirtoa. generate_repo = Luo tietovarasto -tree_path_not_found_commit = Polkua %[1]s ei ole olemassa sitoumuksessa %[2]s -archive.pull.noreview = Tämä tietovarasto on arkistoitu. Et voi arvioida vetopyyntöjä. +tree_path_not_found_commit = Polkua %[1]s ei ole olemassa kommitissa %[2]s +archive.pull.noreview = Tämä tietovarasto on arkistoitu. Et voi katselmoida vetopyyntöjä. tree_path_not_found_tag = Polkua %[1]s ei ole olemassa tagissa %[2]s transfer.no_permission_to_accept = Sinulla ei ole oikeutta hyväksyä tätä siirtoa. settings.web_hook_name_feishu = Feishu / Lark Suite issues.review.reviewers = Katselmoijat issues.new.no_reviewers = Ei katselmoijia -issues.add_label = lisäsi tunnisteen %s %s -issues.due_date_added = lisäsi määräpäivän %s %s +issues.add_label = lisäsi nimilapun %s %s +issues.due_date_added = lisäsi eräpäivän %s %s issues.review.add_review_request = pyysi katselmointia käyttäjältä %[1]s %[2]s -issues.ref_pull_from = `viittasi tähän vetopyyntöön %[4]s %[2]s` -pulls.commit_ref_at = `viittasi tähän vetopyyntöön sitoumuksesta %[2]s` +issues.ref_pull_from = `viittasi tähän vetopyyntöön %[3]s %[1]s` +pulls.commit_ref_at = `viittasi tähän vetopyyntöön kommitista %s` issues.review.comment = katselmoi %s -issues.add_labels = lisäsi tunnisteet %s %s +issues.add_labels = lisäsi nimilaput %s %s issues.review.add_review_requests = pyysi katselmointeja käyttäjiltä %[1]s %[2]s pulls.blocked_by_official_review_requests = Tämä vetopyyntö on estetty, koska siltä puuttuu hyväksyntä yhdeltä tai useammalta viralliselta katselmoijalta. issues.author.tooltip.issue = Tämä käyttäjä on tämän ongelman tekijä. issues.author.tooltip.pr = Tämä käyttäjä on tämän vetopyynnön tekijä. -issues.role.contributor_helper = Tämä käyttäjä on aiemmin sitoutunut tähän tietovarastoon. -settings.event_pull_request_label = Tunnisteet -issues.due_date_remove = poisti määräpäivän %s %s -settings.event_issue_label = Tunnisteet +issues.role.contributor_helper = Tämä käyttäjä on aiemmin kommitoinut tähän tietovarastoon. +settings.event_pull_request_label = Nimilaput +issues.due_date_remove = poisti eräpäivän %s %s +settings.event_issue_label = Nimilaput settings.authorization_header = Authorization-otsake diff.has_escaped = Tällä rivillä on piilotettuja Unicode-merkkejä issues.max_pinned = Et voi kiinnittää enempää ongelmia -settings.external_tracker_url_error = Ulkoisen ongelmienseurannan URL-osoite ei ole kelvollinen. +settings.external_tracker_url_error = Ulkoisen ongelmanseurannan URL-osoite ei ole kelvollinen. settings.event_pull_request_review = Katselmoinnit settings.event_pull_request_review_request = Katselmointipyynnöt issues.num_reviews_one = %d katselmointi @@ -2183,8 +2222,8 @@ issues.num_reviews_few = %d katselmointia issues.filter_no_results = Ei tuloksia issues.filter_no_results_placeholder = Kokeile määrittää eri hakusuodattimet. projects.edit_subheader = Projektit organisoivat ongelmia ja seuraavat edistymistä. -issues.label_templates.title = Lataa tunnisteen esiasetus -issues.label_deletion_desc = Tunnisteen poistaminen poistaa sen kaikista ongelmista. Jatketaanko? +issues.label_templates.title = Lataa nimilapun esiasetus +issues.label_deletion_desc = Nimilapun poistaminen poistaa sen kaikista ongelmista. Jatketaanko? issues.attachment.download = `Napsauta ladataksesi "%s"` issues.review.option.hide_outdated_comments = Piilota vanhentuneet kommentit issues.review.show_outdated = Näytä vanhentuneet @@ -2195,8 +2234,8 @@ issues.unpin_issue = Poista ongelman kiinnitys issues.review.outdated_description = Sisältö on muuttunut siitä ajanhetkestä, kun tämä kommentti luotiin issues.review.option.show_outdated_comments = Näytä vanhentuneet kommentit issues.review.outdated = Vanhentunut -issues.label_templates.use = Käytä tunnisteen esiasetusta -issues.label_deletion_success = Tunniste on poistettu. +issues.label_templates.use = Käytä nimilapun esiasetusta +issues.label_deletion_success = Nimilappu on poistettu. issues.cancel_tracking = Hylkää issues.choose.get_started = Aloitetaan settings.event_fork = Forkkaus @@ -2206,27 +2245,338 @@ delete_preexisting = Poista olemassa olevat tiedostot issues.reaction.add = Lisää reaktio pulls.merged_by = %[3]s:n yhdistettiin %[1]s pulls.merged_by_fake = %[2]s:n yhdistettiin %[1]s -delete_preexisting_success = Poistetut omaksumattomat tiedostot %s:ssa +delete_preexisting_success = Poistetut omaksumattomat tiedostot kohteesta %s pulls.no_merge_wip = Tätä vetopyyntöä ei voida yhdistää, koska se on merkitty keskeneräiseksi työksi. pulls.clear_merge_message = Tyhjennä yhdistämisviesti activity.title.prs_merged_by = %s yhdisti %s -settings.protect_status_check_patterns_desc = Syötä kuviot määrittääksesi, mitkä tilatarkistukset on läpäistävä ennen kuin haarat voidaan yhdistää tätä sääntöä vastaavaan haaraan. Jokainen rivi määrittää kuvion. Kuviot eivät saa olla tyhjiä. -adopt_search = Syötä käyttäjänimi etsiäksesi omaksumattomia tietovarastoja (jätä tyhjäksi löytääksesi kaikki) +settings.protect_status_check_patterns_desc = Syötä kaavat määrittääksesi, mitkä tilatarkistukset on läpäistävä, ennen kuin haarat voidaan yhdistää tätä sääntöä vastaavaan haaraan. Jokainen rivi määrittää kaavan. Kaavat eivät saa olla tyhjiä. +adopt_search = Syötä käyttäjänimi etsiäksesi omaksumattomia tietovarastoja… (jätä tyhjäksi löytääksesi kaikki) pulls.cmd_instruction_merge_warning = Varoitus: Asetusta ”Tunnista manuaalinen yhdistäminen automaattisesti” ei ole otettu käyttöön tässä tietovarastossa. Sinun on merkittävä tämä vetopyyntö manuaalisesti yhdistetyksi jälkikäteen. pulls.cmd_instruction_merge_desc = Yhdistä muutokset ja päivitä Forgejossa. pulls.cannot_auto_merge_desc = Tätä vetopyyntöä ei voida yhdistää automaattisesti ristiriitojen vuoksi. -adopt_preexisting_success = Omaksutut tiedostot ja luotu tietovarasto %s:lta -issues.comment_manually_pull_merged_at = manuaalisesti yhdistetty sitoumus %[1]s %[2]s:hen %[3]s +adopt_preexisting_success = Omaksuttu tiedostot ja luotu tietovarasto lähteestä %s +issues.comment_manually_pull_merged_at = manuaalisesti yhdistetty kommitti %[1]s %[2]s tietovarastoon %[3]s pulls.cmd_instruction_merge_title = Yhdistä pulls.has_merged = Epäonnistui: vetopyyntö on yhdistetty, joten et voi yhdistää uudelleen tai muuttaa kohdehaaraa. pulls.cmd_instruction_checkout_title = Uloskuittaus -pulls.cmd_instruction_checkout_desc = Projektitietovarastostasi, kuita ulos uusi haara ja testaa muutokset. -pulls.clear_merge_message_hint = Yhdistämisviestin tyhjentäminen poistaa vain sitoumusviestin sisällön ja säilyttää luodut git-trailerit, kuten "Yhteistekijänä …". -settings.protect_check_status_contexts_desc = Vaadi tilatarkistusten läpäisy ennen yhdistämistä. Kun se on otettu käyttöön, sitoumukset on ensin työnnettävä toiseen haaraan ja sitten yhdistettävä tai työnnettävä suoraan tätä sääntöä vastaavaan haaraan tilantarkistuksen jälkeen. Jos konteksteja ei löydy, viimeisen sitoumuksen on oltava onnistunut kontekstista riippumatta. -issues.comment_pull_merged_at = yhdistetty sitoumus %[1]s %[2]s :hen %[3]s +pulls.cmd_instruction_checkout_desc = Kuittaa ulos uusi haara projektitietovarastostasi ja testaa muutokset. +pulls.clear_merge_message_hint = Yhdistämisviestin tyhjentäminen poistaa vain kommittiviestin sisällön ja säilyttää luodut git-trailerit, kuten "Co-Authored-By…". +settings.protect_check_status_contexts_desc = Vaadi tilatarkistusten läpäisy ennen yhdistämistä. Kun käytössä, kommitit on ensin työnnettävä toiseen haaraan ja sitten yhdistettävä tai työnnettävä suoraan tätä sääntöä vastaavaan haaraan tilantarkistuksen jälkeen. Jos konteksteja ei löydy, viimeisen kommitin on oltava onnistunut kontekstista riippumatta. +issues.comment_pull_merged_at = yhdistetty kommitti %[1]s %[2]s tietovarastoon %[3]s settings.pulls.enable_autodetect_manual_merge = Ota Tunnista manuaalinen yhdistäminen automaattisesti -asetus käyttöön (Huomaa: joissakin erityistapauksissa voi esiintyä virhearviointeja) pulls.no_merge_desc = Tätä vetopyyntöä ei voida yhdistää, koska kaikki tietovaraston yhdistämisvaihtoehdot ovat poistettu käytöstä. -pulls.no_merge_not_ready = Tämä vetopyyntö ei ole valmis yhdistettäväksi. Tarkista arvioinnin tila ja tilantarkistukset. +pulls.no_merge_not_ready = Tämä vetopyyntö ei ole valmis yhdistettäväksi. Tarkista katselmoinnin tila ja tilantarkistukset. +issues.is_stale = Tähän vetopyyntöön on kohdistunut muutoksia tämän katselmoinnin jälkeen +migrate.repo_desc_helper = Jätä tyhjäksi tuodaksesi olemassa olevan kuvauksen +issues.role.first_time_contributor = Avustaja ensimmäistä kertaa +issues.role.contributor = Avustaja +activity.opened_prs_label = Ehdotettu +settings.require_signed_commits_desc = Hylkää työnnöt tähän haaraan, jos niitä ei ole allekirjoitettu tai ne eivät ole vahvistettuja. +issues.role.first_time_contributor_helper = Tämä on käyttäjän ensimmäinen kontribuutio tähän tietovarastoon. +editor.upload_files_to_dir = Lähetä tiedostot hakemistoon "%s" +adopt_preexisting = Omaksu olemassa olevat tiedostot +issues.dismiss_review_warning = Haluatko hylätä katselmoinnin? +commit.operations = Toimenpiteet +commits.view_single_diff = Näytä tässä kommitissa tähän tiedostoon kohdistuneet muutokset +issues.choose.ignore_invalid_templates = Virheelliset mallipohjat on jätetty huomiotta +migrate.migrating_milestones = Suoritetaan merkkipaalujen migraatiota +migrate.migrating_issues = Suoritetaan ongelmien migraatiota +migrate.clone_local_path = tai paikallisen palvelimen polku +pulls.filter_changes_by_commit = Suodata kommitin perusteella +pulls.show_changes_since_your_last_review = Näytä viimeisimmän katselmointisi jälkeiset muutokset +pulls.cant_reopen_deleted_branch = Tätä vetopyyntöä ei voi avata uudelleen, koska haara poistettiin. +mirror_sync = synkronoitu +mirror_lfs_endpoint = LFS-päätepiste +language_other = Muu +adopt_preexisting_label = Omaksu tiedostot +issues.role.member_helper = Tämä käyttäjä on tietovaraston omistavan organisaation jäsen. +migrate.invalid_local_path = Paikallinen polku on virheellinen. Sitä ei ole olemassa tai se ei ole hakemisto. +migrate.invalid_lfs_endpoint = LFS-päätepiste ei ole kelvollinen. +issues.new.clear_projects = Tyhjennä projektit +mirror_denied_combination = Julkiseen avaimeen ja salasanaan pohjautuvaa todennusta ei voi käyttää yhdessä. +template.git_content = Git-sisältö (Oletushaara) +migrate.migrating_releases = Suoritetaan julkaisujen migraatiota +unit_disabled = Sivuston ylläpitäjä on poistanut käytöstä tämän tietovarasto-osion. +issues.filter_sort.relevance = Asiaankuuluvuus +pulls.reopen_to_merge = Avaa tämä vetopyyntö uudelleen suorittaaksesi yhdistämisen. +archive.nocomment = Kommentointi ei ole mahdollista, koska tietovarasto on arkistoitu. +projects.column.set_default_desc = Aseta tämä sarake oletukseksi luokittelemattomille ongelmille ja vedoille +issues.review.remove_review_request_self = kieltäytyi katselmoimasta %s +from_comment = (kommentti) +issues.dismiss_review = Hylkää katselmointi +editor.file_changed_while_editing = Tiedoston sisältö on muuttunut sen avaamisen jälkeen. Napsauta tästä nähdäksesi muutokset tai kommitoi muutokset uudelleen korvataksesi muutokset. +sync_fork.button = Synkronoi +migrated_from = Suoritettu migraatio lähteestä %[2]s +migrate.migrating_topics = Suoritetaan aiheiden migraatiota +migrate.migrating_pulls = Suoritetaan vetopyyntöjen migraatiota +migrate.cancel_migrating_title = Peruuta migraatio +file_follow = Seuraa symbolista linkkiä +commit.load_referencing_branches_and_tags = Lataa haarat ja tagit, jotka viittaavat tähän kommittiin +editor.commit_id_not_matching = Tiedosto muuttui sillä aikaa, kun muokkasit sitä. Kommitoi uuteen haaraan ja yhdistä sen jälkeen. +projects.new_subheader = Koordinoi, seuraa ja päivitä työtä yhdessä paikassa, jotta projektit pysyvät läpinäkyvinä ja aikataulussa. +issues.del_time_history = `poisti käytetyn ajan %s` +issues.dependency.removed_dependency = `poisti riippuvuuden %s` +issues.dependency.add_error_same_issue = Et voi tehdä ongelmaa riippuvaiseksi itsestään. +projects.card_type.desc = Korttiesikatselut +issues.dependency.added_dependency = `lisäsi uuden riippuvuuden %s` +fork_no_valid_owners = Tätä tietovarastoa ei voi forkata, koska sillä ei ole kelvollisia omistajia. +fork_branch = Forkkiin kloonattava haara +editor.must_be_on_a_branch = Sinun tulee olla haarassa, jotta voit tehdä tai ehdottaa muutoksia tähän tiedostoon. +editor.commit_message_desc = Lisää valinnainen, laajennettu kuvaus… +migrate_options_lfs = Tee migraatio LFS-tiedostoille +migrate_options_lfs_endpoint.label = LFS-päätepiste +commits.browse_further = Selaa kauemmas +issues.filter_projects = Suodata projekti +issues.filter_labels = Suodata nimilappu +commits.no_commits = Ei yhteisiä kommitteja. "%s" ja "%s" omaavat täysin eri historiat. +projects.column.deletion_desc = Projektin sarakkeen poistaminen siirtää kaikki siihen liittyvät ongelmat oletussarakkeeseen. Jatketaanko? +issues.del_time = Poista tämä aikaloki +migrated_from_fake = Suoritettu migraatio lähteestä %[1]s +migrate.migrate = Tee migraatio lähteestä %s +migrate.migrating_labels = Suoritetaan nimilappujen migraatiota +file_view_rendered = Näytä renderöitynä +editor.invalid_commit_mail = Virheellinen sähköposti kommitin luomista varten. +sync_fork.branch_behind_one = Tämä haara on %[1]d kommitin jäljessä %[2]s +sync_fork.branch_behind_few = Tämä haara on %[1]d kommittia jäljessä %[2]s +no_eol.tooltip = Tämä tiedosto ei sisällä lopussa olevaa rivin loppu -merkkiä. +issues.filter_reviewers = Suodata katselmoija +commits.view_path = Näytä tässä historian kohdassa +commit.revert = Palauta +commit.revert-header = Palauta: %s +mirror_use_ssh.not_available = SSH-todennus ei ole käytettävissä. +pulls.showing_specified_commit_range = Näytetään vain muutokset välillä %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range = Valitse kommitti. Pidä pohjassa shift + napsauta valitaksesi alueen +blame_prior = Näytä blame ennen tätä muutosta +migrate.migrating_failed.error = Migraatio epäonnistui: %s +editor.no_commit_to_branch = Kommitointi suoraan haaraan ei onnistu syystä: +editor.user_no_push_to_branch = Käyttäjä ei voi työntää haaraan +issues.delete.text = Haluatko varmasti poistaa tämän ongelman? (Sisältö poistetaan pysyvästi. Harkitse sen sijaan sulkemista, jos haluat pitää ongelman arkistoituna) +issues.context.menu = Kommenttivalikko +archive.title_date = Tämä tietovarasto arkistoitiin %s. Voit tarkastella sen tiedostoja ja kloonata sen, mutta et voi tehdä muutoksia sen tilaan, kuten tehdä työntöjä tai luoda uusia ongelmia, vetopyyntöjä tai kommentteja. +pulls.cannot_auto_merge_helper = Yhdistä manuaalisesti selvittääksesi konfliktit. +pulls.merge_conflict = Yhdistäminen epäonnistui: Yhdistämisen aikana tapahtui ristiriita. Vihje: Kokeile eri strategiaa +pulls.is_checking = Yhdistämisen ristiriidan tarkistus on meneillään. Kokeile uudelleen hetken kuluttua. +pulls.is_empty = Tässä haarassa olevat muutokset ovat jo kohdehaarassa. Tästä tulee tyhjä kommitti. +pulls.required_status_check_administrator = Ylläpitäjänä voit silti yhdistää tämän vetopyynnön. +pulls.required_status_check_failed = Jotkin vaaditut tarkistukset eivät onnistuneet. +pulls.required_status_check_missing = Jotkin vaaditut tarkistukset puuttuvat. +pulls.status_checks_failure = Jotkin tarkistukset epäonnistuivat +pulls.status_checks_show_all = Näytä kaikki tarkistukset +pulls.auto_merge_cancel_schedule = Peru automaattinen yhdistäminen +pulls.auto_merge_newly_scheduled = Tämä vetopyyntö aikataulutettiin yhdistettäväksi, kun kaikki tarkistukset onnistuvat. +pulls.auto_merge_not_scheduled = Tätä vetopyyntöä ei ole aikataulutettu automaattisesti yhdistettäväksi. +comments.edit.already_changed = Muutosten tallentaminen kommenttiin epäonnistui. Vaikuttaa siltä, että sisältöä on jo muutettu toisen käyttäjän toimesta. Päivitä sivu ja muokkaa uudelleen välttääksesi ylikirjoittamasta muiden muutoksia +signing.wont_sign.nokey = Tässä instanssissa ei ole avainta tämän kommitin allekirjoittamiseksi. +pulls.is_ancestor = Tämä haara on jo sisällytetty kohdehaaraan. Yhdistettävää ei ole. +pulls.blocked_by_rejection = Tämä vetopyyntö sisältää virallisen katselmoijan vaatimisia muutoksia. +pulls.status_checks_success = Kaikki tarkistukset onnistuivat +pulls.agit_explanation = Luotu käyttäen AGit-työnkulkua. AGit antaa avustajien ehdottaa muutoksia käyttämällä "git push" ilman, että uutta forkkia tai uutta haaraa luodaan. +milestones.invalid_due_date_format = Eräpäivän muodon tulee olla "yyyy-mm-dd". +wiki.original_git_entry_tooltip = Näytä alkuperäinen Git-tiedosto sen sijaan, että ystävällistä linkkiä käytetään. +pulls.blocked_by_approvals = Tällä vetopyynnöllä ei ole riittävästi hyväksyntöjä. %d/%d hyväksyntää myönnetty. +pulls.status_checks_hide_all = Piilota kaikki tarkistukset +pulls.blocked_by_user = Et voi luoda vetopyyntöä tähän tietovarastoon, koska tietovarastojan omistaja on estänyt sinut. +pulls.delete.text = Haluatko varmasti poistaa tämän vetopyynnön? (Kaikki sisältö poistetaan pysyvästi. Harkitse sen sijaan sulkemista, jos haluat pitää arkistoituna) +wiki.page_name_desc = Kirjoita tämän wikisivun nimi. Joitain erikoisnimiä ovat: "Home", "_Sidebar" ja "_Footer". +pulls.blocked_by_changed_protected_files_1 = Tämä vetopyyntö sisältää suojatun tiedoston ja on siksi estetty: +pulls.status_checks_warning = Jotkin tarkistukset raportoivat varoituksia +pulls.status_checks_error = Jotkin tarkistukset raportoivat virheitä +pulls.reopened_at = `avasi uudelleen tämän vetopyynnön %s` +pulls.auto_merge_when_succeed = Yhdistä automaatisesti kun kaikki tarkistukset onnistuvat +signing.wont_sign.error = Tapahtui virhe tarkistaessa voiko kommitin allekirjoittaa. +signing.wont_sign.twofa = Sinulla tulee olla kaksivaiheinen todennus käytössä, jotta kommitit voi allekirjoittaa. +pulls.data_broken = Tämä vetopyyntö on rikki johtuen puuttuvasta forkkitiedosta. +pulls.files_conflicted = Tämä vetopyyntö sisältää muutoksia, jotka ovat ristiriidassa kohdehaaran kanssa. +pulls.auto_merge_button_when_succeed = (Kun tarkistukset onnistuvat) +pulls.blocked_by_outdated_branch = Tämä vetopyyntö on vanhentunut ja siksi estetty. +pulls.blocked_by_changed_protected_files_n = Tämä vetopyyntö on estetty, koske se muuttaa suojattuja tiedostoja: +pulls.status_checking = Jotkin tarkistukset odottavat +pulls.auto_merge_canceled_schedule = Tämän vetopyynnön automaattinen yhdistäminen peruttiin. +activity.title.prs_opened_by = %s ehdottanut %s +settings.convert_fork = Konvertoi tavalliseksi tietovarastoksi +settings.convert_fork_confirm = Konvertoi tietovarasto +settings.add_collaborator_inactive_user = Inaktiivista käyttäjää ei voi lisätä avustajaksi. +settings.mirror_settings.docs.pulling_remote_title = Etätietovarastosta vetäminen +settings.pull_mirror_sync_in_progress = Vedetään muutoksia etäpuolelta %s tällä hetkellä. +settings.convert = Konvertoi tavalliseksi tietovarastoksi +settings.transfer_in_progress = Meneillään on jo siirto. Peruuta siirto, jos haluat siirtää tämän tietovaraston toiselle käyttäjälle. +settings.trust_model.collaborator.desc = Tämän tietovaraston avustajien kelvolliset allekirjoitukset merkitään luotetuiksi ("trusted") - (ottamatta kantaa, vastaavatko he kommitoijaa vai eivät). Muussa tapauksessa allekirjoitetukset merkitään ei-luotetuiksi ("untrusted"), jos allekirjoitus vastaa kommitoijaa, ja vastaamaton ("unmatched") jos ei. +settings.confirm_wiki_branch_rename = Nimeä uudelleen wikin haara +settings.event_pull_request_assign = Toimeksianto +settings.event_pull_request_assign_desc = Vetopyynnön toimeksianto luotu tai toimeksiannon osoitus poistettu. +settings.event_pull_request_label_desc = Vetopyynnön nimilappuja lisätty tai poistettu. +settings.active = Aktiivinen +settings.packagist_api_token = API-poletti +settings.protect_whitelist_committers = Sallittujen listalla rajoitettu työntö +settings.protect_enable_merge = Ota yhdistäminen käyttöön +settings.protect_enable_merge_desc = Kenen tahansa kirjoituspääsyllä sallitaan yhdistää vetopyynnöt tähän haaraan. +settings.protect_status_check_patterns = Tilatarkistuskaavat +settings.protect_invalid_status_check_pattern = Virheellinen tilatarkistuksen kaava: "%s". +settings.protect_no_valid_status_check_patterns = Ei kelvollisia tilatarkistuksen kaavoja. +settings.protect_required_approvals_desc = Salli yhdistäminen vain vetopyynnöille, joilla on riittävästi positiivisia katselmointeja. +settings.protect_protected_file_patterns = Suojattujen tiedostojen kaavat (erotettu puolipisteellä ";") +settings.remove_protected_branch_success = Haaran suojaus säännölle "%s" on poistettu. +settings.enforce_on_admins = Pakota tämä sääntö tietovaraston ylläpitäjiin +settings.add_key_success = Toimitusavain "%s" on lisätty. +settings.update_mirror_settings = Päivitä peilin asetukset +settings.tracker_url_format_error = Ulkoisen ongelmanseurannan URL-muoto ei ole kelvollinen URL-osoite. +settings.wiki_rename_branch_main = Normalisoi Wiki-haaran nimi +settings.wiki_delete_notices_1 = - Tämä poistaa pysyvästi wikin ja poistaa tietovaraston %s wikin käytöstä. +settings.webhook.replay.description_disabled = Toista webkoukku aktivoimalla se. +settings.add_webhook_desc = Forgejo lähettää POST-pyyntöjä määritetyllä "Content-Type":llä kohde-URL-osoitteeseen. Lue lisää webkoukkujen oppaasta. +activity.title.unresolved_conv_n = %d selvittämätöntä keskustelua +activity.title.releases_published_by = %s julkaissut %s +settings.desc = Asetuksissa voit hallita tietovaraston asetuksia +settings.external_tracker_url_desc = Kävijät ohjataan ulkoisen ongelmanseurannan URL-osoitteeseen, kun Ongelmat-välilehteä napsautetaan. +settings.tracker_issue_style = Ulkoisen ongelmanseurannan numeromuoto +settings.tracker_issue_style.regexp_pattern = Säännöllisen lausekkeen kaava +settings.convert_confirm = Konvertoi tietovarasto +settings.webhook.delivery.success = Tapahtuma on lisätty toimitusjonoon. Saattaa kestää muutama sekunti, ennen kuin se näkyy toimitushistoriassa. +settings.event_pull_request_approvals = Vetopyynnön hyväksynnät +settings.protect_enable_push = Ota työntö käyttöön +settings.protect_whitelist_committers_desc = Vain sallittujen listalla olevat käyttäjät tai tiimit sallitaan työntää tähän haaraan (mutta ei työntää väkisin). +settings.block_rejected_reviews = Estä yhdistäminen hylätyillä katselmoinneilla +settings.is_writable = Ota kirjoituspääsy käyttöön +settings.delete_notices_fork_1 = - Poistamisen jälkeen tämän tietovaraston forkeista tulee itsenäisiä. +settings.event_pull_request_review_request_desc = Vetopyynnön katselmointi pyydetty tai katselmointipyyntö poistettu. +settings.event_pull_request_merge = Vetopyynnön yhdistäminen +settings.protect_approvals_whitelist_enabled = Rajoita hyväksynnät vain sallittujen käyttäjien tai tiimien listoilla oleviin +settings.packagist_package_url = Packagist-paketin URL-osoite +settings.packagist_username = Packagist-käyttäjänimi +settings.sourcehut_builds.manifest_path = Koontimanifestin polku +settings.event_pull_request_sync_desc = Haara päivitetty automaattisesti kohdehaaralla. +settings.trust_model.committer = Kommitoija +settings.convert_succeed = Peili on konvertoitu tavalliseksi tietovarastoksi. +settings.transfer_notices_1 = - Menetät pääsyn tähän tietovarastoon, jos siirrät sen yksittäiselle käyttäjälle. +settings.githooks_desc = Git-koukuista vastaa Git itse. Voit muokata koukkutiedostoja alla määrittääksesi omavalintaisia toimenpiteitä. +settings.update_protect_branch_success = Haaran suojaus säännölle "%s" on päivitetty. +settings.mirror_settings.direction.push = Työntö +settings.convert_desc = Voit konvertoida tämän peilin tavalliseksi tietovarastoksi. Tätä ei voi perua. +settings.trust_model.committer.long = Kommitoija: Luota allekirjoituksiin, jotka vastaavat kommitoijia (Tämä vastaa GitHubia ja pakottaa Forgejo-allekirjoitetut kommitit olemaan Forgejo-käyttäjän kommitoimia) +settings.protected_branch_deletion_desc = Haaran suojauksen poistaminen käytöstä sallii käyttäjien kirjoitusoikeudella työntää haaraan. Jatketaanko? +settings.admin_code_indexer = Koodin indeksoija +settings.remove_protected_branch_failed = Haaran suojaussäännön "%s" poistaminen epäonnistui. +settings.block_outdated_branch = Estä yhdistäminen jos vetopyyntö on vanhentunut +settings.protect_branch_name_pattern_desc = Suojattujen haarojen nimien kaavat. Lue dokumentaatio kaavojen syntaksista. Esimerkkejä: main, release/** +settings.block_rejected_reviews_desc = Yhdistäminen ei ole mahdollista, kun viralliset katselmoijat pyytävät muutoksia, siitä huolimatta että hyväksyntöjä olisi riittävästi. +settings.convert_notices_1 = Tämä toimenpide konvertoi peilin tavalliseksi tietovarastoksi, eikä toimenpidettä voi perua. +settings.convert_fork_notices_1 = Tämä toimenpide konvertoi forkin tavalliseksi tietovarastoksi, eikä toimenpidettä voi perua. +settings.convert_fork_succeed = Forkki on konvertoitu tavalliseksi tietovarastoksi. +settings.transfer_abort_invalid = Et voi perua olematonta tietovaraston siirtoa. +settings.trust_model.default.desc = Käytä oletusarvoista tietovaraston luottamusmallia tälle asennukselle. +settings.enforce_on_admins_desc = Tietovaraston ylläpitäjät eivät voi ohittaa tätä sääntöä. +settings.reindex_requested = Uudelleenindeksointi pyydetty +settings.admin_stats_indexer = Kooditilastojen indeksoija +settings.admin_indexer_commit_sha = Viimeksi indeksoitu kommitti +settings.webhook.replay.description = Toista webkoukku uudelleen. +settings.event_push_only = Työntötapahtumat +settings.authorization_header_desc = Sisällytetään Authorization-otsakkeeseen pyynnöissä. Esimerkkejä: %s. +settings.protect_approvals_whitelist_teams = Katselmointeihin oikeutettujen tiimien lista +settings.federation_not_enabled = Federaatiota ei ole käytössä käyttämässäsi instanssissa. +settings.mirror_settings.push_mirror.none_ssh = Ei mitään +settings.mirror_settings.direction.pull = Veto +settings.push_mirror_sync_in_progress = Työnnetään muutoksia etäpuoleen %s tällä hetkellä. +settings.transfer_notices_2 = - Säilytät pääsyn tietovarastoon, jos siirrät sen organisaatiolle, jonka omistat kokonaan tai osittain. +settings.protect_merge_whitelist_teams = Yhdistämiseen oikeutettujen tiimien lista +settings.protect_check_status_contexts_list = Tilatarkistuksia löytyi tälle tietovarastolle viime viikolta +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning = Tällä hetkellä tämä on mahdollista vain valitsemalla "Uusi migraatio" valikosta. Saadaksesi lisätietoja: +settings.mirror_settings.docs.disabled_push_mirror.info = Työntöpeilit on poistettu käytöstä sivuston ylläpidon toimesta. +settings.mirror_settings.mirrored_repository = Peilattu tietovarasto +settings.protect_check_status_contexts = Ota tilatarkistus käyttöön +settings.pulls.default_delete_branch_after_merge = Poista vetopyynnön haara yhdistämisen jälkeen oletusarvoisesti +settings.content_type = POST-sisältötyyppi +settings.event_pull_request_comment_desc = Vetopyynnön kommentti luotu, muokattu tai poistettu. +settings.event_pull_request_review_desc = Vetopyyntö hyväksytty, hylätty tai katselmoinnin kommentteja lisätty. +settings.admin_indexer_unindexed = Indeksöimätön +settings.reindex_button = Lisää uudelleenindeksoinnin jonoon +settings.web_hook_name_wechatwork = WeCom (Wechat Work) +settings.delete_team_tip = Tällä tiimillä on pääsy kaikkiin tietovarastoihin, eikä sitä voi poistaa +settings.sourcehut_builds.visibility = Työn näkyvyys +settings.sourcehut_builds.access_token_helper = Pääsypoletti, jolla on myöntö JOBS:RW. Luo builds.sr.ht-poletti tai builds.sr.ht-poletti pääsyllä salaisuuksiin meta.sr.ht:ssä. +settings.team_not_in_organization = Tiimi ei ole samassa organisaatiossa kuin tietovarasto +settings.add_team_duplicate = Tiimillä on jo tietovarasto +settings.add_webhook.invalid_channel_name = Webkoukun kanavan nimi ei voi olla tyhjä, eikä se voi sisältää vain merkkiä #. +settings.sourcehut_builds.secrets_helper = Anna työlle pääsy koontisalaisuuksiin (vaatii myönnön SECRETS:RO) +settings.protect_disable_push_desc = Tähän haaraan ei sallita työntämistä. +settings.protect_disable_push = Poista työntö käytöstä +activity.unresolved_conv_desc = Näitä äskettäin muuttuneita ongelmia ja vetopyyntöjä ei ole vielä selvitetty. +activity.title.unresolved_conv_1 = %d selvittämätön keskustelu +activity.git_stats_files_changed_1 = on muuttunut +settings.convert_fork_desc = Voit konvertoida tämän forkin tavalliseksi tietovarastoksi. Tätä ei voi perua. +settings.protect_merge_whitelist_committers = Ota yhdistämiseen sallittu lista käyttöön +settings.protect_whitelist_teams = Työntämiseen oikeutettujen tiimien lista +settings.protect_enable_push_desc = Kenen tahansa kirjoituspääsyllä sallitaan työntää tähän haaraan (mutta ei työntää väkisin). +settings.change_team_access_not_allowed = Tiimin pääsyn muuttaminen tietovarastoon on rajoitettu organisaation omistajaan +settings.change_team_permission_tip = Tiimin käyttöoikeus on asetettu tiimin asetussivulla, eikä sitä voi muuttaa tietovarastokohtaisesti +settings.rename_branch_failed_protected = Haaraa %s ei voi nimetä uudelleen, koska se on suojattu haara. +release.hide_archive_links = Piilota automaattisesti luodut arkistot +diff.review.self_reject = Vetopyyntöjen tekijät eivät voi pyytää muutoksia omiin vetopyyntöihinsä +settings.unarchive.text = Tietovaraston arkistoinnin kumoaminen palauttaa mahdollisuuden vastaanottaa kommitteja ja työntöjä, sekä luoda uusia ongelmia ja vetopyyntöjä. +settings.unarchive.success = Tietovaraston arkistointi kumottiin. +settings.update_avatar_success = Tietovaraston profiilikuva päivitettiin. +diff.review.self_approve = Vetopyyntöjen tekijät eivät voi hyväksyä omia vetopyyntöjään +tag.ahead.target = haaraan %s tämän tagin jälkeen +release.add_tag_msg = Käytä otsikkoa ja julkaisun sisältöä tagin viestinä. +branch.rename_branch_to = Aseta haaran "%s" uudeksi nimeksi: +error.csv.too_large = Tätä tiedostoa ei voi renderöidä, koska se on liian suuri. +settings.unarchive.error = Tietovaraston arkistointia kumotessa tapahtui virhe. Katso lisätietoja lokista. +settings.rename_branch_failed_exist = Haaraa ei voi nimetä uudelleen, koska kohdehaara %s on olemassa. +release.tag_helper_new = Uusi tagi. Tämä tagi luodaan kohteesta. +diff.review = Viimeistele katselmointi +error.broken_git_hook = Tämän tietovaraston Git-koukut vaikuttavat olevan rikki. Seuraa dokumentaation ohjeita korjataksesi koukut, sen jälkeen työnnä kommitteja päivittääksesi tilan. +error.csv.unexpected = Tätä tiedostoa ei voi renderöidä, koska se sisältää odottamattoman merkin rivillä %d ja sarakkeessa %d. +settings.archive.error = Tietovarastoa arkistoitaessa tapahtui virhe. Katso lisätietoja lokista. +diff.comment.add_line_comment = Lisää rivikommentti +settings.lfs_lock = Lukitse +settings.chat_id = Keskustelun ID-tunniste +settings.thread_id = Ketjun ID-tunniste +settings.archive.mirrors_unavailable = Peilit eivät ole saatavilla arkistoiduissa tietovarastoissa. +branch.tag_collision = Haaraa "%s" ei voi luoda, koska tagi samalla nimellä on jo olemassa tietovarastossa. +diff.git-notes.remove-header = Poista huomautus +diff.git-notes.remove-body = Tämä huomautus poistetaan. +diff.git-notes.add = Lisää huomautus +release.invalid_external_url = Virheellinen ulkoinen URL-osoite: "%s" +release.asset_external_url = Ulkoinen URL-osoite +branch.delete_branch_has_new_commits = Haaraa "%s" ei voi poistaa, koska uusia kommitteja on lisätty yhdistämisen jälkeen. +branch.branch_name_conflict = Haaran nimi "%s" on ristiriidassa olemassa olevan haaran "%s" kanssa. +settings.unarchive.header = Kumoa tämän tietovaraston arkistointi +settings.matrix.room_id = Huoneen ID-tunniste +issues.blocked_by_user = Et voi luoda ongelmia tähän tietovarastoon, koska tietovaraston omistaja on estänyt sinut. +pulls.desc = Ota vetopyynnöt ja koodikatselmoinnit käyttöön. +pulls.push_rejected = Työntö epäonnistui: Työntö hylättiin. Katselmoi tämän tietovaraston Git-koukut. +form.name_reserved = Tietovaraston nimi "%s" on varattu. +form.reach_limit_of_creation_1 = Omistajan %d tietovaraston rajoitus on jo täynnä. +form.reach_limit_of_creation_n = Omistajan %d tietovaraston rajoitus on jo täynnä. +form.string_too_long = Merkkijono on pidempi kuin %d merkkiä. +mirror_address_protocol_invalid = Määritetty URL-osoite on virheellinen. Vain http(s):// tai git:// -sijainteja voi käyttää peilaukseen. +form.name_pattern_not_allowed = Kaava "%s" ei ole sallittu tietovaraston nimessä. +migrate_options_lfs_endpoint.description.local = Paikallinen palvelinpolku on myös tuettu. +pulls.showing_only_single_commit = Näytetään vain kommitin %[1]s muutokset +pulls.invalid_merge_option = Et voi käyttää tätä yhdistämisvalintaa tälle vetopyynnölle. +pulls.squash_merge_pull_request = Luo squash-kommitti +issues.label_templates.info = Nimilappuja ei ole. Luo nimilappu napsauttamalla "Uusi nimilappu" tai käytä nimilapun esiasetusta: +issues.label_archive = Arkistoi nimilappu +mirror_lfs_desc = Aktivoi LFS-datan peilaaminen. +editor.directory_is_a_file = Hakemiston nimi "%s" on jo käytössä tiedoston nimenä tässä tietovarastossa. +projects.desc = Hallitse ongelmia ja vetoja projektitauluilla. +ext_issues = Ulkoiset ongelmat +issues.label_archive_tooltip = Arkistoidut nimilaput on suljettu pois ehdotuksista oletusarvoisesti, kun haku suoritetaan nimilapulla. +issues.ref_reopened_from = `avasi uudelleen tämän ongelman %[4]s %[2]s` +issues.ref_closed_from = `sulki tämän ongelman %[4]s %[2]s` +migrate_options_lfs_endpoint.description = Migraatio yrittää käyttää Git-etätietovarastoasi LFS-palvelimen määrittämiseen. Voit määrittää omavalintaisen päätepisteen, jos tietovarastosi LFS-data on talletettu jonnekin muualle. +settings.mirror_settings.docs.pull_mirror_instructions = Vetopeilin määrittämiseksi konsultoi: +issues.archived_label_description = (Arkistoitu) %s +editor.filename_is_a_directory = Tiedoston nimi "%s" on jo käytössä hakemiston nimenä tässä tietovarastossa. +pulls.fast_forward_only_merge_pull_request = Pelkkä fast-forward +pulls.rebase_merge_commit_pull_request = Rebase, luo sitten yhdistämiskommitti +pulls.rebase_merge_pull_request = Rebase, sitten fast-forward +admin.enabled_flags = Tässä tietovarastossa käytössä olevat liput: +admin.update_flags = Päivitä liput +admin.failed_to_replace_flags = Tietovaraston lippujen korvaaminen epäonnistui +admin.flags_replaced = Tietovaraston liput korvattu +admin.manage_flags = Hallitse lippuja +editor.file_is_a_symlink = `"%s" on symbolinen linkki. Symbolisia linkkejä ei voi muokata selainkäyttöliittymän editorissa` +rss.must_be_on_branch = Sinun täytyy olla haarassa saadaksesi RSS-syötteen. @@ -2234,8 +2584,8 @@ pulls.no_merge_not_ready = Tämä vetopyyntö ei ole valmis yhdistettäväksi. T component_loading_info = Tämä saattaa kestää hetken… component_failed_to_load = Odottamaton virhe. component_loading = Ladataan %s… -contributors.what = panokset -recent_commits.what = viimeaikaiset sitoumukset +contributors.what = kontribuutiot +recent_commits.what = viimeaikaiset kommitit code_frequency.what = koodifrekvenssi component_loading_failed = Ei voitu ladata %s @@ -2248,7 +2598,7 @@ repo_updated=Päivitetty %s members=Jäsenet teams=Tiimit lower_members=jäsentä -lower_repositories=repot +lower_repositories=tietovarastot create_new_team=Uusi tiimi create_team=Luo tiimi org_desc=Kuvaus @@ -2263,10 +2613,10 @@ team_unit_desc=Salli pääsy tietovaraston osioihin settings=Asetukset settings.options=Organisaatio settings.full_name=Koko nimi -settings.website=Nettisivu +settings.website=Verkkosivusto settings.location=Sijainti settings.permission=Käyttöoikeudet -settings.repoadminchangeteam=Repon ylläpitäjä voi lisätä ja poistaa pääsyn tiimeihin +settings.repoadminchangeteam=Tietovaraston ylläpitäjä voi lisätä ja poistaa pääsyn tiimeihin settings.visibility=Näkyvyys settings.visibility.public=Julkinen settings.visibility.limited_shortname=Rajattu @@ -2276,10 +2626,10 @@ settings.visibility.private_shortname=Yksityinen settings.update_settings=Päivitä asetukset settings.delete=Poista organisaatio settings.delete_account=Poista tämä organisaatio -settings.delete_prompt=Organisaatio poistetaan pysyvästi, ja tätä EI VOI peruuttaa myöhemmin! +settings.delete_prompt=Organisaatio poistetaan pysyvästi, ja tätä EI VOI perua myöhemmin! settings.confirm_delete_account=Vahvista poisto settings.delete_org_title=Poista organisaatio -settings.hooks_desc=Lisää webkoukkuja, jotka suoritetaan kaikissa repoissa tässä organisaatiossa. +settings.hooks_desc=Lisää webkoukkuja, jotka suoritetaan kaikissa tietovarastoissa tässä organisaatiossa. members.membership_visibility=Jäsenyyden näkyvyys: @@ -2292,52 +2642,52 @@ members.owner=Omistaja members.member=Jäsen members.remove=Poista members.leave=Poistu -members.invite_desc=Lisää uusi jäsen %s: +members.invite_desc=Lisää uusi jäsen organisaatioon %s: members.invite_now=Kutsu nyt teams.join=Liity teams.leave=Poistu -teams.read_access=Luettu +teams.read_access=Lue teams.read_access_helper=Tiimin jäsenet voivat katsella ja kloonata tiimin varastoja. teams.write_access_helper=Tiimin jäsenet voivat lukea ja työntää tiimin varastoja/varastoihin. -teams.admin_access=Järjestelmänvalvojan pääsy -teams.admin_access_helper=Tiimin jäsenet voivat työntää (push) ja vetää (pull) tiimin varastoista/varastoihin ja lisätä yhteistyökumppaneita. +teams.admin_access=Ylläpitopääsy +teams.admin_access_helper=Tiimin jäsenet voivat työntää (push) ja vetää (pull) tiimin varastoista/varastoihin ja lisätä avustajia. teams.no_desc=Tällä tiimillä ei ole kuvausta teams.settings=Asetukset -teams.owners_permission_desc=Omistajilla on täydet käyttöoikeudet kaikkiin organisaation repoihin sekä organisaation ylläpitäjän oikeudet. +teams.owners_permission_desc=Omistajilla on täydet käyttöoikeudet kaikkiin organisaation tietovarastoihin sekä organisaation ylläpitäjän oikeudet. teams.members=Tiimin jäsenet teams.update_settings=Päivitä asetukset teams.delete_team=Poista tiimi teams.add_team_member=Lisää tiimin jäsen teams.delete_team_title=Poista tiimi -teams.delete_team_desc=Tiimin poisto peruuttaa sen jäseniltä oikeuden päästä tiimin varastoihin. Jatketaanko? +teams.delete_team_desc=Tiimin poisto peruuttaa sen jäseniltä oikeuden päästä tiimin tietovarastoihin. Jatketaanko? teams.delete_team_success=Tiimi on poistettu. -teams.read_permission_desc=Tämä tiimi myöntää jäsenille Luku oikeudet: tiimin jäsenet voivat katsella ja kloonata tiimin varastoja. -teams.write_permission_desc=Tämä tiimi myöntää jäsenille Kirjoitus oikeuden: tiimin jäsenet voivat lukea ja kirjoittaa tiimin repoihin. -teams.admin_permission_desc=Tämä joukkue myöntää järjestelmänvalvojapääsyn: jäsenet voivat lukea joukkueen tietovarastoista, vetää ja lisätä yhteistyöhenkilöitä niihin. -teams.repositories=Joukkueen tietovarastot +teams.read_permission_desc=Tämä tiimi myöntää jäsenille Lue-oikeuden: tiimin jäsenet voivat katsella ja kloonata tiimin tietovarastoja. +teams.write_permission_desc=Tämä tiimi myöntää jäsenille Kirjoita-oikeuden: tiimin jäsenet voivat lukea ja kirjoittaa tiimin tietovarastoihin. +teams.admin_permission_desc=Tämä tiimi myöntää Ylläpito-oikeuden: jäsenet voivat lukea tiimin tietovarastoista, työntää ja lisätä avustajia niihin. +teams.repositories=Tiimin tietovarastot teams.members.none=Ei jäseniä tässä tiimissä. -teams.all_repositories=Kaikki repot +teams.all_repositories=Kaikki tietovarastot teams.invite.by = Kutsunut %s members.leave.detail = Haluatko varmasti poistua organisaatiosta "%s"? teams.add_all_repos_title = Lisää kaikki tietovarastot teams.invite_team_member.list = Odottavat kutsut teams.invite.description = Napsauta alla olevaa painiketta liittyäksesi tiimiin. -settings.update_setting_success = Organisaatioasetukset on päivitetty. +settings.update_setting_success = Organisaation asetukset on päivitetty. form.create_org_not_allowed = Sinulla ei ole oikeutta luoda organisaatiota. teams.leave.detail = Haluatko varmasti poistua tiimistä "%s"? teams.invite.title = Sinut on kutsuttu tiimiin %s organisaatiossa %s. -teams.add_duplicate_users = Käyttäjä on jo tiimijäsen. +teams.add_duplicate_users = Käyttäjä on jo tiimin jäsen. settings.visibility.limited = Rajattu (näkyvissä vain kirjautuneille käyttäjille) code = Koodi -teams.remove_all_repos_title = Poista kaikki joukkueen tietovarastot +teams.remove_all_repos_title = Poista kaikki tiimin tietovarastot form.name_reserved = Organisaation nimi "%s" on varattu. settings.delete_org_desc = Organisaatio poistetaan pysyvästi. Jatketaanko? team_access_desc = Tietovarastopääsy teams.specific_repositories = Määritetyt tietovarastot open_dashboard = Avaa kojelauta -teams.remove_all_repos_desc = Tämä poistaa kaikki tietovarastot joukkueelta. -teams.add_all_repos_desc = Tämä lisää kaikki organisaation tietovarastot joukkueelle. +teams.remove_all_repos_desc = Tämä poistaa kaikki tietovarastot tiimiltä. +teams.add_all_repos_desc = Tämä lisää kaikki organisaation tietovarastot tiimille. team_unit_disabled = (Pois käytöstä) follow_blocked_user = Et voi seurata tätä organisaatiota, koska organisaatio on estänyt sinut. teams.can_create_org_repo = Luo tietovarastoja @@ -2347,12 +2697,21 @@ settings.email = Yhteydenoton sähköposti teams.general_access = Mukautettu pääsy settings.change_orgname_redirect_prompt = Vanha nimi uudelleenohjaa, kunnes nimi otetaan uudelleen käyttöön. settings.change_orgname_prompt = Huomio: organisaation nimen vaihtaminen vaihtaa myös organisaation URL-osoitteen ja vapauttaa vanhan nimen. +teams.write_access = Kirjoita +settings.update_avatar_success = Organisaation profiilikuva on päivitetty. +teams.can_create_org_repo_helper = Jäsenet voivat luoda uusia tietovarastoja organisaatiossa. Tietovaraston luonut saa ylläpito-oikeuden uuteen tietovarastoon. +teams.create_repo_permission_desc = Lisäksi tämä tiimi myöntää Luo tietovarasto -oikeuden: jäsenet voivat luoda uusia tietovarastoja organisaatiossa. +teams.add_nonexistent_repo = Tietovarasto, jota yrität lisätä, ei ole olemassa. Luo se ensin. +teams.repos.none = Tällä tiimillä ei ole pääsyä tietovarastoihin. +settings.change_orgname_redirect_prompt.with_cooldown.one = Vanha organisaation nimi on kenen tahansa saatavilla %[1]d päivän suojaamisjakson jälkeen. Voit palauttaa organisaation nimen itsellesi suojaamisjakson aikana. +settings.change_orgname_redirect_prompt.with_cooldown.few = Vanha organisaation nimi on kenen tahansa saatavilla %[1]d päivän suojaamisjakson jälkeen. Voit palauttaa organisaation nimen itsellesi suojaamisjakson aikana. +teams.all_repositories_helper = Tiimillä on pääsy kaikkiin tietovarastoihin. Tämän valitseminen lisää kaikki olemassa olevat tietovarastot tiimiin. [admin] dashboard=Kojelauta users=Käyttäjätilit organizations=Organisaatiot -repositories=Repot +repositories=Tietovarastot authentication=Todennuslähteet emails=Käyttäjien sähköpostit config=Asetukset @@ -2365,10 +2724,10 @@ total=Yhteensä: %d dashboard.statistic=Yhteenveto dashboard.operations=Huoltotoimet dashboard.system_status=Järjestelmän tila -dashboard.operation_name=Toiminnon nimi +dashboard.operation_name=Toimenpiteen nimi dashboard.operation_switch=Vaihda dashboard.operation_run=Suorita -dashboard.delete_inactive_accounts=Poista kaikki aktivoimattomat käyttäjät +dashboard.delete_inactive_accounts=Poista kaikki aktivoimattomat tilit dashboard.delete_repo_archives=Poista kaikki tietovarastojen arkistot (ZIP, TAR.GZ, jne.) dashboard.server_uptime=Palvelimen uptime dashboard.current_goroutine=Nykyiset goroutinet @@ -2400,13 +2759,13 @@ dashboard.gc_times=Roskienkeruuajat users.user_manage_panel=Käyttäjätilien hallinta users.new_account=Luo käyttäjätili -users.name=Käyttäjätunnus +users.name=Käyttäjänimi users.full_name=Koko nimi users.activated=Aktivoitu users.admin=Ylläpito users.restricted=Rajoitettu users.2fa=2FA -users.repos=Repot +users.repos=Tietovarastot users.created=Luotu users.last_login=Viimeksi kirjautunut users.never_login=Ei koskaan kirjautunut @@ -2466,7 +2825,7 @@ repos.size=Koko packages.owner=Omistaja packages.name=Nimi packages.type=Tyyppi -packages.repository=Repo +packages.repository=Tietovarasto packages.size=Koko @@ -2488,7 +2847,7 @@ auths.user_base=Käyttäjähakukanta auths.user_dn=Käyttäjä DN auths.search_page_size=Sivukoko auths.filter=Käyttäjäsuodatin -auths.admin_filter=Järjestelmänvalvojasuodatin +auths.admin_filter=Ylläpitosuodatin auths.restricted_filter=Rajoitettu suodatin auths.smtp_auth=SMTP-todennustyyppi auths.smtphost=SMTP-isäntä @@ -2496,7 +2855,7 @@ auths.smtpport=SMTP-portti auths.allowed_domains=Sallitut verkkotunnukset auths.skip_tls_verify=Ohita TLS-vahvistus auths.pam_service_name=PAM-palvelun nimi -auths.oauth2_tokenURL=Pääsymerkki URL +auths.oauth2_tokenURL=Pääsypoletin URL-osoite auths.enable_auto_register=Ota käyttöön automaattinen rekisteröinti auths.tips=Vinkit auths.tips.oauth2.general=OAuth2-autentikointi @@ -2509,7 +2868,7 @@ auths.delete_auth_desc=Todennuslähteen poisto estää käyttäjiä käyttämäs auths.deletion_success=Todennuslähde on poistettu. config.server_config=Palvelimen asetukset -config.app_name=Ilmentymän otsikko +config.app_name=Instanssin otsikko config.app_ver=Forgejo-versio config.disable_router_log=Poista reitittimen lokinkirjaaminen käytöstä config.run_mode=Suoritustila @@ -2524,7 +2883,7 @@ config.ssh_port=Portti config.ssh_listen_port=Kuuntele porttia config.ssh_root_path=Juuren polku config.ssh_key_test_path=Avaimen testipolku -config.ssh_keygen_path=Keygen ('ssh-keygen') polku +config.ssh_keygen_path=Keygen-polku ('ssh-keygen') config.ssh_minimum_key_size_check=Avaimen vähimmäiskoon tarkistus config.ssh_minimum_key_sizes=Avaimen vähimmäiskoot @@ -2534,7 +2893,7 @@ config.db_config=Tietokannan asetukset config.db_type=Tyyppi config.db_host=Isäntä config.db_name=Nimi -config.db_user=Käyttäjätunnus +config.db_user=Käyttäjänimi config.db_ssl_mode=SSL config.db_path=Polku @@ -2573,12 +2932,12 @@ config.https_only=Vain HTTPS config.cookie_life_time=Evästeen elinikä config.picture_service=Kuvapalvelu -config.disable_gravatar=Poista käytöstä Gravatar +config.disable_gravatar=Poista Gravatar käytöstä config.git_gc_args=Roskienkeruu-argumentit -config.git_migrate_timeout=Siirron aikakatkaisu +config.git_migrate_timeout=Migraation aikakatkaisu config.git_mirror_timeout=Peilin päivityksen aikakatkaisu -config.git_clone_timeout=Kloonitoiminnon aikakatkaisu +config.git_clone_timeout=Kloonaustoimenpiteen aikakatkaisu config.git_gc_timeout=Roskienkeruun aikakatkaisu config.log_config=Lokiasetukset @@ -2609,12 +2968,12 @@ notices.inverse_selection=Käänteinen valinta notices.delete_selected=Poista valitut notices.delete_all=Poista kaikki ilmoitukset notices.type=Tyyppi -notices.type_1=Repo +notices.type_1=Tietovarasto notices.desc=Kuvaus notices.op=Toiminta auths.sspi_auto_create_users = Luo käyttäjät automaattisesti integrations = Integraatiot -emails.change_email_header = Päivitä sähköpostiominaisuudet +emails.change_email_header = Päivitä sähköpostin ominaisuudet emails.change_email_text = Haluatko varmasti päivittää tämän sähköpostiosoitteen? emails.updated = Sähköpostiosoite päivitetty users.organization_creation.description = Salli uusien organisaatioiden luonti. @@ -2630,8 +2989,8 @@ auths.force_smtps = Pakota SMTPS config.mailer_use_sendmail = Käytä Sendmailia users.new_success = Käyttäjätili "%s" on luotu. config.disable_register = Poista itserekisteröinti käytöstä -config.enable_openid_signin = Käytä OpenID-kirjautumista -config.enable_openid_signup = Käytä OpenID-itserekisteröintiä +config.enable_openid_signin = Ota OpenID-kirjautuminen käyttöön +config.enable_openid_signup = Ota OpenID-itserekisteröinti käyttöön monitor.queue.settings.changed = Asetukset päivitetty config.db_schema = Skeema settings = Ylläpitäjän asetukset @@ -2647,7 +3006,7 @@ users.details = Käyttäjän tiedot config_summary = Yhteenveto config.send_test_mail = Lähetä testisähköposti auths.oauth2_icon_url = Kuvakkeen URL-osoite -config.mail_notify = Käytä sähköposti-ilmoituksia +config.mail_notify = Ota sähköposti-ilmoitukset käyttöön config.send_test_mail_submit = Lähetä systemhooks = Järjestelmän webkoukut packages.total_size = Koko yhteensä: %s @@ -2676,7 +3035,7 @@ repos.lfs_size = LFS:n koko config.lfs_config = LFS-asetukset config.register_email_confirm = Vaadi sähköpostivahvistus rekisteröitymiseen config.ssh_domain = SSH-palvelimen verkkotunnus -config.app_slogan = Ilmentymän tunnuslause +config.app_slogan = Instanssin tunnuslause config.lfs_content_path = LFS-sisällön polku users.max_repo_creation = Tietovarastojen enimmäismäärä defaulthooks.update_webhook = Päivitä oletusarvoinen webkoukku @@ -2701,31 +3060,116 @@ monitor.download_diagnosis_report = Lataa diagnostiikkaraportti monitor.duration = Kesto (s) monitor.last_execution_result = Tulos users.bot = Botti -auths.syncenabled = Käytä käyttäjäsynkronointia -auths.enable_ldap_groups = Käytä LDAP-ryhmiä +auths.syncenabled = Ota käyttäjäsynkronointi käyttöön +auths.enable_ldap_groups = Ota LDAP-ryhmät käyttöön dashboard.sync_branch.started = Haarasynkronointi aloitettu dashboard.sync_tag.started = Tagisynkronointi aloitettu auths.login_source_exist = Todennuslähde "%s" on jo olemassa. -config.enable_timetracking = Ota ajan seuranta käyttöön -config.default_enable_timetracking = Ota ajan seuranta käyttöön oletuksena -config.no_reply_address = Piilotetun sähköpostin toimialue +config.enable_timetracking = Ota ajanseuranta käyttöön +config.default_enable_timetracking = Ota ajanseuranta käyttöön oletuksena +config.no_reply_address = Piilotetun sähköpostin verkkotunnus config.allow_dots_in_usernames = Salli käyttäjien käyttää pisteitä käyttäjänimissään. Ei vaikuta olemassa oleviin tileihin. repos.unadopted.no_more = Omaksumattomia tietovarastoja ei löytynyt. repos.unadopted = Omaksumattomat tietovarastot +dashboard.repo_health_check = Tee terveystarkastus kaikille tietovarastoille +users.reserved = Varattu +users.purge = Hävitä käyttäjä +dashboard.cron.process = Cron: %[1]s +auths.tip.github = Rekisteröi uusi OAuth-sovellus %sissa +config.app_data_path = Sovellusdatan polku +config.cache_test_slow = Välimuistin testi onnistui, mutta vastaus on hidas: %s. +dashboard.delete_repo_archives.started = Poista kaikki tietovarastojen arkistot -tehtävä aloitettu. +dashboard.check_repo_stats = Tarkista kaikkien tietovarastojen tilastot +users.still_own_packages = Tämä käyttäjä omistaa yhden tai useamman paketin. Poista paketit ensin. +users.block.description = Estä tätä käyttäjää olemasta vuorovaikutuksessa tämän palvelun kanssa tilinsä välityksellä ja estä sisäänkirjautuminen. +auths.attribute_username = Käyttäjänimen attribuutti +auths.oauth2_emailURL = Sähköpostin URL-osoite +auths.tip.discord = Rekisteröi uusi sovellus %sissa +config.default_enable_dependencies = Ota ongelmariippuvuudet käyttöön oletuksena +config.mailer_config = Postittimen asetukset +config.cache_test_succeeded = Välimuistin testi onnistui, vastauksen saamisessa kesti %s. +dashboard.sync_external_users = Synkronoi ulkoinen käyttäjädata +auths.tip.gitea = Rekisteröi uusi OAuth2-sovellus. Ohje on osoitteessa %s +config.test_mail_failed = Testisähköpostin lähettäminen osoitteeseen "%s" epäonnistui: %v +auths.attribute_surname = Sukunimen attribuutti +config.mailer_enable_helo = Ota HELO käyttöön +auths.attribute_username_placeholder = Jätä tyhjäksi käyttääksesi Forgejo:ssa asetettua käyttänimeä. +auths.oauth2_authURL = Valtuutuksen URL-osoite +auths.new_success = Todennus "%s" on lisätty. +users.still_own_repo = Tämä käyttäjä omistaa yhden tai useamman tietovaraston. Poista tai siirrä nämä tietovarastot ensin. +dashboard.cleanup_hook_task_table = Siivoa hook_task-taulu +dashboard.delete_old_actions = Poista kaikki vanhat aktiviteetit tietokannasta +auths.attribute_mail = Sähköpostiosoitteen attribuutti +auths.attribute_ssh_public_key = Julkisen SSH-avaimen attribuutti +auths.group_attribute_list_users = Ryhmäattribuutti sisältäen listan käyttäjistä +auths.oauth2_profileURL = Profiilin URL-osoite +auths.skip_local_two_fa = Ohita paikallinen 2FA +auths.oauth2_scopes = Lisäskoopit +auths.tip.gitlab_new = Rekisteröi uusi sovellus %sissa +config.cache_test = Testaa välimuisti +auths.still_in_used = Todennuslähde on edelleen käytössä. Konvertoi tai poista ensin käyttäjät, jotka käyttävät tätä todennuslähdettä. +users.admin.description = Myönnä tälle käyttäjälle täydet oikeudet selainkäyttöliittymän ja rajapinnan kautta saatavilla oleviin ylläpito-ominaisuuksiin. +dashboard.delete_inactive_accounts.started = Poista kaikki aktivoimattomat tilit -tehtävä aloitettu. +config.run_user = Suorita käyttäjänä +config.mailer_sendmail_args = Lisäargumentit Sendmailille +dashboard.archive_cleanup = Poista vanhat tietovarastojen arkistot +dashboard.deleted_branches_cleanup = Siivoa poistetut haarat +dashboard.update_checker = Päivitysten tarkistaja +auths.allowed_domains_helper = Jätä tyhjäksi salliaksesi kaikki verkkotunnukset. Erota useat verkkotunnukset pilkulla (","). +auths.activated = Tämä todennuslähde on aktivoitu +auths.login_source_of_type_exist = Tätä tyyppiä oleva todennuslähde on jo olemassa. +dashboard.memory_allocate_times = Muistiallokaatiot +users.send_register_notify = Ilmoita rekisteröitymisestä sähköpostitse +config.offline_mode = Paikallinen tila +config.cache_item_ttl = Välimuistitietueen TTL +config.log_file_root_path = Lokipolku +config.lfs_root_path = LFS-juuren polku +users.allow_import_local = Voi tuoda paikallisia tietovarastoja +users.still_has_org = Tämä käyttäjä on organisaation jäsen. Poista käyttäjä organisaatiosta ensin. +packages.creator = Luoja +auths.attribute_avatar = Profiilikuvan attribuutti +users.remote = Etä +auths.disable_helo = Poista HELO käytöstä +defaulthooks.desc = Webkoukut tekevät automaattisesti HTTP POST -pyyntöjä palvelimelle, kun tietyt Forgejo-tapahtumat ilmenevät. Tässä määritetyt webkoukut ovat oletusarvot ja ne kopioidaan kaikkiin uusiin tietovarastoihin. Lue lisää webkoukkujen oppaasta. +auths.attribute_name = Etunimen attribuutti +users.local_import.description = Salli tietovarastojen tuominen palvelimen paikallisesta tiedostojärjestelmästä. Tämä voi olla tietoturvaongelma. +emails.not_updated = Pyydetyn sähköpostiosoitteen päivittäminen epäonnistui: %v +dashboard.update_mirrors = Päivitä peilit +config.mailer_protocol = Protokolla +users.activated.description = Sähköpostivahvistuksen valmistuminen. Aktivoimattoman tilin omistaja ei voi kirjautua sisään, ennen kuin sähköpostivahvistus on suoritettu. +users.purge_help = Poista käyttäjä pakottaen, sekä kaikki käyttäjän omistamat tietovarastot, organisaatiot ja paketit. Kaikki käyttäjän luomat kommentit ja ongelmat poistetaan myös. +users.restricted.description = Salli vuorovaikutus vain niihin tietovarastoihin ja organisaatioihin, joissa käyttäjä on avustajan roolissa. Tämä estää pääsyn tässä instanssissa oleviin julkisiin tietovarastoihin. +config.git_pull_timeout = Vetotoimenpiteen aikakatkaisu +monitor.process.cancel_desc = Prosessin peruuttaminen saattaa aiheuttaa datan menetyksen +self_check.no_problem_found = Ongelmia ei ole vielä löytynyt. +config.git_disable_diff_highlight = Poista diff-syntaksin korostus käytöstä +config.git_max_diff_lines = Diff-rivejä enintään tiedostoa kohden +config.access_log_template = Pääsylokin mallipohja +monitor.process.cancel_notices = Perutaanko: %s? +config.enable_federated_avatar = Ota federoidut profiilikuvat käyttöön +notices.operations = Toimenpiteet +config.xorm_log_sql = Lokita SQL +monitor.queue.settings.remove_all_items_done = Kaikki jonossa olleet tietueet on poistettu. +config.logger_name_fmt = Lokittaja: %s +config.git_max_diff_line_characters = Diff-merkkejä enintään riviä kohden +config.git_max_diff_files = Diff-tiedostoja enintään näytettäväksi +config.access_log_mode = Pääsylokin tila +config.picture_config = Kuvan ja avatarin asetukset +notices.delete_success = Järjestelmäilmoitukset on poistettu. [action] create_repo=loi tietovaraston %s -rename_repo=uudelleennimetty repo %[1]s nimelle %[3]s -transfer_repo=siirretty repo %s kohteeseen %s +rename_repo=asetti tietovaraston %[1]s uudeksi nimeksi %[3]s +transfer_repo=siirsi tietovaraston %s käyttäjälle %s push_tag=työnsi tagin %[3]s kohteeseen %[4]s delete_tag=poisti tagin %[2]s kohteesta %[3]s -compare_commits_general=Vertaa committeja -create_branch=loi haaran %[3]s repossa %[4]s -compare_commits = Vertaa %d sitoumukset +compare_commits_general=Vertaa kommitteja +create_branch=loi haaran %[3]s tietovarastossa %[4]s +compare_commits = Vertaa %d kommittia compare_branch = Vertaa review_dismissed_reason = Syy: -commit_repo = työnsi haaraan %[3]s %[4]s:ssa +commit_repo = työnsi haaraan %[3]s tietovarastossa %[4]s create_issue = `avasi ongelman %[3]s#%[2]s` reopen_issue = `avasi uudelleen ongelman %[3]s#%[2]s` create_pull_request = `loi vetopyynnön %[3]s#%[2]s` @@ -2735,7 +3179,12 @@ comment_issue = `kommentoi ongelmaa %[3]s#%[2]s` close_issue = `sulki ongelman %[3]s#%[2]s` merge_pull_request = `yhdisti vetopyynnön %[3]s#%[2]s` comment_pull = `kommentoi vetopyyntöä %[3]s#%[2]s` -auto_merge_pull_request = `automaattisesti yhdistetty vetopyyntö %[3]s#%[2]s` +auto_merge_pull_request = `automaattisesti yhdisti vetopyynnön %[3]s#%[2]s` +delete_branch = poisti haaran %[2]s tietovarastosta %[3]s +watched_repo = aloitti tietovaraston %[2]s tarkkailun +approve_pull_request = `hyväksyi %[3]s#%[2]s` +starred_repo = lisäsi tähden tietovarastolle %[2]s +reject_pull_request = `ehdotti muutoksia kohteeseen %[3]s#%[2]s` [tool] now=nyt @@ -2757,8 +3206,8 @@ raw_seconds=sekuntia raw_minutes=minuuttia [dropzone] -default_message=Pudota tiedostot tähän tai klikkaa aluetta ladataksesi tiedoston. -invalid_input_type=Tämäntyyppisiä tiedostoja ei voi ladata. +default_message=Pudota tiedostot tähän tai napsauta tästä lähettääksesi tiedoston. +invalid_input_type=Tätä tyyppiä olevia tiedostoja ei voi lähettää. remove_file=Poista tiedosto file_too_big = Tiedoston koko ({{filesize}} Mt) ylittää enimmäisrajan ({{maxFilesize}} Mt). @@ -2768,7 +3217,7 @@ unread=Lukematon read=Luettu no_unread=Ei lukemattomia ilmoituksia. no_read=Ei luettuja ilmoituksia. -pin=Merkitse ilmoitus +pin=Kiinnitä ilmoitus mark_as_read=Merkitse luetuksi mark_as_unread=Merkitse lukemattomaksi mark_all_as_read=Merkitse kaikki luetuiksi @@ -2777,19 +3226,24 @@ no_subscriptions = Ei tilauksia subscriptions = Tilaukset [gpg] -error.no_committer_account=Committaajan sähköpostiosoitteeseen ei ole linkitetty tiliä -error.not_signed_commit=Ei allekirjoitettu sitoumus +error.no_committer_account=Kommitin tekijän sähköpostiosoitteeseen ei ole linkitetty tiliä +error.not_signed_commit=Kommitti ei ole allekirjoitettu error.extract_sign = Allekirjoituksen purkaminen epäonnistui default_key = Allekirjoitettu oletusavaimella -error.failed_retrieval_gpg_keys = Sitoumuksen toimijan tiliin liitetyn avaimen nouto epäonnistui -error.generate_hash = Sitoumuksen tiivisteen luominen epäonnistui +error.failed_retrieval_gpg_keys = Ei saatu yhtäkään kommitin tekijän tiliin liitettyä avainta +error.generate_hash = Kommitin tiivisteen luominen epäonnistui +error.probable_bad_signature = VAROITUS! Vaikka tietokannassa on avain tällä ID-tunnistella, se ei vahvista tätä kommittia! Tämä kommitti on EPÄILYTTÄVÄ. +error.probable_bad_default_signature = VAROITUS! Vaikka oletusavaimella on tämä ID-tunniste, se ei vahvista tätä kommittia! Tämä kommitti on EPÄILYTTÄVÄ. +error.no_gpg_keys_found = Tälle allekirjoitukselle ei löytynyt tunnettua avainta tietokannasta [units] unit = Yksikkö +error.unit_not_allowed = Sinulla ei ole pääsyä tähän tietovaraston osioon. +error.no_unit_allowed_repo = Sinulla ei ole pääsyä mihinkään tämän tietovaraston osioon. [packages] title=Paketit -desc=Hallitse repon paketteja. +desc=Hallitse tietovaraston paketteja. empty=Täällä ei vielä ole paketteja. filter.type=Tyyppi filter.type.all=Kaikki @@ -2797,12 +3251,12 @@ filter.no_result=Suodattimesi ei tuottanut tuloksia. installation=Asennus details.author=Tekijä alpine.repository.branches=Haarat -alpine.repository.repositories=Repot -conan.details.repository=Repo +alpine.repository.repositories=Tietovarastot +conan.details.repository=Tietovarasto owner.settings.cleanuprules.enabled=Käytössä details.license = Lisenssi about = Tietoja tästä paketista -debian.install = Asenna paketti seuraavalla komennolla: +debian.install = Asenna paketti komennolla: owner.settings.cleanuprules.edit = Muokkaa siivoussääntöä arch.version.groups = Ryhmä details.project_site = Projektin verkkosivusto @@ -2814,11 +3268,11 @@ keywords = Avainsanat dependencies = Riippuvuudet container.labels.key = Avain container.labels.value = Arvo -pypi.install = Asenna paketti pipillä seuraavalla komennolla: -npm.install = Asenna paketti npm:llä seuraavalla komennolla: +pypi.install = Asenna paketti pipillä komennolla: +npm.install = Asenna paketti npm:llä komennolla: npm.install2 = tai lisää se package.json-tiedostoon: empty.documentation = Lisätietoja pakettirekisteristä on saatavilla dokumentaatiossa. -helm.install = Asenna paketti seuraavalla komennolla: +helm.install = Asenna paketti komennolla: owner.settings.chef.keypair = Luo avainpari settings.delete.error = Paketin poistaminen epäonnistui. requirements = Vaatimukset @@ -2826,7 +3280,7 @@ published_by_in = Julkaistu %[1]s, julkaisija %[3]s projekti pypi.requires = Vaatii Pythonin alpine.install = Asenna paketti seuraavalla komennolla: debian.repository.components = Komponentit -cran.install = Asenna paketti seuraavalla komennolla: +cran.install = Asenna paketti komennolla: settings.link.select = Valitse tietovarasto owner.settings.chef.title = Chef-rekisteri owner.settings.cleanuprules.add = Lisää siivoussääntö @@ -2835,7 +3289,7 @@ versions.view_all = Näytä kaikki debian.repository.architectures = Arkkitehtuurit container.details.type = Levykuvan tyyppi arch.version.properties = Version ominaisuudet -rpm.install = Asenna paketti seuraavalla komennolla: +rpm.install = Asenna paketti komennolla: owner.settings.cleanuprules.none = Siivoussääntöjä ei vielä ole. container.details.platform = Alusta npm.dependencies = Riippuvuudet @@ -2847,47 +3301,47 @@ settings.delete.success = Paketti on poistettu. npm.dependencies.optional = Valinnaiset riippuvuudet debian.repository.distributions = Jakelut composer.dependencies = Riippuvuudet -chef.install = Asenna paketti seuraavalla komennolla: +chef.install = Asenna paketti komennolla: details.documentation_site = Dokumentaation verkkosivusto go.install = Asenna paketti komentoriviltä: alpine.repository.architectures = Arkkitehtuurit composer.registry = Määritä tämä rekisteri ~/.composer/config.json-tiedostossa: debian.registry = Määritä tämä rekisteri komentoriviltä: rpm.registry = Määritä rekisteri komentoriviltä: -maven.install = Käytä pakettia sisällyttämällä seuraava dependencies-lohkoon pom.xml-tiedostossa: +maven.install = Käytä pakettia sisällyttämällä seuraava sisältö dependencies-lohkoon pom.xml-tiedostossa: npm.registry = Määritä rekisteri projektin .npmrc-tiedostossa: alpine.repository = Tietovaraston tiedot cargo.registry = Määritä tämä rekisteri Cargon asetustiedostossa (esimerkiksi ~/.cargo/config.toml): cargo.install = Asenna paketti Cargolla suorittamalla seuraava komento: -composer.install = Asenna paketti Composerilla suorittamalla seuraava komento: +composer.install = Asenna paketti Composerilla suorittamalla komento: rpm.distros.redhat = RedHatiin pohjautuvilla jakeluilla rpm.distros.suse = SUSE:en pohjautuvilla jakeluilla rpm.repository.architectures = Arkkitehtuurit cran.registry = Määritä rekisteri Rprofile.site-tiedostossa: -swift.install2 = ja suorita seuraava komento: +swift.install2 = ja suorita komento: maven.registry = Määritä tämä rekisteri projektin pom.xml-tiedostossa: maven.install2 = Suorita komentoriviltä: nuget.registry = Määritä rekisteri komentoriviltä: -nuget.install = Asenna paketti NuGetillä suorittamalla seuraava komento: -rubygems.install = Asenna paketti gemillä suorittamalla seuraava komento: +nuget.install = Asenna paketti NuGetillä suorittamalla komento: +rubygems.install = Asenna paketti gemillä suorittamalla komento: rubygems.install2 = tai lisää se Gemfileen: swift.registry = Määritä rekisteri komentoriviltä: swift.install = Lisää paketti Package.swift-tiedostoon: owner.settings.cleanuprules.keep.count.1 = 1 versio per paketti owner.settings.cleanuprules.keep.count.n = %d versiota per paketti -conan.install = Asenna paketti Conanilla suorittamalla seuraava komento: +conan.install = Asenna paketti Conanilla suorittamalla komento: chef.registry = Määritä tämä rekisteri ~/.chef/config.rb-tiedostossa: conan.registry = Määritä tämä rekisteri komentoriviltä: -conda.install = Asenna paketti Condalla suorittamalla seuraava komento: +conda.install = Asenna paketti Condalla suorittamalla komento: helm.registry = Määritä tämä rekisteri komentoriviltä: -pub.install = Asenna paketti Dartilla suorittamalla seuraava komento: -owner.settings.cargo.title = Cargon rekisteri-indeksi +pub.install = Asenna paketti Dartilla suorittamalla komento: +owner.settings.cargo.title = Cargo-rekisterin indeksi settings.delete.description = Paketin poistaminen on peruuttamaton toimenpide, sitä ei voi perua. settings.link.success = Tietovaraston linkki päivitettiin onnistuneesti. settings.link.button = Päivitä tietovaraston linkki owner.settings.cleanuprules.preview.overview = %d pakettia on ajastettu poistettavaksi. owner.settings.cargo.initialize.success = Cargo-indeksi luotiin onnistuneesti. -vagrant.install = Lisää Vagrant-boksi suorittamalla seuraava komento: +vagrant.install = Lisää Vagrant-boksi suorittamalla komento: rubygems.dependencies.development = Kehitysriippuvuudet owner.settings.cleanuprules.preview = Siivoussäännön esikatselu npm.dependencies.development = Kehitysriippuvuudet @@ -2899,7 +3353,7 @@ maven.download = Lataa riippuvuus suorittamalla komentorivillä: registry.documentation = Lisätietoja %s-rekisteristä on dokumentaatiossa. owner.settings.chef.keypair.description = Chef-rekisteriin lähetettävät pyynnöt on allekirjoitettava salauskirjoituksella todennuskeinona. Avainparia luotaessa vain julkinen avain tallennetaan Forgejoon. Yksityinen avain toimitetaan sinulle käytettäväksi knifen kanssa. Uuden avainparin luominen korvaa edellisen. owner.settings.cleanuprules.keep.pattern = Säilytä kaavaa vastaavat versiot -owner.settings.cleanuprules.pattern_full_match = Toteuta kaavio paketin koko nimeen +owner.settings.cleanuprules.pattern_full_match = Toteuta kaava paketin koko nimeen owner.settings.cleanuprules.keep.title = Näitä sääntöjä vastaavat versiot säilytetään, vaikka ne vastaisivat alla olevaa poistosääntöä. owner.settings.cleanuprules.keep.count = Säilytä viimeisimmät owner.settings.cleanuprules.remove.pattern = Poista kaavaa vastaavat versiot @@ -2919,7 +3373,7 @@ alpine.registry = Aseta tämä rekisteri lisäämällä URL-osoite tiedostoon paketin asetuksiin ja linkitä se tähän tietovarastoon. +empty.repo = Lähetitkö paketin, mutta se ei näy täällä? Siirry paketin asetuksiin ja linkitä se tähän tietovarastoon. alpine.registry.info = Valitse $branch ja $repository alla olevasta listasta. container.images.title = Levykuvat owner.settings.cargo.initialize = Alusta indeksi @@ -2928,7 +3382,22 @@ settings.link.error = Tietovaraston linkin päivittäminen epäonnistui. alt.repository.multiple_groups = Tämä paketti on saatavilla useissa ryhmissä. alt.repository.architectures = Arkkitehtuurit alt.install = Asenna paketti -alt.registry.install = Asenna paketti suorittamalla komento: +alt.registry.install = Asenna paketti komennolla: +details = Yksityiskohdat +arch.version.provides = Tarjoaa +rpm.repository = Tietovaraston tiedot +rubygems.required.ruby = Vaatii Ruby-version +settings.delete.notice = Olet aikeissa poistaa %s (%s). Tätä toimenpidettä ei voi perua. Haluatko varmasti jatkaa? +owner.settings.cargo.initialize.error = Cargo-indeksin alustaminen epäonnistui: %v +owner.settings.cargo.rebuild.no_index = Ei voi rakentaa uudelleen, indeksiä ei ole alustettu. +rubygems.required.rubygems = Vaatii RubyGem-version +alt.registry = Määritä tämä rekisteri komentoriviltä: +alt.repository = Tietovaraston tiedot +arch.version.replaces = Korvaa +debian.repository = Tietovaraston tiedot +conda.registry = Määritä tämä rekisteri Conda-tietovarastoksi .condarc-tiedostossa: +container.labels = Nimilaput +settings.link.description = Jos linkität paketin tietovarastoon, paketti listataan tietovaraston pakettilistalla. [secrets] creation.failed = Salaisuuden lisääminen epäonnistui. @@ -2942,16 +3411,18 @@ secrets = Salaisuudet deletion.description = Salaisuuden poistaminen on pysyvä toimenpide, eikä sitä voi perua. Jatketaanko? deletion.success = Salaisuus on poistettu. description = Salaisuudet välitetään tietyille toimenpiteille, eikä niitä voi muuten lukea. +creation.name_placeholder = kirjoinkoolla ei merkitystä, vain aakkosnumeerisia merkkejä ja alaviivoja, ei voi alkaa GITEA_ tai GITHUB_ +creation.value_placeholder = Syötä mitä tahansa sisältöä. Tyhjätila alussa ja lopussa jätetään huomiotta. [actions] runners.name=Nimi runners.owner_type=Tyyppi runners.description=Kuvaus runners.task_list.run=Suorita -runners.task_list.repository=Repo -runners.task_list.commit=Commit +runners.task_list.repository=Tietovarasto +runners.task_list.commit=Kommitti -runs.commit=Commit +runs.commit=Kommitti status.success = Onnistunut status.unknown = Tuntematon status.waiting = Odotustilassa @@ -2966,21 +3437,21 @@ runners.update_runner = Päivitä muutokset runners.edit_runner = Muokkaa testinajajaa runners.update_runner_success = Testinajaja päivitetty onnistuneesti runners.delete_runner_success = Testinajaja poistettu onnistuneesti -runners.reset_registration_token = Uudelleenaseta rekisteröintiavain +runners.reset_registration_token = Uudelleenaseta rekisteröintipoletti runs.scheduled = Ajastettu runs.status = Tila -runs.empty_commit_message = (tyhjä sitoumusviesti) +runs.empty_commit_message = (tyhjä kommittiviesti) variables.deletion = Poista muuttuja runners.new_notice = Testinajajan aloitusohjeet workflow.dispatch.input_required = Arvo syötteelle "%s" vaadittu. runners.status.active = Aktiivinen runs.no_workflows.documentation = Katso lisätietoja Forgejo Actions -ohjelmistosta dokumentaatiosta. variables.description = Muuttujat asetetaan tietyille toiminnoille eikä niitä voida lukea muutoin. -runners.labels = Tunnisteet +runners.labels = Nimilaput runners.delete_runner_failed = Testinajajan poisto epäonnistui runners.delete_runner_header = Varmista testinajajan poisto runners.task_list.status = Tila -runners.reset_registration_token_success = Testiajajan rekisteröintiavain uudelleenasetettu onnistuneesti +runners.reset_registration_token_success = Testiajajan rekisteröintipoletti asetettu uudelleen onnistuneesti variables.none = Ei muuttujia vielä. runners.id = Tunniste runners.status = Tila @@ -2989,21 +3460,21 @@ runners.task_list.no_tasks = Tehtäviä ei ole vielä määritelty. runners.last_online = Viimeisin käynnissäoloajankohta runners.runner_title = Testinajaja runners.task_list.done_at = Valmistunut ajankohtana -runs.no_matching_online_runner_helper = Testiajajaa tunnisteella %s ei löytynyt +runs.no_matching_online_runner_helper = Testiajajaa nimilapulla %s ei löytynyt runs.no_results = Ei tuloksia. runners.delete_runner = Poista testinajaja -variables.deletion.description = Muuttujan poistaminen on lopullista eikä sitä voi peruuttaa. Jatketaanko? +variables.deletion.description = Muuttujan poistaminen on lopullista, eikä sitä voi perua. Jatketaanko? workflow.dispatch.invalid_input_type = Syötetyyppi "%s" ei kelpaa. workflow.dispatch.warn_input_limit = Näytetään vain ensimmäiset %d syötettä. runners.runner_manage_panel = Hallinnoi testinajajia variables = Muuttujat -variables.management = Hallinnoi muuttujia +variables.management = Hallitse muuttujia variables.creation = Lisää muuttuja runs.no_workflows.quick_start = Etkö tiedä kuinka Forgejo Actions toimii? Katso aloitusohje. runners.new = Luo uusi testinajaja runners.version = Versio runs.expire_log_message = Lokitiedostot on tyhjätty vanhenemisen vuoksi. -runners.delete_runner_notice = Jos tehtävä on käynnissä tällä suorittajalla, se lopetetaan ja merkitään epäonnistuneeksi. Se voi rikkoa koonnin työnkulun. +runners.delete_runner_notice = Jos tehtävä on käynnissä tällä testinajajalla, se lopetetaan ja merkitään epäonnistuneeksi. Se voi rikkoa koonnin työnkulun. runners.update_runner_failed = Testinajajan päivitys epäonnistui variables.deletion.success = Muuttuja poistettu. variables.edit = Muokkaa muuttujaa @@ -3015,11 +3486,11 @@ variables.update.success = Muuttuja muokattu. variables.id_not_exist = Muuttujaa tunnisteella %d ei ole olemassa. runs.all_workflows = Kaikki työnkulut workflow.dispatch.run = Suorita työnkulku -workflow.enable = Käytä työnkulkua +workflow.enable = Ota työnkulku käyttöön runs.no_workflows = Ei työnkulkuja vielä. runs.actors_no_select = Kaikki toimijat runs.workflow = Työnkulku -workflow.enable_success = Työnkulku "%s" otettu käyttöön. +workflow.enable_success = Työnkulku "%s" on otettu käyttöön. workflow.disabled = Työnkulku on poistettu käytöstä. runs.actor = Toimija workflow.disable = Poista työnkulku käytöstä @@ -3028,11 +3499,15 @@ runs.no_job = Työnkulun tulee sisältää vähintään yksi työ runs.invalid_workflow_helper = Työnkulun asetustiedosto on virheellinen. Tarkista asetustiedosto: %s runners = Ajajat actions = Actions -unit.desc = Hallitse integroituja CI/CD-putkia Forgejo Actionsia hyödyntäen. +unit.desc = Hallitse integroituja CI-/CD-putkia Forgejo Actionsia hyödyntäen. runs.pushed_by = työntänyt runs.no_workflows.help_no_write_access = Lisätietoja Forgejo Actionsista on saatavilla dokumentaatiosta. -runners.status.idle = Tyhjäkäynti +runners.status.idle = Jouten runners.status.offline = Ei-verkkotilassa +runs.no_job_without_needs = Työnkulun tulee sisältää vähintään yksi työ ilman riippuvuuksia. +runs.no_runs = Työnkululla ei ole vielä suorituksia. +variables.not_found = Muuttujaa ei löytynyt. +runs.no_workflows.help_write_access = Etkö tiedä, miten aloittaa Forgejo Actionsin käyttö? Lue pikaopas kirjoittaaksesi ensimmäisen työnkulun, sen jälkeen määritä Forgejo-ajaja suorittamaan asettamiasi töitä. @@ -3040,13 +3515,16 @@ runners.status.offline = Ei-verkkotilassa [projects] type-1.display_name = Yksittäinen projekti deleted.display_name = Poistettu projekti +type-3.display_name = Organisaatioprojekti +type-2.display_name = Tietovarastoprojekti [git.filemode] changed_filemode = %[1]s -> %[2]s -executable_file = Ajettava tiedosto +executable_file = Suoritettava tiedosto symbolic_link = Symbolinen linkki normal_file = Tavallinen tiedosto -directory = Kansio +directory = Hakemisto +submodule = Alimoduuli [search] search = Hae… @@ -3059,20 +3537,38 @@ exact = Täsmällinen exact_tooltip = Sisällytä vain täsmälleen hakusanaa vastaavat tulokset team_kind = Etsi tiimejä… code_kind = Etsi koodia… -code_search_unavailable = Koodihaku ei tällä hetkellä ole saatavilla. Ota yhteyttä järjestelmänvalvojaan. +code_search_unavailable = Koodihaku ei tällä hetkellä ole saatavilla. Ota yhteyttä sivuston ylläpitoon. union = yhdistelmähaku union_tooltip = Sisällytä tulokset, jotka vastaavat minkä tahansa välilyönnillä erotetuista avainsanoista project_kind = Etsi projekteja… no_results = Hakutuloksia ei löytynyt. -keyword_search_unavailable = Avainsanahaku ei tällä hetkellä ole saatavilla. Ota yhteyttä järjestelmänvalvojaan. +keyword_search_unavailable = Avainsanahaku ei tällä hetkellä ole saatavilla. Ota yhteyttä sivuston ylläpitoon. repo_kind = Etsi tietovarastoja… user_kind = Etsi käyttäjiä… org_kind = Etsi organisaatioita… branch_kind = Etsi haaroja… -issue_kind = Etsi vikoja… +issue_kind = Etsi ongelmia… milestone_kind = Etsi merkkipaaluja... -pull_kind = Etsi pull-vetoja… -commit_kind = Etsi sitoutumisia… +pull_kind = Etsi vetoja… +commit_kind = Etsi kommitteja… fuzzy = Sumea runner_kind = Etsi ajajia… code_search_by_git_grep = Nykyiset koodin hakutulokset pohjautuvat komentoon "git grep". Parempia tuloksia on mahdollista saada, jos sivuston ylläpitäjä ottaa käyttöön koodin indeksoijan. + + +[repo.permissions] +code.read = Lue: Pääsy koodiin ja tietovaraston kloonaaminen. +code.write = Kirjoita: Työnnä tietovarastoon, luo haaroja ja tageja. +issues.read = Lue: Lue ja luo ongelmia ja kommentteja. +releases.read = Lue: Katsele ja lataa julkaisuja. +pulls.read = Lue: Vetopyyntöjen lukeminen ja luominen. +ext_issues = Pääsy ulkoisen ongelmanseurannan linkkiin. Käyttöoikeuksia hallitaan ulkoisesti. +ext_wiki = Pääsy ulkoisen wikin linkkiin. Käyttöoikeuksia hallitaan ulkoisesti. +projects.read = Lue: Pääsy tietovaraston projektitauluille. +wiki.write = Kirjoita: Luo, päivitä ja poista integroidun wikin sivuja. + +[markup] +filepreview.truncated = Esikatselu on typistetty + +[translation_meta] +test = This is a test string. It is not displayed in Forgejo UI but is used for testing purposes. Feel free to enter "ok" to save time (or a fun fact of your choice) to hit that sweet 100% completion mark :) :) :) \ No newline at end of file diff --git a/options/locale/locale_fil.ini b/options/locale/locale_fil.ini index 6c338e1583..8c9badb04b 100644 --- a/options/locale/locale_fil.ini +++ b/options/locale/locale_fil.ini @@ -38,9 +38,9 @@ logo = Logo sign_in = Mag-sign in sign_in_with_provider = Mag-sign in gamit ang %s sign_in_or = o -sign_out = Mag-Sign Out +sign_out = Mag-sign out sign_up = Magrehistro -link_account = Mag-link ng Account +link_account = Mag-link ng account template = Template tracked_time_summary = Buod ng mga nakasubaybay na oras base sa filter ng listahan ng isyu webauthn_sign_in = Pindutin ang button ng iyong security key. Kung walang button ang iyong security key, ilagay muli. @@ -365,7 +365,7 @@ table_modal.label.columns = Mga Column link_modal.header = Magdagdag ng link link_modal.url = Url link_modal.description = Deskripsyon -link_modal.paste_reminder = Pahiwatig: Kapag may URL sa clipboard, maari mong direktang i-paste sa editor para gumawa ng link. +link_modal.paste_reminder = Pahiwatig: Kapag may URL sa clipboard, maaari mong direktang i-paste sa editor para gumawa ng link. [filter] string.asc = A - Z @@ -432,7 +432,7 @@ openid_connect_desc = Ang piniling OpenID URI ay hindi alam. Iugnay iyan sa bago invalid_code = Ang iyong confirmation code ay hindi wasto o nag-expire na. oauth_signin_title = Mag-sign in para pahintulutan ang naka-link na account invalid_code_forgot_password = Ang iyong confirmation code ay hindi wasto o nag-expire na. Mag-click dito para magsimula ng bagong session. -confirmation_mail_sent_prompt = Ang isang bagong email na pang-kumpirma ay ipinadala sa %s. Para kumpletuhin ang proseso ng pagrehistro, pakisuri ang iyong inbox at sundan ang ibinigay na link sa loob ng %s. Kung mali ang email, maari kang mag-log in, at humingi ng isa pang email pang-kumpirma na ipapadala sa ibang address. +confirmation_mail_sent_prompt = Ang isang bagong email na pang-kumpirma ay ipinadala sa %s. Para kumpletuhin ang proseso ng pagrehistro, pakisuri ang iyong inbox at sundan ang ibinigay na link sa loob ng %s. Kung mali ang email, maaari kang mag-log in, at humingi ng isa pang email pang-kumpirma na ipapadala sa ibang address. invalid_password = Ang iyong password ay hindi tugma sa password na ginamit para gawin ang account. twofa_scratch_used = Ginamit mo na ang scratch code. Na-redirect ka sa two-factor settings page para tanggalin ang device enrollment o mag-generate ng bagong scratch code. manual_activation_only = Makipag-ugnayan sa tagapangangasiwa ng site para kumpletuhin ang pagrehistro. @@ -484,7 +484,7 @@ admin.new_user.text = Mangyaring mag-click dito para ipamahala register_notify = Maligayang Pagdating sa %s register_notify.title = %[1]s, maligayang pagdating sa %[2]s register_notify.text_1 = ito ang iyong registration confirmation email para sa %s! -register_notify.text_2 = Maari kang mag-sign in sa iyong account gamit ng iyong username: %s +register_notify.text_2 = Maaari kang mag-sign in sa iyong account gamit ng iyong username: %s reset_password = I-recover ang iyong account reset_password.title = %s, nagkaroon kami ng hiling para i-recover ang iyong account reset_password.text = Kung ikaw ito, paki-click ang sumusunod na link para i-recover ang iyong account sa loob ng %s: @@ -535,7 +535,7 @@ totp_disabled.text_1 = Ngayon lang na-disable ang Time-based one-time password ( totp_disabled.no_2fa = Wala nang mga ibang paraan ng 2FA ang naka-configure, nangangahulugan na hindi na kailangang mag-log in sa iyong account gamit ang 2FA. removed_security_key.subject = May tinanggal na security key removed_security_key.text_1 = Tinanggal ngayon lang ang security key na "%[1]s" sa iyong account. -account_security_caution.text_1 = Kung ikaw ito, maari mong ligtas na huwag pansinin ang mail na ito. +account_security_caution.text_1 = Kung ikaw ito, maaari mong ligtas na huwag pansinin ang mail na ito. account_security_caution.text_2 = Kung hindi ito ikaw, nakompromiso ang iyong account. Mangyaring makipag-ugnayan sa mga tagapangasiwa ng site na ito. totp_enrolled.subject = Nag-activate ka ng TOTP bilang paraan ng 2FA totp_enrolled.text_1.has_webauthn = Na-enable mo lang ang TOTP para sa iyong account. Nangangahulugan ito na para sa lahat ng mga hinaharap na pag-login sa iyong account, kailangan mong gumamit ng TOTP bilang paraan ng 2FA o gamitin ang iyong mga security key. @@ -644,7 +644,7 @@ AccessToken = Token ng pag-access Biography = Byograpya Location = Lokasyon visit_rate_limit = Natugunan ang limitasyon sa rate ng malayuang pagbisita. -username_claiming_cooldown = Hindi ma-claim ang username na ito, dahil hindi pa tapos ang panahon ng cooldown. Maari itong i-claim sa %[1]s. +username_claiming_cooldown = Hindi ma-claim ang username na ito, dahil hindi pa tapos ang panahon ng cooldown. Maaari itong i-claim sa %[1]s. email_domain_is_not_allowed = Sumasalungat ang domain ng email address ng user %s sa EMAIL_DOMAIN_ALLOWLIST o EMAIL_DOMAIN_BLOCKLIST. Siguraduhing natakda mo ang email address nang tama. [user] @@ -685,7 +685,7 @@ followers.title.few = Mga tagasunod following.title.one = Sinusundan followers.title.one = Tagasunod public_activity.visibility_hint.self_public = Nakikita ng lahat ang iyong aktibidad, maliban sa mga interaksyon sa pribadong espasyo. I-configure. -public_activity.visibility_hint.admin_public = Nakikita ng lahat ang aktibidad na ito, ngunit bilang tagapangasiwa maari mo ring makita ang mga interaksyon sa mga pribadong espasyo. +public_activity.visibility_hint.admin_public = Nakikita ng lahat ang aktibidad na ito, ngunit bilang tagapangasiwa maaari mo ring makita ang mga interaksyon sa mga pribadong espasyo. public_activity.visibility_hint.self_private = Nakikita mo lang at mga tagapangasiwa ng instansya ang iyong aktibidad. I-configure. public_activity.visibility_hint.admin_private = Nakikita mo ang aktibidad na ito dahil isa kang tagapangasiwa, ngunit gusto ng user na panatilihin itong pribado. public_activity.visibility_hint.self_private_profile = Ikaw lang at ang mga tagapangasiwa ng instansya ang makakakita ng iyong aktibidad dahil pribado ang iyong profile. I-configure. @@ -701,7 +701,7 @@ ssh_gpg_keys = Mga SSH / GPG key applications = Mga Aplikasyon orgs = Ipamahala ang mga organisasyon repos = Mga Repositoryo -delete = Burahin ang Account +delete = Burahin ang account twofa = Authentikasyong two-factor (TOTP) account_link = Mga naka-link na account uid = UID @@ -842,7 +842,7 @@ gpg_key_verify = I-verify gpg_invalid_token_signature = Ang ibinigay na GPG key, signature, at token ay hindi tumutugma o luma. gpg_token_required = Kailangan mong magbigay ng signature para sa token sa ibaba gpg_token = Token -gpg_token_help = Maari kang mag-generate ng signature gamit ng: +gpg_token_help = Maaari kang mag-generate ng signature gamit ng: gpg_token_signature = Naka-armor na GPG signature key_signature_gpg_placeholder = Nagsisimula sa "-----BEGIN PGP SIGNATURE-----" verify_gpg_key_success = Na-verify na ang GPG key na "%s". @@ -851,7 +851,7 @@ ssh_key_verify = I-verify ssh_invalid_token_signature = Ang ibinigay na SSH key, signature, o token ay hindi tumutugma o luma. ssh_token_required = Kailangan mong magbigay ng signature para sa token sa ibaba ssh_token = Token -ssh_token_help = Maari kang mag-generate ng signature gamit ng: +ssh_token_help = Maaari kang mag-generate ng signature gamit ng: ssh_token_signature = Naka-armor na SSH signature key_signature_ssh_placeholder = Nagsisimula sa "-----BEGIN SSH SIGNATURE-----" verify_ssh_key_success = Na-verify na ang SSH key na "%s". @@ -912,10 +912,10 @@ create_oauth2_application_success = Matagumpay kang gumawa ang bagong OAuth2 app oauth2_confidential_client = Kumpidensyal na kliyente. Piliin para sa mga app na pinapatilihing kumpidensyal ang sikreto, tulad ng mga web app. Huwag piliin para sa mga web app kasama ang mga desktop at mobile app. twofa_desc = Para protektahin ang iyong account laban sa pagnanakaw ng password, pwede mo gamitin ang iyong smartphone o ibang device para sa pagtanggap ng time-based one-time password ("TOTP"). twofa_scratch_token_regenerated = Ang iyong isang-beses na paggamit na recovery key ngayon ay %s. Ilagay ito sa ligtas na lugar, dahil hindi na ito ipapakita muli. -regenerate_scratch_token_desc = Kapag nawala mo ang iyong recovery key o ginamit mo na oara mag-sign in, maari mong i-reset dito. +regenerate_scratch_token_desc = Kapag nawala mo ang iyong recovery key o ginamit mo na oara mag-sign in, maaari mong i-reset dito. twofa_disable_desc = Ang pag-disable ng authentikasyong two-factor ay gagawing hindi gaanong ligtas ang iyong account. Magpatuloy? twofa_enrolled = Matagumpay na na-enroll ang iyong account. Ilagay ang iyong isang-beses na paggamit na recovery key (%s) sa isang ligtas na lugar, dahil hindi na ito ipapakita muli. -webauthn_desc = Ang mga security key ay isang hardware device na naglalaman ng mga cryptographic key. Maari silang gamitin para sa authentikasyong two-factor. Ang mga security key ay dapat suportahan ang WebAuthn Authenticator na standard. +webauthn_desc = Ang mga security key ay isang hardware device na naglalaman ng mga cryptographic key. Maaari silang gamitin para sa authentikasyong two-factor. Ang mga security key ay dapat suportahan ang WebAuthn Authenticator na standard. remove_oauth2_application = Tanggalin ang OAuth2 Application remove_oauth2_application_desc = Ang pagtanggal ng OAuth2 application ay babawiin ang access sa lahat ng mga naka-sign na access token. Magpatuloy? remove_oauth2_application_success = Binura na ang application. @@ -931,13 +931,13 @@ oauth2_regenerate_secret = I-regenerate ang sikreto oauth2_regenerate_secret_hint = Nawala mo ang iyong sikreto? oauth2_client_secret_hint = Ang sikreto ay hindi ipapakita muli pagkatapos umalis ka o i-refresh ang page na ito. Mangyaring siguraduhin na na-save mo iyan. oauth2_application_edit = I-edit -twofa_recovery_tip = Kapag mawala mo ang iyong device, maari kang gumamit ng isang isang-beses na paggamit na recovery key para makakuha muli ng access sa iyong account. +twofa_recovery_tip = Kapag mawala mo ang iyong device, maaari kang gumamit ng isang isang-beses na paggamit na recovery key para makakuha muli ng access sa iyong account. twofa_is_enrolled = Ang iyong account ay kasalukuyang naka-enroll sa autentikasyong two-factor. twofa_not_enrolled = Kasalukuyang hindi naka-enroll ang iyong account sa authentikasyong two-factor. twofa_disable = I-disable ang authentikasyong two-factor twofa_scratch_token_regenerate = I-regenerate ang isang-beses na paggamit na recovery key twofa_enroll = Mag-enroll sa authentikasyong two-factor -twofa_disable_note = Maari mong i-disable ang authentikasyong two-factor kapag kinakailangan. +twofa_disable_note = Maaari mong i-disable ang authentikasyong two-factor kapag kinakailangan. twofa_disabled = Na-disable na ang authentikasyong two-factor. scan_this_image = I-scan ang image na ito gamit ng iyong aplikasyong pang-authentikasyon: or_enter_secret = O ilagay ang sikreto: %s @@ -1005,8 +1005,8 @@ language.description = Mase-save ang wika sa iyong account at gagamitin bilang d language.localization_project = Tulungan kaming isalin ang Forgejo sa iyong wika! Matuto pa. pronouns_custom_label = Mga pasadyang pronoun user_block_yourself = Hindi mo maaaring harangan ang sarili mo. -change_username_redirect_prompt.with_cooldown.one = Magiging available ang lumang username sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw, maari mo pa ring ma-claim muli ang lumang username sa panahon ng panahon ng cooldown. -change_username_redirect_prompt.with_cooldown.few = Magiging available ang lumang username sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw, maari mo pa ring ma-claim muli ang lumang username sa panahon ng panahon ng cooldown. +change_username_redirect_prompt.with_cooldown.one = Magiging available ang lumang username sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw. Maaari mo pa ring ma-claim muli ang lumang username sa panahon ng panahon ng cooldown. +change_username_redirect_prompt.with_cooldown.few = Magiging available ang lumang username sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw. Maaari mo pa ring ma-claim muli ang lumang username sa panahon ng panahon ng cooldown. keep_pronouns_private = Ipakita lang ang mga panghalip sa mga naka-authenticate na user keep_pronouns_private.description = Itatago nito ang iyong mga panghalip mula sa mga bisita na hindi naka-log in. quota.applies_to_user = Nag-aapply ang mga sumusunod na panuntunan ng quota sa iyong account @@ -1071,7 +1071,7 @@ readme_helper_desc = Ito ang lugar kung saan makakasulat ka ng kumpletong deskri trust_model_helper_collaborator_committer = Katulong+Committer: I-trust ang mga signature batay sa mga katulong na tumutugma sa committer mirror_interval = Interval ng mirror (ang mga wastong unit ng oras ay "h", "m", "s"). 0 para i-disable ang periodic sync. (Pinakamababang interval: %s) transfer.reject_desc = Kanselahin ang pag-transfer mula sa "%s" -mirror_lfs_endpoint_desc = Ang sync ay susubukang gamitin ang clone url upang matukoy ang LFS server. Maari ka rin tumukoy ng isang custom na endpoint kapag ang LFS data ng repositoryo ay nilalagay sa ibang lugar. +mirror_lfs_endpoint_desc = Ang sync ay susubukang gamitin ang clone url upang matukoy ang LFS server. Maaari ka rin tumukoy ng isang custom na endpoint kapag ang LFS data ng repositoryo ay nilalagay sa ibang lugar. adopt_search = Ilagay ang username para maghanap ng mga unadopted na repositoryo… (iwanang walang laman para hanapin lahat) object_format = Format ng object readme_helper = Pumili ng README file template @@ -1164,8 +1164,8 @@ tree_path_not_found_commit = Hindi umiiral ang path na %[1]s sa commit %[2]s tree_path_not_found_branch = Hindi umiiral ang daanang %[1]s sa branch %[2]s migrate_items_pullrequests = Mga hiling sa paghila archive.pull.nocomment = Naka-archive ang repositoryong ito. Hindi ka makakakomento sa mga pull request. -archive.title = Naka-archive ang repositoryong ito. Maari mong itignan ang mga file at i-clone ito, pero hindi ka makakagawa ng anumang pagbabago sa estado ito, tulad ng pagtulak at paggawa ng mga isyu, pull request o mga komento. -archive.title_date = Naka-archive ang repositoryo na ito noong %s. Maari mong itignan ang mga file at i-clone ito, pero hindi ka makakagawa ng anumang pagbabago sa estado nito, tulad ng pagtulak o paggawa ng mga bagong isyu, mga pull request, o komento. +archive.title = Naka-archive ang repositoryong ito. Maaari mong itignan ang mga file at i-clone ito, pero hindi ka makakagawa ng anumang pagbabago sa estado ito, tulad ng pagtulak at paggawa ng mga isyu, pull request o mga komento. +archive.title_date = Naka-archive ang repositoryo na ito noong %s. Maaari mong itignan ang mga file at i-clone ito, pero hindi ka makakagawa ng anumang pagbabago sa estado nito, tulad ng pagtulak o paggawa ng mga bagong isyu, mga pull request, o komento. pulls = Mga hiling sa paghila activity.merged_prs_count_n = Mga naisamang hiling sa paghila wiki.last_updated = Huling binago %s @@ -1183,7 +1183,7 @@ issues.action_open = Buksan issues.closed_title = Sarado issues.reopen_issue = Buksang muli pulls.merged = Naisama na -pulls.merged_info_text = Maari nang burahin ang branch %s. +pulls.merged_info_text = Maaari nang burahin ang branch %s. milestones.update_ago = Binago %s activity.closed_issue_label = Sarado activity.merged_prs_label = Naisama @@ -1205,7 +1205,7 @@ migrate.clone_address_desc = Ang HTTP(S) o Git "clone" URL ng umiiral na reposit need_auth = Awtorisasyon migrate.github_token_desc = Maaari kang maglagay ng isa o higit pang mga token na hinihiwalay ng kuwit dito upang gawing mas-mabilis ang pagmigrate dahil sa rate limit ng GitHub API. BABALA: Ang pagabuso ng feature na ito ay maaaring maglabag sa patakaran ng tagapagbigay ng serbisyo at maaaring magdulot ng pag-block ng account. template.invalid = Kailangang pumili ng kahit isang template na repositoryo -migrate_options_lfs_endpoint.description = Susubukan ng migration na gamitin ang iyong Git remote upang matukoy ang LFS server. Maari mong magtiyak ng custom na endpoint kapag ang LFS data ng repositoryo ay nakalagay sa ibang lugar. +migrate_options_lfs_endpoint.description = Susubukan ng migration na gamitin ang iyong Git remote upang matukoy ang LFS server. Maaari mong magtiyak ng custom na endpoint kapag ang LFS data ng repositoryo ay nakalagay sa ibang lugar. blame.ignore_revs.failed = Nabigong hindi pansinin ang mga rebisyon sa .git-blame-ignore-revs. tree_path_not_found_tag = Hindi umiiral ang path na %[1]s sa tag %[2]s form.reach_limit_of_creation_n = Naabot na ng may-ari ang limitasyon na %d mga repositoryo. @@ -1471,10 +1471,10 @@ activity.new_issue_label = Nabuksan activity.merged_prs_count_1 = Naisamang hiling sa paghila activity.opened_prs_count_1 = Inimungkahing hiling sa paghila activity.opened_prs_label = Inimungkahi -pulls.reopened_at = `nabuksang muli ang hiling sa paghatak na %[2]s` +pulls.reopened_at = `binuksan muli ang hiling sa paghila %s` issues.opened_by_fake = binuksan ang %[1]s ni/ng %[2]s pulls.reopen_failed.base_branch = Hindi mabuksang muli ang hiling sa paghatak na ito dahil hindi na umiiral ang base branch. -issues.reopened_at = `binuksang muli ang isyung ito %[2]s` +issues.reopened_at = `binuksang muli ang isyung ito %s` pulls.reopen_failed.head_branch = Hindi mabubuksan muli ang hiling sa paghila, dahil hindi na umiiral ang head branch. settings.event_pull_request_desc = Binuksan, sinara, muling binuksan, o binago ang hiling sa paghatak. activity.opened_prs_count_n = Mga inimungkahing hiling sa paghila @@ -1500,7 +1500,7 @@ issues.content_history.created = ginawa editor.patching = Pina-patch: editor.fail_to_apply_patch = Hindi malapat ang patch na "%s" settings.danger_zone = Mapanganib na lugar -issues.closed_at = `isinara ang isyung %[2]s` +issues.closed_at = `isinara ang isyung ito %s` settings.collaboration.admin = Tagapangasiwa settings.admin_settings = Mga setting ng tagapangasiwa issues.start_tracking_history = `sinimulan ang trabaho %s` @@ -1627,7 +1627,7 @@ projects.column.edit_title = Pangalan projects.column.new_title = Pangalan projects.card_type.desc = Mga preview ng card commits.desc = I-browse ang history ng pagbabago ng source code. -commits.search.tooltip = Maari kang mag-prefix ng mga keyword gamit ang "author:", "committer:", "after:", o "before:", hal. "revert author:Nijika before:2022-10-09". +commits.search.tooltip = Maaari kang mag-prefix ng mga keyword gamit ang "author:", "committer:", "after:", o "before:", hal. "revert author:Nijika before:2022-10-09". issues.force_push_codes = `puwersahang itinulak ang %[1]s mula %[2]s sa %[4]s %[6]s` issues.push_commit_1 = idinagdag ang %d commit %s issues.push_commits_n = idinagdag ang %d mga commit %s @@ -1674,10 +1674,10 @@ issues.new_label = Bagong label issues.label_templates.title = Mag-load ng isang label preset issues.new.clear_milestone = I-clear ang milestone issues.new.open_milestone = Mga bukas na milestone -issues.filter_milestones = I-filter ang Milestone -issues.filter_projects = I-filter ang Proyekto -issues.filter_labels = I-filter ang Label -issues.filter_reviewers = I-filter ang Tagasuri +issues.filter_milestones = I-filter ang milestone +issues.filter_projects = I-filter ang proyekto +issues.filter_labels = I-filter ang label +issues.filter_reviewers = I-filter ang tagasuri issues.remove_labels = tinanggal ang mga label na %s %s issues.add_remove_labels = idinagdag ang %s at tinanggal ang %s na mga label %s issues.add_milestone_at = `idinagdag ito sa %s na milestone %s` @@ -1688,7 +1688,7 @@ issues.add_label = idinagdag ang %s na label %s issues.add_labels = idinagdag ang mga label na %s %s issues.remove_label = tinanggal ang %s na label %s issues.desc = Ayusin ang mga ulat ng bug, gawain, at milestone. -issues.filter_assignees = I-filter ang Mangangasiwa +issues.filter_assignees = I-filter ang mangangasiwa issues.new.labels = Mga label issues.new.no_label = Walang mga label issues.new.clear_labels = I-clear ang mga label @@ -1707,7 +1707,7 @@ issues.action_milestone = Milestone issues.action_milestone_no_select = Walang milestone issues.delete_branch_at = `binura ang branch na %s %s` issues.filter_label = Label -issues.filter_label_exclude = `Gamitin ang alt + click/enter para hindi isama ang mga label` +issues.filter_label_exclude = `Gamitin ang Alt + Click para hindi isama ang mga label` issues.filter_label_no_select = Lahat ng mga label issues.filter_milestone_closed = Mga nakasarang milestone issues.filter_assignee = Mangangasiwa @@ -1771,7 +1771,7 @@ issues.lock = I-lock ang usapan issues.unlock = I-unlock ang usapan issues.unlock_comment = na-unlock ang usapang ito %s issues.unlock.notice_1 = - Makakakomento muli ang lahat ng mga tao sa isyung ito. -issues.unlock.notice_2 = - Maari mong i-lock muli ang isyung ito sa hinaharap. +issues.unlock.notice_2 = - Maaari mong i-lock muli ang isyung ito sa hinaharap. issues.comment_on_locked = Hindi ka makakakomento sa naka-lock na isyu. issues.closed_by_fake = ni/ng %[2]s ay isinara %[1]s issues.comment_manually_pull_merged_at = manwal na isinama ang commit %[1]s sa %[2]s %[3]s @@ -1787,10 +1787,10 @@ issues.label_archive_tooltip = Ang mga naka-archive na label ay hindi isasama bi issues.is_stale = May mga pagbabago sa PR na ito mula sa pagsuri na ito issues.role.first_time_contributor = Unang-beses na contributor issues.lock.notice_1 = - Hindi makakadagdag ng mga bagong komento ang mga ibang user sa isyu na ito. -issues.lock.notice_3 = - Maari mong i-unlock muli ang isyung ito sa hinaharap. +issues.lock.notice_3 = - Maaari mong i-unlock muli ang isyung ito sa hinaharap. issues.label_deletion_desc = Ang pagbura ng label ay tatanggalin ito sa lahat ng mga isyu. Magpatuloy? -issues.commit_ref_at = `isinangguni ang isyu na ito mula sa commit %[2]s` -issues.ref_issue_from = `isinangguni ang isyu na ito sa %[4]s %[2]s` +issues.commit_ref_at = `isinangguni ang isyu na ito mula sa commit %s` +issues.ref_issue_from = `isinangguni ang isyu na ito sa %[3]s %[1]s` issues.num_participants_one = %d kasali issues.attachment.download = `I-click para i-download ang "%s" ` issues.num_participants_few = %d mga kasali @@ -1815,10 +1815,10 @@ issues.sign_in_require_desc = Mag-sign in upang sumali sa usapa issues.num_comments = %d mga komento issues.role.contributor_helper = Nakaraang nag-commit ang user na ito sa repositoryo na ito. issues.comment_pull_merged_at = isinama ang commit %[1]s sa %[2]s %[3]s -pulls.commit_ref_at = `isinangguni ang hiling sa paghila mula sa isang commit %[2]s` +pulls.commit_ref_at = `isinangguni ang hiling sa paghila mula sa isang commit %s` wiki.last_commit_info = Binago ni %s ang pahinang ito %s issues.content_history.edited = binago -issues.ref_pull_from = `isinangguni ang hiling sa paghila na ito %[4]s %[2]s` +issues.ref_pull_from = `isinangguni ang hiling sa paghila na ito %[3]s %[1]s` pulls.merged_title_desc_few = isinali ang %[1]d mga commit mula sa %[2]s patungong %[3]s %[4]s settings.org_not_allowed_to_be_collaborator = Hindi maaaring idagdag ang mga organisasyon bilang tagatulong. settings.add_collaborator_success = Naidagdag ang tagatulong. @@ -1828,7 +1828,7 @@ pulls.create = Gumawa ng hiling sa paghila issues.dependency.pr_close_blocked = Kailangan mong isara ang lahat ng mga isyu na humaharang sa hiling sa paghila na ito bago mo ito isama. pulls.delete.title = Burahin ang hiling sa paghila na ito? issues.dependency.pr_closing_blockedby = Hinarang ng mga sumusunod na isyu mula sa pagsara ng hiling sa paghila na ito -pulls.closed_at = `isinara ang hiling sa paghila na %[2]s` +pulls.closed_at = `isinara ang hiling sa paghila na ito %s` pulls.close = Isara ang hiling sa paghila pulls.cmd_instruction_hint = Tingnan ang mga panuto para sa command line project = Mga proyekto @@ -1836,8 +1836,8 @@ issues.content_history.deleted = binura pulls.no_results = Walang mga nahanap na resulta. pulls.closed = Sarado ang hiling sa paghila pulls.is_closed = Naisara na ang hiling sa paghila. -issues.ref_closing_from = `nagsangguni ang isyu mula sa hiling sa paghila %[4]s na magsasara sa isyu, %[2]s` -issues.ref_reopening_from = `nagsangguni ang isyu na ito mula sa hiling sa paghila %[4]s na muling bubukas, %[2]s` +issues.ref_closing_from = `nagsangguni ang isyu mula sa hiling sa paghila %[3]s na magsasara sa isyu, %[1]s` +issues.ref_reopening_from = `nagsangguni ang isyu na ito mula sa hiling sa paghila %[3]s na muling bubukas nito, %[1]s` issues.ref_closed_from = `isinara ang isyung ito %[4]s%[2]s` issues.review.wait = hiniling sa pagsuri %s issues.review.reject = hinihiling ang mga pagbago %s @@ -1893,7 +1893,7 @@ settings.collaboration.owner = May-ari pulls.showing_only_single_commit = Ipinapakita lamang ang mga pagbago ng commit na %[1]s comments.edit.already_changed = Hindi maimbak ang mga pagbabago sa komento. Mukhang nabago na ng ibang tagagamit ang nilalaman. Mangyaring i-refresh ang pahina at subukang baguhin muli upang maiwasang ma-overwrite ang kanilang pagbago milestones.completeness = %d%% nakumpleto -wiki.welcome = Maligayang pagdating sa Wiki. +wiki.welcome = Maligayang pagdating sa wiki. wiki.create_first_page = Gawin ang unang pahina pulls.switch_comparison_type = Ilipat ang uri ng pagkumpara settings.collaboration.read = Basahin @@ -2015,14 +2015,14 @@ wiki.cancel = Kanselahin settings.collaboration.undefined = Hindi Natukoy settings.federation_settings = Mga Setting ng Federation settings = Mga Setting -settings.desc = Ang mga setting ang lugar kung saan maari mong ipamahala ang mga setting para sa repositoryo +settings.desc = Ang mga setting ang lugar kung saan maaari mong ipamahala ang mga setting para sa repositoryo pulls.collapse_files = I-collapse ang lahat ng mga file pulls.add_prefix = Magdagdag ng %s na prefix pulls.still_in_progress = Ginagawa pa? activity.title.prs_1 = %d hiling sa paghila activity.active_issues_count_n = %d mga aktibong isyu pulls.required_status_check_missing = Nawawala ang ilang mga kinakailangang pagsusuri. -pulls.required_status_check_administrator = Bilang tagapangasiwa, maari mo pa ring isama ang hiling sa paghila na ito. +pulls.required_status_check_administrator = Bilang tagapangasiwa, maaari mo pa ring isama ang hiling sa paghila na ito. pulls.blocked_by_approvals = Wala pang sapat na pag-apruba ang hiling sa paghila na ito. %d ng %d na pag-apruba ang ibinigay. settings.options = Repositoryo wiki.back_to_wiki = Bumalik sa pahina ng wiki @@ -2110,7 +2110,7 @@ settings.actions_desc = I-enable ang mga kasamang CI/CD pipeline gamit ang Forge settings.admin_indexer_commit_sha = Huling na-index na commit settings.admin_indexer_unindexed = Hindi naka-index settings.transfer_notices_3 = - Kung pribado ang repositoryo at ilipat sa isang indibidwal na user, ang aksyon na ito ay sinisigurado na ang user ay may pahintulot na basahin (at palitan ang mga pahintulot kung kailangan). -settings.convert_desc = Maari mong i-convert ang repositoryo na ito sa regular na repositoryo. Hindi ito mababawi. +settings.convert_desc = Maaari mong i-convert ang repositoryo na ito sa regular na repositoryo. Hindi ito mababawi. settings.transfer.button = Ilipat ang pagmamay-ari settings.signing_settings = Mga setting sa pagpapatunay ng pag-sign settings.admin_enable_close_issues_via_commit_in_any_branch = Isara ang isyu sa pamamagitan ng commit na ginawa sa hindi default na branch @@ -2137,7 +2137,7 @@ settings.deploy_key_deletion = Tanggalin ang deploy key settings.protect_enable_push = I-enable ang pagtulak settings.discord_icon_url.exceeds_max_length = Kailangang bababa o equal sa 2048 characters ang URL ng icon settings.protected_branch.save_rule = I-save ang rule -settings.mirror_settings.docs.can_still_use = Bagama't na hindi ka makakabago ng mga umiiral na mirror o gumawa ng bago, maari mo pa rin gamitin ang iyong umiiral na mirror. +settings.mirror_settings.docs.can_still_use = Bagama't na hindi ka makakabago ng mga umiiral na mirror o gumawa ng bago, maaari mo pa rin gamitin ang iyong umiiral na mirror. settings.slack_color = Kulay settings.discord_icon_url = URL ng icon settings.convert_fork_confirm = I-convert ang repositoryo @@ -2254,7 +2254,7 @@ settings.pulls.allow_rebase_update = I-enable ang pag-update ng hiling sa paghil settings.admin_enable_health_check = I-enable ang pagsusuri ng kalusugan ng repositoryo (git fsck) settings.new_owner_has_same_repo = Ang bagong may-ari ay may repositoryo na may katulad na pangalan. Mangyaring pumili ng ibang pangalan. settings.convert = I-convert sa regular na repositoryo -settings.convert_fork_desc = Maari mong i-convert ang fork na ito bilang regular na repositoryo. Hindi ito mababawi. +settings.convert_fork_desc = Maaari mong i-convert ang fork na ito bilang regular na repositoryo. Hindi ito mababawi. settings.convert_fork_notices_1 = Ang operasyon na ito ay ico-convert ang fork bilang regular na repositoryo at hindi mababawi. settings.transfer_abort_invalid = Hindi mo makakansela ang isang hindi umiiral na paglipat ng repositoryo. settings.transfer_quota_exceeded = Ang bagong may-ari (%s) ay lumalagpas sa quota. Hindi nailipat ang repositoryo. @@ -2278,7 +2278,7 @@ settings.add_collaborator = Magdagdag ng katulong settings.add_collaborator_duplicate = Nadagdag na ang tagatulong na ito sa repositoryo. settings.add_collaborator_blocked_our = Hindi madagdag ang tagatulong, dahil hinarang siya ng may-ari ng repositoryo. settings.add_collaborator_blocked_them = Hindi madagdag ang tagatulong, dahil hinarang niya ang may-ari ng repositoryo. -settings.collaborator_deletion = Tanggalin ang Tagatulong +settings.collaborator_deletion = Tanggalin ang tagatulong settings.team_not_in_organization = Ang koponan ay hindi nasa katulad na organisasyon sa repositoryo settings.teams = Mga Koponan settings.add_team_success = May access na ang koponan sa repositoryo na ito. @@ -2290,8 +2290,8 @@ settings.webhook.headers = Mga header settings.webhook.payload = Nilalaman settings.webhook.body = Katawan settings.webhook.replay.description = I-replay ang webhook na ito. -settings.webhook.delivery.success = May nadagdag na event sa delivery queue. Maari magtagal ng ilang segundo bago makita sa delivery history. -settings.githooks_desc = Pinapagana ng Git ang mga Git hook. Maari mong baguhin ang mga hook file sa ibaba para mag-set up ng mga custom na operasyon. +settings.webhook.delivery.success = May nadagdag na event sa delivery queue. Maaari magtagal ng ilang segundo bago makita sa delivery history. +settings.githooks_desc = Pinapagana ng Git ang mga Git hook. Maaari mong baguhin ang mga hook file sa ibaba para mag-set up ng mga custom na operasyon. settings.githook_name = Pangalan ng hook settings.githook_content = Nilalaman ng hook settings.update_githook = I-update ang hook @@ -2362,7 +2362,7 @@ settings.mirror_settings.docs.pull_mirror_instructions = Para mag-set up ng pull milestones.invalid_due_date_format = Kailangang "yyyy-mm-dd" na format ang takdang petsa. signing.wont_sign.nokey = Walang key ang instansya na ito para i-sign ang commit na ito. activity.title.releases_1 = %d paglabas -settings.mirror_settings.docs.more_information_if_disabled = Maari kang matuto pa tungkol sa mga push at pull na mirror dito: +settings.mirror_settings.docs.more_information_if_disabled = Maaari kang matuto pa tungkol sa mga push at pull na mirror dito: settings.branches.switch_default_branch = Magpalit ng default branch settings.convert_notices_1 = Ang operasyon na ito ay ico-covert ang mirror sa regular na repositoryo at hindi mababawi. settings.convert_fork_succeed = Na-convert na ang fork sa regular na repositoryo. @@ -2732,7 +2732,7 @@ settings.protect_protected_file_patterns = Mga pattern ng nakaprotektang file (h settings.update_protect_branch_success = Binago na ang branch protection rule na "%s". settings.remove_protected_branch_success = Tinanggal ang branch protection rule na "%s". settings.tags.protection.pattern = Pattern ng tag -settings.tags.protection.pattern.description = Maari kang gumamit ng iisang pangalan o glob pattern o regular expression para magtugma ng maraming tag. Magbasa pa sa guide ng mga nakaprotektang tag. +settings.tags.protection.pattern.description = Maaari kang gumamit ng iisang pangalan o glob pattern o regular expression para magtugma ng maraming tag. Magbasa pa sa guide ng mga nakaprotektang tag. settings.thread_id = ID ng thread settings.matrix.room_id = ID ng room diff.has_escaped = May mga nakatagong Unicode character ang linya na ito @@ -2746,7 +2746,7 @@ diff.bin = BIN settings.default_update_style_desc = Ang default na istilio na gagamitin sa pag-update ng mga hiling sa paghila na nalilipas sa base branch. pulls.sign_in_require = Mag-sign in para gumawa ng bagong hiling sa paghila. new_from_template = Gumamit ng template -new_from_template_description = Maari kang pumili ng umiiral na repository template sa instansya na ito at i-apply ang mga setting nito. +new_from_template_description = Maaari kang pumili ng umiiral na repository template sa instansya na ito at i-apply ang mga setting nito. new_advanced = Mga advanced na setting new_advanced_expand = I-click para i-expand auto_init_description = Simulan ang kasaysayan ng Git gamit ang README at opsyonal na magdagdag ng mga lisensya at .gitignore na file. @@ -2773,6 +2773,14 @@ comment.blocked_by_user = Hindi posible ang pagkomento dahil hinarang ka ng may- sync_fork.button = I-sync sync_fork.branch_behind_one = Ang branch na ito ay %[1]d commit sa likod ng %[2]s sync_fork.branch_behind_few = Ang branch na ito ay %[1]d mga commit sa likod ng %[2]s +settings.event_header_action = Mga event sa run ng aksyon +settings.event_action_failure = Pagkabigo +settings.event_action_failure_desc = Natapos ang action run bilang pagkabigo. +settings.event_action_recover = I-recover +settings.event_action_success = Matagumpay +settings.event_action_success_desc = Matagumpay na natapos ang Action Run. +settings.event_action_recover_desc = Matagumpay na natapos ang Action Run pagkatapos na nabigo ang huling Action Run sa katulad na workflow. +issues.filter_type.all_pull_requests = Lahat ng mga hiling sa paghila [search] commit_kind = Maghanap ng mga commit… @@ -3198,7 +3206,7 @@ self_check.database_collation_mismatch = Inaasahan ang database na gamitin ang c auths.oauth2_admin_group = Group claim value para sa mga tagapangasiwa. (Opsyonal - kinakailangan ang claim name sa itaas) auths.tip.facebook = Magrehistro ng bagong application sa %s at idagdag ang produktong "Facebook Login" users.restricted.description = Payagan lamang ang interaksyon sa mga repositoryo at organisasyon kung saan ang user ay dinagdag bilang tagatulong. Iniiwasan nito ang pag-access sa publikong repositoryo sa instansya na ito. -users.local_import.description = Payagan ang pag-import ng mga repositoryo mula sa local file system ng user. Maari itong maging isyu sa seguridad. +users.local_import.description = Payagan ang pag-import ng mga repositoryo mula sa local file system ng user. Maaari itong maging isyu sa seguridad. emails.delete = Burahin ang Email emails.deletion_success = Binura na ang email address. auths.oauth2_required_claim_value = Kinakailangan na claim value @@ -3443,8 +3451,8 @@ teams.owners_permission_desc = Ang mga owner ay may punong access sa lah teams.add_nonexistent_repo = Hindi pa umiiral ang repositoryo na sinusubukan mong idagdag. Mangyaring gawin iyan muna. teams.all_repositories = Lahat ng mga repositoryo teams.all_repositories_helper = Ang koponan ay may access sa lahat ng mga repositoryo. Ang pagpili nito ay idadagdag ang lahat ng mga umiiral na repositoryo sa koponan. -settings.change_orgname_redirect_prompt.with_cooldown.few = Magiging available ang lumang pangalan ng organisasyon sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw, maari mo pa ring ma-claim muli ang lumang pangalan sa panahon ng cooldown. -settings.change_orgname_redirect_prompt.with_cooldown.one = Magiging available ang lumang pangalan ng organisasyon sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw, maari mo pa ring ma-claim muli ang lumang pangalan ng panahon ng cooldown. +settings.change_orgname_redirect_prompt.with_cooldown.few = Magiging available ang lumang pangalan ng organisasyon sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw. Maaari mo pa ring ma-claim muli ang lumang pangalan sa panahon ng cooldown. +settings.change_orgname_redirect_prompt.with_cooldown.one = Magiging available ang lumang pangalan ng organisasyon sa lahat pagkatapos ng panahon ng cooldown ng %[1]d araw. Maaari mo pa ring ma-claim muli ang lumang pangalan ng panahon ng cooldown. [packages] @@ -3568,8 +3576,8 @@ npm.details.tag = Tag swift.install = Idagdag ang package sa iyong Package.swift na file: vagrant.install = Para magdagdag ng Vagrant box, patakbuhin ang sumusunod na command: settings.link = I-link ang package na ito sa repository -settings.link.select = Pumili ng Repositoryo -settings.link.button = I-update ang Link ng Repositoryo +settings.link.select = Pumili ng repositoryo +settings.link.button = I-update ang link ng repositoryo settings.link.error = Nabigong i-update ang link ng repositoryo. settings.delete = Burahin ang package owner.settings.cargo.initialize = I-initialize ang index @@ -3712,7 +3720,7 @@ runners.reset_registration_token = I-reset ang token ng pagrehistro runners.status.offline = Offline workflow.dispatch.invalid_input_type = Hindi wastong input type "%s". runners.task_list.commit = Commit -runners.task_list.done_at = Natapos Sa +runners.task_list.done_at = Natapos sa runners.reset_registration_token_success = Matagumpay na na-reset ang token ng pagrehistro ng runner workflow.dispatch.input_required = Kumailangan ng value para sa input na "%s". workflow.dispatch.warn_input_limit = Pinapakita lamang ang unang %d na mga input. @@ -3830,7 +3838,7 @@ deletion.success = Natanggal na ang lihim. deletion.failed = Nabigong tanggalin ang lihim. creation.failed = Nabigong idagdag ang lihim. deletion = Tanggalin ang lihim -creation = Idagdag ang Lihim +creation = Idagdag ang lihim description = Ang mga sikreto ay ipapasa sa ilang mga aksyon at hindi mababasa kung hindi. none = Wala pang mga sikreto sa ngayon. creation.name_placeholder = case-insensitive, alphanumeric character o underscore lamang, hindi dapat magsimula sa GITEA_ o GITHUB_ @@ -3844,7 +3852,7 @@ filepreview.truncated = Na-truncate ang preview filepreview.lines = Mga linya %[1]d hanggang %[2]d sa %[3]s [projects] -deleted.display_name = Binurang Proyekto +deleted.display_name = Binurang proyekto type-2.display_name = Proyekto ng repositoryo type-1.display_name = Indibidwal na proyekto type-3.display_name = Proyekto ng organisasyon diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c4b0aa832e..1cb7103bc0 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -9,7 +9,7 @@ sign_in_with_provider=Se connecter avec %s sign_in_or=ou sign_out=Déconnexion sign_up=S'inscrire -link_account=Lier un Compte +link_account=Lier un compte register=S'inscrire version=Version powered_by=Propulsé par %s @@ -1062,8 +1062,8 @@ language.localization_project = Aidez-nous à traduire Forgejo dans votre langue language.description = Cette langue sera enregistrée dans votre compte et utilisée comme langue par défaut après votre connexion. user_block_yourself = Vous ne pouvez pas vous bloquer vous même. pronouns_custom_label = Pronoms personnalisés -change_username_redirect_prompt.with_cooldown.one = L'ancien pseudonyme sera disponible pour n'importe qui après une période d'%[1]d jour, vous pouvez toujours réclamer votre ancien pseudonyme pendant cette période. -change_username_redirect_prompt.with_cooldown.few = L'ancien pseudonyme sera disponible pour n'importe qui après une période de %[1]d jours, vous pouvez toujours réclamer votre ancien pseudonyme pendant cette période. +change_username_redirect_prompt.with_cooldown.one = L'ancien pseudonyme sera disponible pour n'importe qui après une période d'%[1]d jour. Vous pouvez toujours réclamer votre ancien pseudonyme pendant cette période. +change_username_redirect_prompt.with_cooldown.few = L'ancien pseudonyme sera disponible pour n'importe qui après une période de %[1]d jours. Vous pouvez toujours réclamer votre ancien pseudonyme pendant cette période. quota.rule.exceeded = Dépassé regenerate_token = Régénérer access_token_regeneration = Régénérer le token d'accès @@ -1516,7 +1516,7 @@ issues.desc=Organiser les rapports de bug, les tâches et les jalons. issues.filter_assignees=Filtrer par assignation issues.filter_milestones=Filtrer le jalon issues.filter_projects=Filtrer par projet -issues.filter_labels=Filtrer par labels +issues.filter_labels=Filtrer par étiquettes issues.filter_reviewers=Filtrer par évaluateur issues.new=Nouveau ticket issues.new.title_empty=Le titre ne peut pas être vide @@ -1653,13 +1653,13 @@ issues.close_comment_issue=Fermer avec le commentaire issues.reopen_issue=Rouvrir issues.reopen_comment_issue=Réouvrir avec le commentaire issues.create_comment=Commenter -issues.closed_at=`a fermé ce ticket %[2]s.` -issues.reopened_at=`a rouvert ce ticket %[2]s.` -issues.commit_ref_at=`a référencé ce ticket depuis une révision %[2]s.` -issues.ref_issue_from=`a fait référence à %[4]s ce ticket %[2]s.` -issues.ref_pull_from=`a fait référence à cette demande d'ajout %[4]s %[2]s.` -issues.ref_closing_from=`a fait référence à une demande d'ajout %[4]s qui clora ce ticket, %[2]s.` -issues.ref_reopening_from=`a référencé une pull request %[4]s qui va ré-ouvrir ce ticket %[2]s` +issues.closed_at=`a fermé ce ticket %s` +issues.reopened_at=`a rouvert ce ticket %s` +issues.commit_ref_at=`a référencé ce ticket depuis une révision %s` +issues.ref_issue_from=`a fait référence à ce ticket %[3]s %[1]s` +issues.ref_pull_from=`a fait référence à cette demande d'ajout %[3]s %[1]s` +issues.ref_closing_from=`a fait référence à une demande d'ajout %[3]s qui clora ce ticket, %[1]s` +issues.ref_reopening_from=`a référencé ce ticket dans une pull request %[3]s qui va ré-ouvrir ce ticket, %[1]s` issues.ref_closed_from=`a fermé ce ticket %[4]s %[2]s` issues.ref_reopened_from=`a rouvert ce ticket %[4]s %[2]s.` issues.ref_from=`de %[1]s` @@ -1967,8 +1967,8 @@ pulls.update_branch_success=La mise à jour de la branche a réussi pulls.update_not_allowed=Vous n'êtes pas autorisé à mettre à jour la branche pulls.outdated_with_base_branch=Cette branche est désynchronisée avec la branche de base pulls.close=Fermer la demande d’ajout -pulls.closed_at=`a fermé cette demande d'ajout %[2]s.` -pulls.reopened_at=`a rouvert cette demande d'ajout %[2]s.` +pulls.closed_at=`a fermé cette demande d'ajout %s` +pulls.reopened_at=`a rouvert cette demande d'ajout %s` pulls.cmd_instruction_hint=Voir les instructions en ligne de commande pulls.cmd_instruction_checkout_title=Basculer pulls.cmd_instruction_checkout_desc=Depuis votre dépôt, basculer sur une nouvelle branche et tester des modifications. @@ -2044,7 +2044,7 @@ ext_wiki=Wiki externe ext_wiki.desc=Lier un wiki externe. wiki=Wiki -wiki.welcome=Bienvenue sur le Wiki. +wiki.welcome=Bienvenue sur le wiki. wiki.welcome_desc=Le wiki vous permet d'écrire ou de partager de la documentation avec vos collaborateurs. wiki.desc=Écrire et partager de la documentation avec vos collaborateurs. wiki.create_first_page=Créer la première page @@ -2762,7 +2762,7 @@ issues.blocked_by_user = Vous ne pouvez pas créer de tickets sur ce dépôt car pulls.blocked_by_user = Vous ne pouvez pas créer une pull request sur ce dépôt car vous êtes bloqué par son propriétaire. wiki.cancel = Annuler settings.wiki_globally_editable = Permettre l'édition du wiki a tout le monde -pulls.commit_ref_at = `a référencé cette pull request depuis le commit %[2]s` +pulls.commit_ref_at = `a référencé cette pull request depuis un commit %s` settings.new_owner_blocked_doer = Le nouveau propriétaire vous a bloqué. settings.enter_repo_name = Confirmez en entrant le propriétaire et le nom du dépôt exactement comme affiché : settings.wiki_rename_branch_main = Normalise le nom de la branche du Wiki @@ -2913,6 +2913,14 @@ pulls.editable_explanation = Cette pull request peut être éditée par les main sync_fork.branch_behind_one = Cette branche a %[1]d commits de retard sur %[2]s sync_fork.branch_behind_few = Cettte branche a %[1]d commits de retard sur %[2]s sync_fork.button = Sync +settings.event_action_failure = Échec +settings.event_action_recover = Récupérer +settings.event_action_success = Réussite +settings.event_header_action = Événements d'exécution d'action +settings.event_action_success_desc = L'exécution de l'action a réussi. +settings.event_action_failure_desc = L'exécution de l'action a échoué. +settings.event_action_recover_desc = L'exécution de l'action a réussi après l'échec de la dernière exécution de l'action dans le même workflow. +issues.filter_type.all_pull_requests = Toutes les demandes d'ajout [graphs] component_loading = Chargement %s… @@ -2921,7 +2929,7 @@ component_loading_failed = Échec de chargement de %s component_loading_info = Cela peut prendre du temps… component_failed_to_load = Une erreur inattendue s'est produite. contributors.what = contributions -code_frequency.what = fŕequence de code +code_frequency.what = fréquence de code recent_commits.what = commits récents @@ -3050,8 +3058,8 @@ teams.invite.by=Invité par %s teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe. follow_blocked_user = Vous ne pouvez pas suivre cette organisation car elle vous a bloqué. open_dashboard = Ouvrir le tableau de bord -settings.change_orgname_redirect_prompt.with_cooldown.few = L'ancien nom d'organisation sera disponible pour n'importe qui après une période de %[1]d jours, vous pouvez toujours réclamer votre ancien nom d'organisation pendant cette période. -settings.change_orgname_redirect_prompt.with_cooldown.one = L'ancien nom d'organisation sera disponible pour n'importe qui après une période d'%[1]d jour, vous pouvez toujours réclamer votre ancien nom d'organisation pendant cette période. +settings.change_orgname_redirect_prompt.with_cooldown.few = L'ancien nom d'organisation sera disponible pour n'importe qui après une période de %[1]d jours. Vous pouvez toujours réclamer votre ancien nom d'organisation pendant cette période. +settings.change_orgname_redirect_prompt.with_cooldown.one = L'ancien nom d'organisation sera disponible pour n'importe qui après une période d'%[1]d jour. Vous pouvez toujours réclamer votre ancien nom d'organisation pendant cette période. [admin] dashboard=Tableau de bord @@ -4004,7 +4012,7 @@ variables.not_found = La variable n'a pas été trouvée. type-1.display_name=Projet personnel type-2.display_name=Projet du dépôt type-3.display_name=Projet de l'organisation -deleted.display_name = Projet Supprimé +deleted.display_name = Projet supprimé [git.filemode] changed_filemode=%[1]s → %[2]s @@ -4083,4 +4091,4 @@ issues.write = Écrire : Fermer des tickets et gérer les métadonnées t pulls.read = Lire : Lire et créer des demandes de tirage. [translation_meta] -test = Ceci est une chaîne de test. Elle n'est pas affichée dans l'interface de Forgejo mais est utilisée à des fins de test. N'hésitez pas à entrer 'ok' pour gagner du temps (ou un fait amusant de votre choix) pour atteindre ce doux 100 % de complétion. :-) +test = Ceci est une chaîne de test. Elle n'est pas affichée dans l'interface de Forgejo mais est utilisée à des fins de test. N'hésitez pas à entrer 'ok' pour gagner du temps (ou un fait amusant de votre choix) pour atteindre ce difficile 100 % de complétion. :-) diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index d2d960b627..3bb06e8c21 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -1219,11 +1219,11 @@ issues.close_comment_issue = Dún le trácht issues.reopen_issue = Athoscail issues.reopen_comment_issue = Athoscail le trácht issues.create_comment = Trácht -issues.closed_at = `dhún an cheist seo %[2]s` -issues.reopened_at = `athoscail an t-eagrán seo %[2]s` -issues.commit_ref_at = `rinne tagairt don cheist seo ó ghealltanas %[2]s` -issues.ref_issue_from = `rinne dagairt don cheist seo %[4]s %[2]s` -issues.ref_pull_from = `rinne dagairt don iarratas tarraingthe seo %[4]s %[ 2]s` +issues.closed_at = `dhún an cheist seo %s` +issues.reopened_at = `athoscail an t-eagrán seo %s` +issues.commit_ref_at = `rinne tagairt don cheist seo ó ghealltanas %s` +issues.ref_issue_from = `rinne dagairt don cheist seo %[3]s %[1]s` +issues.ref_pull_from = `rinne dagairt don iarratas tarraingthe seo %[3]s %[1]s` issues.ref_closed_from = `dhún an cheist seo %[4]s %[2]s` issues.ref_reopened_from = `d'athoscail an eagrán seo %[4]s %[2]s` issues.ref_from = `ó %[1]s` @@ -1456,8 +1456,8 @@ pulls.update_branch_success = Bhí nuashonrú brainse rathúil pulls.update_not_allowed = Ní cheadaítear duit brainse a nuashonrú pulls.outdated_with_base_branch = Tá an brainse seo as dáta leis an mbunbhrainse pulls.close = Dún Iarratas Tarraing -pulls.closed_at = `dhún an t-iarratas tarraingthe seo %[2]s` -pulls.reopened_at = `athoscail an t-iarratas tarraingthe seo %[2]s` +pulls.closed_at = `dhún an t-iarratas tarraingthe seo %s` +pulls.reopened_at = `athoscail an t-iarratas tarraingthe seo %s` pulls.cmd_instruction_checkout_title = Seiceáil pulls.cmd_instruction_checkout_desc = Ó stór tionscadail, seiceáil brainse nua agus déan tástáil ar na hathruithe. pulls.cmd_instruction_merge_title = Cumaisc diff --git a/options/locale/locale_gl.ini b/options/locale/locale_gl.ini index 7730782a0a..3854b375af 100644 --- a/options/locale/locale_gl.ini +++ b/options/locale/locale_gl.ini @@ -190,6 +190,7 @@ table_modal.placeholder.header = Cabeceira link_modal.header = Engadir ligazón link_modal.url = Url link_modal.description = Descrición +link_modal.paste_reminder = Consello: Coa URL no portapapeis, podes pegala directamente no editor para crear unha ligazón. [search] @@ -225,6 +226,8 @@ app_desc = Um servizo Git autoxestionado e fácil de usar install = Fácil de instalar install_desc = Simplemente executa o binario para a túa plataforma, envíao con Docker ou consígueo empaquetado. license = Código aberto +lightweight_desc = Forgejo precisa duns requerimentos mínimos e pode funcionar nunha Raspberry Pi barata. Aforra enerxía na túa máquina! +lightweight = Lixeiro [error] occurred = Ocorreu un erro @@ -291,6 +294,7 @@ app_slogan = Slogan da instancia app_slogan_helper = Escribe o slogan da túa instancia aqui. Ou deixao baleiro para desabilitala. domain = Dominio do servidor ssh_port = Porto do servidor SSH +require_db_desc = Forgejo precisa MySQL, PostgreSQL, SQLite3 ou TiDB (protocolo MySQL). [repo] sync_fork.branch_behind_few = Esta rama ten %d achegas por detrás de %s diff --git a/options/locale/locale_he.ini b/options/locale/locale_he.ini index bff7682a95..19c4815277 100644 --- a/options/locale/locale_he.ini +++ b/options/locale/locale_he.ini @@ -663,4 +663,10 @@ issues.label_archived_filter = הצגת תוויות מהארכיון issues.label_archive_tooltip = תוויות בארכיון לא מוצעות בחיפוש על־בסיס תווית כברירת מחדל. [translation_meta] -test = ואהבת לרעך כמוך \ No newline at end of file +test = ואהבת לרעך כמוך + +[git.filemode] +executable_file = קובץ הרצה +directory = תיקיה +normal_file = קובץ רגיל +symbolic_link = קישור סמלי \ No newline at end of file diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 411bad835a..3e93ee8ba9 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -932,7 +932,7 @@ issues.close_comment_issue=Hozzászólás és lezárás issues.reopen_issue=Újranyitás issues.reopen_comment_issue=Hozzászólás és újranyitás issues.create_comment=Hozzászólás -issues.commit_ref_at=`hivatkozott erre a hibajegyre egy commit-ból %[2]s` +issues.commit_ref_at=`hivatkozott erre a hibajegyre egy commit-ból %s` issues.role.owner=Tulajdonos issues.role.member=Tag issues.re_request_review=Véleményezés újrakérése diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 673d1464b1..f1a392105e 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -796,7 +796,7 @@ issues.close_comment_issue=Komentar dan Tutup issues.reopen_issue=Buka kembali issues.reopen_comment_issue=Komentar dan Buka Kembali issues.create_comment=Komentar -issues.commit_ref_at=`merujuk masalah dari komit %[2]s` +issues.commit_ref_at=`merujuk masalah dari komit %s` issues.role.owner=Pemilik issues.role.member=Anggota issues.sign_in_require_desc=Masuk untuk bergabung dengan percakapan ini. diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 9b1d56fed9..baf8286923 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -805,8 +805,8 @@ issues.close_comment_issue=Senda ummæli og Loka issues.reopen_issue=Enduropna issues.reopen_comment_issue=Senda ummæli og Enduropna issues.create_comment=Senda Ummæli -issues.closed_at=`lokaði þessu vandamáli %[2]s` -issues.reopened_at=`enduropnaði þetta vandamál %[2]s` +issues.closed_at=`lokaði þessu vandamáli %s` +issues.reopened_at=`enduropnaði þetta vandamál %s` issues.ref_reopened_from=`enduropnaði þetta vandamál %[4]s %[2]s` issues.author=Höfundur issues.role.owner=Eigandi diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index c4083f0ce8..d46f709cde 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -8,7 +8,7 @@ sign_in=Accedi sign_in_or=o sign_out=Esci sign_up=Registrati -link_account=Collega Profilo +link_account=Collega profilo register=Registrati version=Versione powered_by=Gestito da %s @@ -54,7 +54,7 @@ mirror=Mirror new_repo=Nuovo repository new_migrate=Nuova migrazione new_mirror=Nuovo mirror -new_fork=Nuova derivazione +new_fork=Nuova biforcazione new_org=Nuova organizzazione new_project=Nuovo progetto manage_org=Gestisci le organizzazioni @@ -99,7 +99,7 @@ preview=Anteprima loading=Caricamento… error=Errore -error404=La pagina che stai cercando di raggiungere non esiste oppure non sei autorizzato a visualizzarla. +error404=La pagina che stai cercando di raggiungere non esiste, è stata rimossa oppure non sei autorizzato a visualizzarla. never=Mai @@ -143,12 +143,12 @@ confirm_delete_selected = Confermare l'eliminazione di tutti gli elementi selezi sign_in_with_provider = Accedi con %s new_project_column = Nuova colonna toggle_menu = Mostra/Nascondi menu -filter.not_fork = Non fork +filter.not_fork = Non biforcazioni filter = Filtro filter.clear = Rimuovi filtri filter.is_archived = Archiviato filter.not_archived = Non archiviato -filter.is_fork = Da fork +filter.is_fork = Biforcazioni filter.is_mirror = Mirror filter.not_mirror = Non mirror filter.is_template = Modelli @@ -209,6 +209,7 @@ table_modal.label.columns = Colonne link_modal.header = Aggiungi collegamento link_modal.url = Url link_modal.description = Descrizione +link_modal.paste_reminder = Suggerimento: se hai già copiato un URL negli appunti, puoi incollarlo direttamente nell’editor per creare un collegamento. [filter] string.asc = A - Z @@ -232,6 +233,7 @@ lightweight_desc=Forgejo ha requisiti minimi bassi e può funzionare su un econo license=Open Source license_desc=Ottieni Forgejo! Partecipa per contribuire a rendere questo progetto ancora più bello. Non aver paura di diventare collaborante! install_desc = Semplicemente avvia l'eseguibile per la tua piattaforma, distribuiscilo con Docker, oppure scarica il pacchetto. +platform_desc = È stato verificato che Forgejo è pienamente compatibile con sistemi operativi liberi, come Linux e FreeBSD, nonché con diverse architetture CPU. Scegli liberamente la piattaforma che preferisci! [install] install=Installazione @@ -396,12 +398,12 @@ go_to = Vai a search.type.tooltip = Tipo di ricerca search.fuzzy.tooltip = Includi anche i risultati che corrispondono parzialmente ai termini di ricerca code_search_results = Risultati di ricerca per "%s" -relevant_repositories_tooltip = I repositori derivati o che non hanno argomento, icona, né descrizione sono nascosti. +relevant_repositories_tooltip = I repositori che sono biforcazioni o che non hanno argomento, icona, né descrizione sono nascosti. relevant_repositories = Sono visibili solo i repositori pertinenti, mostra risultati non filtrati. search.match.tooltip = Includi solo risultati che combaciano perfettamente con i termini di ricerca stars_few = %d stelle -forks_one = %d fork -forks_few = %d fork +forks_one = %d biforcazioni +forks_few = %d biforcazioni stars_one = %d stella [auth] @@ -485,6 +487,8 @@ sign_in_openid = Procedi con OpenID hint_login = Hai già un'utenza? Accedi! hint_register = Non hai un'utenza? Registrati ora. sign_up_button = Registrati ora. +unauthorized_credentials = Le credenziali non sono corrette o sono scadute. Controlla il comando o vedi %s per maggiori informazioni +use_onetime_code = Usa un codice monouso [mail] view_it_on=Visualizza su %s @@ -680,6 +684,8 @@ Location = Posizione AccessToken = Token di accesso FullName = Nome e cognome To = Nome del ramo +email_domain_is_not_allowed = Il dominio dell'indirizzo email dell'utente %s è in conflitto con EMAIL_DOMAIN_ALLOWLIST o EMAIL_DOMAIN_BLOCKLIST. Assicurati di aver inserito correttamente l'indirizzo email. +username_claiming_cooldown = Il nome utente non può essere assegnato, poiché il periodo di attesa non è ancora terminato. Sarà disponibile il %[1]s. [user] @@ -723,6 +729,7 @@ followers.title.one = Seguace followers.title.few = Seguaci following.title.one = Seguito following.title.few = Osservato +public_activity.visibility_hint.self_private_profile = Poiché il tuo profilo è privato, la tua attività è visibile solo a te e agli amministratori dell'istanza. Configura. [settings] @@ -1045,7 +1052,7 @@ added_on = Aggiunto su %s additional_repo_units_hint = Suggerisci l'attivazione di unità aggiuntive nel repositorio update_hints = Aggiorna suggerimenti update_hints_success = I suggerimenti sono stati aggiornati. -additional_repo_units_hint_description = Mostra un pulsante "Aggiungi più sezioni..." per i repositori che non hanno tutte le sezioni disponibili aggiunte. +additional_repo_units_hint_description = Visualizza un suggerimento “Abilita altro” per i repositori che non hanno tutte le unità disponibili abilitate. hints = Suggerimenti pronouns = Pronomi pronouns_custom = Personalizzato @@ -1053,6 +1060,34 @@ pronouns_unspecified = Non specificato language.title = Lingua predefinita language.description = Questa lingua verrà salvata nella tua utenza e verrà usata come predefinita ogni volta che farai l'accesso. language.localization_project = Aiutaci a tradurre Forgejo nella tua lingua! Più informazioni. +quota.sizes.assets.attachments.all = Allegati +quota.rule.no_limit = Illimitato +quota.sizes.assets.attachments.releases = Allegati del rilascio +quota.rule.exceeded = Superato +regenerate_token = Rigenera +access_token_regeneration = Rigenera il token d'accesso +access_token_regeneration_desc = Rigenerare un token comporterà la revoca dell'accesso al tuo account per tutte le applicazioni che lo utilizzano. Questa operazione è irreversibile. Vuoi procedere? +regenerate_token_success = Il token è stato rigenerato. Le applicazioni che lo utilizzano non hanno più accesso alla tua utenza e devono essere aggiornate con il nuovo token. +user_block_yourself = Non puoi bloccare te stesso. +quota.applies_to_user = Le seguenti regole di quota si applicano al tuo account +quota.applies_to_org = Le seguenti regole di quota si applicano a questa organizzazione +quota.rule.exceeded.helper = La dimensione totale degli oggetti per questa regola ha superato la quota. +quota.sizes.all = Tutti +quota.sizes.repos.all = Repositori +quota.sizes.repos.public = Repositori pubblici +quota.sizes.repos.private = Repositori privati +quota.sizes.git.all = Contenuto git +quota.sizes.git.lfs = Git LFS +quota.sizes.assets.all = Risorse +quota.sizes.assets.attachments.issues = Allegati della segnalazione +quota.sizes.assets.artifacts = Artefatti +quota.sizes.assets.packages.all = Pacchetti +quota.sizes.wiki = Wiki +keep_pronouns_private = Mostra i pronomi solo agli utenti che hanno effettuato il login +keep_pronouns_private.description = Questa impostazione nasconderà i tuoi pronomi agli utenti non ancora autenticati. +storage_overview = Panoramica spazio di archiviazione +quota = Quota +change_username_redirect_prompt.with_cooldown.one = Il vecchio nome utente sarà disponibile per tutti dopo un periodo di protezione di %\[1]d giorni. Durante questo periodo di attesa potrai comunque tornare al vecchio nome utente. [repo] owner=Proprietario @@ -1067,10 +1102,10 @@ template_description=I modelli di repositori consentono allɜ utenti di generare visibility=Visibilità visibility_description=Solo il proprietario o i membri dell'organizzazione se hanno diritti, saranno in grado di vederlo. visibility_helper_forced=L'amministratorə del sito impone che i nuovi repositori siano privati. -visibility_fork_helper=(Questa modifica influenzerà la visibilità di tutti i fork.) +visibility_fork_helper=(Questa modifica influenzerà la visibilità di tutte le biforcazioni.) clone_helper=Hai bisogno di aiuto per la clonazione? Visita Help. fork_repo=Deriva repositorio -fork_from=Deriva da +fork_from=Biforcazione di already_forked=Hai già fatto il fork di %s fork_to_different_account=Fai Fork a un account diverso fork_visibility_helper=La visibilità di un repositorio derivato non può essere modificata. @@ -1514,13 +1549,13 @@ issues.close_comment_issue=Commenta e chiudi issues.reopen_issue=Riapri issues.reopen_comment_issue=Commenta e riapri issues.create_comment=Commento -issues.closed_at=`ha chiuso questa segnalazione %[2]s` -issues.reopened_at=`ha riaperto questa segnalazione %[2]s` -issues.commit_ref_at=`ha fatto riferimento a questa segnalazione dal commit %[2]s` -issues.ref_issue_from=`ha fatto riferimento a questa segnalazione %[4]s %[2]s` -issues.ref_pull_from=`ha fatto riferimento a questa richiesta di modifica %[4]s %[2]s` -issues.ref_closing_from=`ha fatto riferimento a questa segnalazione da una richiesta di modifica %[4]s che la chiuderà, %[2]s` -issues.ref_reopening_from=`ha fatto riferimento a questa segnalazione da una richiesta di modifica %[4]s che la riaprirà, %[2]s` +issues.closed_at=`ha chiuso questa segnalazione %s` +issues.reopened_at=`ha riaperto questa segnalazione %s` +issues.commit_ref_at=`ha fatto riferimento a questa segnalazione dal commit %s` +issues.ref_issue_from=`ha fatto riferimento a questa segnalazione %[3]s %[1]s` +issues.ref_pull_from=`ha fatto riferimento a questa richiesta di modifica %[3]s %[1]s` +issues.ref_closing_from=`ha fatto riferimento a questa segnalazione da una richiesta di modifica %[3]s che la chiuderà, %[1]s` +issues.ref_reopening_from=`ha fatto riferimento a questa segnalazione da una richiesta di modifica %[3]s che la riaprirà, %[1]s` issues.ref_closed_from=`chiuso questa segnalazione %[4]s %[2]s` issues.ref_reopened_from=`ha riaperto questa segnalazione %[4]s %[2]s` issues.ref_from=`da %[1]s` @@ -1718,7 +1753,7 @@ pulls.cannot_merge_work_in_progress=Questa richiesta di modifica è contrassegna pulls.still_in_progress=Ancora in corso? pulls.add_prefix=Aggiungi prefisso %s pulls.remove_prefix=Rimuovi il prefisso %s -pulls.data_broken=Questa richiesta di modifica è rovinata a causa di informazioni mancanti riguardo la derivazione. +pulls.data_broken=Questa richiesta di modifica non è valida a causa di informazioni mancanti sulla biforcazione. pulls.files_conflicted=Questa richiesta di modifica va in conflitto con il ramo di destinazione. pulls.is_checking=Verifica dei conflitti di fusione in corso. Riprova tra qualche istante. pulls.is_ancestor=Questo ramo è già incluso nel ramo di destinazione. Non c'è nulla da fondere. @@ -1776,8 +1811,8 @@ pulls.update_branch_rebase=Aggiorna il ramo per cambio base pulls.update_branch_success=Ramo aggiornato con successo pulls.update_not_allowed=Non ti è permesso aggiornare il ramo pulls.outdated_with_base_branch=Questo ramo non è aggiornato con il ramo di base -pulls.closed_at=`ha chiuso questa richiesta di modifica %[2]s` -pulls.reopened_at=`ha riaperto questa richiesta di modifica %[2]s` +pulls.closed_at=`ha chiuso questa richiesta di modifica %s` +pulls.reopened_at=`ha riaperto questa richiesta di modifica %s` pulls.auto_merge_button_when_succeed=(Quando i controlli sono superati) pulls.auto_merge_when_succeed=Unione automatica quando tutti i controlli sono superati @@ -2100,7 +2135,7 @@ settings.event_create_desc=Ramo o etichetta creati. settings.event_delete=Elimina settings.event_delete_desc=Ramo o etichetta eliminati. settings.event_fork=Deriva -settings.event_fork_desc=Repository derivato. +settings.event_fork_desc=Creata una biforcazione del repositorio. settings.event_wiki=Wiki settings.event_release=Release settings.event_release_desc=Release pubblicata, aggiornata o rimossa in una repository. @@ -2137,7 +2172,7 @@ settings.event_pull_request_sync_desc=Pull request sincronizzata. settings.event_package=Pacchetto settings.event_package_desc=Pacchetto creato o eliminato in un repository. settings.branch_filter=Filtro rami -settings.branch_filter_desc=Whitelist dei rami per gli eventi di spinta, creazione dei rami e cancellazione dei rami, specificati come modello globo. Se vuoto o *, gli eventi per tutti i rami sono segnalati. Vedi la documentazione %[2]s per la sintassi. Esempi: master, {master,release*}. +settings.branch_filter_desc=Filtro, scritto come pattern glob, da applicare ai rami per gli eventi di tipo immissione, creazione di rami e rimozione di rami. Se vuoto o *, vengono considerati tutti gli eventi di tutti i rami. Maggiori dettagli sulla sintassi presso %[2]s. Esempi: master, {master,release*}. settings.active=Attivo settings.active_helper=Le informazioni sugli eventi innescati saranno inviate a questo URL del webhook. settings.add_hook_success=Il webhook è stato aggiunto. @@ -2167,8 +2202,8 @@ settings.web_hook_name_packagist=Packagist settings.packagist_username=Nome utente Packagist settings.packagist_api_token=API token settings.packagist_package_url=Url pacchetto pacchetti -settings.deploy_keys=Dispiega chiavi -settings.add_deploy_key=Aggiungi chiave di dispiego +settings.deploy_keys=Chiavi di distribuzione +settings.add_deploy_key=Aggiungi chiave di distribuzione settings.deploy_key_desc=Le deploy key possiedono l'accesso solamente alla lettura di un repository. settings.is_writable=Abilita accesso scrittura settings.is_writable_info=Permetti a questa deploy key di pushare nella repository. @@ -2177,7 +2212,7 @@ settings.title=Titolo settings.deploy_key_content=Contenuto settings.key_been_used=Una deploy key con contenuto identico è già in uso. settings.key_name_used=Esiste già una deploy key con questo nome. -settings.deploy_key_deletion=Rimuovi chiave di dispiego +settings.deploy_key_deletion=Rimuovi chiave di distribuzione settings.deploy_key_deletion_desc=Rimuovere una chiave di distribuzione ne revocherà l'accesso a questo repository. Continuare? settings.deploy_key_deletion_success=La chiave di distribuzione è stata rimossa. settings.branches=Rami @@ -2620,7 +2655,7 @@ issues.filter_type.reviewed_by_you = Revisionati da te projects.edit_success = Il progetto "%s" è stato aggiornato. issues.keyword_search_unavailable = La ricerca per parola chiave non è attualmente disponibile. Contatta l'amministratore del sito. issues.role.collaborator_helper = Quest*utente è statə invitatə a collaborare al progetto. -pulls.commit_ref_at = `ha fatto riferimento a questa richiesta di modifica da un commit %[2]s` +pulls.commit_ref_at = `ha fatto riferimento a questa richiesta di modifica da un commit %s` settings.thread_id = ID della discussione release.title = Titolo del rilascio visibility_helper = Rendi il repositorio privato @@ -2660,7 +2695,7 @@ wiki.page_title = Titolo della pagina wiki.page_content = Contenuto della pagina settings.mirror_settings.pushed_repository = Repositorio immesso settings.mirror_settings.push_mirror.edit_sync_time = Modifica intervallo di sincronizzazione degli specchi -settings.units.units = Unità della repository +settings.units.units = Sezioni del repositorio settings.units.add_more = Aggiungi ancora... settings.wiki_globally_editable = Consenti a tutti di modificare la wiki settings.pull_mirror_sync_in_progress = Prelevando cambiamenti dal progetto remoto %s. @@ -2732,7 +2767,7 @@ pulls.merged_title_desc_one = ha fuso %[1]d commit da %[2]s in Accedi per creare una richiesta di modifica. +settings.mirror_settings.push_mirror.none_ssh = Nessuno +sync_fork.branch_behind_one = Questo ramo è indietro di %[1]d commit rispetto a %[2]s +sync_fork.branch_behind_few = Questo ramo è indietro di %[1]d commit rispetto a %[2]s +no_eol.text = Nessun fine linea +no_eol.tooltip = Questo file non contiene un carattere di fine linea finale. +milestones.filter_sort.name = Nome +settings.protect_new_rule = Crea una nuova regola di protezione dei rami +editor.commit_email = E-mail di commit +mirror_public_key = Chiave SSH pubblica +mirror_denied_combination = Non è possibile utilizzare contemporaneamente l'autenticazione tramite chiave pubblica e password. +release.type_attachment = Allegato +release.invalid_external_url = URL esterno invalido: "%s" +new_from_template = Utilizza un modello +new_from_template_description = Puoi selezionare un modello di repositorio esistente su questa istanza e applicare le sue impostazioni. +new_advanced = Impostazioni avanzate +new_advanced_expand = Clicca per espandere +summary_card_alt = Scheda riepilogativa del repository %s +issues.filter_sort.relevance = Rilevanza +issues.num_reviews_one = %d revisioni +issues.num_reviews_few = %d revisioni +issues.reaction.add = Aggiungi reazione +issues.reaction.alt_many = %[1] e altri %[2]d hanno reagito %[3]s. +issues.reaction.alt_remove = Rimuovi la reazione %[1]s dal commento. +issues.reaction.alt_add = Aggiungi la reazione %[1]s al commento. +issues.review.remove_review_requests = rimosso richieste di revisione per %\[1]s %\[2]s +comment.blocked_by_user = Non è possibile commentare perché sei stato bloccato dal proprietario del repositorio o dall'autore. +issues.summary_card_alt = Scheda riepilogativa di una segnalazione intitolata "%s" nel repositorio %s +pulls.delete_after_merge.head_branch.is_default = Il ramo head che desideri eliminare è il ramo predefinito e non può essere eliminato. +settings.event_action_success = Successo +settings.event_action_success_desc = L'esecuzione dell'azione è andata a buon fine. +diff.git-notes.remove-header = Rimuovi nota +diff.git-notes.remove-body = Questa nota verrà rimossa. +activity.commit = Attività di commit [graphs] contributors.what = contribuzioni @@ -2839,7 +2947,7 @@ team_name_helper=I nomi dei team devono essere brevi e semplici da ricordare. team_desc_helper=Descrivi lo scopo o il ruolo del team. team_access_desc=Accesso al repository team_permission_desc=Autorizzazione -team_unit_desc=Consenti l'accesso a sezioni di progetto +team_unit_desc=Consenti l'accesso alle sezioni del repositorio team_unit_disabled=(Disabilitato) form.create_org_not_allowed=Non disponi dell'autorizzazione per creare un organizzazione. @@ -3493,6 +3601,12 @@ config.cache_test_slow = Successo nel controllo della cache, ma la risposta è l config.app_slogan = Slogan dell'istanza auths.default_domain_name = Nome di dominio predefinito utilizzato per l'indirizzo e-mail users.restricted.description = Permetti di interagire solo con i repositori e le organizzazioni in cui l'utente è aggiuntə come collaborante. Ciò evita l'accesso ai repositori pubblici di quest'istanza. +emails.deletion_success = L'indirizzo e-mail è stato eliminato. +monitor.duration = Durata (s) +emails.delete_desc = Confermare l’eliminazione di questo indirizzo email? +emails.delete_primary_email_error = Non puoi eliminare la e-mail primaria. +emails.delete = Elimina e-mail +users.organization_creation.description = Abilita la creazione di nuove organizzazioni. [action] @@ -3737,6 +3851,31 @@ owner.settings.cargo.initialize.success = L'indice di Cargo è stato creato corr owner.settings.cargo.rebuild.no_index = Impossibile ricostruire, nessun indice è inizializzato. owner.settings.cargo.rebuild.description = La ricostruzione può essere utile se l'indice non è sincronizzato con i pacchetti Cargo conservati. npm.dependencies.bundle = Dipendenze raggruppate +arch.version.groups = Gruppo +arch.version.conflicts = Va in conflitto con +arch.version.depends = Dipende da +arch.version.makedepends = Dipendenze di build +arch.version.checkdepends = Dipendenze di controllo +arch.version.replaces = Sostituisce +arch.version.optdepends = Dipende opzionalmente da +arch.version.backup = Backup +search_in_external_registry = Cerca in %s +arch.version.provides = Fornisce +arch.pacman.conf = Aggiungi il server con la relativa distribuzione e architettura a /etc/pacman.conf: +alt.setup = Aggiungi il repositorio alla lista dei repositori in rete (seleziona l'architettura necessaria al posto di "_arch_"): +container.images.title = Immagini +arch.version.properties = Proprietà della versione +alt.registry.install = Per installare il pacchetto, esegui il comando seguente: +alt.install = Installa pacchetto +alt.registry = Configura questo registro dalla riga di comando: +arch.pacman.helper.gpg = Aggiungi il certificato a pacman: +arch.pacman.repo.multi = %s ha la stessa versione in diverse distribuzioni. +arch.pacman.repo.multi.item = Configurazione per %s +arch.pacman.sync = Sincronizza il paccketto con pacman: +arch.version.description = Descrizione +alt.repository = Informazioni del repositorio +alt.repository.architectures = Architetture +alt.repository.multiple_groups = Questo pacchetto è disponibile per più gruppi. [secrets] secrets = Segreti @@ -3834,7 +3973,7 @@ runs.empty_commit_message = (messaggio di commit vuoto) runs.no_runs = Il flusso di lavoro non è stato ancora eseguito. variables.creation.success = La variabile "%s" è stata aggiunta. variables.description = Le variabili saranno passate a determinate azioni e non possono essere lette altrimenti. -need_approval_desc = È necessaria l'approvazione per eseguire flussi di lavoro per richieste di modifica da derivazioni. +need_approval_desc = È necessaria l'approvazione per eseguire flussi di lavoro per richieste di modifica da biforcazioni. runs.no_workflows.documentation = Per ulteriori informazioni sulle Forgejo Actions vedi la documentazione. runs.no_workflows.quick_start = Non sai come iniziare con le Forgejo Actions? Vedi la guida rapida. runners.delete_runner_notice = Se un'attività è in esecuzione su questo esecutore sarà terminata ed etichettata fallito. Potrebbe rompere flussi di lavoro di costruzione. @@ -3848,6 +3987,8 @@ workflow.dispatch.invalid_input_type = Tipo ingresso "%s" non valido. workflow.dispatch.warn_input_limit = Visualizzati solo i primi %d ingressi. runs.no_job = Il flusso di lavoro deve contenere almeno un incarico workflow.dispatch.use_from = Usa flusso di lavoro da +variables.not_found = Non è stato possibile trovare la variabile. +runs.expire_log_message = I log sono stati eliminati in quanto troppo vecchi. @@ -3856,6 +3997,7 @@ workflow.dispatch.use_from = Usa flusso di lavoro da type-3.display_name = Progetto dell'organizzazione type-1.display_name = Progetto individuale type-2.display_name = Progetto +deleted.display_name = Progetto eliminato [git.filemode] symbolic_link=Link Simbolico @@ -3869,7 +4011,7 @@ changed_filemode = %[1]s → %[2]s [search] type_tooltip = Tipo ricerca -search = Cerca... +search = Cerca… fuzzy = Approssimativa match = Precisa org_kind = Cerca organizzazioni... @@ -3896,6 +4038,7 @@ milestone_kind = Ricerca tappe... regexp_tooltip = Interpreta i termini di ricerca come un'espressione regolare regexp = Espressione Regolare union_tooltip = Include i risultati che combaciano con una qualsiasi delle parole chiave separata da spazi +union = Parole chiavi [munits.data] gib = GiB @@ -3914,4 +4057,16 @@ filepreview.line = Linea %[1]d in %[2]s [repo.permissions] issues.write = Scrittura: Chiudere segnalazioni e gestire metadati come etichette, traguardi, assegnatarɜ, scadenze e dipendenze. -pulls.write = Scrittura: Chiudere richieste di modifica e gestire metadati come etichette, traguardi, assegnatarɜ, scadenze e dipendenze. \ No newline at end of file +pulls.write = Scrittura: Chiudere richieste di modifica e gestire metadati come etichette, traguardi, assegnatarɜ, scadenze e dipendenze. +releases.write = Scrittura: Può pubblicare, modificare ed eliminare rilasci e le risorse ad essi allegate. +code.write = Scrittura: Può aggiungere commit al repositorio, creare rami ed etichette. +wiki.read = Lettura: Può leggere la wiki integrata e la sua cronologia. +releases.read = Lettura: Può visualizzare e scaricare i rilasci. +projects.read = Lettura: Può accedere alle board di progetto del repositorio. +code.read = Lettura: Può accedere e clonare il codice del repositorio. +wiki.write = Scrittura: Può creare, aggiornare ed eliminare pagine nella wiki integrata. +issues.read = Lettura: Può leggere e creare segnalazioni e commenti. +pulls.read = Lettura: Può leggere e creare richieste di modifica. + +[translation_meta] +test = daje Roma \ No newline at end of file diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index d4d7024f5d..555f5c6a75 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1610,13 +1610,13 @@ issues.close_comment_issue=コメントしてクローズ issues.reopen_issue=再オープンする issues.reopen_comment_issue=コメントして再オープン issues.create_comment=コメントする -issues.closed_at=`がイシューをクローズ %[2]s` -issues.reopened_at=`がイシューを再オープン %[2]s` -issues.commit_ref_at=`がコミットでこのイシューを参照 %[2]s` -issues.ref_issue_from=`が%[4]s、このイシューを参照 %[2]s` -issues.ref_pull_from=`が%[4]s、このプルリクエストを参照 %[2]s` -issues.ref_closing_from=`が%[4]s、プルリクエストがこのイシューをクローズするよう参照 %[2]s` -issues.ref_reopening_from=`が%[4]s、プルリクエストがこのイシューを再オープンするよう参照 %[2]s` +issues.closed_at=`がイシューをクローズ %s` +issues.reopened_at=`がイシューを再オープン %s` +issues.commit_ref_at=`がコミットでこのイシューを参照 %s` +issues.ref_issue_from=`が%[3]s、このイシューを参照 %[1]s` +issues.ref_pull_from=`が%[3]s、このプルリクエストを参照 %[1]s` +issues.ref_closing_from=`が%[3]s、プルリクエストがこのイシューをクローズするよう参照 %[1]s` +issues.ref_reopening_from=`が%[3]s、プルリクエストがこのイシューを再オープンするよう参照 %[1]s` issues.ref_closed_from=`が%[4]s、このイシューをクローズ %[2]s` issues.ref_reopened_from=`が%[4]s、このイシューを再オープン %[2]s` issues.ref_from=` %[1]s にて` @@ -1923,8 +1923,8 @@ pulls.update_branch_success=ブランチの更新が成功しました pulls.update_not_allowed=ブランチを更新する権限がありません pulls.outdated_with_base_branch=このブランチはベースブランチに対して最新ではありません pulls.close=プルリクエストをクローズ -pulls.closed_at=`がプルリクエストをクローズ %[2]s` -pulls.reopened_at=`がプルリクエストを再オープン %[2]s` +pulls.closed_at=`がプルリクエストをクローズ %s` +pulls.reopened_at=`がプルリクエストを再オープン %s` pulls.cmd_instruction_hint=コマンドラインの手順を表示 pulls.cmd_instruction_checkout_title=チェックアウト pulls.cmd_instruction_checkout_desc=プロジェクトリポジトリから新しいブランチをチェックアウトし、変更内容をテストします。 @@ -2721,7 +2721,7 @@ settings.wiki_rename_branch_main = wikiのブランチ名を正規化する settings.wiki_rename_branch_main_desc = wikiによって内部的に使われているブランチ名を "%s" に変更します。これは恒久的で元に戻すことはできません。 contributors.contribution_type.additions = 追加 vendored = vendor済み -pulls.commit_ref_at = `このプルリクエストを言及するコミット %[2]s` +pulls.commit_ref_at = `このプルリクエストを言及するコミット %s` pulls.fast_forward_only_merge_pull_request = Fast-forwardのみ admin.manage_flags = フラグ管理 admin.update_flags = フラグを更新 diff --git a/options/locale/locale_jbo.ini b/options/locale/locale_jbo.ini new file mode 100644 index 0000000000..947bb298de --- /dev/null +++ b/options/locale/locale_jbo.ini @@ -0,0 +1,13 @@ + + + +[common] +home = zdani +dashboard = jitypalna +explore = sisku +help = se sidju +logo = se'isni +sign_in = co'a nerkla +sign_in_with_provider = co'a nerka sepi'o la .%s. +sign_out = co'a cliva +sign_up = co'a gumri \ No newline at end of file diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 91c7c02d64..be0400bea4 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -312,7 +312,7 @@ default_allow_create_organization=조직 생성 허용을 기본값으로 설정 default_allow_create_organization.description=신규 사용자에게 기본적으로 조직 생성 권한을 부여합니다. 이 옵션이 꺼져있다면, 관리자가 신규 사용자에게 조직 생성 권한을 부여해야합니다. default_enable_timetracking=시간 기록 기능을 기본적으로 사용 default_enable_timetracking.description=신규 저장소가 시간기록 기능을 기본적으로 사용할 수 있습니다. -no_reply_address=가려진 이메일 도메인 +no_reply_address=숨겨진 이메일 도메인 no_reply_address_helper=이메일을 가린 사용자에게 적용될 이메일 도메인입니다. 예를 들어, 사용자명 'joe'가 도메인'noreply.example.org'로 이메일을 가리면 Git에 'joe@noreply.example.org'로 로그인 하게 됩니다. db_schema_helper = 데이터베이스 기본값 ("공개")를 사용하려면 빈 칸으로 두세요. require_db_desc = Forgejo를 사용하려면 MySQL, PostgreSQL, SQLite3 또는 TiDB (MySQL 프로토콜) 이 설치되어 있어야 합니다. @@ -330,6 +330,12 @@ app_slogan_helper = 인스턴스의 슬로건을 입력하세요. 비워두면 reinstall_confirm_check_1 = app.ini의 SECRET_KEY로 암호화 되어있는 데이터를 잃을 수 있습니다: 2FA/OTP를 통해 로그인 할 수 없으며 & 미러가 제대로 작동하지 않게됩니다. app.ini 파일에 정확한 SECRET_KEY가 있는것이 확실하다면 체크하세요. run_user_helper = Forgejo를 구동하는 운영체제의 사용자명입니다. 이 사용자는 저장소 루트 경로에 접근권한이 있어야 합니다. reinstall_confirm_check_2 = 저장소와 설정에 재동기화가 요구될 수 있습니다. 이 박스에 체크하면 저장소의 훅과 authorized_key 들을 수동으로 재동기화해야 한다는 것을 인지한다는 것을 의미합니다. 저장소와 미러의 설정이 올바른지 확인하세요. +password_algorithm = 암호 해시 알고리즘 +enable_update_checker = 업데이트 확인 활성화 +secret_key_failed = 비밀 키 생성 실패: %v +env_config_keys = 환경 설정 +invalid_password_algorithm = 올바르지 않은 암호 해시 알고리즘 +invalid_db_table = 데이터베이스 테이블 "%s"이(가) 올바르지 않습니다: %v [home] uname_holder=사용자명 또는 이메일 주소 @@ -347,6 +353,8 @@ search_repos=저장소 찾기.. show_private=비공개 issues.in_your_repos=당신의 저장소에 +feed_of = "%s"의 피드 +filter = 다른 필터 [explore] repos=저장소 @@ -935,7 +943,7 @@ issues.close_comment_issue=클로즈 및 코멘트 issues.reopen_issue=다시 열기 issues.reopen_comment_issue=다시 오픈 및 코멘트 issues.create_comment=코멘트 -issues.commit_ref_at=` 커밋 %[2]s에서 이 이슈 언급` +issues.commit_ref_at=` 커밋 %s에서 이 이슈 언급` issues.role.owner=소유자 issues.role.member=멤버 issues.sign_in_require_desc=로그인하여 이 대화에 참여하세요. @@ -1370,7 +1378,7 @@ issues.closed_by_fake = %[2]s님이 %[1]s에 닫음 issues.new.closed_projects = 닫힌 프로젝트 pulls.merged_by_fake = %[2]s님이 %[1]s 병합함 issues.closed_by = %[3]s님이 %[1]s에 닫음 -issues.closed_at = `%[2]s`에 이 이슈를 닫음 +issues.closed_at = `%s`에 이 이슈를 닫음 issues.filter_milestone_closed = 닫힌 마일스톤 issues.opened_by_fake = %[2]s님이 %[1]s에 열음 issues.filter_project_none = 프로젝트 없음 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index a7604220e0..98baff217b 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -162,7 +162,7 @@ filter.not_archived = Nav arhivētas filter.is_fork = Atzarojumi filter.not_fork = Nav atzarojumi filter.is_mirror = Spoguļglabātavas -filter.public = Atklātas +filter.public = Publiskas filter.private = Privātas filter.clear = Notīrīt atlasi confirm_delete_artifact = Vai tiešām izdzēst artefaktu '%s'? @@ -374,9 +374,9 @@ show_only_archived=Attēlot tikai arhivētos show_only_unarchived=Attēlot tikai nearhivētos show_private=Privāts -show_both_private_public=Rāda gan atklātās, gan privātās +show_both_private_public=Rāda gan publiskās, gan privātās show_only_private=Attēlot tikai privātos -show_only_public=Attēlo tikai atklātās +show_only_public=Tiek rādītas tikai publiskās issues.in_your_repos=Manās glabātavās @@ -690,7 +690,7 @@ email_domain_is_not_allowed = Lietotāja e-pasta adreses %s domēna vārd change_avatar=Mainīt profila attēlu… joined_on=Pievienojās %s repositories=Glabātavas -activity=Atklāti notikumi +activity=Publiskas darbības followers_few=%d sekotāji starred=Izlasei pievienotās glabātavas watched=Vērotās glabātavas @@ -701,7 +701,7 @@ following_few=%d seko follow=Sekot unfollow=Pārtraukt sekot user_bio=Apraksts par sevi -disabled_public_activity=Šis lietotājs ir atspējojis darbību redzamību visiem. +disabled_public_activity=Šis lietotājs ir atspējojis darbību redzamību citiem. email_visibility.limited=E-pasta adrese ir redzama visiem autentificētajiem lietotājiem email_visibility.private=E-pasta adrese ir redzama tikai administratoriem show_on_map=Rādīt šo vietu kartē @@ -749,7 +749,7 @@ organization=Apvienības uid=UID webauthn=Divpakāpju pieteikšanās (drošības atslēgas) -public_profile=Visiem pieejamais profils +public_profile=Publiskais profils biography_placeholder=Pastāsti citiem mazliet par sevi! (Tiek atbalstīts Markdown) location_placeholder=Kopīgot savu aptuveno atrašanās vietu ar citiem profile_desc=Par Tevi @@ -940,8 +940,8 @@ access_token_deletion_confirm_action=Dzēst access_token_deletion_desc=Pilnvaras izdzēšana atsauks lietotņu, kas to izmanto, piekļuvi kontam. Šo darbību nevar atsaukt. Turpināt? delete_token_success=Pilnvara tika izdzēsta. Lietotnēm, kas to izmanto, vairs nav piekļuves kontam. repo_and_org_access=Glabātavas un apvienības piekļuve -permissions_public_only=Tikai atklātās -permissions_access_all=Visas (atklātās, privātās un ierobežotās) +permissions_public_only=Tikai publiskās +permissions_access_all=Visas (publiskās, privātās un ierobežotās) select_permissions=Atlasīt atļaujas permission_no_access=Nav piekļuves permission_read=Lasīt @@ -1035,14 +1035,14 @@ email_notifications.submit=Iestatīt e-pasta iestatījumus email_notifications.andyourown=Un manus paziņojumus visibility=Lietotāja redzamība -visibility.public=Atklāta +visibility.public=Publiska visibility.public_tooltip=Redzams ikvienam visibility.limited=Ierobežota visibility.limited_tooltip=Redzams tikai lietotājiem, kuri ir pieteikušies visibility.private=Privāta visibility.private_tooltip=Redzams tikai apvienību, kurās pievienojies, dalībniekiem change_password = Mainīt paroli -keep_activity_private.description = Tavas atklātās darbības būs redzamas tikai Tev un servera pārvaldītājiem. +keep_activity_private.description = Tavas publiskās darbības būs redzamas tikai Tev un servera pārvaldītājiem. update_hints = Atjaunināt norādes update_hints_success = Norādes tika atjauninātas. user_block_success = Lietotājs tika sekmīgi liegts. @@ -1083,7 +1083,7 @@ quota.applies_to_org = Uz apvienību attiecas zemāk esošās ierobežojuma kār quota.rule.no_limit = Neierobežots quota.sizes.all = Viss quota.sizes.repos.all = Glabātavas -quota.sizes.repos.public = Atklātās glabātavas +quota.sizes.repos.public = Publiskās glabātavas quota.sizes.repos.private = Privātās glabātavas regenerate_token = Izveidot no jauna access_token_regeneration = Izveidot piekļuves pilnvaru no jauna @@ -1195,7 +1195,7 @@ transfer.no_permission_to_accept=Nav atļaujas pieņemt šo nodošanu. transfer.no_permission_to_reject=Nav atļaujas noraidīt šo nodošanu. desc.private=Privāts -desc.public=Atklāts +desc.public=Publisks desc.template=Sagatave desc.internal=Iekšējs desc.archived=Arhivēts @@ -1464,7 +1464,7 @@ commit.cherry-pick-content=Atlasīt zaru, uz kuru izlasīt: commitstatus.error=Kļūda commitstatus.failure=Atteice commitstatus.pending=Nav iesūtīts -commitstatus.success=Pabeigts +commitstatus.success=Sekmīgs ext_issues=Ārēji pieteikumi ext_issues.desc=Saite uz ārējo problēmu sekotāju. @@ -1515,7 +1515,7 @@ issues.filter_assignees=Atlasīt pēc atbildīgajiem issues.filter_milestones=Atlasīt pēc atskaites punkta issues.filter_projects=Atlasīt pēc projekta issues.filter_labels=Atlasīt pēc iezīmes -issues.filter_reviewers=Atlasīt izskatītājus +issues.filter_reviewers=Atlasīt pēc izskatītājiem issues.new=Jauns pieteikums issues.new.title_empty=Nosaukums nevar būt tukšs issues.new.labels=Iezīmes @@ -1651,13 +1651,13 @@ issues.close_comment_issue=Aizvērt ar piebildi issues.reopen_issue=Atvērt atkārtoti issues.reopen_comment_issue=Atkārtoti atvērt ar piebildi issues.create_comment=Pievienot piebildi -issues.closed_at=`aizvēra šo pieteikumu %[2]s` -issues.reopened_at=`atkārtoti atvēra šo pieteikumu %[2]s` -issues.commit_ref_at=`atsaucās uz šo pieteikumu iesūtījumā %[2]s` -issues.ref_issue_from=`atsaucās uz šo pieteikumu %[4]s %[2]s` -issues.ref_pull_from=`atsaucās uz šo izmaiņu pieprasījumu %[4]s %[2]s` -issues.ref_closing_from=`atsaucās uz šo pieteikumu izmaiņu pieprasījumā %[4]s, kas aizvērs to, %[2]s` -issues.ref_reopening_from=`atsaucās uz šo pieteikumu izmaiņu pieprasījumā %[4]s, kas atkārtoti atvērs to, %[2]s` +issues.closed_at=`aizvēra šo pieteikumu %s` +issues.reopened_at=`atkārtoti atvēra šo pieteikumu %s` +issues.commit_ref_at=`atsaucās uz šo pieteikumu iesūtījumā %s` +issues.ref_issue_from=`atsaucās uz šo pieteikumu %[3]s %[1]s` +issues.ref_pull_from=`atsaucās uz šo izmaiņu pieprasījumu %[3]s %[1]s` +issues.ref_closing_from=`atsaucās uz šo pieteikumu izmaiņu pieprasījumā %[3]s, kas aizvērs to, %[1]s` +issues.ref_reopening_from=`atsaucās uz šo pieteikumu izmaiņu pieprasījumā %[3]s, kas atkārtoti atvērs to, %[1]s` issues.ref_closed_from=`aizvēra pieteikumu %[4]s %[2]s` issues.ref_reopened_from=`atkārtoti atvēra pieteikumu %[4]s %[2]s` issues.ref_from=`no %[1]s` @@ -1964,8 +1964,8 @@ pulls.update_branch_success=Zara atjaunināšana bija sekmīga pulls.update_not_allowed=Nav ļauts atjaunināt zaru pulls.outdated_with_base_branch=Šis zars ir novecojis salīdzinājumā ar pamata zaru pulls.close=Aizvērt izmaiņu pieprasījumu -pulls.closed_at=`aizvēra šo izmaiņu pieprasījumu %[2]s` -pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu %[2]s` +pulls.closed_at=`aizvēra šo izmaiņu pieprasījumu %s` +pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu %s` pulls.cmd_instruction_hint=Apskatīt komandrindas izmantošanas norādes pulls.cmd_instruction_checkout_title=Paņemt pulls.cmd_instruction_checkout_desc=Projekta glabātavā jāizveido jauns zars un jāpārbauda izmaiņas. @@ -2041,7 +2041,7 @@ ext_wiki=Ārēja vikivietne ext_wiki.desc=Ārējā vikivietne norāda uz ārējo vikivietnes adresi. wiki=Vikivietne -wiki.welcome=Laipni lūdzam vikivietnē. +wiki.welcome=Laipni lūdzam vikivietnē! wiki.welcome_desc=Vikivietne ļauj rakstīt un kopīgot dokumentāciju ar līdzdalībniekiem. wiki.desc=Dokumentācijas rakstīšana un kopīgošana ar līdzdalībniekiem. wiki.create_first_page=Izveidot pirmo lapu @@ -2769,7 +2769,7 @@ no_eol.text = Nav EOL size_format = %[1]s: %[2]s; %[3]s: %[4]s mirror_public_key = Publiskā SSH atslēga mirror_use_ssh.text = Izmantot SSH autentificēšanos -mirror_use_ssh.helper = Forgejo spoguļos glabātavu ar Git un SSH un izveidos atslēgu pāri, kad tiks atlasīta šī iespēja. Jānodrošina, ka izveidotais atslēgu pāris ir pilnvarots aizgādāt mērķa glabātavā. Nevarēs izmantot pilnvarošanu ar paroli, kad šis tiek atlasīts. +mirror_use_ssh.helper = Forgejo spoguļos glabātavu ar Git un SSH un izveidos atslēgu pāri, kad tiks atlasīta šī iespēja. Jānodrošina, ka izveidotā publiskāš atslēga ir pilnvarota aizgādāt mērķa glabātavā. Nevarēs izmantot pilnvarošanu ar paroli, kad šis tiek atlasīts. mirror_use_ssh.not_available = SSH autentificēšanās nav pieejama. mirror_denied_combination = Nevar izmantot autentificēšanos ar publiskās atslēgas un paroles apvienojumu. migrate.forgejo.description = Pārcelt datus no codeberg.org vai citiem Fogejo serveriem. @@ -2826,7 +2826,7 @@ issues.author.tooltip.pr = Šis lietotājs ir šī izmaiņu pieprasījuma izveid pulls.edit.already_changed = Neizdevās saglabāt izmaiņu pieprasījuma izmaiņas. Izskatās, ka saturu jau ir mainījis kāds cits lietotājs. Lūgums atsvaidzināt lapu un mēģināt labot vēlreiz, lai izvairītos no izmaiņu pārrakstīšanas pulls.blocked_by_user = Tu nevari izveidot izmaiņu pieprasījumu šajā glabātavā, jo tās īpašnieks ir Tevi liedzis. issues.all_title = Visi -pulls.commit_ref_at = ` atsaucāš uz šo izmaiņu pieprasījumu iesūtījumā %[2]s` +pulls.commit_ref_at = ` atsaucās uz šo izmaiņu pieprasījumu iesūtījumā %s` issues.num_participants_one = %d dalībnieks pulls.title_desc_one = vēlas iekļaut %[1]d iesūtījumu no %[2]s %[3]s issues.archived_label_description = (Arhivēts) %s @@ -2913,6 +2913,14 @@ comment.blocked_by_user = Piebilžu pievienošana nav iespējama, jo glabātavas sync_fork.branch_behind_one = Šis zars ir %[1]d iesūtījumu aiz %[2]s sync_fork.button = Sinhronizēt sync_fork.branch_behind_few = Šis zars ir %[1]d iesūtījumus aiz %[2]s +settings.event_action_failure = Kļūme +settings.event_action_failure_desc = Darbības izpilde beidzās ar kļūmi. +settings.event_header_action = Darbības izpildes notikumi +settings.event_action_recover = Atgūt +settings.event_action_recover_desc = Darbības izpilde bija sekmīga pēc kļūmes iepriekšējā darbības izpildē tajā pašā darbplūsmā. +settings.event_action_success = Sekmīgi +settings.event_action_success_desc = Darbības izpilde bija sekmīga. +issues.filter_type.all_pull_requests = Visi izmaiņu pieprasījumi [graphs] component_loading=Ielādē %s… @@ -2959,7 +2967,7 @@ settings.location=Atrašanās vieta settings.permission=Tiesības settings.repoadminchangeteam=Glabātavas pārvaldītājs var pievienot un noņemt komandu piekļuvi settings.visibility=Redzamība -settings.visibility.public=Atklāta +settings.visibility.public=Publiska settings.visibility.limited=Ierobežota (redzama tikai lietotājiem, kuri ir pieteikušies) settings.visibility.limited_shortname=Ierobežota settings.visibility.private=Privāta (redzama tikai apvienības dalībniekiem) @@ -3594,7 +3602,7 @@ self_check.database_collation_mismatch = Sagaidīt, ka datubāzē tiek izmantota self_check.database_fix_mysql = MySQL/MariaDB lietotāji var izmantot komandu "forgejo doctor convert", lai novērstu salīdzināšanas sarežģījumus, vai arī tos var pašrocīgi novērst ar "ALTER ... COLLATE ..." vaicājumiem. config.app_slogan = Servera sauklis config.allow_dots_in_usernames = Ļaut lietotājiem izmantot punktus savā lietotājvārdā. Neietekmē esošos kontus. -users.restricted.description = Ļaut mijiedarbību tikai ar glabātavām un apvienībām, kurās šis lietotājs ir pievienots kā līdzdalībnieks. Tas neļauj piekļūt šī servera atklātajām glabātavām. +users.restricted.description = Ļaut mijiedarbību tikai ar glabātavām un apvienībām, kurās šis lietotājs ir pievienots kā līdzdalībnieks. Tas neļauj piekļūt šī servera publiskajām glabātavām. dashboard.sync_tag.started = Uzsākta birku sinhronizēšana users.organization_creation.description = Ļaut jaunu apvienību izveidošanu. users.block.description = Liegt šī lietotāja mijiedarbību ar šo serveri caur tā kontu un neļaut pieteikšanos. @@ -3925,7 +3933,7 @@ runners.task_list.run=Izpildījums runners.task_list.status=Stāvoklis runners.task_list.repository=Glabātava runners.task_list.commit=Iesūtījums -runners.task_list.done_at=Beigu laiks +runners.task_list.done_at=Pabeigts runners.edit_runner=Labot izpildītāju runners.update_runner=Atjaunināt izmaiņas runners.update_runner_success=Izpildītājs sekmīgi atjaunināts diff --git a/options/locale/locale_nds.ini b/options/locale/locale_nds.ini index d059c85064..68fe899d6e 100644 --- a/options/locale/locale_nds.ini +++ b/options/locale/locale_nds.ini @@ -1347,7 +1347,7 @@ issues.change_title_at = `hett %[3]s de Titel vun %[1]s issues.change_ref_at = `hett %[3]s de Nömen vun %[1]s to %[2]s ännert` issues.delete_branch_at = `hett %[2]s de Twieg %[1]s lösket` issues.filter_label = Vermark -issues.filter_label_exclude = `Bruuk Alt+Klick/Enter, um Vermarkens uttosluten` +issues.filter_label_exclude = Bruuk Alt + Klick, um Vermarkens uttosluten issues.filter_label_no_select = All Vermarkens issues.filter_label_select_no_label = Keen Vermark issues.filter_milestone = Marksteen @@ -1434,12 +1434,12 @@ issues.comment_pull_merged_at = hett Kommitteren %[1]s in %[2]s %[3]s tosamenfö issues.close_comment_issue = Mit Kommentaar dichtmaken issues.reopen_comment_issue = Mit Kommentaar weer opmaken issues.create_comment = Kommenteren -issues.reopened_at = `hett deeses Gefall %[2]s weer opmaakt` +issues.reopened_at = `hett deeses Gefall %s weer opmaakt` issues.comment_manually_pull_merged_at = hett Kommitteren %[1]s in %[2]s %[3]s vun Hand tosamenföhrt issues.reopen_issue = Weer opmaken -issues.closed_at = `hett deeses Gefall %[2]s dichtmaakt` -issues.commit_ref_at = `hett deeses Gefall %[2]s vun eenem Kommitteren benöömt` -issues.ref_closing_from = `hett deeses Gefall vun eenem Haalvörslag, wat ’t %[4]s dichtmaken word, %[2]s benöömt` +issues.closed_at = `hett deeses Gefall %s dichtmaakt` +issues.commit_ref_at = `hett deeses Gefall %s vun eenem Kommitteren benöömt` +issues.ref_closing_from = `hett deeses Gefall %[1]s vun eenem Haalvörslag, wat ’t %[3]s dichtmaken word, benöömt` issues.ref_closed_from = `hett deeses Gefall %[4]s %[2]s dichtmaakt` issues.ref_reopened_from = `hett deeses Gefall %[4]s %[2]s weer opmaakt` issues.ref_from = `vun %[1]s` @@ -1477,12 +1477,12 @@ issues.label.filter_sort.reverse_alphabetically = Umdreiht na de Alphabeet issues.label.filter_sort.by_size = Lüttste Grött issues.num_participants_one = %d Mitmaker issues.num_participants_few = %d Mitmakers -issues.ref_pull_from = `hett deesen Haalvörslag %[4]s %[2]s benöömt` +issues.ref_pull_from = `hett deesen Haalvörslag %[3]s %[1]s benöömt` issues.label_title = Naam issues.label_archived_filter = Archiveert Vermarkens wiesen issues.archived_label_description = (Archiveert) %s -issues.ref_issue_from = `hett deeses Gefall %[4]s %[2]s benöömt` -issues.ref_reopening_from = `hett deeses Gefall vun eenem Haalvörslag, wat ’t %[4]s weer opmaken word, %[2]s benöömt` +issues.ref_issue_from = `hett deeses Gefall %[3]s %[1]s benöömt` +issues.ref_reopening_from = `hett deeses Gefall vun eenem Haalvörslag, wat ’t %[3]s weer opmaken word, %[1]s benöömt` issues.author.tooltip.issue = Deeser Bruker is de Autor vun deesem Gefall. issues.role.member_helper = Deeser Bruker is een Liddmaat vun de Vereenigung, wat de Eegner vun deesem Repositorium is. issues.role.collaborator_helper = Deeser Bruuker is inladen worden, in deesem Repositorium mittoarbeiden. @@ -1740,8 +1740,8 @@ pulls.status_checks_show_all = All Överprüfens wiesen pulls.update_branch_rebase = Twieg mit Umbaseren vernejen pulls.outdated_with_base_branch = De Twieg is tegen de Grund-Twieg verollt pulls.close = Haalvörslag dichtmaken -pulls.closed_at = `hett deesen Haalvörslag %[2]s dichtmaakt` -pulls.reopened_at = `hett deesen Haalvörslag %[2]s weer opmaakt` +pulls.closed_at = `hett deesen Haalvörslag %s dichtmaakt` +pulls.reopened_at = `hett deesen Haalvörslag %s weer opmaakt` pulls.cmd_instruction_hint = Wies Oorderreeg-Instruksjes pulls.cmd_instruction_checkout_title = Utchecken pulls.cmd_instruction_merge_title = Tosamenföhren @@ -1771,7 +1771,7 @@ milestones.deletion = Marksteen lösken pulls.has_merged = Fehlslagen: De Haalvörslag is tosamenföhrt worden, du kannst nich noch eenmaal tosamenföhren of de Enn-Twieg ännern. pulls.unrelated_histories = Tosamenföhren fehlslagen: De Tosamenföhrens-Kopp un -Grund hebben keene gemeensame Histoorje. Wenk: Versöök eene anner Tosamenföhrens-Aard pulls.update_not_allowed = Du düürst deesen Twieg nich vernejen -pulls.commit_ref_at = `hett deesen Haalvörslag %[2]s vun eenem Kommitteren benöömt` +pulls.commit_ref_at = `hett deesen Haalvörslag %s vun eenem Kommitteren benöömt` pulls.auto_merge_newly_scheduled = De Haalvörslag weer sett, sik tosamentoföhren, wenn all Överprüfens kumpleet sünd. milestones.clear = Leeg maken pulls.push_rejected_no_message = Schuven fehlslagen: Dat Schuven is sünner feerne Naricht oflehnt worden. Bidde överprüüf de Git-Hakens för deeses Repositorium @@ -2614,6 +2614,14 @@ archive.nocomment = Kommenteren gaht hier nich, denn dat Repositorium is archive sync_fork.button = Vernejen sync_fork.branch_behind_one = Deeser Twieg is %[1]d Kommitteren achter %[2]s sync_fork.branch_behind_few = Deeser Twieg is %[1]d Kommitterens achter %[2]s +settings.event_action_failure = Fehlslagen +settings.event_action_success = Daankregen +settings.event_action_success_desc = Aktioons-Loop is all daankregen worden. +settings.event_action_recover = Verhaalt +settings.event_header_action = Aktioons-Loop-Vörfallen +settings.event_action_failure_desc = Aktioons-Loop is as fehlslagen ennt. +settings.event_action_recover_desc = Aktioons-Loop is daankregen worden, nadeem de leste Aktioons-Loop in de sülven Warkwies fehlslagen is. +issues.filter_type.all_pull_requests = All Haalvörslagen [repo.permissions] code.read = Lesen: De Quelltext vun deesem Repositorium ankieken un klonen. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 68f84ee2df..48442bc39f 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -8,7 +8,7 @@ sign_in=Aanmelden sign_in_or=of sign_out=Uitloggen sign_up=Registreren -link_account=Account Koppelen +link_account=Account koppelen register=Registreren version=Versie powered_by=Mogelijk gemaakt door %s @@ -1554,13 +1554,13 @@ issues.close_comment_issue=Sluit met commentaar issues.reopen_issue=Heropen issues.reopen_comment_issue=Heropen met commentaar issues.create_comment=Reageer -issues.closed_at=`heeft dit probleem gesloten %[2]s` -issues.reopened_at=`heropende dit probleem %[2]s` -issues.commit_ref_at=`verwees naar dit probleem vanuit commit %[2]s'` -issues.ref_issue_from=`refereerde aan dit issue %[4]s %[2]s` -issues.ref_pull_from=`refereerde aan deze pull request %[4]s %[2]s` -issues.ref_closing_from=`verwees naar deze issue van een pull request %[4]s dat het zal sluiten, %[2]s` -issues.ref_reopening_from=`verwees naar een pull request %[4]s dat dit issue heropent %[2]s ` +issues.closed_at=`heeft dit probleem gesloten %s` +issues.reopened_at=`heropende dit probleem %s` +issues.commit_ref_at=`verwees naar dit probleem vanuit commit %s` +issues.ref_issue_from=`refereerde aan dit issue %[3]s %[1]s` +issues.ref_pull_from=`refereerde aan deze pull request %[3]s %[1]s` +issues.ref_closing_from=`verwees naar deze issue van een pull request %[3]s dat het zal sluiten, %[1]s` +issues.ref_reopening_from=`verwees naar een pull request %[3]s dat dit issue heropent %[1]s ` issues.ref_closed_from=`sloot dit issue %[4]s %[2]s` issues.ref_reopened_from=`heropende dit issue %[4]s %[2]s` issues.ref_from=`van %[1]s` @@ -1815,8 +1815,8 @@ pulls.update_branch_rebase=Update branch via herbaseren pulls.update_branch_success=Branch update is geslaagd pulls.update_not_allowed=Je hebt geen toestemming om branch bij te werken pulls.outdated_with_base_branch=Deze branch is verouderd met de basis branch -pulls.closed_at=`heeft deze pull request gesloten %[2]s` -pulls.reopened_at=`heropende deze pull request %[2]s` +pulls.closed_at=`heeft deze pull request gesloten %s` +pulls.reopened_at=`heropende deze pull request %s` pulls.auto_merge_button_when_succeed=(Bij geslaagde controles) pulls.auto_merge_when_succeed=Automatisch samenvoegen wanneer alle controles gelukt zijn @@ -2031,7 +2031,7 @@ settings.add_collaborator_success=De medewerker is toegevoegd. settings.add_collaborator_inactive_user=Kan geen inactieve gebruiker toevoegen als medewerker. settings.add_collaborator_duplicate=De collaborator is al toegevoegd aan deze repository. settings.delete_collaborator=Verwijder -settings.collaborator_deletion=Verwijder medewerker +settings.collaborator_deletion=Verwijder samenwerker settings.collaborator_deletion_desc=Het verwijderen van een collaborator zal hun toegang tot deze repository intrekken. Doorgaan? settings.remove_collaborator_success=De medewerker is verwijderd. settings.search_user_placeholder=Zoek gebruiker… @@ -2627,7 +2627,7 @@ projects.column.set_default_desc = Stel deze kolom in als standaard voor ongecat issues.action_check = Aanvinken/uitvinken issues.dependency.issue_batch_close_blocked = Het is niet mogelijk om de issues die u gekozen heeft in bulk te sluiten, omdat issue #%d nog open afhankelijkheden heeft pulls.review_only_possible_for_full_diff = Beoordeling is alleen mogelijk bij het bekijken van de volledige diff -pulls.commit_ref_at = `heeft naar deze pull request verwezen vanuit een commit %[2]s` +pulls.commit_ref_at = `heeft naar deze pull request verwezen vanuit een commit %s` pulls.cmd_instruction_hint = Bekijk opdrachtregelinstructies pulls.cmd_instruction_checkout_desc = Vanuit uw project repository, schakel over naar een nieuwe branch en test de veranderingen. pulls.showing_specified_commit_range = Alleen veranderingen weergeven tussen %[1]s..%[2]s @@ -3914,7 +3914,7 @@ runners.task_list.no_tasks = Er is nog geen taak. runners.labels = Labels runners.last_online = Laatste online tijd runners.task_list.status = Status -runners.task_list.done_at = Gedaan Op +runners.task_list.done_at = Gedaan op runners.id = ID runs.actor = Acteur actions = Actions diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 86a333a886..189e663618 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1460,13 +1460,13 @@ issues.close_comment_issue=Zamknij z komentarzem issues.reopen_issue=Otwórz ponownie issues.reopen_comment_issue=Otwórz ponownie z komentarzem issues.create_comment=Skomentuj -issues.closed_at=`zamknął(-ęła) to zgłoszenie %[2]s` -issues.reopened_at=`otworzył(-a) ponownie to zgłoszenie %[2]s` -issues.commit_ref_at=`wspomniał(-a) to zgłoszenie z commita %[2]s` -issues.ref_issue_from=`odwołał(-a) się do tego zgłoszenia %[4]s %[2]s` -issues.ref_pull_from=`odwołał(-a) się do tego Pull Requesta %[4]s %[2]s` -issues.ref_closing_from=`odwołał(-a) się do pull requesta %[4]s, który zamknie to zgłoszenie %[2]s` -issues.ref_reopening_from=`odwołał(-a) się z pull requesta %[4]s, który otworzy na nowo to zgłoszenie %[2]s` +issues.closed_at=`zamknął(-ęła) to zgłoszenie %s` +issues.reopened_at=`otworzył(-a) ponownie to zgłoszenie %s` +issues.commit_ref_at=`wspomniał(-a) to zgłoszenie z commita %s` +issues.ref_issue_from=`odwołał(-a) się do tego zgłoszenia %[3]s %[1]s` +issues.ref_pull_from=`odwołał(-a) się do tego Pull Requesta %[3]s %[1]s` +issues.ref_closing_from=`odwołał(-a) się do pull requesta %[3]s, który zamknie to zgłoszenie %[1]s` +issues.ref_reopening_from=`odwołał(-a) się z pull requesta %[3]s, który otworzy na nowo to zgłoszenie %[1]s` issues.ref_closed_from=`zamknął(-ęła) to zgłoszenie %[4]s %[2]s` issues.ref_reopened_from=`ponownie otworzył(-a) to zgłoszenie %[4]s %[2]s` issues.ref_from=`z %[1]s` @@ -1679,8 +1679,8 @@ pulls.update_branch_rebase=Aktualizuj branch przez rebase pulls.update_branch_success=Aktualizacja gałęzi powiodła się pulls.update_not_allowed=Nie masz uprawnień do aktualizacji gałęzi pulls.outdated_with_base_branch=Ta gałąź jest przestarzała w stosunku do gałęzi bazowej -pulls.closed_at=`zamknął(-ęła) ten pull request %[2]s` -pulls.reopened_at=`otworzył(-a) ponownie ten Pull Request %[2]s` +pulls.closed_at=`zamknął(-ęła) ten pull request %s` +pulls.reopened_at=`otworzył(-a) ponownie ten Pull Request %s` @@ -2643,7 +2643,7 @@ pulls.closed = Pull request zamknięty pulls.blocked_by_outdated_branch = Ten pull request jest zablokowany ponieważ jest przedawniony. pulls.blocked_by_changed_protected_files_1 = Ten pull request jest zablokowany ponieważ wprowadza zmiany do chronionego pliku: pulls.push_rejected_no_message = Wypchnięcie nie powiodło się: Wypchnięcie zostało odrzucone, ale nie otrzymano zdalnej wiadomości. Sprawdź hooki Git dla tego repozytorium.= -pulls.commit_ref_at = `odniósł się do tego pull requesta z commita %[2]s` +pulls.commit_ref_at = `odniósł się do tego pull requesta z commita %s` pulls.cmd_instruction_checkout_desc = Ze swojego repozytorium projektu, utwórz nową gałąź i przetestuj zmiany. pulls.clear_merge_message_hint = Wyczyszczenie wiadomości scalenia usunie tylko treść wiadomości commitu pozostawiając wygenerowane przez git dopiski takie jak "Co-Authored-By ...". pulls.delete_after_merge.head_branch.insufficient_branch = Nie masz uprawnień by usunąć head gałęzi. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index fdaccce641..8de0374eb2 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1063,8 +1063,8 @@ language.localization_project = Ajude-nos a traduzir Forgejo para o seu idioma! language.description = Essa língua será salva em sua conta e será usada como padrão após você iniciar a sessão. user_block_yourself = Você não pode se bloquear. pronouns_custom_label = Pronomes personalizados -change_username_redirect_prompt.with_cooldown.one = O nome de usuário antigo ficará disponível para qualquer pessoa após um período de espera de %[1]d dia, você ainda pode recuperar o nome de usuário antigo durante este período de espera. -change_username_redirect_prompt.with_cooldown.few = O nome de usuário antigo ficará disponível para qualquer pessoa após um período de espera de %[1]d dias, você ainda pode recuperar o nome de usuário antigo durante este período de espera. +change_username_redirect_prompt.with_cooldown.one = O nome de usuário antigo ficará disponível para qualquer pessoa após um período de proteção de %[1]d dia. Você ainda pode recuperar o nome de usuário antigo durante este período de proteção. +change_username_redirect_prompt.with_cooldown.few = O nome de usuário antigo ficará disponível para qualquer pessoa após um período de proteção de %[1]d dias. Você ainda pode recuperar o nome de usuário antigo durante este período de proteção. quota.applies_to_user = As seguintes regras de cota se aplicam à sua conta quota.rule.exceeded.helper = O tamanho total de objetos para esta regra excedeu a cota. keep_pronouns_private = Mostrar pronomes apenas para usuários autenticados @@ -1503,11 +1503,11 @@ projects.card_type.images_and_text=Imagens e texto projects.card_type.text_only=Somente texto issues.desc=Organize relatórios de bugs, tarefas e marcos. -issues.filter_assignees=Filtrar Atribuição -issues.filter_milestones=Filtrar Marco -issues.filter_projects=Filtrar Projeto -issues.filter_labels=Filtrar Rótulo -issues.filter_reviewers=Filtrar Revisor +issues.filter_assignees=Filtrar atribuição +issues.filter_milestones=Filtrar marco +issues.filter_projects=Filtrar projeto +issues.filter_labels=Filtrar rótulo +issues.filter_reviewers=Filtrar revisor issues.new=Novo issue issues.new.title_empty=Título não pode ser em branco issues.new.labels=Etiquetas @@ -1568,7 +1568,7 @@ issues.remove_ref_at=`removeu a referência %s %s` issues.add_ref_at=`adicionou a referência %s %s` issues.delete_branch_at=`excluiu branch %s %s` issues.filter_label=Etiqueta -issues.filter_label_exclude=`Use alt + clique/enter para excluir etiquetas` +issues.filter_label_exclude=Use Alt + Clique para excluir etiquetas issues.filter_label_no_select=Todas as etiquetas issues.filter_label_select_no_label=Sem etiqueta issues.filter_milestone=Marco @@ -1642,13 +1642,13 @@ issues.close_comment_issue=Comentar e fechar issues.reopen_issue=Reabrir issues.reopen_comment_issue=Comentar e reabrir issues.create_comment=Comentar -issues.closed_at=`fechou esta issue %[2]s` -issues.reopened_at=`reabriu esta issue %[2]s` -issues.commit_ref_at=`citou esta issue em um commit %[2]s` -issues.ref_issue_from=`referenciado esta issue %[4]s %[2]s` -issues.ref_pull_from=`referenciado este pull request %[4]s %[2]s` -issues.ref_closing_from=`referenciado esta issue de um pull request %[4]s que a fechará %[2]s` -issues.ref_reopening_from=`referenciado esta issue de um pull request %[4]s que a reabrirá %[2]s` +issues.closed_at=`fechou esta issue %s` +issues.reopened_at=`reabriu esta issue %s` +issues.commit_ref_at=`citou esta issue de um commit %s` +issues.ref_issue_from=`citou esta issue %[3]s %[1]s` +issues.ref_pull_from=`citou este pull request %[3]s %[1]s` +issues.ref_closing_from=`citou esta issue de um pull request %[3]s que a fechará %[1]s` +issues.ref_reopening_from=`citou esta issue de um pull request %[3]s que a reabrirá, %[1]s` issues.ref_closed_from=`fechou esta issue %[4]s %[2]s` issues.ref_reopened_from=`reabriu esta issue %[4]s %[2]s` issues.ref_from=`de %[1]s` @@ -1942,8 +1942,8 @@ pulls.update_branch_success=Atualização do branch foi bem-sucedida pulls.update_not_allowed=Você não tem permissão para atualizar o branch pulls.outdated_with_base_branch=Este branch está desatualizado com o branch base pulls.close=Fechar pull request -pulls.closed_at=`fechou este pull request %[2]s` -pulls.reopened_at=`reabriu este pull request %[2]s` +pulls.closed_at=`fechou este pull request %s` +pulls.reopened_at=`reabriu este pull request %s` pulls.clear_merge_message=Limpar mensagem do merge pulls.clear_merge_message_hint=Limpar a mensagem de merge só irá remover o conteúdo da mensagem de commit e manter trailers git gerados, como "Co-Authored-By …". @@ -2013,7 +2013,7 @@ ext_wiki=Wiki Externa ext_wiki.desc=Link para uma wiki externa. wiki=Wiki -wiki.welcome=Bem-vindo a wiki. +wiki.welcome=Bem-vindo à wiki. wiki.welcome_desc=A wiki permite que você escreva e compartilhe a documentação com os colaboradores. wiki.desc=Escrever e compartilhar a documentação com os colaboradores. wiki.create_first_page=Criar a primeira página @@ -2719,7 +2719,7 @@ issues.label_archive_tooltip = Etiquetas arquivadas não serão exibidas nas sug activity.navbar.pulse = Recente settings.units.overview = Geral settings.units.add_more = Habilitar mais -pulls.commit_ref_at = `referenciou este pedido de mesclagem no commit %[2]s` +pulls.commit_ref_at = `citou este pull request de um commit %s` pulls.cmd_instruction_merge_title = Mesclar settings.units.units = Unidades vendored = Externo @@ -2913,6 +2913,14 @@ comment.blocked_by_user = Não é possível comentar pois você foi bloqueado pe sync_fork.branch_behind_few = Este branch está %[1]d commits atrás de %[2]s sync_fork.branch_behind_one = Este branch está %[1]d commit atrás de %[2]s sync_fork.button = Sincronizar +settings.event_header_action = Eventos de execução de Actions +settings.event_action_failure = Falha +settings.event_action_failure_desc = Execução da Action terminou com falha. +settings.event_action_recover = Recuperar +settings.event_action_recover_desc = A execução da Action teve sucesso após a última execução no mesmo workflow ter falhado. +settings.event_action_success = Sucesso +settings.event_action_success_desc = A execução da Action foi bem sucedida. +issues.filter_type.all_pull_requests = Todos os pull requests [graphs] component_loading = Carregando %s… @@ -3049,8 +3057,8 @@ open_dashboard = Abrir painel settings.change_orgname_prompt = Obs.: Alterar o nome de uma organização resultará na alteração do URL dela e disponibilizará o nome antigo para uso. follow_blocked_user = Não foi possível seguir esta organização porque ela bloqueou-o(a). form.name_pattern_not_allowed = O padrão "%s" não é permitido no nome de uma organização. -settings.change_orgname_redirect_prompt.with_cooldown.one = O nome de organização antigo ficará disponível para qualquer pessoa após um período de proteção de %[1]d dia, você ainda pode recuperar o nome antigo durante este período de proteção. -settings.change_orgname_redirect_prompt.with_cooldown.few = O nome de organização antigo ficará disponível para qualquer pessoa após um período de espera de %[1]d dia, você ainda pode recuperar o nome antigo durante este período de espera. +settings.change_orgname_redirect_prompt.with_cooldown.one = O nome de organização antigo ficará disponível para qualquer pessoa após um período de proteção de %[1]d dia. Você ainda pode recuperar o nome antigo durante este período de proteção. +settings.change_orgname_redirect_prompt.with_cooldown.few = O nome de organização antigo ficará disponível para qualquer pessoa após um período de proteção de %[1]d dia. Você ainda pode recuperar o nome antigo durante este período de proteção. [admin] dashboard=Painel @@ -3808,8 +3816,8 @@ swift.install2=e execute o seguinte comando: vagrant.install=Para adicionar uma Vagrant box, execute o seguinte comando: settings.link=Vincular este pacote a um repositório settings.link.description=Se você vincular um pacote a um repositório, o pacote será listado na lista de pacotes do repositório. -settings.link.select=Selecionar Repositório -settings.link.button=Atualizar Link do Repositório +settings.link.select=Selecionar repositório +settings.link.button=Atualizar link do repositório settings.link.success=Link do repositório foi atualizado com sucesso. settings.link.error=Falha ao atualizar o link do repositório. settings.delete=Excluir o pacote @@ -4004,7 +4012,7 @@ variables.not_found = Não foi possível encontrar a variável. type-1.display_name=Projeto individual type-2.display_name=Projeto do repositório type-3.display_name=Projeto da organização -deleted.display_name = Projeto Apagado +deleted.display_name = Projeto apagado [git.filemode] symbolic_link=Ligação simbólica diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index df7af1b542..0e8f2d485e 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -9,7 +9,7 @@ sign_in_with_provider=Iniciar sessão com %s sign_in_or=ou sign_out=Terminar sessão sign_up=Fazer inscrição -link_account=Vincular conta +link_account=Associar conta register=Inscrição version=Versão powered_by=Implementado com %s @@ -20,18 +20,18 @@ notifications=Notificações active_stopwatch=Cronómetro em andamento tracked_time_summary=Resumo do tempo rastreado com base em filtros da lista de questões create_new=Criar… -user_profile_and_more=Perfil e configurações… +user_profile_and_more=Perfil e definições… signed_in_as=Sessão iniciada como enable_javascript=Este sítio Web requer JavaScript. toc=Índice licenses=Licenças -return_to_forgejo=Retornar ao Forgejo +return_to_forgejo=Voltar ao Forgejo username=Nome de utilizador email=Endereço de email password=Senha access_token=Código de acesso -re_type=Confirme a senha +re_type=Confirme a palavra-passe captcha=CAPTCHA twofa=Autenticação com dois passos twofa_scratch=Código de uso único em dois passos @@ -59,10 +59,10 @@ new_migrate=Nova migração new_mirror=Nova réplica new_fork=Nova derivação do repositório new_org=Nova organização -new_project=Novo planeamento +new_project=Novo projeto new_project_column=Nova coluna manage_org=Gerir organizações -admin_panel=Administração do sítio +admin_panel=Administração do site account_settings=Configurações da conta settings=Configurações your_profile=Perfil @@ -155,7 +155,7 @@ invalid_data = Dados inválidos: %v filter.clear = Retirar filtros filter.is_archived = Arquivado filter.not_template = Não modelos -toggle_menu = Comutar menu +toggle_menu = Alternar menu filter = Filtrar copy_generic = Copiar para a área de transferência test = Teste @@ -233,7 +233,7 @@ platform_desc=Está confirmado que Forgejo corre em sistemas operativos livres, lightweight=Leve lightweight_desc=Forgejo requer poucos recursos e pode correr num simples Raspberry Pi. Economize a energia da sua máquina! license=Código aberto -license_desc=Vá buscá-lo em Forgejo! Junte-se a nós dando a sua contribuição para tornar este programa ainda melhor. Não se acanhe e contribua! +license_desc=Todas as fontes estão disponíveis no Forgejo! Junte-se a nós e contribua para tornar este projeto ainda melhor. Não receie tornar-se colaborador! [install] install=Instalação @@ -291,7 +291,7 @@ smtp_port=Porto do SMTP smtp_from=Email do remetente smtp_from_helper=Endereço de email que o Forgejo vai usar. Insira um endereço de email simples ou use o formato "Nome" . mailer_user=Nome de utilizador do SMTP -mailer_password=Senha do SMTP +mailer_password=Palavra-passe do SMTP register_confirm=Exigir confirmação de email para se inscrever mail_notify=Habilitar notificações por email server_service_title=Configurações do servidor e de terceiros @@ -316,7 +316,7 @@ admin_setting.description=A criação de uma conta de administração é opciona admin_title=Configurações da conta de administração admin_name=Nome de utilizador do administrador admin_password=Senha -confirm_password=Confirme a senha +confirm_password=Confirme a palavra-passe admin_email=Endereço de email install_btn_confirm=Instalar Forgejo test_git_failed=Não foi possível testar o comando "git": %v @@ -339,7 +339,7 @@ default_enable_timetracking=Habilitar, por norma, a contagem do tempo default_enable_timetracking.description=Habilitar, por norma, a contagem do tempo nos novos repositórios. no_reply_address=Domínio dos emails ocultos no_reply_address_helper=Nome de domínio para utilizadores com um endereço de email oculto. Por exemplo, o nome de utilizador "silva" será registado no Git como "silva@semresposta.exemplo.org" se o domínio de email oculto estiver definido como "semresposta.exemplo.org". -password_algorithm=Algoritmo de Hash da Senha +password_algorithm=Algoritmo de Hash da palavra-passe invalid_password_algorithm=Algoritmo de hash da senha inválido password_algorithm_helper=Definir o algoritmo de hash da senha. Os algoritmos têm requisitos e resistência distintos. `argon2` é bastante seguro, mas usa muita memória e pode ser inapropriado para sistemas pequenos. enable_update_checker=Habilitar verificador de novidades @@ -415,7 +415,7 @@ disable_register_mail=A confirmação por email da inscrição está desabilitad manual_activation_only=Contacte o administrador para completar a habilitação. remember_me=Memorizar este dispositivo remember_me.compromised=O identificador da sessão já não é válido, o que pode indicar uma conta comprometida. Verifique se a sua conta apresenta operações pouco habituais. -forgot_password_title=Esqueci-me da senha +forgot_password_title=Esqueci-me da palavra-passe forgot_password=Esqueceu a sua senha? sign_up_now=Precisa de uma conta? Inscreva-se agora. sign_up_successful=A conta foi criada com sucesso. Bem-vindo/a! @@ -744,7 +744,7 @@ social=Contas sociais applications=Aplicações orgs=Organizações repos=Repositórios -delete=Eliminar a conta +delete=Eliminar conta twofa=Autenticação em dois passos (TOTP) account_link=Contas vinculadas organization=Organizações @@ -1060,7 +1060,7 @@ user_unblock_success = O utilizador foi desbloqueado com sucesso. language.title = Idioma predefinido keep_activity_private.description = O seu trabalho público apenas estará visível para si e para os administradores da instância. language.description = Este idioma vai ser guardado na sua conta e ser usado como o predefinido depois de iniciar sessão. -language.localization_project = Ajude-nos a traduzir o Forgejo para o seu idioma! Saiba mais. +language.localization_project = Ajude-nos a traduzir o Forgejo para o seu idioma! Ler mais. pronouns_custom_label = Pronomes personalizados user_block_yourself = Não se pode bloquear a si próprio. change_username_redirect_prompt.with_cooldown.one = O nome de utilizador antigo estará disponível para todos após um período de espera de %[1]d dia, podendo ainda reivindicar o nome de utilizador antigo durante o período de espera. @@ -1093,7 +1093,7 @@ regenerate_token = Regenerar access_token_regeneration_desc = A regeneração de um código irá revogar o acesso à sua conta para as aplicações que o utilizam. Isto não pode ser anulado. Continuar? [repo] -new_repo_helper=Um repositório contém todos os ficheiros do trabalho, incluindo o histórico das revisões. Já tem um hospedado noutro sítio? Migre o repositório. +new_repo_helper=Um repositório contém todos os ficheiros do projeto, incluindo o histórico das revisões. Já tem um hospedado noutro sítio? Migre o repositório. owner=Proprietário(a) owner_helper=Algumas organizações podem não aparecer na lista suspensa devido a um limite máximo de contagem de repositórios. repo_name=Nome do repositório @@ -1133,7 +1133,7 @@ issue_labels=Rótulos issue_labels_helper=Escolha um conjunto de rótulos license=Licença license_helper=Escolha um ficheiro de licença -license_helper_desc=Uma licença rege o que os outros podem, ou não, fazer com o seu código fonte. Não tem a certeza sobre qual a mais indicada para o seu trabalho? Veja: Escolher uma licença. +license_helper_desc=Uma licença rege o que os outros podem, ou não, fazer com o seu código fonte. Não tem a certeza sobre qual a mais indicada para o seu projeto? Veja: Escolher uma licença. object_format=Formato dos elementos object_format_helper=Formato dos elementos do repositório. Não poderá ser alterado mais tarde. SHA1 é o mais compatível. readme=README @@ -1479,23 +1479,23 @@ projects=Planeamentos projects.desc=Gerir questões e integrações nos quadros do planeamento. projects.description=Descrição (opcional) projects.description_placeholder=Descrição -projects.create=Criar planeamento +projects.create=Criar projeto projects.title=Título -projects.new=Novo planeamento +projects.new=Novo projeto projects.new_subheader=Coordene, acompanhe e modifique o seu trabalho num só lugar, para que os planeamentos se mantenham transparentes e cumpram o calendário. projects.create_success=O planeamento "%s" foi criado. -projects.deletion=Eliminar planeamento +projects.deletion=Eliminar projeto projects.deletion_desc=Eliminar um planeamento remove-o de todas as questões relacionadas. Continuar? projects.deletion_success=O planeamento foi eliminado. -projects.edit=Editar planeamentos +projects.edit=Editar projeto projects.edit_subheader=Planeamentos organizam questões e acompanham o progresso. -projects.modify=Editar planeamento +projects.modify=Editar projeto projects.edit_success=O planeamento "%s" foi modificado. projects.type.none=Nenhum projects.type.basic_kanban=Kanban básico projects.type.bug_triage=Triagem de erros projects.template.desc=Modelo -projects.template.desc_helper=Escolha um modelo de planeamento para começar +projects.template.desc_helper=Escolha um modelo de projeto para começar projects.type.uncategorized=Sem categoria projects.column.edit=Editar coluna projects.column.edit_title=Nome @@ -1507,7 +1507,7 @@ projects.column.set_default_desc=Definir esta coluna como a predefinida para que projects.column.unset_default=Deixar de ser a predefinida projects.column.unset_default_desc=Faz com que esta coluna deixe de ser a predefinida projects.column.delete=Eliminar coluna -projects.column.deletion_desc=Eliminar uma coluna de um planeamento faz com que todas as questões que nela constam sejam movidas para a coluna padrão. Continuar? +projects.column.deletion_desc=Eliminar uma coluna de um projeto faz com que todas as questões que nela constam sejam movidas para a coluna padrão. Continuar? projects.column.color=Colorido projects.open=Abrir projects.close=Fechar @@ -1519,7 +1519,7 @@ projects.card_type.text_only=Apenas texto issues.desc=Organize relatórios de erros, tarefas e etapas. issues.filter_assignees=Filtrar encarregado issues.filter_milestones=Filtrar etapa -issues.filter_projects=Filtrar planeamento +issues.filter_projects=Filtrar projeto issues.filter_labels=Filtrar rótulo issues.filter_reviewers=Filtrar revisor issues.new=Questão nova @@ -1530,8 +1530,8 @@ issues.new.clear_labels=Retirar rótulos issues.new.projects=Planeamentos issues.new.clear_projects=Limpar planeamentos issues.new.no_projects=Nenhum planeamento -issues.new.open_projects=Planeamentos abertos -issues.new.closed_projects=Planeamentos fechados +issues.new.open_projects=Projetos abertos +issues.new.closed_projects=Projetos fechados issues.new.no_items=Sem itens issues.new.milestone=Etapa issues.new.no_milestone=Sem etapa @@ -1657,13 +1657,13 @@ issues.close_comment_issue=Fechar com comentário issues.reopen_issue=Reabrir issues.reopen_comment_issue=Reabrir com comentário issues.create_comment=Comentar -issues.closed_at=`encerrou esta questão %[2]s` -issues.reopened_at=`reabriu esta questão %[2]s` -issues.commit_ref_at=`referenciou esta questão num cometimento %[2]s` -issues.ref_issue_from=`referiu esta questão %[4]s %[2]s` -issues.ref_pull_from=`referiu este pedido de integração %[4]s %[2]s` -issues.ref_closing_from=`referiu esta questão a partir de um pedido de integração %[4]s que a fechará %[2]s` -issues.ref_reopening_from=`referiu esta questão a partir de um pedido de integração %[4]s que a reabrirá %[2]s` +issues.closed_at=`encerrou esta questão %s` +issues.reopened_at=`reabriu esta questão %s` +issues.commit_ref_at=`referenciou esta questão num cometimento %s` +issues.ref_issue_from=`referiu esta questão %[3]s %[1]s` +issues.ref_pull_from=`referiu este pedido de integração %[3]s %[1]s` +issues.ref_closing_from=`referiu esta questão a partir de um pedido de integração %[3]s que a fechará %[1]s` +issues.ref_reopening_from=`referiu esta questão a partir de um pedido de integração %[3]s que a reabrirá %[1]s` issues.ref_closed_from=`encerrou esta questão %[4]s %[2]s` issues.ref_reopened_from=`reabriu esta questão %[4]s %[2]s` issues.ref_from=`de %[1]s` @@ -1972,8 +1972,8 @@ pulls.update_branch_success=A sincronização do ramo foi bem sucedida pulls.update_not_allowed=Não tem autorização para sincronizar o ramo pulls.outdated_with_base_branch=Este ramo é obsoleto em relação ao ramo base pulls.close=Encerrar pedido de integração -pulls.closed_at=`fechou este pedido de integração %[2]s` -pulls.reopened_at=`reabriu este pedido de integração %[2]s` +pulls.closed_at=`fechou este pedido de integração %s` +pulls.reopened_at=`reabriu este pedido de integração %s` pulls.cmd_instruction_hint=Ver instruções para a linha de comandos pulls.cmd_instruction_checkout_title=Conferir pulls.cmd_instruction_checkout_desc=No seu repositório, irá criar um novo ramo para que possa testar as modificações. @@ -2049,7 +2049,7 @@ ext_wiki=Wiki externo ext_wiki.desc=Ligação para um wiki externo. wiki=Wiki -wiki.welcome=Bem-vindo(a) ao Wiki. +wiki.welcome=Bem-vindo(a) à wiki. wiki.welcome_desc=O wiki permite escrever e partilhar documentação com os colaboradores. wiki.desc=Escrever e partilhar documentação com os colaboradores. wiki.create_first_page=Criar a primeira página @@ -2236,7 +2236,7 @@ settings.pulls.default_delete_branch_after_merge=Eliminar, por norma, o ramo do settings.pulls.default_allow_edits_from_maintainers=Permitir, por norma, que os responsáveis editem settings.releases_desc=Habilitar lançamentos no repositório settings.packages_desc=Habilitar o registo de pacotes do repositório -settings.projects_desc=Habilitar planeamentos no repositório +settings.projects_desc=Habilitar projetos no repositório settings.actions_desc=Habilitar sequências CI/CD integradas com Forgejo Actions settings.admin_settings=Configurações do administrador settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório @@ -2785,7 +2785,7 @@ settings.wiki_rename_branch_main_desc = Renomear o ramo usado internamente pelo settings.add_collaborator_blocked_our = Não foi possível adicionar o/a colaborador/a porque o/a proprietário/a do repositório bloqueou-os. settings.add_webhook.invalid_path = A localização não pode conter "." ou ".." ou ficar em branco. Não pode começar ou terminar com uma barra. settings.graphql_url = URL do GraphQL -pulls.commit_ref_at = `referiu este pedido de integração a partir de um cometimento %[2]s` +pulls.commit_ref_at = `referiu este pedido de integração a partir de um cometimento %s` settings.confirm_wiki_branch_rename = Renomear o ramo do wiki settings.wiki_branch_rename_success = O nome do ramo do wiki do repositório foi normalizado com sucesso. settings.wiki_branch_rename_failure = Falhou a normalização do nome do ramo do wiki do repositório. @@ -2834,7 +2834,7 @@ form.string_too_long = O texto fornecido é mais comprido do que %d caracteres. settings.federation_settings = Configurações da federação settings.federation_apapiurl = URL de federação deste repositório. Copie e cole nas configurações de federação de outro repositório como um URL de um repositório que está a ser seguido. issues.edit.already_changed = Não foi possível guardar as modificações desta questão. O conteúdo parece ter sido modificado por outro utilizador. Refresque a página e tente editar novamente para evitar sobrescrever as modificações que fizeram -project = Planeamentos +project = Projetos pulls.edit.already_changed = Não foi possível guardar as modificações do pedido de integração. O conteúdo parece ter sido modificado por outro utilizador. Refresque a página e tente editar novamente para evitar sobrescrever as modificações que fizeram subscribe.issue.guest.tooltip = Inicie sessão para subscrever esta questão. subscribe.pull.guest.tooltip = Inicie sessão para subscrever este pedido de integração. @@ -2915,6 +2915,13 @@ comment.blocked_by_user = Não é possível comentar porque está bloqueado pelo sync_fork.branch_behind_few = Este ramo está %[1]d cometimentos atrás de %[2]s sync_fork.button = Sincronizar sync_fork.branch_behind_one = Este ramo está %[1]d cometimento atrás de %[2]s +settings.event_action_failure = Falha +settings.event_action_failure_desc = A execução da ação terminou com falha. +settings.event_action_recover = Recuperar +settings.event_header_action = Eventos da execução de ações +settings.event_action_recover_desc = A execução de ação foi bem sucedida depois da última execução de ação na mesma sequência de trabalho ter falhado. +settings.event_action_success = Sucesso +settings.event_action_success_desc = A Execução de ação foi bem sucedida. [graphs] component_loading=A carregar %s… @@ -3811,7 +3818,7 @@ swift.install2=e execute o seguinte comando: vagrant.install=Para adicionar uma máquina virtual Vagrant, execute o seguinte comando: settings.link=Vincular este pacote a um repositório settings.link.description=Se você vincular um pacote a um repositório, o pacote será listado na lista de pacotes do repositório. -settings.link.select=Escolha o repositório +settings.link.select=Escolher repositório settings.link.button=Modificar vínculo ao repositório settings.link.success=O vínculo ao repositório foi modificado com sucesso. settings.link.error=Falhou a modificação do vínculo ao repositório. @@ -4002,10 +4009,10 @@ runs.no_workflows.help_write_access = Não sabe como começar com o Forgejo Acti variables.not_found = Não foi possível encontrar a variável. [projects] -type-1.display_name=Planeamento individual -type-2.display_name=Planeamento do repositório -type-3.display_name=Planeamento da organização -deleted.display_name = Planeamento eliminado +type-1.display_name=Projeto individual +type-2.display_name=Projeto do repositório +type-3.display_name=Projeto da organização +deleted.display_name = Projeto eliminado [git.filemode] changed_filemode=%[1]s → %[2]s @@ -4024,7 +4031,7 @@ code_search_by_git_grep = Os resultados da pesquisa no código-fonte neste momen no_results = Não foram encontrados resultados correspondentes. package_kind = Pesquisar pacotes… runner_kind = Pesquisar executores… -project_kind = Pesquisar planeamentos… +project_kind = Pesquisar projetos… branch_kind = Pesquisar ramos… commit_kind = Pesquisar cometimentos… search = Procurar… @@ -4068,8 +4075,8 @@ test = ok :) [repo.permissions] code.read = Ler: Aceder e clonar o código-fonte do repositório. releases.read = Ler: Ver e descarregar lançamentos. -projects.read = Ler: Aceder aos quadros de planeamento do repositório. -projects.write = Escrever: Criar planeamentos e colunas e editá-las. +projects.read = Ler: Aceder aos quadros de projeto do repositório. +projects.write = Escrever: Criar projetos e colunas e editá-las. packages.read = Ler: Ver e descarregar pacotes atribuídos ao repositório. packages.write = Escrever: Publicar e eliminar pacotes atribuídos ao repositório. actions.read = Ler: Ver sequências CI/CD integrados e os seus registos. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 8e310eaf28..2ef1b868d4 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1408,7 +1408,7 @@ editor.fail_to_update_file=Не удалось обновить/создать editor.fail_to_update_file_summary=Ошибка: editor.push_rejected_no_message=Изменение отклонено сервером без сообщения. Пожалуйста, проверьте Git-хуки. editor.push_rejected=Изменение отклонено сервером. Пожалуйста, проверьте Git-хуки. -editor.push_rejected_summary=Причина отклонения: +editor.push_rejected_summary=Полная причина отклонения: editor.add_subdir=Добавить каталог… editor.unable_to_upload_files=Не удалось загрузить файлы в «%s» из-за ошибки: %v editor.upload_file_is_locked=Файл «%s» заблокирован %s. @@ -1450,7 +1450,7 @@ commit.cherry-pick-content=Выбрать ветвь для переноса: commitstatus.error=Ошибка commitstatus.failure=Неудача commitstatus.pending=Ожидание -commitstatus.success=Успешно +commitstatus.success=Успех ext_issues=Внешние задачи ext_issues.desc=Ссылка на внешнюю систему отслеживания задач. @@ -1545,27 +1545,27 @@ issues.add_labels=добавлены метки %s %s issues.remove_label=удалил(а) метку %s %s issues.remove_labels=удалил(а) метки %s %s issues.add_remove_labels=добавлены метки %s и убраны метки %s %s -issues.add_milestone_at=`добавлено в этап %s %s` -issues.add_project_at=`добавлено в проект %s %s` -issues.change_milestone_at=`изменил(а) целевой этап с %s на %s %s` -issues.change_project_at=`изменил(а) проект с %s на %s %s` -issues.remove_milestone_at=`удалил(а) это из этапа %s %s` -issues.remove_project_at=`удалил(а) это из проекта %s %s` +issues.add_milestone_at=`добавление в этап %s %s` +issues.add_project_at=`добавление в проект %s %s` +issues.change_milestone_at=`этап изменён с %s на %s %s` +issues.change_project_at=`проект изменён с %s на %s %s` +issues.remove_milestone_at=`удаление из этапа %s %s` +issues.remove_project_at=`удаление из проекта %s %s` issues.deleted_milestone=`(удалено)` issues.deleted_project=`(удалено)` -issues.self_assign_at=`назначил(а) на себя %s` -issues.add_assignee_at=`был(а) назначен(а) %s %s` -issues.remove_assignee_at=`был снят с назначения %s %s` -issues.remove_self_assignment=`убрал(а) их назначение %s` -issues.change_title_at=`изменил(а) заголовок с %s на %s %s` -issues.change_ref_at=`изменил(а) ссылку с %s на %s %s` -issues.remove_ref_at=`убрал(а) ссылку %s %s` -issues.add_ref_at=`добавлена ссылка %s %s` +issues.self_assign_at=`назначение себя %s` +issues.add_assignee_at=`назначение %s %s` +issues.remove_assignee_at=`снятие с назначения %s %s` +issues.remove_self_assignment=`снято назначение с себя %s` +issues.change_title_at=`заголовок изменён с %s на %s %s` +issues.change_ref_at=`изменена ссылка с %s на %s %s` +issues.remove_ref_at=`убрана ссылка на %s %s` +issues.add_ref_at=`добавлена ссылка на %s %s` issues.delete_branch_at=`удалена ветвь %s %s` -issues.filter_label=Метка -issues.filter_label_exclude=`Используйте alt + click/enter, чтобы исключить метки` -issues.filter_label_no_select=Все метки -issues.filter_label_select_no_label=Нет метки +issues.filter_label=Метки +issues.filter_label_exclude=Исключайте метки с помощью Alt + ЛКМ +issues.filter_label_no_select=Любые метки +issues.filter_label_select_no_label=Без меток issues.filter_milestone=Этап issues.filter_milestone_all=Все этапы issues.filter_milestone_none=Нет этапов @@ -1637,13 +1637,13 @@ issues.close_comment_issue=Закрыть комментарием issues.reopen_issue=Открыть снова issues.reopen_comment_issue=Открыть снова комментарием issues.create_comment=Комментировать -issues.closed_at=`задача была закрыта %[2]s` -issues.reopened_at=`задача была открыта снова %[2]s` -issues.commit_ref_at=`упоминание этой задачи в коммите %[2]s` -issues.ref_issue_from=`упоминание этой задачи %[4]s %[2]s` -issues.ref_pull_from=`упоминание этого запроса слияния %[4]s %[2]s` -issues.ref_closing_from=`упоминание из запроса на слияние %[4]s, который закроет эту задачу %[2]s` -issues.ref_reopening_from=`упоминание из запроса на слияние %[4]s, который повторно откроет эту задачу %[2]s` +issues.closed_at=`задача была закрыта %s` +issues.reopened_at=`задача была открыта снова %s` +issues.commit_ref_at=`упоминание этой задачи в коммите %s` +issues.ref_issue_from=`упоминание этой задачи %[3]s %[1]s` +issues.ref_pull_from=`упоминание этого запроса слияния %[3]s %[1]s` +issues.ref_closing_from=`упоминание из запроса на слияние %[3]s, который закроет эту задачу %[1]s` +issues.ref_reopening_from=`упоминание из запроса на слияние %[3]s, который повторно откроет эту задачу %[1]s` issues.ref_closed_from=`закрыл этот запрос %[4]s %[2]s` issues.ref_reopened_from=`задача была открыта снова %[4]s %[2]s` issues.ref_from=`из %[1]s` @@ -1943,8 +1943,8 @@ pulls.update_branch_success=Ветвь успешно обновлена pulls.update_not_allowed=Недостаточно прав для обновления ветви pulls.outdated_with_base_branch=Эта ветвь отстает от базовой ветви pulls.close=Закрыть запрос слияния -pulls.closed_at=`закрыл этот запрос на слияние %[2]s` -pulls.reopened_at=`переоткрыл этот запрос на слияние %[2]s` +pulls.closed_at=`закрыл этот запрос на слияние %s` +pulls.reopened_at=`переоткрыл этот запрос на слияние %s` pulls.cmd_instruction_hint=Показать инструкции для командной строки pulls.cmd_instruction_merge_title=Слейте изменения pulls.cmd_instruction_merge_desc=Слейте изменения и отправьте их обратно. @@ -2772,7 +2772,7 @@ settings.ignore_stale_approvals = Игнорировать устаревшие contributors.contribution_type.additions = Добавления contributors.contribution_type.deletions = Удаления contributors.contribution_type.filter_label = Вид деятельности: -pulls.commit_ref_at = `упоминание этого запроса слияния в коммите %[2]s` +pulls.commit_ref_at = `сослался на этот запрос слияния в коммите %s` settings.thread_id = ИД обсуждения pulls.made_using_agit = AGit activity.navbar.contributors = Соавторы @@ -2916,6 +2916,14 @@ comment.blocked_by_user = Комментирование невозможно, sync_fork.branch_behind_few = Эта ветвь отстаёт от %[2]s на %[1]d коммитов sync_fork.button = Синхронизировать sync_fork.branch_behind_one = Эта ветвь отстаёт от %[2]s на %[1]d коммит +settings.event_header_action = События выполнений Действий +settings.event_action_failure = Неудача +settings.event_action_failure_desc = Выполнение завершилось неудачно. +settings.event_action_recover = Восстановлен +settings.event_action_recover_desc = После неудачи повторное выполнение рабочего потока было успешно. +settings.event_action_success = Успех +settings.event_action_success_desc = Выполнение завершилось успешно. +issues.filter_type.all_pull_requests = Все запросы на слияние [graphs] component_loading_failed = Не удалось загрузить %s @@ -3914,10 +3922,10 @@ unit.desc=Управление встроенными конвейерами CI/ status.unknown=Неизвестно status.waiting=Ожидает status.running=Запущено -status.success=Успешно +status.success=Успех status.failure=Неудача status.cancelled=Отменено -status.skipped=Пропущено +status.skipped=Пропущен status.blocked=Заблокировано runners=Исполнители @@ -3938,7 +3946,7 @@ runners.task_list.run=Запуск runners.task_list.status=Состояние runners.task_list.repository=Репозиторий runners.task_list.commit=Коммит -runners.task_list.done_at=Время завершения +runners.task_list.done_at=Завершено runners.edit_runner=Изменить исполнитель runners.update_runner=Обновить изменения runners.update_runner_success=Исполнитель успешно обновлён @@ -3960,7 +3968,7 @@ runners.reset_registration_token_success=Токен регистрации ис runs.all_workflows=Все рабочие потоки runs.commit=коммит runs.scheduled=Запланировано -runs.pushed_by=отправлено +runs.pushed_by=отправлен runs.invalid_workflow_helper=Файл конфигурации рабочего потока некорректен. Пожалуйста, проверьте конфигурационный файл: %s runs.actor=Автор runs.status=Состояние diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index d55b238b1c..54b0b246db 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -1100,12 +1100,12 @@ issues.close_comment_issue=අදහස් දක්වා වසන්න issues.reopen_issue=නැවත විවෘත කරන්න issues.reopen_comment_issue=අදහස් දක්වා විවෘත කරන්න issues.create_comment=අදහස -issues.closed_at=`මෙම ගැටළුව වසා %[2]s` -issues.reopened_at=`මෙම ගැටළුව නැවත විවෘත කරන ලදි %[2]s` -issues.ref_issue_from=`මෙම නිකුතුව %[4]s හි %[2]s` -issues.ref_pull_from=`මෙම අදින්න ඉල්ලීම%[4]s %[2]s` -issues.ref_closing_from=`මෙම ගැටළුව වසා දමනු ඇත%[4]s මෙම ගැටළුව %[2]s` -issues.ref_reopening_from=`මෙම ගැටළුව නැවත විවෘත කරනු ඇත%[4]s මෙම ගැටළුව %[2]s` +issues.closed_at=`මෙම ගැටළුව වසා %s` +issues.reopened_at=`මෙම ගැටළුව නැවත විවෘත කරන ලදි %s` +issues.ref_issue_from=`මෙම නිකුතුව %[3]s හි %[1]s` +issues.ref_pull_from=`මෙම අදින්න ඉල්ලීම%[3]s %[1]s` +issues.ref_closing_from=`මෙම ගැටළුව වසා දමනු ඇත%[3]s මෙම ගැටළුව %[1]s` +issues.ref_reopening_from=`මෙම ගැටළුව නැවත විවෘත කරනු ඇත%[3]s මෙම ගැටළුව %[1]s` issues.ref_closed_from=`මෙම නිකුතුව%[4]s %[2]s` issues.ref_reopened_from=`මෙම නිකුතුව%[4]s %[2]sනැවත විවෘත කරන ලදි` issues.ref_from=`හිම%[1]s` @@ -1342,8 +1342,8 @@ pulls.update_branch_rebase=රිබේස් මගින් ශාඛාව pulls.update_branch_success=ශාඛා යාවත්කාලීන කිරීම සාර්ථක විය pulls.update_not_allowed=ශාඛාව යාවත්කාලීන කිරීමට ඔබට අවසර නැත pulls.outdated_with_base_branch=මෙම ශාඛාව මූලික ශාඛාව සමඟ දිවයයි -pulls.closed_at=`මෙම අදින්න ඉල්ලීම වසා %[2]s` -pulls.reopened_at=`මෙම අදින්න ඉල්ලීම නැවත විවෘත කරන ලදි %[2]s` +pulls.closed_at=`මෙම අදින්න ඉල්ලීම වසා %s` +pulls.reopened_at=`මෙම අදින්න ඉල්ලීම නැවත විවෘත කරන ලදි %s` diff --git a/options/locale/locale_sr-SP.ini b/options/locale/locale_sr-SP.ini index 56c1a7e650..b14fdc1a35 100644 --- a/options/locale/locale_sr-SP.ini +++ b/options/locale/locale_sr-SP.ini @@ -326,7 +326,7 @@ issues.no_content=Још нема садржаја. issues.close_issue=Затвори issues.reopen_issue=Поново отвори issues.create_comment=Коментирај -issues.commit_ref_at=`поменуо овај задатак у комит %[2]s` +issues.commit_ref_at=`поменуо овај задатак у комит %s` issues.poster=Аутор issues.collaborator=Коаутор issues.owner=Власник diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index e2d82ee28b..8b43cb29b8 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -200,6 +200,10 @@ table_modal.placeholder.header = Sidhuvud table_modal.placeholder.content = Innehåll table_modal.label.rows = Rader table_modal.label.columns = Kolumner +buttons.switch_to_legacy.tooltip = Använd legacy-redigeraren istället +link_modal.url = Url +link_modal.description = Beskrivning +link_modal.header = Lägg till en länk [filter] string.asc = A - Ö @@ -328,6 +332,7 @@ invalid_app_data_path = Sökvägen för appdata är ogiltig: %v internal_token_failed = Misslyckades att generera intern token: %v password_algorithm = Hashalgoritm för lösenord invalid_password_algorithm = Ogiltig hashalgoritm för lösenord +env_config_keys_prompt = Följande miljövariabler kommer också att tillämpas på din konfigurationsfil: [home] uname_holder=Användarnamn eller e-postadress @@ -461,6 +466,41 @@ reply = eller svara på detta e-postmeddelande direkt hi_user_x = Hej %s, admin.new_user.user_info = Användarinformation admin.new_user.text = Vänligen klicka här för att hantera denna användare från administratörspanelen. +admin.new_user.subject = Ny användare %s har just registrerat sig +totp_disabled.no_2fa = Det finns inga andra 2FA-metoder konfigurerade längre, vilket innebär att det inte längre är nödvändigt att logga in på ditt konto med 2FA. +removed_security_key.text_1 = Säkerhetsnyckeln ”%[1]s” har just tagits bort från ditt konto. +repo.transfer.to_you = dig +repo.transfer.body = För att acceptera eller avvisa det, besök %s eller ignorera det helt enkelt. +removed_security_key.no_2fa = Det finns inga andra 2FA-metoder konfigurerade längre, vilket innebär att det inte längre är nödvändigt att logga in på ditt konto med 2FA. +release.note = Notera: +totp_enrolled.subject = Du har aktiverat TOTP som 2FA-metod +totp_enrolled.text_1.no_webauthn = Du har just aktiverat TOTP för ditt konto. Det innebär att du måste använda TOTP som 2FA-metod vid alla framtida inloggningar på ditt konto. +totp_enrolled.text_1.has_webauthn = Du har just aktiverat TOTP för ditt konto. Det innebär att du vid alla framtida inloggningar på ditt konto kan använda TOTP som 2FA-metod eller någon av dina säkerhetsnycklar. +link_not_working_do_paste = Fungerar inte länken? Prova att kopiera och klistra in den i webbläsarens adressfält. +primary_mail_change.text_1 = Den primära e-postadressen för ditt konto har just ändrats till %[1]s. Det innebär att denna e-postadress inte längre kommer att ta emot e-postmeddelanden för ditt konto. +totp_disabled.subject = TOTP har inaktiverats +totp_disabled.text_1 = Tidsbaserat engångslösenord (TOTP) på ditt konto har just inaktiverats. +account_security_caution.text_2 = Om detta inte var du, har ditt konto blivit kompromitterat. Kontakta administratören för denna webbplats. +account_security_caution.text_1 = Om detta var du, kan du tryggt ignorera detta meddelande. +activate_account.text_2 = Klicka på följande länk för att aktivera ditt konto inom %s: +activate_email.text = Klicka på följande länk för att verifiera din e-postadress inom %s: +register_notify.text_3 = Om någon annan har skapat det här kontot åt dig måste du först ställa in ditt lösenord. +issue.x_mentioned_you = @%s2 nämnde dig: +repo.collaborator.added.subject = %s har lagt till dig som medarbetare i %s +repo.collaborator.added.text = Du har lagts till som medarbetare i förrådet: +team_invite.subject = %[1]s har bjudit in dig att gå med i organisationen %[2]s +register_notify.text_1 = detta är din registreringsbekräftelse via e-post för %s! +release.downloads = Hämtningar: +release.download.zip = Källkod (ZIP) +release.download.targz = Källkod (TAR.GZ) +repo.transfer.subject_to = %s vill överföra förrådet ”%s” till %s +removed_security_key.subject = En säkerhetsnyckel har tagits bort +issue_assigned.pull = @%[1] har tilldelat dig pull-begäran %[2]s i förrådet %[3]s. +issue_assigned.issue = @%[1] har tilldelat dig ärendet %[2] i förrådet %[3]. +register_notify.text_2 = Du kan logga in på ditt konto med ditt användarnamn: %s +reset_password.text = Om detta var du, klicka på följande länk för att återställa ditt konto inom %s: +issue.action.force_push = %[1]s2 gjorde en force-push av %[2]s från %[3]s till %[4]s. +repo.transfer.subject_to_you = %s vill överföra förrådet ”%s” till dig @@ -544,6 +584,12 @@ auth_failed=Autentisering misslyckades: %v target_branch_not_exist=Målgrenen finns inte. +org_still_own_repo = Denna organisation äger fortfarande ett eller flera förråd, ta bort eller överför dem först. +must_use_public_key = Den nyckel du angav är en privat nyckel. Skicka inte upp din privata nyckel någonstans. Använd istället din publika nyckel. +unable_verify_ssh_key = SSH-nyckeln kan inte verifieras. Kontrollera att den är korrekt. +still_own_repo = Ditt konto äger ett eller flera förråd, ta bort eller överför dem först. +still_has_org = Ditt konto är medlem i en eller flera organisationer. Lämna dem först. +still_own_packages = Ditt konto har ett eller flera paket, ta bort dem först. [user] @@ -559,6 +605,13 @@ follow=Följ unfollow=Sluta följa user_bio=Biografi disabled_public_activity=Den här användaren har inaktiverat den publika synligheten av aktiviteten. +code = Kod +watched = Övervakade förråd +unblock = Avblockera +email_visibility.limited = Din e-postadress är synlig för alla autentiserade användare +show_on_map = Visa denna plats på en karta +settings = Användarinställningar +block = Blockera [settings] @@ -759,6 +812,17 @@ email_notifications.submit=Ställ in e-postpreferenser visibility.public=Offentlig visibility.private=Privat change_password = Byt lösenord +user_block_success = Användaren har blockerats. +blocked_since = Blockerad sedan %s +user_unblock_success = Användaren har blivit avblockerad. +visibility.limited = Begränsad +visibility.limited_tooltip = Synlig endast för inloggade användare +visibility.private_tooltip = Synlig endast för medlemmar i organisationer som du har gått med i +select_permissions = Välj behörigheter +permission_no_access = Ingen åtkomst +permission_write = Läs och skriv +user_block_yourself = Du kan inte blockera dig själv. +gpg_token_help = Du kan skapa en signatur med hjälp av: [repo] owner=Ägare @@ -1093,13 +1157,13 @@ issues.close_comment_issue=Stäng med kommentar issues.reopen_issue=Återöppna issues.reopen_comment_issue=Öppna igen med kommentar issues.create_comment=Kommentera -issues.closed_at=`stängde ärendet %[2]s` -issues.reopened_at=`återöppnade detta ärende %[2]s` -issues.commit_ref_at=`refererade till detta ärende från en incheckning %[2]s` -issues.ref_issue_from=`refererade till detta ärende %[4]s %[2]s` -issues.ref_pull_from=`refererade till denna pull-förfrågan %[4]s %[2]s` -issues.ref_closing_from=`hänvisade till detta ärende från en pull-förfrågan %[4]s som kommer att stänga det %[2]s` -issues.ref_reopening_from=`hänvisade till detta ärende från en pull-förfrågan %[4]s som kommer att öppna ärendet på nytt %[2]s` +issues.closed_at=`stängde ärendet %s` +issues.reopened_at=`återöppnade detta ärende %s` +issues.commit_ref_at=`refererade till detta ärende från en incheckning %s` +issues.ref_issue_from=`refererade till detta ärende %[3]s %[1]s` +issues.ref_pull_from=`refererade till denna pull-förfrågan %[3]s %[1]s` +issues.ref_closing_from=`hänvisade till detta ärende från en pull-förfrågan %[3]s som kommer att stänga det %[1]s` +issues.ref_reopening_from=`hänvisade till detta ärende från en pull-förfrågan %[3]s som kommer att öppna ärendet på nytt %[1]s` issues.ref_closed_from=`stängde detta ärende %[4]s %[2]s` issues.ref_reopened_from=`öpnnade detta ärende igen %[4]s %[2]s` issues.ref_from=`från %[1]s` @@ -1692,6 +1756,21 @@ topic.manage_topics=Hantera ämnen topic.done=Klar topic.count_prompt=Du kan inte välja fler än 25 ämnen settings.enter_repo_name = Ange ägar- och utvecklingskatalog-namnet exakt som det visas: +release = Utgåva +commitstatus.success = Lyckades +visibility_helper = Gör förrådet privat +download_bundle = Hämta BUNDLE +download_zip = Hämta ZIP +download_tar = Hämta TAR.GZ +repo_desc_helper = Ange kort beskrivning (valfritt) +all_branches = Alla grenar +fork_no_valid_owners = Detta förråd kan inte förgrenas eftersom det inte finns några giltiga ägare. +fork_to_different_account = Förgrena till ett annat konto +size_format = %[1]s: %[2]s, %[3]s: %[4]s +already_forked = Du har redan förgrenat %s +commitstatus.failure = Fel +ext_issues = Externa fel +open_with_editor = Öppna med %s @@ -2254,7 +2333,7 @@ project_kind = Sök projekt... search = Sök… type_tooltip = Söktyp team_kind = Sök lag... -org_kind = Sök organisationer... +org_kind = Sök organisationer… issue_kind = Sök ärenden... regexp_tooltip = Tolka söktermen som ett reguljärt uttryck code_search_unavailable = Kodsökning är för närvarande inte tillgänglig. Vänligen kontakta webbplatsadministratören. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 0945d64752..c07cefdab9 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -296,7 +296,7 @@ federated_avatar_lookup.description=Libravatar kullanarak federe profil resmi ar disable_registration=Kendi Kendine Kaydolmayı Devre Dışı Bırak disable_registration.description=Kullanıcının kendi kendine kaydolmasını devre dışı bırak. Yalnızca yöneticiler yeni hesaplar oluşturabilecek. allow_only_external_registration.description=Sadece belirlenen dış hizmetler aracılığıyla kullanıcı kaydına izin ver. -openid_signin=OpenID Oturum Açmayı Etkinleştiriniz +openid_signin=OpenID Oturum Açmayı Etkinleştir openid_signin.description=OpenID ile kullanıcı girişini etkinleştir. openid_signup=OpenID ile Kendi Kendine Kaydı Etkinleştir openid_signup.description=OpenID Tabanlı Kendi Kendi Kullanıcı Kaydını Etkinleştir. @@ -668,6 +668,7 @@ username_error_no_dots = ` sadece alfanumerik karakterler ("0-9","a-z","A-Z"), t unset_password = Oturum açma kullanıcısı parola belirlemedi. unsupported_login_type = Oturum açma türü hesap silmeyi desteklemiyor. +email_domain_is_not_allowed = Kullanıcı e-posta adresi %s alan adı EMAIL_DOMAIN_ALLOWLIST veya EMAIL_DOMAIN_BLOCKLIST ile çelişiyor. Lütfen işleminizin beklendiğinden emin olun. [user] change_avatar=Profil resmini değiştir… @@ -1593,13 +1594,13 @@ issues.close_comment_issue=Yorum Yap ve Kapat issues.reopen_issue=Yeniden aç issues.reopen_comment_issue=Yorum Yap ve Yeniden Aç issues.create_comment=Yorum yap -issues.closed_at=`%[2]s konusunu kapattı` -issues.reopened_at=`%[2]s konusunu yeniden açtı` -issues.commit_ref_at=`%[2]s işlemesinde bu konuyu işaret etti` -issues.ref_issue_from=`bu konuya referansta bulundu %[4]s %[2]s` -issues.ref_pull_from=`bu değişiklik isteğine referansta bulundu %[4]s %[2]s` -issues.ref_closing_from=`bir değişiklik isteğine referansta bulundu %[4]s bu konu kapatılacak %[2]s` -issues.ref_reopening_from=`bir değişiklik isteğine referansta bulundu %[4]s bu konu yeniden açılacak %[2]s` +issues.closed_at=`%s konusunu kapattı` +issues.reopened_at=`%s konusunu yeniden açtı` +issues.commit_ref_at=`%s işlemesinde bu konuyu işaret etti` +issues.ref_issue_from=`bu konuya referansta bulundu %[3]s %[1]s` +issues.ref_pull_from=`bu değişiklik isteğine referansta bulundu %[3]s %[1]s` +issues.ref_closing_from=`bir değişiklik isteğine referansta bulundu %[3]s bu konu kapatılacak %[1]s` +issues.ref_reopening_from=`bir değişiklik isteğine referansta bulundu %[3]s bu konu yeniden açılacak %[1]s` issues.ref_closed_from=`bu konuyu kapat%[4]s %[2]s` issues.ref_reopened_from=`konuyu yeniden aç%[4]s %[2]s` issues.ref_from=`%[1]s'den` @@ -1906,8 +1907,8 @@ pulls.update_branch_success=Dal güncellemesi başarıyla gerçekleştirildi pulls.update_not_allowed=Dalı güncelleme izniniz yok pulls.outdated_with_base_branch=Bu dal, temel dal ile güncel değil pulls.close=Değişiklik İsteğini Kapat -pulls.closed_at=`%[2]s değişiklik isteğini kapattı` -pulls.reopened_at=`%[2]s değişiklik isteğini yeniden açtı` +pulls.closed_at=`%s değişiklik isteğini kapattı` +pulls.reopened_at=`%s değişiklik isteğini yeniden açtı` pulls.cmd_instruction_hint=`Komut satırı talimatlarını görüntüleyin.` pulls.cmd_instruction_checkout_title=Çekme pulls.cmd_instruction_checkout_desc=Proje deponuzdan yeni bir dalı çekin ve değişiklikleri test edin. @@ -2163,7 +2164,7 @@ settings.pulls.allow_rebase_update=Değişiklik isteği dalının yeniden yapıl settings.pulls.default_delete_branch_after_merge=Varsayılan olarak birleştirmeden sonra değişiklik isteği dalını sil settings.pulls.default_allow_edits_from_maintainers=Bakımcıların düzenlemelerine izin ver settings.releases_desc=Depo Sürümlerini Etkinleştir -settings.packages_desc=Depo Paket Kütüğünü Etkinleştir +settings.packages_desc=Depo paket kütüğünü etkinleştir settings.projects_desc=Depo Projelerini Etkinleştir settings.actions_desc=Depo İşlemlerini Etkinleştir settings.admin_settings=Yönetici Ayarları @@ -3061,13 +3062,13 @@ packages.repository=Depo packages.size=Boyut packages.published=Yayınlandı -defaulthooks=Varsayılan Web İstemcileri -defaulthooks.desc=Web İstemcileri, belirli Forgejo olayları tetiklendiğinde otomatik olarak HTTP POST isteklerini sunucuya yapar. Burada tanımlanan Web İstemcileri varsayılandır ve tüm yeni depolara kopyalanır. web istemcileri kılavuzunda daha fazla bilgi edinin. +defaulthooks=Varsayılan web kancaları +defaulthooks.desc=Web Kancaları, belirli Forgejo olayları tetiklendiğinde otomatik olarak HTTP POST isteklerini sunucuya yapar. Burada tanımlanan Web kancaları varsayılandır ve tüm yeni depolara kopyalanır. web kancaları kılavuzunda daha fazla bilgi edinin. defaulthooks.add_webhook=Varsayılan Web İstemcisi Ekle defaulthooks.update_webhook=Varsayılan Web İstemcisini Güncelle -systemhooks=Sistem Web İstemcileri -systemhooks.desc=Belirli Forgejo olayları tetiklendiğinde Web istemcileri otomatik olarak bir sunucuya HTTP POST istekleri yapar. Burada tanımlanan web istemcileri sistemdeki tüm depolar üzerinde çalışır, bu yüzden lütfen bunun olabilecek tüm performans sonuçlarını göz önünde bulundurun. web istemcileri kılavuzunda daha fazla bilgi edinin. +systemhooks=Sistem web kancaları +systemhooks.desc=Belirli Forgejo olayları tetiklendiğinde Web kancaları otomatik olarak bir sunucuya HTTP POST istekleri yapar. Burada tanımlanan web kancaları sistemdeki tüm depolar üzerinde çalışır, bu yüzden lütfen bunun olabilecek tüm performans sonuçlarını göz önünde bulundurun. web kancaları kılavuzunda daha fazla bilgi edinin. systemhooks.add_webhook=Sistem Web İstemcisi Ekle systemhooks.update_webhook=Sistem Web İstemcisi Güncelle @@ -3241,7 +3242,7 @@ config.disable_register=Kullanıcı Kaydını Devre Dışı Bırak config.allow_only_internal_registration=Kayda Sadece Forgejo'nın Kendisi Üzerinden İzin Ver config.allow_only_external_registration=Sadece Dış Hizmetler Aracılığıyla Kullanıcı Kaydına İzin Ver config.enable_openid_signup=OpenID Kendinden Kaydı'nı Etkinleştir -config.enable_openid_signin=OpenID Oturum Açmayı Etkinleştiriniz +config.enable_openid_signin=OpenID Oturum Açmayı Etkinleştir config.show_registration_button=Kaydolma Düğmesini Göster config.require_sign_in_view=Sayfaları Görüntülemek için Giriş Yapmaya Zorla config.mail_notify=E-Posta Bildirimlerini Etkinleştir @@ -3492,7 +3493,7 @@ error.unit_not_allowed=Bu depo bölümüne erişme izniniz yok. title=Paketler desc=Depo paketlerini yönet. empty=Henüz hiçbir paket yok. -empty.documentation=Paket kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz. +empty.documentation=Paket deposu hakkında daha fazla bilgi için, belgeye bakabilirsiniz. empty.repo=Bir paket yüklediniz ama burada gösterilmiyor mu? Paket ayarlarına gidin ve bu depoya bağlantı verin. registry.documentation=%s kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz. filter.type=Tür @@ -3635,9 +3636,9 @@ owner.settings.cleanuprules.remove.days=Şundan eski sürümleri kaldır owner.settings.cleanuprules.remove.pattern=Eşleşen sürümlari kaldır owner.settings.cleanuprules.success.update=Temizleme kuralı güncellendi. owner.settings.cleanuprules.success.delete=Temizleme kuralı silindi. -owner.settings.chef.title=Chef Kütüğü +owner.settings.chef.title=Chef deposu owner.settings.chef.keypair=Anahtar çifti üret -owner.settings.chef.keypair.description=Chef kütüğünde kimlik doğrulaması için bir anahtar çifti gereklidir. Eğer daha önce bir anahtar çifti ürettiyseniz, yeni bir anahtar çifti üretmek eski anahtar çiftini ıskartaya çıkartacaktır. +owner.settings.chef.keypair.description=Chef kayıt defterine gönderilen istekler kimlik doğrulama yöntemi olarak kriptografik olarak imzalanmalıdır. Bir anahtar çifti oluştururken, yalnızca genel anahtar Forgejo'da saklanır. Özel anahtar size knife ile kullanılmak üzere sağlanır. Yeni bir anahtar çifti oluşturmak öncekini geçersiz kılar. npm.dependencies.bundle = Paketlenmiş Bağımlılıklar rpm.repository.multiple_groups = Bu paket birçok grupta mevcut. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 42773de48d..faa3f2a56e 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -694,7 +694,7 @@ disabled_public_activity=Цей користувач вимкнув публіч joined_on = Реєстрація %s email_visibility.private = Ваш email видно лише вам і адміністраторам email_visibility.limited = Вашу е-пошту видно всім авторизованим -settings = Користувацькі параметри +settings = Користувацькі налаштування block_user.detail_3 = Ви не зможете додати один одного в якості співавтора репозиторію. show_on_map = Показати це місце на мапі block_user.detail_2 = Цей користувач не зможе взаємодіяти з репозиторіями, власником яких є ви, а також із задачами та коментарями, які ви створили. @@ -1447,7 +1447,7 @@ issues.remove_ref_at=`видалив посилання %s %s` issues.add_ref_at=`додав посилання %s %s` issues.delete_branch_at=`видалена гілка %s %s` issues.filter_label=Мітка -issues.filter_label_exclude=`Використовуйте Alt + клік/Enter для виключення міток` +issues.filter_label_exclude=Використовуйте Alt + клік для виключення міток issues.filter_label_no_select=Всі мітки issues.filter_milestone=Етап issues.filter_project=Проєкт @@ -1496,17 +1496,17 @@ issues.context.quote_reply=Цитувати відповідь issues.context.reference_issue=Послатися в новій задачі issues.context.edit=Редагувати issues.context.delete=Видалити -issues.close_comment_issue=Прокоментувати і закрити +issues.close_comment_issue=Закрити з коментарем issues.reopen_issue=Відкрити знову -issues.reopen_comment_issue=Прокоментувати та відкрити знову +issues.reopen_comment_issue=Відкрити знову з коментарем issues.create_comment=Коментар -issues.closed_at=`закрив цю задачу %[2]s` -issues.reopened_at=`повторно відкрив цю задачу %[2]s` -issues.commit_ref_at=`згадано цю задачу в коміті %[2]s` -issues.ref_issue_from=`посилається на цю задачу %[4]s %[2]s` -issues.ref_pull_from=`послався на цей запит злиття %[4]s %[2]s` -issues.ref_closing_from=`згадав запит на злиття %[4]s, які закриють цю задачу %[2]s` -issues.ref_reopening_from=`згадав запит на злиття %[4]s, які повторно відкриють цю задачу %[2]s` +issues.closed_at=`закриває цю задачу %s` +issues.reopened_at=`повторно відкриває цю задачу %s` +issues.commit_ref_at=`посилається на цю задачу в коміті %s` +issues.ref_issue_from=`посилається на цю задачу %[3]s %[1]s` +issues.ref_pull_from=`посилається на цей запит злиття %[3]s %[1]s` +issues.ref_closing_from=`посилається в запиті на злиття %[3]s, який закриє цю задачу, %[1]s` +issues.ref_reopening_from=`посилається в запиті на злиття %[3]s, який повторно відкриє цю задачу, %[1]s` issues.ref_closed_from=`закрив цю задачу %[4]s %[2]s` issues.ref_reopened_from=`повторно відкрито цю задачу %[4]s %[2]s` issues.ref_from=`із %[1]s` @@ -1743,8 +1743,8 @@ pulls.update_branch_rebase=Оновити гілку перебазування pulls.update_branch_success=Оновлення гілки пройшло успішно pulls.update_not_allowed=Ви не можете оновити гілку pulls.outdated_with_base_branch=Ця гілка застаріла відносно базової гілки -pulls.closed_at=`закрив цей запит на злиття %[2]s` -pulls.reopened_at=`повторно відкрив цей запит на злиття %[2]s` +pulls.closed_at=`закриває цей запит на злиття %s` +pulls.reopened_at=`повторно відкриває цей запит на злиття %s` @@ -1780,7 +1780,7 @@ milestones.filter_sort.least_issues=Найменш задач ext_wiki.desc=Посилання на зовнішню вікі. wiki=Вікі -wiki.welcome=Ласкаво просимо до Вікі. +wiki.welcome=Ласкаво просимо до вікі. wiki.welcome_desc=Wiki дозволяє писати та ділитися документацією з співавторами. wiki.desc=Пишіть та обмінюйтеся документацією із співавторами. wiki.create_first_page=Створити першу сторінку @@ -1887,7 +1887,7 @@ settings.collaboration.owner=Власник settings.collaboration.undefined=Не визначено settings.hooks=Веб-хуки settings.githooks=Git хуки -settings.basic_settings=Базові налаштування +settings.basic_settings=Основні налаштування settings.mirror_settings=Налаштування дзеркала settings.mirror_settings.mirrored_repository=Віддзеркалений репозиторій settings.mirror_settings.direction=Напрямок @@ -2055,12 +2055,12 @@ settings.event_issue_assign=Призначення settings.event_issue_assign_desc=Задачу призначено або скасовано. settings.event_issue_label=Мітки settings.event_issue_label_desc=Додавання або видалення міток задач. -settings.event_issue_milestone=Задача з етапом +settings.event_issue_milestone=Етапи settings.event_issue_milestone_desc=Етап призначено, видалено або змінено. settings.event_issue_comment=Коментарі settings.event_issue_comment_desc=Коментар задачі створено, видалено чи відредаговано. settings.event_header_pull_request=Події запиту на злиття -settings.event_pull_request=Запити до злиття +settings.event_pull_request=Зміна settings.event_pull_request_desc=Запит до злиття відкрито, закрито, перевідкрито або відредаговано. settings.event_pull_request_assign=Призначення settings.event_pull_request_assign_desc=Запит про злиття призначено або скасовано. @@ -2485,7 +2485,7 @@ signing.will_sign = Коміт буде підписано ключем «%s». signing.wont_sign.error = Під час перевірки можливості підписати коміт сталася помилка. commits.search_branch = У цій гілці ext_wiki = Зовнішня вікі -pulls.commit_ref_at = `посилається на цей запит на злиття в коміті %[2]s` +pulls.commit_ref_at = `посилається на цей запит на злиття в коміті %s` pulls.cmd_instruction_hint = Переглянути інструкції для командного рядка issues.max_pinned = Неможливо закріпити більше задач issues.unpin_comment = відкріпив %s @@ -2663,6 +2663,12 @@ sync_fork.button = Синхронізувати sync_fork.branch_behind_one = Ця гілка на %[1]d коміт позаду %[2]s sync_fork.branch_behind_few = Ця гілка на %[1]d комітів позаду %[2]s issues.role.first_time_contributor = Новий учасник +settings.event_action_failure = Помилка +settings.event_action_success = Успіх +settings.event_action_recover = Відновлено +commitstatus.success = Успіх +commitstatus.failure = Збій +issues.filter_type.all_pull_requests = Усі запити на злиття [graphs] contributors.what = внески @@ -2841,7 +2847,7 @@ dashboard.update_migration_poster_id=Оновити мігровані ID авт dashboard.git_gc_repos=Виконати очистку сміття для всіх репозиторіїв dashboard.resync_all_sshkeys=Оновити файл «.ssh/authorized_keys» з SSH-ключами Forgejo. dashboard.resync_all_sshprincipals=Оновити файл «.ssh/authorized_principals» з SSH даними користувача Forgejo. -dashboard.resync_all_hooks=Пересинхронізувати перед-прийнятні, оновлюючі та пост-прийнятні хуки в усіх репозиторіях +dashboard.resync_all_hooks=Пересинхронізувати хуки pre-receive, update та post-receive в усіх репозиторіях dashboard.reinit_missing_repos=Переініціалізувати усі репозитрії git-файли яких втрачено dashboard.sync_external_users=Синхронізувати дані зовнішніх користувачів dashboard.cleanup_hook_task_table=Очистити hook_task таблицю @@ -2851,12 +2857,12 @@ dashboard.current_memory_usage=Поточне використання пам'я dashboard.total_memory_allocated=Виділено пам'яті загалом dashboard.memory_obtained=Отримано пам'яті dashboard.pointer_lookup_times=Пошуків вказівника -dashboard.memory_allocate_times=Виділення пам'яті +dashboard.memory_allocate_times=Виділень пам'яті dashboard.memory_free_times=Звільнень пам'яті dashboard.current_heap_usage=Поточне використання динамічної пам'яті dashboard.heap_memory_obtained=Отримано динамічної пам'яті -dashboard.heap_memory_idle=Не використовується динамічною пам'яттю -dashboard.heap_memory_in_use=Використовується динамічною пам'яттю +dashboard.heap_memory_idle=Динамічної пам'яті простоює +dashboard.heap_memory_in_use=Динамічної пам'яті використовується dashboard.heap_memory_released=Звільнено динамічної пам'яті dashboard.heap_objects=Об'єктів динамічної пам'яті dashboard.bootstrap_stack_usage=Використання стеку Bootstrap @@ -2900,7 +2906,7 @@ users.edit_account=Редагувати обліковий запис users.max_repo_creation=Максимальна кількість репозиторіїв users.max_repo_creation_desc=(Введіть -1, щоб використовувати глобальний ліміт за замовчуванням.) users.is_activated=Обліковий запис користувача увімкнено -users.prohibit_login=Вимкнути вхід +users.prohibit_login=Заблокований обліковий запис users.is_admin=Обліковий запис адміністратора users.is_restricted=Обмежений users.allow_git_hook=Може створювати Git хуки @@ -3226,7 +3232,7 @@ notices.view_detail_header=Переглянути деталі повідомл notices.select_all=Вибрати все notices.deselect_all=Скасувати виділення notices.inverse_selection=Інвертувати виділене -notices.delete_selected=Видалити обране +notices.delete_selected=Видалити вибране notices.delete_all=Видалити всі cповіщення notices.type=Тип notices.type_1=Репозиторій @@ -3298,6 +3304,8 @@ config.mailer_protocol = Протокол dashboard.cron.cancelled = Cron: %[1]s скасовано: %[3]s defaulthooks.desc = Вебхуки автоматично сповіщають HTTP-сервер POST-запитами, коли в Forgejo відбуваються певні події. Вказані тут вебхуки є типовими і будуть скопійовані до всіх нових репозиторіїв. Докладніше — в посібнику з вебхуків. assets = Ресурси коду +auths.invalid_openIdConnectAutoDiscoveryURL = Неправильна URL-адреса автоматичного виявлення (повинна бути дійсна URL-адреса, що починається з http:// або https://) +settings = Налаштування адміністратора [action] @@ -3619,6 +3627,8 @@ runners.runner_title = Ранер runners.task_list = Нещодавні завдання ранера runners.update_runner_success = Ранер оновлено runners.delete_runner_header = Підтвердіть видалення ранера +runners.status.offline = Неактивний +runners.status.idle = Простоює diff --git a/options/locale/locale_vi.ini b/options/locale/locale_vi.ini index 57e592a209..28577bc3f7 100644 --- a/options/locale/locale_vi.ini +++ b/options/locale/locale_vi.ini @@ -9,7 +9,7 @@ sign_up = Đăng ký link_account = Liên kết tài khoản register = Đăng ký version = Phiên bản -powered_by = Sử dụng %s +powered_by = Được cung cấp bởi %s page = Trang template = Mẫu language = Ngôn ngữ @@ -25,7 +25,7 @@ access_token = Mã truy cập captcha = CAPTCHA twofa = Xác thực hai lớp webauthn_insert_key = Cắm khóa bảo mật của bạn vào -copy_hash = Chép chuỗi băm +copy_hash = Sao chép chuỗi băm sign_in_with_provider = Đăng nhập bằng %s webauthn_press_button = Hãy nhấn nút trên khóa bảo mật… webauthn_use_twofa = Dùng mã xác thực hai lớp ở trên điện thoại @@ -36,7 +36,7 @@ webauthn_error_insecure = WebAuthn chỉ hỗ trợ kết nối mã hóa. Nếu webauthn_error_unable_to_process = Máy chủ không thể xử lý yêu cầu của bạn. webauthn_error_empty = Bạn phải đặt tên cho khóa này. webauthn_error_timeout = Hết thời gian đọc khóa mất rồi. Hãy tải lại trang và thử lại. -copy_type_unsupported = Không chép được +copy_type_unsupported = Không thể sao chép loại tệp này repository = Kho mã organization = Tổ chức new_fork = Tạo một nhánh mới @@ -55,17 +55,17 @@ all = Tất cả sources = Nguồn forks = Các phân nhánh activities = Hoạt động -pull_requests = Yêu cầu thêm mã +pull_requests = Yêu cầu kéo mã save = Lưu -issues = +issues =Vấn đề enabled = Bật disabled = Tắt -copy = Chép -copy_generic = Chép vào bộ nhớ tạm -copy_url = Chép URL -copy_content = Chép nội dung -copy_success = Đã chép! -copy_error = Không chép được +copy = Sao chép +copy_generic = Sao chép vào bộ nhớ tạm +copy_url = Sao chép URL +copy_content = Sao chép nội dung +copy_success = Đã sao chép! +copy_error = Sao chép thất bại write = Viết preview = Xem trước error = Lỗi @@ -73,7 +73,7 @@ error413 = Bạn đã dùng hết định mức. go_back = Quay lại invalid_data = Dữ liệu không hợp lệ: %v never = Không bao giờ -unknown = Không biết +unknown = Không xác định unpin = Bỏ ghim pin = Ghim archived = Đã lưu trữ @@ -81,6 +81,60 @@ signed_in_as = Đăng nhập bằng re_type = Xác nhận mật khẩu webauthn_sign_in = Nhấn nút trên khóa bảo mật, nếu không có nút thì bạn hãy rút ra rồi cắm lại. new_org.link = Tạo tổ chức -error404 = Trang bạn đang tìm không tồn tại hoặc bạn không có quyền xem. +error404 = Trang bạn đang tìm không tồn tại, đã bị xoá hoặc bạn không có quyền để xem nó. edit = Chỉnh sửa -filter = Lọc \ No newline at end of file +filter = Bộ lọc +dashboard = Trang quản lý +logo = Logo +toc = Mục lục +user_profile_and_more = Hồ sơ và cài đặt… +passcode = Mã xác thực +webauthn_error_duplicated = Khóa bảo mật không được phép cho yêu cầu này. Vui lòng đảm bảo rằng khóa chưa được đăng ký trước đó. +mirror = Bản sao +new_mirror = Tạo bản sao mới +your_starred = Đã đánh sao +mirrors = Các bản sao +concept_system_global = Chung +concept_user_individual = Cá nhân +show_log_seconds = Hiện giây +show_full_screen = Toàn màn hình +download_logs = Tải xuống nhật ký +confirm_delete_selected = Xác nhận xoá tất cả mục được chọn? +name = Tên +filter.clear = Xoá bộ lọc +filter.not_fork = Không phải phân nhánh +filter.not_archived = Không bị lưu trữ +filter.is_archived = Bị lưu trữ +filter.is_fork = Phân nhánh +filter.is_mirror = Bản sao +filter.is_template = Mẫu +filter.not_template = Không phải mẫu +filter.public = Công khai +filter.private = Riêng tư +twofa_scratch = Mã xác thực 2 lớp dự phòng +collaborative = Cộng tác +milestones = Cột mốc +cancel = Huỷ bỏ +retry = Thử lại +rerun = Chạy lại +rerun_all = Chạy lại tất cả +ok = Đồng ý +add = Thêm +add_all = Thêm tất cả +remove = Xoá +remove_all = Xoá tất cả +remove_label_str = Xoá "%s" +locked = Bị khoá +copy_branch = Sao chép tên nhánh +loading = Đang tải… +rss_feed = Nguồn RSS +confirm_delete_artifact = Bạn có chắc muốn xoá "%s" ? +value = Giá trị +copy_path = Sao chép đường dẫn +filter.not_mirror = Không phải bản sao +show_timestamps = Hiện mốc thời gian +concept_code_repository = Kho mã +concept_user_organization = Tổ chức + +[search] +search = Tìm kiếm… \ No newline at end of file diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index a3ddf05ff9..c6c534df9f 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -7,9 +7,9 @@ logo=徽标 sign_in=登录 sign_in_with_provider=使用 %s 登录 sign_in_or=或 -sign_out=退出 +sign_out=登出 sign_up=注册 -link_account=链接账户 +link_account=链接账号 register=注册 version=当前版本 powered_by=由 %s 提供支持 @@ -743,7 +743,7 @@ social=社交帐号 applications=应用 orgs=组织 repos=仓库列表 -delete=删除帐户 +delete=删除账号 twofa=两步验证(TOTP) account_link=已绑定的帐户 organization=组织 @@ -1062,8 +1062,8 @@ language.description = 此语言将保存到您的账号中,并在您登录后 language.localization_project = 帮助我们将 Forgejo 翻译成您的语言!了解更多。 user_block_yourself = 您不能屏蔽自己。 pronouns_custom_label = 自定义代词 -change_username_redirect_prompt.with_cooldown.one = 旧的用户名将在%[1]d天的保护期后对所有人可用,您仍可以在此期间重新认领旧的用户名。 -change_username_redirect_prompt.with_cooldown.few = 旧的用户名将在%[1]d天的保护期后对所有人可用,您仍可以在此期间重新认领旧的用户名。 +change_username_redirect_prompt.with_cooldown.one = 旧用户名将在 %[1]d 天的保护期后对所有人可用,您仍可以在此期间重新认领旧用户名。 +change_username_redirect_prompt.with_cooldown.few = 旧用户名将在 %[1]d 天的保护期后对所有人可用,您仍可以在此期间重新认领旧用户名。 keep_pronouns_private = 仅向已认证用户显示代词 keep_pronouns_private.description = 这将对未登录的访问者隐藏您的代词。 quota = 配额 @@ -1515,7 +1515,7 @@ projects.card_type.images_and_text=图标和文字 projects.card_type.text_only=仅文本 issues.desc=组织 bug 报告、任务和里程碑。 -issues.filter_assignees=筛选指派人 +issues.filter_assignees=筛选指派成员 issues.filter_milestones=筛选里程碑 issues.filter_projects=筛选项目 issues.filter_labels=筛选标签 @@ -1581,7 +1581,7 @@ issues.remove_ref_at=`删除了引用 %s %s` issues.add_ref_at=`添加了引用 %s %s` issues.delete_branch_at=`于 %[2]s 删除了分支 %[1]s` issues.filter_label=标签筛选 -issues.filter_label_exclude=`使用 alt + 鼠标左键 / 回车 排除标签` +issues.filter_label_exclude=使用 Alt + 单击 排除标签 issues.filter_label_no_select=所有标签 issues.filter_label_select_no_label=无标签 issues.filter_milestone=里程碑筛选 @@ -1653,15 +1653,15 @@ issues.comment_pull_merged_at=已合并提交 %[1]s 到 %[2]s %[3]s issues.comment_manually_pull_merged_at=手动合并提交 %[1]s 到 %[2]s %[3]s issues.close_comment_issue=评论并关闭 issues.reopen_issue=重新开放 -issues.reopen_comment_issue=重新打开评论 +issues.reopen_comment_issue=重新打开并评论 issues.create_comment=评论 -issues.closed_at=`于%[2]s关闭此议题` -issues.reopened_at=`重新打开此问题 %[2]s` -issues.commit_ref_at=`于%[2]s在代码提交中引用了该议题` -issues.ref_issue_from=`引用了议题 %[4]s %[2]s` -issues.ref_pull_from=`引用了合并请求 %[4]s %[2]s` -issues.ref_closing_from=`于 %[2]s 从合并请求 %[4]s引用了此议题,将关闭此议题` -issues.ref_reopening_from=`于 %[2]s 引用了合并请求 %[4]s 将重新讨论此议题 ` +issues.closed_at=`于 %s 关闭了此议题` +issues.reopened_at=`于 %s 重新打开了此议题` +issues.commit_ref_at=`于 %s 从提交中引用了此议题` +issues.ref_issue_from=`引用了此议题 %[3]s %[1]s` +issues.ref_pull_from=`引用了此合并请求 %[3]s %[1]s` +issues.ref_closing_from=`于 %[1]s 从合并请求 %[3]s 引用了此议题,将关闭此议题` +issues.ref_reopening_from=`于 %[1]s 从合并请求 %[3]s 引用了此议题,将重新打开此议题 ` issues.ref_closed_from=`关闭了这个议题 %[4]s %[2]s` issues.ref_reopened_from=`重新打开这个议题 %[4]s %[2]s` issues.ref_from=`来自 %[1]s` @@ -1969,8 +1969,8 @@ pulls.update_branch_success=分支更新成功 pulls.update_not_allowed=您无权更新分支 pulls.outdated_with_base_branch=此分支相比基础分支已过期 pulls.close=关闭 -pulls.closed_at=`于%[2]s关闭此合并请求 ` -pulls.reopened_at=`重新打开此合并请求 %[2]s` +pulls.closed_at=`于 %s 关闭了此合并请求 ` +pulls.reopened_at=`于 %s 重新打开了此合并请求` pulls.cmd_instruction_hint=查看命令行说明 pulls.cmd_instruction_checkout_title=检出 pulls.cmd_instruction_checkout_desc=从你的仓库中检出一个新的分支并测试变更。 @@ -2304,7 +2304,7 @@ settings.add_collaborator_inactive_user=无法添加未激活的用户作为合 settings.add_collaborator_owner=不能将所有者添加为协作者。 settings.add_collaborator_duplicate=合作者已经被添加到本仓库。 settings.delete_collaborator=删除 -settings.collaborator_deletion=删除协作者 +settings.collaborator_deletion=移除协作者 settings.collaborator_deletion_desc=删除协作者后他将无法再对此仓库的访问。继续? settings.remove_collaborator_success=已成功删除协作者。 settings.search_user_placeholder=搜索用户... @@ -2770,7 +2770,7 @@ settings.wiki_rename_branch_main = 标准化百科分支名称 settings.wiki_rename_branch_main_notices_1 = 此操作无法撤消。 settings.wiki_branch_rename_success = 百科仓库的分支名称已成功规范化。 settings.confirm_wiki_branch_rename = 重命名百科分支 -pulls.commit_ref_at = `在提交 %[2]s 中引用了此合并请求` +pulls.commit_ref_at = `于 %s 从提交中引用了此合并请求` settings.wiki_rename_branch_main_notices_2 = 这将永久重命名 %s 的仓库百科的内部分支。现存的检出方式需要更新。 settings.wiki_branch_rename_failure = 无法标准化仓库百科的分支名称。 settings.add_collaborator_blocked_our = 因仓库所有者已将其拉黑,不能添加该用户为协作者。 @@ -2915,6 +2915,14 @@ comment.blocked_by_user = 您无法评论,因为您已被仓库所有者或作 sync_fork.button = 同步 sync_fork.branch_behind_one = 此分支落后于 %[2]s %[1]d 个提交 sync_fork.branch_behind_few = 此分支落后于 %[2]s %[1]d 个提交 +settings.event_action_failure = 失败 +settings.event_action_recover = 恢复 +settings.event_action_recover_desc = Action运行在同一工作流上次失败后成功。 +settings.event_action_success = 成功 +settings.event_action_success_desc = Action运行以成功结束。 +settings.event_action_failure_desc = Action运行以失败结束。 +settings.event_header_action = Action运行事件 +issues.filter_type.all_pull_requests = 所有合并请求 [graphs] component_loading=正在加载 %s… @@ -3050,8 +3058,8 @@ teams.invite.by=邀请人 %s teams.invite.description=请点击下面的按钮加入团队。 follow_blocked_user = 你无法关注此组织,因为此组织已屏蔽你。 open_dashboard = 打开仪表盘 -settings.change_orgname_redirect_prompt.with_cooldown.one = 旧的组织名将在%[1]d天的保护期后对所有人可用,您仍可以在此期间重新认领旧的名字。 -settings.change_orgname_redirect_prompt.with_cooldown.few = 旧的组织名将在%[1]d天的保护期后对所有人可用,您仍可以在此期间重新认领旧名字。 +settings.change_orgname_redirect_prompt.with_cooldown.one = 旧组织名将在 %[1]d 天的保护期后对所有人可用,您仍可以在此期间重新认领旧名称。 +settings.change_orgname_redirect_prompt.with_cooldown.few = 旧组织名将在 %[1]d 天的保护期后对所有人可用,您仍可以在此期间重新认领旧名称。 [admin] dashboard=管理面板 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index e2cb0d8b2c..45534801de 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -574,7 +574,7 @@ issues.delete_comment_confirm=您確定要刪除該條評論嗎? issues.context.edit=編輯 issues.reopen_issue=重新開啟 issues.create_comment=評論 -issues.commit_ref_at=`在代碼提交 %[2]s 中引用了該問題` +issues.commit_ref_at=`在代碼提交 %s 中引用了該問題` issues.role.owner=管理員 issues.role.member=普通成員 issues.sign_in_require_desc= 登入 才能加入這對話。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 396e1a571d..fba51a391e 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -207,6 +207,9 @@ table_modal.header = 新增表格 buttons.indent.tooltip = 使項目縮排一層 buttons.unindent.tooltip = 使項目取消縮排一層 link_modal.header = 新增連結 +link_modal.url = 網址 +link_modal.description = 描述 +link_modal.paste_reminder = 提示:當您的剪貼簿中有網址時,可以直接貼到編輯器中來建立連結。 [filter] string.asc=A - Z @@ -218,7 +221,7 @@ missing_csrf=錯誤的請求:未提供 CSRF 符記 invalid_csrf=錯誤的請求:無效的 CSRF 符記 not_found=找不到目標。 network_error=網路錯誤 -report_message = 如果您相信這是一個 Forgejo 的錯誤,請在 Codeberg 上搜尋相關問題,或在必要時提出一個新問題。 +report_message = 如果您相信這是一個 Forgejo 的錯誤,請在 Codeberg 上搜尋相關議題,或在必要時提出一個新議題。 server_internal = 伺服器內部錯誤 [startpage] @@ -711,7 +714,7 @@ show_on_map = 在地圖上顯示這個地點 settings = 使用者設定 block_user = 封鎖使用者 block_user.detail_1 = 你們將停止互相關注,並且無法互相關注。 -block_user.detail_2 = 此使用者將無法與你擁有的儲存庫或由你建立的問題和評論進行互動。 +block_user.detail_2 = 此使用者將無法與你擁有的儲存庫或由你建立的議題和評論進行互動。 followers_one = %d 位追蹤者 following_one = 追蹤 %d 個人 block_user.detail_3 = 你們將無法互相新增為儲存庫協作者。 @@ -1011,7 +1014,7 @@ uploaded_avatar_is_too_big = 上傳檔案的大小 (%d KiB)超過了上限 select_permissions = 選擇權限 permission_write = 讀寫 permissions_list = 權限: -add_email_confirmation_sent = 我們已發送一封確認信至 「%s」。請檢查您的信箱並在 %s 內確認您的信箱地址。 +add_email_confirmation_sent = 確認信已發送至「%s」。請在接下來的 %s 內前往收件匣查看該郵件,並點擊其中的連結以完成電子郵件地址的確認。 repo_and_org_access = 儲存庫和組織存取權 permissions_public_only = 僅公開 permissions_access_all = 全部(公開、私有和受限) @@ -1025,9 +1028,9 @@ unbind_success = 已成功移除該社群帳號。 create_oauth2_application_success = 您已成功建立一個新的 OAuth2 應用程式。 change_username_prompt = 註:更改您的使用者名稱也會更改您的帳號 URL。 change_username_redirect_prompt = 舊的使用者名稱在被其他使用者認領之前將會轉址到新的使用者名稱。 -visibility.limited_tooltip = 只有已登入的使用者能看見 +visibility.limited_tooltip = 僅對已登入的使用者可見 visibility.private_tooltip = 只有您加入的組織之成員能看見 -keep_email_private_popup = 這將在您的個人資料頁面、合併請求或網頁檔案編輯器中隱藏您的電子信箱地址。已推送的提交不會被修改。在提交中使用 %s 來將其連結至您的帳號。 +keep_email_private_popup = 您的電子郵件地址不會顯示在個人資料頁面中,也不會成為透過網頁介面(例如上傳檔案、編輯或合併提交)所建立的提交紀錄的預設地址。取而代之的是,可以使用特殊地址 %s 將這些提交關聯到您的帳號。此設定不會影響既有的提交紀錄。 ssh_signonly = 因為目前 SSH 已被停用,這個金鑰只被用來校驗提交簽署。 email_desc = 您的主要電子信箱將被用於通知、密碼復原、和網頁 Git 操作(如果您的信箱不是隱藏的)。 pronouns_custom = 自訂 @@ -1062,6 +1065,32 @@ pronouns_custom_label = 自訂代名詞 change_username_redirect_prompt.with_cooldown.one = 舊的使用者名稱將在 %[1]d 天的冷卻期後對所有人開放,你仍然可以在冷卻期內重新獲得舊的使用者名稱。 change_username_redirect_prompt.with_cooldown.few = 舊的使用者名稱將在 %[1]d 天的冷卻期後對所有人開放,你仍然可以在冷卻期內重新獲得舊的使用者名稱。 keep_activity_private.description = 你的公開活動只有你和站點管理員可見。 +quota.rule.exceeded = 已超出 +quota.sizes.assets.packages.all = 軟體包 +storage_overview = 儲存空間概覽 +quota.rule.no_limit = 無限制 +quota.sizes.all = 全部 +regenerate_token = 重新產生 +quota.sizes.repos.all = 儲存庫 +quota.sizes.assets.all = 資產 +quota.sizes.assets.attachments.all = 附件 +quota.sizes.assets.artifacts = 製品 +quota.sizes.wiki = 百科 +quota = 配額 +access_token_regeneration = 重新產生存取符記 +access_token_regeneration_desc = 重新產生存取符記將會撤銷使用該符記的應用程式對您帳號的存取權限,此操作無法還原。是否繼續? +regenerate_token_success = 符記已重新產生。使用該符記的應用程式將不再具有你帳號的存取權限,必須更新為新的符記後才能繼續使用。 +quota.applies_to_user = 以下配額規則適用於您的帳號 +quota.applies_to_org = 以下配額規則適用於此組織 +quota.rule.exceeded.helper = 符合此規則的物件總大小已超出配額限制。 +quota.sizes.repos.public = 公開儲存庫 +quota.sizes.repos.private = 私有儲存庫 +quota.sizes.git.all = Git 內容 +quota.sizes.git.lfs = Git LFS +quota.sizes.assets.attachments.issues = 問題附件 +quota.sizes.assets.attachments.releases = 版本發布附件 +keep_pronouns_private = 僅向已驗證的使用者顯示代名詞 +keep_pronouns_private.description = 這將對未登入的訪客隱藏您的代名詞。 [repo] owner=所有者 @@ -1133,7 +1162,7 @@ forks=分叉 reactions_more=和其他 %d 個 unit_disabled=網站管理員已經停用這個儲存庫區域。 language_other=其他 -adopt_search=輸入帳號以搜尋未接管的儲存庫... (留白以查詢全部) +adopt_search=輸入帳號以搜尋未接管的儲存庫... (留白以查詢全部) adopt_preexisting_label=接管檔案 adopt_preexisting=接管既有的檔案 adopt_preexisting_content=從 %s 建立儲存庫 @@ -1348,7 +1377,7 @@ editor.filename_cannot_be_empty=檔案名稱不能為空。 editor.filename_is_invalid=檔名無效:「%s」。 editor.branch_does_not_exist=此儲存庫沒有名為「%s」的分支。 editor.branch_already_exists=此儲存庫已有名為「%s」的分支。 -editor.file_changed_while_editing=檔案內容在您編輯時已被更改。按一下此處來檢視被更改的地方或再次提交以覆蓋這些變更。 +editor.file_changed_while_editing=檔案內容自您開啟後已有變更。點此查看,或再次提交變更以覆寫原內容。 editor.file_already_exists=此儲存庫已有名為「%s」的檔案。 editor.commit_empty_file_header=提交空白檔案 editor.commit_empty_file_text=你準備提交的檔案是空白的,是否繼續? @@ -1567,7 +1596,7 @@ issues.commented_at=`已留言 %s` issues.delete_comment_confirm=您確定要刪除這則留言嗎? issues.context.copy_link=複製連結 issues.context.quote_reply=引用回覆 -issues.context.reference_issue=新增問題並參考 +issues.context.reference_issue=在新問題中引用 issues.context.edit=編輯 issues.context.delete=刪除 issues.close=關閉問題 @@ -1575,13 +1604,13 @@ issues.close_comment_issue=留言並關閉 issues.reopen_issue=重新開放 issues.reopen_comment_issue=留言並重新開放 issues.create_comment=留言 -issues.closed_at=`關閉了這個問題 %[2]s` -issues.reopened_at=`重新開放了這個問題 %[2]s` -issues.commit_ref_at=`在提交中關聯了這個問題 %[2]s` -issues.ref_issue_from=`關聯了這個問題 %[4]s %[2]s` -issues.ref_pull_from=`關聯了這個合併請求 %[4]s %[2]s` -issues.ref_closing_from=`關聯了合併請求 %[4]s 將關閉這個問題 %[2]s` -issues.ref_reopening_from=`關聯了合併請求 %[4]s 將重新開放這個問題 %[2]s` +issues.closed_at=`關閉了這個問題 %s` +issues.reopened_at=`重新開放了這個問題 %s` +issues.commit_ref_at=`在提交中關聯了這個問題 %s` +issues.ref_issue_from=`關聯了這個問題 %[3]s %[1]s` +issues.ref_pull_from=`關聯了這個合併請求 %[3]s %[1]s` +issues.ref_closing_from=從將關閉此問題的拉取請求 %[3]s 中提及了此問題,%[1]s +issues.ref_reopening_from=從將重新開啟此問題的拉取請求 %[3]s 中提及了此問題,%[1]s issues.ref_closed_from=`關閉了這個問題 %[4]s %[2]s` issues.ref_reopened_from=`重新開放了這個問題 %[4]s %[2]s` issues.ref_from=`自 %[1]s` @@ -1630,8 +1659,8 @@ issues.unlock=解鎖對話 issues.lock.unknown_reason=由於未知的原因而無法鎖定問題。 issues.lock_duplicate=問題無法被鎖定兩次。 issues.unlock_error=無法解鎖未被鎖定的問題。 -issues.lock_with_reason=因為 %s 而鎖定,並將對話設為協作者限定 %s -issues.lock_no_reason=鎖定並將對話設為協作者限定 %s +issues.lock_with_reason=因為 %s 而鎖定,並將對話限制為協作者 %s +issues.lock_no_reason=鎖定並將對話限制為協作者 %s issues.unlock_comment=解鎖這個對話 %s issues.lock_confirm=鎖定 issues.unlock_confirm=解除鎖定 @@ -1725,8 +1754,8 @@ issues.review.left_comment=留下了回應 issues.review.content.empty=您必須留下訊息指出需要修正的地方。 issues.review.reject=請求了變更 %s issues.review.wait=被請求進行審核 %s -issues.review.add_review_request=請求了 %s 來審核 %s -issues.review.remove_review_request=移除了對 %s 的審核請求 %s +issues.review.add_review_request=請求 %[1]s 進行審查 %[2]s +issues.review.remove_review_request=移除了對 %[1]s 的審查請求 %[2]s issues.review.remove_review_request_self=拒絕了審核 %s issues.review.pending=待處理 issues.review.review=審核 @@ -1810,7 +1839,7 @@ pulls.reject_count_1=%d 個變更請求 pulls.reject_count_n=%d 個變更請求 pulls.waiting_count_1=%d 個正在等待審核 pulls.waiting_count_n=%d 個正在等待審核 -pulls.wrong_commit_id=提交 id 必須存在於目標分支上 +pulls.wrong_commit_id=提交 ID 必須存在於目標分支上 pulls.no_merge_desc=無法進行合併,因為所有儲存庫的合併選項已被停用。 pulls.no_merge_helper=在儲存庫設定啟用合併選項或手動合併該合併請求。 @@ -1826,13 +1855,13 @@ pulls.merge_commit_id=合併提交 ID pulls.require_signed_wont_sign=該分支需要經簽署的提交,但此合併將不會被簽署 pulls.invalid_merge_option=您無法對此合併請求使用這個合併選項。 -pulls.merge_conflict=合併失敗:合併時發生衝突。 提示:請嘗試不同的策略 +pulls.merge_conflict=合併失敗:合併時發生衝突。 提示:請嘗試其他的合併策略 pulls.merge_conflict_summary=錯誤訊息 -pulls.rebase_conflict=合併失敗:Rebase 提交時發生衝突:%[1]s。 提示:請嘗試不同的策略 +pulls.rebase_conflict=合併失敗:Rebase 提交時發生衝突:%[1]s。 提示:請嘗試其他的合併策略 pulls.rebase_conflict_summary=錯誤訊息 -pulls.unrelated_histories=合併失敗:要合併的 HEAD 和基底分支沒有共同的歷史。 提示:請嘗試不同的策略 +pulls.unrelated_histories=合併失敗:要合併的 HEAD 和基底分支沒有共同的歷史。 提示:請嘗試其他的合併策略 pulls.merge_out_of_date=合併失敗:產生合併時,基底已被更新。提示:再試一次。 -pulls.head_out_of_date=合併失敗:產生合併時,head 已被更新。提示:再試一次。 +pulls.head_out_of_date=合併失敗:產生合併時,HEAD 已被更新。提示:再試一次。 pulls.push_rejected=合併失敗:此推送被拒絕。請檢查此儲存庫的 Git Hook。 pulls.push_rejected_summary=完整的拒絕訊息 pulls.push_rejected_no_message=推送失敗:此推送被拒絕但未提供其他資訊。請檢查此儲存庫的 Git Hook @@ -1850,8 +1879,8 @@ pulls.update_branch_success=分支更新成功 pulls.update_not_allowed=您無權更新分支 pulls.outdated_with_base_branch=相對於基底分支,此分支已過時 pulls.close=關閉合併請求 -pulls.closed_at=`關閉了這個合併請求 %[2]s` -pulls.reopened_at=`重新開放了這個合併請求 %[2]s` +pulls.closed_at=`關閉了這個合併請求 %s` +pulls.reopened_at=`重新開放了這個合併請求 %s` pulls.clear_merge_message=清除合併訊息 pulls.clear_merge_message_hint=清除合併訊息將僅移除提交訊息內容,留下產生的 git 結尾,如「Co-Authored-By …」。 @@ -1900,7 +1929,7 @@ milestones.filter_sort.most_issues=問題由多到少 milestones.filter_sort.least_issues=問題由少到多 -ext_wiki=外部 Wiki +ext_wiki=外部百科 ext_wiki.desc=連結外部 Wiki。 wiki=Wiki @@ -1927,7 +1956,7 @@ wiki.page_already_exists=相同名稱的 Wiki 頁面已經存在。 wiki.reserved_page=「%s」是保留的 Wiki 頁面名稱。 wiki.pages=所有頁面 wiki.last_updated=最後更新於 %s -wiki.page_name_desc=輸入此 Wiki 頁面的名稱。一些特殊名稱有:「Home」、「_Sidebar」、「_Footer」等。 +wiki.page_name_desc=輸入此百科頁面的名稱。一些特殊名稱有:「Home」、「_Sidebar」、「_Footer」等。 activity=動態 activity.period.filter_label=期間: @@ -2036,10 +2065,10 @@ settings.update_settings=儲存設定 settings.branches.update_default_branch=更新預設分支 settings.branches.add_new_rule=增加新規則 settings.advanced_settings=進階設定 -settings.wiki_desc=啟用儲存庫 Wiki -settings.use_internal_wiki=使用內建 Wiki -settings.use_external_wiki=使用外部 Wiki -settings.external_wiki_url=外部 Wiki 網址 +settings.wiki_desc=啟用儲存庫百科 +settings.use_internal_wiki=使用內建百科 +settings.use_external_wiki=使用外部百科 +settings.external_wiki_url=外部百科網址 settings.external_wiki_url_error=外部 Wiki 網址不是有效的網址。 settings.external_wiki_url_desc=點擊百科分頁時,使用者會被轉址至外部百科的 URL。 settings.issues_desc=啟用儲存庫問題追蹤器 @@ -2068,7 +2097,7 @@ settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行 settings.releases_desc=啟用儲存庫版本發佈 settings.packages_desc=啟用儲存庫軟體包註冊中心 settings.projects_desc=啟用儲存庫專案 -settings.actions_desc=啟用儲存庫 Actions +settings.actions_desc=啟用與 Forgejo Actions 整合的 CI/CD 流程 settings.admin_settings=管理員設定 settings.admin_enable_health_check=啟用儲存庫的健康檢查(git fsck) settings.admin_code_indexer=程式碼索引器 @@ -2118,10 +2147,10 @@ settings.trust_model.committer.desc=提交者的有效簽署將被標記為「 settings.trust_model.collaboratorcommitter=協作者 + 提交者 settings.trust_model.collaboratorcommitter.long=協作者 + 提交者:信任協作者同時是提交者的簽署 settings.trust_model.collaboratorcommitter.desc=此儲存庫協作者的有效簽署在他同時是提交者時將被標記為「受信任」,簽署只符合提交者時將標記為「不受信任」,都不符合時標記為「不符合」。這會強制 Forgejo 成為受簽署提交的提交者,實際的提交者將於提交訊息結尾被標記為「Co-Authored-By:」和「Co-Committed-By:」。預設的 Forgejo 金鑰必須符合資料庫中的一位使用者。 -settings.wiki_delete=刪除 Wiki 資料 +settings.wiki_delete=刪除百科資料 settings.wiki_delete_desc=刪除儲存庫 Wiki 資料是永久的且不可還原。 settings.wiki_delete_notices_1=- 這將會永久刪除與停用 %s 的儲存庫 Wiki。 -settings.confirm_wiki_delete=刪除 Wiki 資料 +settings.confirm_wiki_delete=刪除百科資料 settings.wiki_deletion_success=已刪除儲存庫的 Wiki 資料。 settings.delete=刪除此儲存庫 settings.delete_desc=刪除儲存庫是永久的且不可還原。 @@ -2167,7 +2196,7 @@ settings.webhook.payload=內容 settings.webhook.body=本體 settings.webhook.replay.description=再次執行此 Webhook。 settings.webhook.delivery.success=已將事件加入到傳送佇列,可能需要等待幾分鐘才會出現於傳送紀錄。 -settings.githooks_desc=Git Hook 是 Git 本身提供的功能。您可以在下方編輯 hook 檔案以設定自訂作業。 +settings.githooks_desc=Git hook 是 Git 本身提供的功能。您可以在下方編輯 hook 檔案以設定自訂作業。 settings.githook_edit_desc=如果 Hook 未啟動,則會顯示範例文件中的內容。如果想要刪除某個 Hook,則送出空白內容即可。 settings.githook_name=Hook 名稱 settings.githook_content=Hook 內容 @@ -2218,15 +2247,15 @@ settings.event_pull_request_desc=建立、編輯、關閉及重新開放合併 settings.event_pull_request_assign=指派 settings.event_pull_request_assign_desc=指派或取消指派合併請求。 settings.event_pull_request_label=標籤 -settings.event_pull_request_label_desc=更新或清除合併請求標籤。 +settings.event_pull_request_label_desc=已新增或移除合併請求的標籤。 settings.event_pull_request_milestone=里程碑 settings.event_pull_request_milestone_desc=里程碑已新增、已移除或已修改。 settings.event_pull_request_comment=評註 settings.event_pull_request_comment_desc=建立、編輯或刪除合併請求的留言。 settings.event_pull_request_review=審核 -settings.event_pull_request_review_desc=核准、退回或提出審核留言。 +settings.event_pull_request_review_desc=合併請求已被核准、拒絕,或已有審查留言新增。 settings.event_pull_request_sync=同步 -settings.event_pull_request_sync_desc=合併請求同步。 +settings.event_pull_request_sync_desc=分支已自動與目標分支同步更新。 settings.event_package=軟體包 settings.event_package_desc=已在儲存庫中建立或刪除軟體包。 settings.branch_filter=分支篩選 @@ -2338,7 +2367,7 @@ settings.choose_branch=選擇一個分支… settings.no_protected_branch=沒有受保護的分支。 settings.edit_protected_branch=編輯 settings.protected_branch_required_rule_name=必須填寫規則名稱 -settings.protected_branch_duplicate_rule_name=規則名稱已存在 +settings.protected_branch_duplicate_rule_name=已經存在一條針對這組分支的規則 settings.protected_branch_required_approvals_min=需要的核可數量不能為負數。 settings.tags=標籤 settings.tags.protection=標籤保護 @@ -2472,7 +2501,7 @@ release.tag_helper=新增或選擇現有的標籤。 release.tag_helper_new=新標籤,將在目標上建立此標籤。 release.tag_helper_existing=現有的標籤。 release.title_empty=標題不可為空。 -release.prerelease_desc=標記為 Pre-Release +release.prerelease_desc=標記為預發行 release.prerelease_helper=標記此版本不適合生產使用。 release.cancel=取消 release.publish=發佈發行 @@ -2556,8 +2585,8 @@ tree_path_not_found_tag = 路徑 %[1]s 不存在於標籤 %[2]s 中 tree_path_not_found_commit = 路徑 %[1]s 不存在於提交 %[2]s 中 tree_path_not_found_branch = 路徑 %[1]s 不存在於分支 %[2]s 中 transfer.no_permission_to_accept = 您沒有權限接受這項轉讓。 -archive.title = 這個儲存庫被封存了。您可以檢視其中的檔案或是 Clone 它,但您無法推送提交,提出問題或合併請求。 -archive.title_date = 這個儲存庫在 %s 被封存了。您可以檢視其中的檔案或 clone 它,但您無法推送提交,提出問題或合併請求。 +archive.title = 這個儲存庫已被封存。您可以檢視其中的檔案或拓製儲存庫,但您無法提交推送和創建新議題、合併請求或評論。 +archive.title_date = 這個儲存庫在 %s 被封存了。您可以檢視其中的檔案或拓製儲存庫,但您無法提交推送和創建新議題、合併請求或評論。 migrate.forgejo.description = 從 codeberg.org 或其他 Forgejo 站點遷移資料。 migrate.cancel_migrating_title = 取消遷移 executable_file = 可執行檔 @@ -2605,7 +2634,7 @@ commits.search_branch = 此分支 commits.browse_further = 進一步瀏覽 commits.renamed_from = 自 %s 重新命名 issues.filter_milestone_none = 沒有里程碑 -issues.num_comments_1 = %s 則留言 +issues.num_comments_1 = %d 則留言 issues.no_content = 沒有提供敘述。 settings.new_owner_blocked_doer = 新的所有者已封鎖您。 new_repo_helper = 一個儲存庫包含專案的所有檔案和它們的修訂歷史。在別處已經有儲存庫了嗎?遷移儲存庫。 @@ -2615,10 +2644,10 @@ issues.filter_milestone_all = 所有里程碑 issues.author_helper = 此使用者是作者。 pulls.blocked_by_approvals = 此合併請求還沒有足夠的核可。已有 %d 個,總共需要 %d 個核可。 wiki.search = 搜尋百科 -settings.mirror_settings.docs.disabled_pull_mirror.instructions = 設定您的儲存庫以自動推送提交、標簽、和分支至另一個儲存庫。Pull 鏡像已被您的管理員停用。 +settings.mirror_settings.docs.disabled_pull_mirror.instructions = 設定你的專案使其自動將提交、標籤與分支推送到另一個儲存庫。您的網站管理員已停用拉取鏡像功能。 settings.mirror_settings.docs = 設定您的儲存庫以自動與另一個儲存庫同步提交、標籤、和分支。 settings.mirror_settings.docs.disabled_push_mirror.info = 推送鏡像已被您的網站管理員停用。 -settings.mirror_settings.docs.pull_mirror_instructions = 如需建立一個 pull 鏡像,請參閱: +settings.mirror_settings.docs.pull_mirror_instructions = 如需建立一個拉取鏡像,請參閱: blame.ignore_revs = .git-blame-ignore-revs 中的修訂已被忽略。點擊這裡來檢視一般的責任歸屬界面。 editor.file_is_a_symlink = `「%s」是一個符號連結。網頁編輯器無法編輯符號連結` issues.label_archive = 封存標籤 @@ -2630,7 +2659,7 @@ pulls.showing_only_single_commit = 只顯示提交 %[1]s 的變更 pulls.cmd_instruction_merge_desc = 合併更改並在 Forgejo 上更新。 signing.wont_sign.pubkey = 無法簽署該提交,因為您沒有與您帳號連結的公鑰。 signing.wont_sign.twofa = 您必須啟用兩步驟認證才能簽署提交。 -signing.wont_sign.basesigned = 因為 base 提交沒有被簽署,無法簽署該提交。 +signing.wont_sign.basesigned = 合併將不會被簽署,因為其基礎提交尚未簽署。 settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning = 目前,這個操作只能在「新的遷移」目錄完成。如需更多資訊,請參閱: settings.mirror_settings.docs.disabled_push_mirror.instructions = 設定您的儲存庫以自動從另一個儲存庫拉取提交、標籤和分支。 issues.role.contributor_helper = 該使用者過去曾經提交至此儲存庫。 @@ -2643,14 +2672,14 @@ signing.wont_sign.error = 在檢查是否能簽署提交時發生錯誤。 issues.num_participants_one = %d 位參與者 pulls.showing_specified_commit_range = 只顯示 %[1]s 至 %[2]s 之間的變更 blame.ignore_revs.failed = 忽略 .git-blame-ignore-revs 中的修訂失敗。 -issues.blocked_by_user = 因為您被該儲存庫的所有者封鎖,您不能提出一個新的問題。 +issues.blocked_by_user = 因為您已被儲存庫擁有者封鎖,您無法在此儲存庫中建立問題。 pulls.blocked_by_user = 因為您被這個儲存庫的所有者封鎖,您不能在這裡開啟一個合併請求。 pulls.has_merged = 失敗:該合併請求已被合併,您無法再合併一次或更改目標分支。 wiki.cancel = 取消 activity.navbar.code_frequency = 寫程式頻率 settings.mirror_settings.docs.no_new_mirrors = 您的儲存庫目前是另一個儲存庫的鏡像。請記住,您目前不能建立新的鏡像。 settings.mirror_settings.docs.can_still_use = 雖然您不能建立新的鏡像,您還是可以使用既有的鏡像。 -pulls.reopen_failed.base_branch = 因為這個合併請求的 base 分支不存在,它無法被開啟。 +pulls.reopen_failed.base_branch = 因為這個合併請求的基礎分支不存在,它無法被開啟。 issues.label_archive_tooltip = 在預設情況下,標籤搜尋時會排除被封存的標籤。 signing.wont_sign.approved = 因為合併請求沒有被核可,這個合併不會被簽署。 activity.navbar.recent_commits = 最近的提交 @@ -2664,8 +2693,8 @@ signing.wont_sign.never = 永不簽署提交。 editor.push_out_of_date = 該推送似乎過期了。 issues.cancel_tracking_history = `已取消時間追蹤 %s` issues.due_date_not_writer = 您需要有寫入這個儲存庫的權限才能更新其問題的到期日。 -pulls.commit_ref_at = `在提交 %[2]s 引用了這個合併請求` -pulls.cmd_instruction_checkout_desc = 從您的專案儲存庫 checkout 一個新的分支來測試這些更改。 +pulls.commit_ref_at = `在提交 %s 引用了這個合併請求` +pulls.cmd_instruction_checkout_desc = 從您的專案儲存庫中,建立並切換到一個新分支以測試這些變更。 pulls.cmd_instruction_merge_title = 合併 pulls.ready_for_review = 可以開始審閱了嗎? pulls.cmd_instruction_hint = `檢視命令列指示` @@ -2690,7 +2719,7 @@ pulls.blocked_by_changed_protected_files_1 = 這個合併請求被暫止,因 pulls.blocked_by_changed_protected_files_n = 這個合併請求被暫止,因為它更改了這些受保護的檔案: pulls.status_checks_hide_all = 隱藏所有檢查 pulls.status_checks_show_all = 顯示所有檢查 -pulls.reopen_failed.head_branch = 因為這個合併請求的 head 分支不存在,它無法被開啟。 +pulls.reopen_failed.head_branch = 因為這個合併請求的 HEAD 分支不存在,無法重新開啟此合併請求。 activity.navbar.pulse = 動態 signing.will_sign = 將以金鑰「%s」簽署這個提交。 signing.wont_sign.headsigned = 因為 head 提交沒有被簽署,這個合併不會被簽署。 @@ -2707,7 +2736,7 @@ pulls.show_changes_since_your_last_review = 顯示自您上次審閱的變更 pulls.blocked_by_rejection = 這個合併請求有正式審閱者所請求的更改。 pulls.blocked_by_official_review_requests = 因為這個合併請求還缺少至少一個正式審閱者的核可,它已被暫止。 wiki.original_git_entry_tooltip = 與其使用友善連結,檢視原始 Git 檔案。 -settings.mirror_settings.docs.more_information_if_disabled = 您可以在這裡找到更多關於 push 和 pull 鏡像的資訊: +settings.mirror_settings.docs.more_information_if_disabled = 您可以在這裡找到更多關於推送和拉取鏡像的資訊: settings.mirror_settings.docs.doc_link_title = 如何建立儲存庫鏡像? settings.mirror_settings.docs.pulling_remote_title = 從遠端儲存庫拉取 issues.author.tooltip.pr = 此使用者是這個合併請求的作者。 @@ -2791,9 +2820,9 @@ settings.unarchive.text = 取消封存儲存庫將恢復其接收提交和推送 release.hide_archive_links = 隱藏自動產生的封存 settings.protect_no_valid_status_check_patterns = 沒有有效的狀態檢查式樣。 settings.enforce_on_admins = 為儲存庫管理員強制執行此規則 -settings.wiki_rename_branch_main_notices_2 = 這將永久重新命名儲存庫 %s 的 Wiki 的內部分支。現有的簽出將需要更新。 +settings.wiki_rename_branch_main_notices_2 = 這將永久重新命名儲存庫 %s 的百科的內部分支。現有的簽出將需要更新。 settings.discord_icon_url.exceeds_max_length = 圖示網址長度必須小於或等於 2048 個字符 -settings.wiki_branch_rename_success = 儲存庫 Wiki 的分支名稱已成功規範化。 +settings.wiki_branch_rename_success = 儲存庫百科的分支名稱已成功規範化。 commits.view_single_diff = 查看此提交中對此提交的變更 issues.new.assign_to_me = 指派給我 mirror_denied_combination = 不能組合使用公鑰和基於密碼的驗證。 @@ -2805,9 +2834,9 @@ settings.federation_following_repos = 關注儲存庫的網址。以半形分號 settings.federation_not_enabled = 你的站點上未啟用聯邦。 settings.federation_apapiurl = 此儲存庫的聯邦網址。將其複製並貼上至另一個儲存庫的聯邦設定中作為關注儲存庫的網址。 settings.enter_repo_name = 準確輸入擁有者和儲存庫名稱,如下所示: -settings.wiki_rename_branch_main = 規範化 Wiki 分支名稱 -settings.wiki_branch_rename_failure = 無法規範化儲存庫 Wiki 的分支名稱。 -settings.confirm_wiki_branch_rename = 重新命名 Wiki 分支 +settings.wiki_rename_branch_main = 規範化百科分支名稱 +settings.wiki_branch_rename_failure = 無法規範化儲存庫百科的分支名稱。 +settings.confirm_wiki_branch_rename = 重新命名百科分支 settings.transfer_quota_exceeded = 新擁有者(%s)已超出配額。儲存庫尚未轉移。 settings.wiki_rename_branch_main_notices_1 = 此操作無法撤銷。 settings.push_mirror_sync_in_progress = 目前正在將變更推送至遠端 %s。 @@ -2818,13 +2847,13 @@ pulls.delete_after_merge.head_branch.is_protected = 你要刪除的頭分支是 pulls.delete_after_merge.head_branch.insufficient_branch = 你沒有權限刪除頭分支。 settings.pull_mirror_sync_in_progress = 目前正在從遠端 %s 拉取變更。 settings.pull_mirror_sync_quota_exceeded = 配額已超出,不拉取變更。 -settings.wiki_globally_editable = 允許任何人編輯 Wiki +settings.wiki_globally_editable = 允許任何人編輯百科 settings.transfer_abort_success = 轉移儲存庫至 %s 已成功取消。 settings.add_collaborator_blocked_them = 無法新增協作者,因為他們已封鎖儲存庫擁有者。 settings.add_webhook.invalid_path = 路徑不能包含「.」、「..」或空字串。它不能以斜線開頭或結尾。 settings.webhook.test_delivery_desc_disabled = 要使用虛假事件測試此 Webhook,請啟動它。 settings.webhook.replay.description_disabled = 要重播此 Webhook,請啟動它。 -settings.wiki_rename_branch_main_desc = 將 Wiki 內部使用的分支重新命名為「%s」。此變更是永久性的,無法撤消。 +settings.wiki_rename_branch_main_desc = 將百科內部使用的分支重新命名為「%s」。此變更是永久性的,無法撤消。 settings.mirror_settings.push_mirror.copy_public_key = 複製公鑰 settings.default_update_style_desc = 用於更新落後於基礎分支的合併請求的預設更新模式。 summary_card_alt = 儲存庫 %s 的摘要卡 @@ -2858,12 +2887,40 @@ settings.units.units = 功能 diff.git-notes.add = 增加註釋 diff.git-notes.remove-header = 移除註釋 settings.event_pull_request_enforcement = 執行 -sync_fork.branch_behind_few = 此分支落後 %[2]s %[1]d 次提交 +sync_fork.branch_behind_few = 此分支落後 %[2]s 共 %[1]d 次提交 sync_fork.button = 同步 sync_fork.branch_behind_one = 此分支落後 %[2]s %[1]d 次提交 +issues.review.remove_review_requests = 移除了對 %[1]s 的審查請求 %[2]s +issues.filter_no_results_placeholder = 嘗試調整您的搜尋篩選條件。 +issues.reaction.alt_add = 對留言添加 %[1]s 個反應。 +issues.reopen.blocked_by_user = 您無法重新開啟此問題,因為您已被儲存庫擁有者或此問題的發佈者封鎖。 +issues.filter_no_results = 沒有結果 +archive.nocomment = 您無法留言,因為此儲存庫已被封存。 +migrate.repo_desc_helper = 如欲匯入現有描述請留空 +comment.blocked_by_user = 您無法留言,因為您已被儲存庫擁有者或作者封鎖。 +pulls.editable_explanation = 此合併請求允許維護者進行編輯。您可以直接參與貢獻。 +pulls.agit_explanation = 此合併請求使用 AGit 工作流程建立。AGit 允許貢獻者透過「git push」提出變更,無需建立分支或分叉。 +issues.review.add_remove_review_requests = 請求 %[1]s 進行審查,並取消了對 %[2]s 的審查請求 %[3]s +pulls.cmd_instruction_merge_warning = 警告:此儲存庫尚未啟用「自動偵測手動合併」設定,您之後需要手動標記此合併請求為已合併。 +issues.review.add_review_requests = 請求 %[1]s 進行審查 %[2]s +settings.event_header_action = Action 執行事件 +settings.event_action_failure = 失敗 +settings.event_action_recover = 恢復 +settings.event_action_recover_desc = Action 執行成功,先前同一工作流程中的 Action 執行曾經失敗。 +settings.event_action_success = 成功 +settings.event_action_success_desc = Action 執行成功。 +settings.sourcehut_builds.manifest_path = 建置清單路徑 +settings.sourcehut_builds.visibility = 作業可見性 +settings.event_action_failure_desc = Action 執行以失敗結束。 +settings.protect_branch_name_pattern_desc = 受保護分支名稱的模式。請參閱文件說明以了解模式語法。範例:main、release/** +settings.sourcehut_builds.access_token_helper = 具有 JOBS:RW 權限的存取權杖。請在 meta.sr.ht 上產生一個 builds.sr.ht 權杖,或一個 具有機密存取權限的 builds.sr.ht 權杖。 +settings.matrix.room_id_helper = 可從 Element 網頁版客戶端取得 Room ID:進入房間設定 > 進階 > Internal room ID。範例:%s。 +settings.web_hook_name_sourcehut_builds = SourceHut Builds +diff.git-notes.remove-body = 此注釋將會被移除。 +settings.sourcehut_builds.secrets_helper = 授權此作業存取建置機密(需要 SECRETS:RO 權限) [graphs] -component_loading = %s載入中… +component_loading = 正在載入 %s… code_frequency.what = 寫程式頻率 recent_commits.what = 最近的提交 contributors.what = 貢獻 @@ -2906,7 +2963,7 @@ settings.permission=權限 settings.repoadminchangeteam=儲存庫管理者可增加與移除團隊權限 settings.visibility=瀏覽權限 settings.visibility.public=公開 -settings.visibility.limited=受限(只有已驗證的使用者才可以看到) +settings.visibility.limited=受限(僅對已登入使用者可見) settings.visibility.limited_shortname=受限 settings.visibility.private=私有(只有組織成員才能看到) settings.visibility.private_shortname=私有 @@ -3082,8 +3139,8 @@ dashboard.total_gc_time=總 GC 暫停時間 dashboard.total_gc_pause=總 GC 暫停時間 dashboard.last_gc_pause=上次 GC 暫停時間 dashboard.gc_times=GC 執行次數 -dashboard.delete_old_actions=從資料庫刪除所有舊行為 -dashboard.delete_old_actions.started=從資料庫刪除所有舊行為的任務已啟動。 +dashboard.delete_old_actions=從資料庫刪除所有舊操作紀錄 +dashboard.delete_old_actions.started=從資料庫刪除所有舊操作紀錄的任務已啟動。 dashboard.update_checker=更新檢查器 dashboard.delete_old_system_notices=從資料庫刪除所有舊系統提示 dashboard.gc_lfs=對 LFS meta objects 進行垃圾回收 @@ -3103,7 +3160,7 @@ users.repos=儲存庫數 users.created=建立時間 users.last_login=上次登入 users.never_login=從未登入 -users.send_register_notify=寄送使用者註冊通知 +users.send_register_notify=透過電子郵件傳送註冊通知 users.new_success=已建立新帳號「%s」。 users.edit=編輯 users.auth_source=認證來源 @@ -3119,7 +3176,7 @@ users.prohibit_login=已停權帳號 users.is_admin=管理員帳號 users.is_restricted=受限制的帳號 users.allow_git_hook=可以建立 Git Hook -users.allow_git_hook_tooltip=Git Hook 將以和 Forgejo 相同的作業系統使用者執行,並擁有同等的主機存取權限。因此擁有此特殊 Git Hook 權限的使用者可存取和修改所有的 Forgejo 儲存庫和 Forgejo 的資料庫。他們甚至能取得 Forgejo 的管理員權限。 +users.allow_git_hook_tooltip=Git hooks 會以執行 Forgejo 的作業系統使用者身分執行,並擁有相同等級的主機存取權限。因此,擁有此特殊 Git hook 權限的使用者,可以存取並修改所有 Forgejo 儲存庫,以及 Forgejo 使用的資料庫。換句話說,他們也具備取得 Forgejo 管理員權限的能力。 users.allow_import_local=可以匯入本地儲存庫 users.allow_create_organization=可以建立組織 users.update_profile=更新使用者帳號 @@ -3321,7 +3378,7 @@ config.custom_file_root_path=自訂檔案根路徑 config.domain=伺服器域名 config.offline_mode=本地模式 config.disable_router_log=停用路由日誌 -config.run_user=以使用者名稱執行 +config.run_user=以使用者執行 config.run_mode=執行模式 config.git_version=Git 版本 config.repo_root_path=儲存庫根路徑 @@ -3426,8 +3483,8 @@ config.enable_federated_avatar=啟用聯邦式大頭貼 config.git_config=Git 設定 config.git_disable_diff_highlight=停用比較語法高亮 -config.git_max_diff_lines=差異比較時顯示的最多行數 (單檔) -config.git_max_diff_line_characters=差異比較時顯示的最多字元數 (單行) +config.git_max_diff_lines=差異比較時顯示的最多行數 +config.git_max_diff_line_characters=差異比較時顯示的最多字元數 config.git_max_diff_files=差異比較時顯示的最多檔案數 config.git_gc_args=GC 參數 config.git_migrate_timeout=遷移逾時 @@ -3585,7 +3642,7 @@ mirror_sync_create=從鏡像同步了新參考 %[3]s 到 %[3]s 刪除了參考 %[2]s approve_pull_request=`核可了 %[3]s#%[2]s` reject_pull_request=`提出了修改建議 %[3]s#%[2]s` -publish_release=`發布了 %[3]s 的 "%[4]s" ` +publish_release=`已於 %[3]s 發佈 %[4]s` review_dismissed=`取消了 %[4]s 對 %[3]s#%[2]s 的審核` review_dismissed_reason=原因: create_branch=在 %[4]s 中建立了分支 %[3]s @@ -3803,7 +3860,7 @@ arch.version.depends = 依賴 owner.settings.cargo.rebuild.no_index = 無法重建,未初始化任何索引。 cran.registry = 在你的 Rprofile.site 檔案中設定此註冊表: debian.repository.distributions = 發行版 -owner.settings.chef.keypair.description = 需要金鑰對才能向 Chef 註冊表進行身份驗證。如果你之前已經產生過金鑰對,產生新的金鑰對將會丟棄舊的金鑰對。 +owner.settings.chef.keypair.description = 送往 Chef 註冊表的請求必須經過加密簽署,以作為驗證身分的方式。在產生金鑰對時,只有公鑰會儲存在 Forgejo 上,私鑰則會提供給你用於 knife 工具。產生新的金鑰對將會覆蓋先前的金鑰。 owner.settings.cargo.initialize.description = 使用 Cargo 註冊表需要一個特殊的索引 Git 儲存庫。使用此選項將會(重新)建立儲存庫並自動配置它。 rpm.repository.multiple_groups = 此套件可以在多個群組中使用。 rpm.distros.suse = 在基於 SUSE 的發行版上 @@ -3825,6 +3882,7 @@ registry.documentation = 有關 %s 註冊表的更多資訊,請參閱讀取:查看並建立問題與留言。 +releases.write = 寫入:發布、編輯與刪除發行版本及其資源。 +wiki.read = 讀取:查看整合的 Wiki 及其歷史紀錄。 +code.write = 寫入:推送到儲存庫,建立分支與標籤。 +code.read = 讀取:存取並複製此儲存庫的原始碼。 +issues.write = 寫入:關閉問題,並管理標籤、里程碑、指派對象、到期日與依賴關係等中繼資料。 +pulls.read = 讀取:查看並建立合併請求。 +pulls.write = 寫入:關閉合併請求,並管理標籤、里程碑、指派對象、到期日與依賴關係等中繼資料。 +releases.read = 讀取:檢視並下載發行版本。 +wiki.write = 寫入:在整合的 Wiki 中建立、更新與刪除頁面。 +projects.read = 讀取:存取儲存庫的專案看板。 +packages.write = 寫入:發布與刪除指派給此儲存庫的套件。 +projects.write = 寫入:建立專案與欄位,並編輯它們。 +packages.read = 讀取:檢視並下載指派給此儲存庫的套件。 +actions.read = 讀取:查看整合的 CI/CD 流程及其紀錄。 +actions.write = 寫入:手動觸發、重新啟動、取消或核准待處理的 CI/CD 流程。 \ No newline at end of file diff --git a/options/locale_next/locale_bg.json b/options/locale_next/locale_bg.json index 02144c8b38..1faf269aba 100644 --- a/options/locale_next/locale_bg.json +++ b/options/locale_next/locale_bg.json @@ -1,10 +1,89 @@ { - "repo.pulls.merged_title_desc": { - "one": "сля %[1]d подаване от %[2]s в %[3]s %[4]s", - "other": "сля %[1]d подавания от %[2]s в %[3]s %[4]s" - }, - "repo.pulls.title_desc": { - "one": "иска да слее %[1]d подаване от %[2]s в %[3]s", - "other": "иска да слее %[1]d подавания от %[2]s в %[3]s" - } + "repo.pulls.merged_title_desc": { + "one": "сля %[1]d подаване от %[2]s в %[3]s %[4]s", + "other": "сля %[1]d подавания от %[2]s в %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "иска да слее %[1]d подаване от %[2]s в %[3]s", + "other": "иска да слее %[1]d подавания от %[2]s в %[3]s" + }, + "mail.actions.run_info_ref": "Клон: %[1]s (%[2]s)", + "mail.actions.run_info_trigger": "Задействано поради: %[1]s от: %[2]s", + "meta.last_line": "В България расте най-старото дърво в страната, Байкушевата мура, на възраст над 1300 години.", + "relativetime.1day": "вчера", + "relativetime.2months": "преди два месеца", + "home.explore_repos": "Разглеждане на хранилища", + "home.explore_users": "Разглеждане на потребители", + "home.explore_orgs": "Разглеждане на организации", + "relativetime.mins": { + "one": "преди %d минута", + "other": "преди %d минути" + }, + "repo.form.cannot_create": "Всички пространства, в които можете да създавате хранилища, са достигнали лимита си на хранилища.", + "moderation.report_remarks": "Подробности", + "moderation.submit_report": "Изпращане на доклада", + "followers.incoming.list.self.none": "Никой не следва вашия профил.", + "followers.incoming.list.none": "Никой не следва този потребител.", + "relativetime.now": "сега", + "home.welcome.no_activity": "Няма дейност", + "relativetime.1year": "миналата година", + "moderation.abuse_category": "Категория", + "moderation.abuse_category.illegal_content": "Незаконно съдържание", + "home.welcome.activity_hint": "Все още няма нищо в емисията ви. Вашите действия и дейност от хранилищата, които наблюдавате, ще се появят тук.", + "stars.list.none": "Никой не е отбелязал това хранилище със звезда.", + "moderation.report_abuse_form.invalid": "Невалидни аргументи", + "moderation.report_abuse_form.already_reported": "Вече сте докладвали това съдържание", + "moderation.abuse_category.placeholder": "Изберете категория", + "moderation.abuse_category.spam": "Спам", + "repo.issue_indexer.title": "Индексатор на задачи", + "moderation.report_abuse_form.details": "Този формуляр трябва да се използва за докладване на потребители, които създават спам профили, хранилища, задачи, коментари или се държат неподходящо.", + "moderation.report_remarks.placeholder": "Моля, предоставете подробности относно злоупотребата, за която докладвате.", + "moderation.reporting_failed": "Не може да се изпрати новият доклад за злоупотреба: %v", + "moderation.reported_thank_you": "Благодарим ви за доклада. Администрацията е уведомена.", + "error.not_found.title": "Страницата не е намерена", + "incorrect_root_url": "Тази инстанция на Forgejo е конфигурирана да се сервира на \"%s\". В момента разглеждате Forgejo през друг URL адрес, което може да доведе до неправилно функциониране на части от приложението. Каноничният URL адрес се контролира от администраторите на Forgejo чрез настройката ROOT_URL в app.ini.", + "themes.names.forgejo-dark": "Forgejo тъмна", + "themes.names.forgejo-auto": "Forgejo (следване на системната тема)", + "themes.names.forgejo-light": "Forgejo светла", + "watch.list.none": "Никой не наблюдава това хранилище.", + "followers.outgoing.list.self.none": "Не следвате никого.", + "followers.outgoing.list.none": "%s не следва никого.", + "relativetime.future": "в бъдеще", + "relativetime.2days": "преди два дни", + "relativetime.1week": "миналата седмица", + "relativetime.2weeks": "преди две седмици", + "relativetime.1month": "миналия месец", + "relativetime.2years": "преди две години", + "moderation.report_abuse_form.header": "Докладване на злоупотреба до администратора", + "moderation.abuse_category.malware": "Зловреден софтуер", + "moderation.abuse_category.other_violations": "Други нарушения на правилата на платформата", + "alert.asset_load_failed": "Неуспешно зареждане на файлове с ресурси от {path}. Моля, уверете се, че файловете с ресурси са достъпни.", + "alert.range_error": " трябва да бъде число между %[1]s и %[2]s.", + "install.invalid_lfs_path": "Не може да се създаде LFS корен в посочения път: %[1]s", + "search.milestone_kind": "Търсене на етапи…", + "admin.config.moderation_config": "Конфигурация на модерацията", + "moderation.report_abuse": "Докладване на злоупотреба", + "moderation.report_content": "Докладване на съдържание", + "relativetime.hours": { + "one": "преди %d час", + "other": "преди %d часа" + }, + "relativetime.days": { + "one": "преди %d ден", + "other": "преди %d дни" + }, + "relativetime.weeks": { + "one": "преди %d седмица", + "other": "преди %d седмици" + }, + "relativetime.months": { + "one": "преди %d месец", + "other": "преди %d месеца" + }, + "relativetime.years": { + "one": "преди %d година", + "other": "преди %d години" + }, + "editor.textarea.tab_hint": "Редът вече е с отстъп. Натиснете отново Tab или Escape, за да излезете от редактора.", + "editor.textarea.shift_tab_hint": "Няма отстъп на този ред. Натиснете отново Shift + Tab или Escape, за да излезете от редактора." } diff --git a/options/locale_next/locale_cs-CZ.json b/options/locale_next/locale_cs-CZ.json index 95c230e37a..97a8536d4f 100644 --- a/options/locale_next/locale_cs-CZ.json +++ b/options/locale_next/locale_cs-CZ.json @@ -72,5 +72,38 @@ "one": "Před %d měsícem", "few": "Před %d měsíci", "other": "Před %d měsíci" - } + }, + "moderation.report_content": "Nahlásit obsah", + "moderation.report_abuse_form.details": "Tento formulář je určen k nahlašování uživatelů, kteří si vytvářejí spamové profily, repozitáře, problémy, komentáře nebo se chovají nevhodně.", + "admin.config.moderation_config": "Nastavení moderování", + "moderation.report_abuse": "Nahlásit zneužití", + "moderation.report_abuse_form.header": "Nahlásit zneužití administrátorovi", + "moderation.report_abuse_form.invalid": "Neplatné argumenty", + "moderation.report_abuse_form.already_reported": "Tento obsah jste již nahlásili", + "moderation.abuse_category": "Kategorie", + "moderation.submit_report": "Odeslat hlášení", + "moderation.reporting_failed": "Nepodařilo se odeslat nové hlášení zneužití: %v", + "moderation.report_remarks.placeholder": "Zadejte prosím podrobnosti o zneužití, které nahlašujete.", + "moderation.reported_thank_you": "Děkujeme za vaše hlášení. Správa platformy na ni byla upozorněna.", + "moderation.report_remarks": "Informace", + "moderation.abuse_category.placeholder": "Vyberte kategorii", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Nezákonný obsah", + "moderation.abuse_category.other_violations": "Jiné porušení pravidel platformy", + "repo.form.cannot_create": "Všechny prostory, ve kterých můžete vytvářet repozitáře, dosáhly svého limitu.", + "repo.issue_indexer.title": "Indexování problémů", + "watch.list.none": "Tento repozitář nikdo nesleduje.", + "followers.incoming.list.self.none": "Váš profil nikdo nesleduje.", + "followers.incoming.list.none": "Tohoto uživatele nikdo nesleduje.", + "followers.outgoing.list.none": "%s nikoho nesleduje.", + "stars.list.none": "Tento repozitář si nikdo nepřidal do oblíbených.", + "followers.outgoing.list.self.none": "Nikoho nesledujete.", + "editor.textarea.tab_hint": "Řádek je již odsazen. Pro opuštění editoru stiskněte znovu Tab nebo Escape.", + "editor.textarea.shift_tab_hint": "Na tomto řádku není žádné odsazení. Pro opuštění editoru stiskněte znovu Shift + Tab nebo Escape.", + "admin.dashboard.cleanup_offline_runners": "Vymazat offline runnery", + "settings.visibility.description": "Viditelnost profilu ovlivňuje možnost ostatních přistupovat k vašim veřejným repozitářům. Zjistit více", + "avatar.constraints_hint": "Velikost vlastního avataru nesmí překročit %[1]s nebo být větší než %[2]dx%[3]d pixelů", + "repo.diff.commit.next-short": "Další", + "repo.diff.commit.previous-short": "Předchozí" } diff --git a/options/locale_next/locale_da.json b/options/locale_next/locale_da.json index 31e17c1d10..8315e06bcc 100644 --- a/options/locale_next/locale_da.json +++ b/options/locale_next/locale_da.json @@ -64,5 +64,38 @@ "other": "%d år siden" }, "relativetime.2days": "2 dage siden", - "relativetime.1month": "sidste måned" + "relativetime.1month": "sidste måned", + "repo.form.cannot_create": "Alle områder, hvor du kan oprette depoter, har nået grænsen for antal depoter.", + "repo.issue_indexer.title": "Problemindekser", + "moderation.report_remarks": "Bemærkninger", + "admin.config.moderation_config": "Moderationskonfiguration", + "moderation.report_abuse": "Rapportér misbrug", + "moderation.report_content": "Rapportér indhold", + "moderation.report_abuse_form.header": "Rapportér misbrug til administrator", + "moderation.report_abuse_form.details": "Denne formular skal bruges til at rapportere brugere, der opretter spamprofiler, arkiver, problemer, kommentarer eller opfører sig upassende.", + "moderation.report_abuse_form.invalid": "Ugyldige argumenter", + "moderation.report_abuse_form.already_reported": "Du har allerede rapporteret dette indhold", + "moderation.abuse_category": "Kategori", + "moderation.abuse_category.placeholder": "Vælg en kategori", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Ulovligt indhold", + "moderation.abuse_category.other_violations": "Andre overtrædelser af platformens regler", + "moderation.report_remarks.placeholder": "Angiv venligst nogle detaljer vedrørende det misbrug, du anmelder.", + "moderation.submit_report": "Indsend rapport", + "moderation.reporting_failed": "Den nye misbrugsrapport kunne ikke indsendes: %v", + "moderation.reported_thank_you": "Tak for din rapport. Administrationen er blevet gjort opmærksom på den.", + "stars.list.none": "Ingen har markeret dette depot med en stjerne.", + "watch.list.none": "Ingen ser dette depot.", + "followers.incoming.list.self.none": "Ingen følger din profil.", + "followers.incoming.list.none": "Ingen følger denne bruger.", + "followers.outgoing.list.self.none": "Du følger ikke nogen.", + "followers.outgoing.list.none": "%s følger ikke nogen.", + "editor.textarea.tab_hint": "Linjen er allerede indrykket. Tryk på Tab igen eller Escape for at forlade editoren.", + "editor.textarea.shift_tab_hint": "Ingen indrykning på denne linje. Tryk på Shift + Tab igen eller Escape for at forlade editoren.", + "admin.dashboard.cleanup_offline_runners": "Ryd op offline runners", + "settings.visibility.description": "Profilsynlighed påvirker andres adgang til dine ikke-private depoter. Læs mere", + "avatar.constraints_hint": "Brugerdefineret avatar må ikke overstige %[1]s i størrelse eller være større end %[2]dx%[3]d pixels", + "repo.diff.commit.next-short": "Næste", + "repo.diff.commit.previous-short": "Forrige" } diff --git a/options/locale_next/locale_de-DE.json b/options/locale_next/locale_de-DE.json index cbc69826cb..3847de2b43 100644 --- a/options/locale_next/locale_de-DE.json +++ b/options/locale_next/locale_de-DE.json @@ -64,5 +64,43 @@ "other": "vor %d Minuten" }, "relativetime.2days": "vorgestern", - "relativetime.future": "in der Zukunft" + "relativetime.future": "in der Zukunft", + "admin.config.moderation_config": "Moderations-Konfiguration", + "moderation.report_abuse": "Missbrauch melden", + "moderation.report_content": "Inhalt melden", + "moderation.report_abuse_form.header": "Dem Administrator Missbrauch melden", + "moderation.report_abuse_form.details": "Dieses Formular soll genutzt werden um Benutzer zu melden die Spam-Profile, -Repositorys, -Issues, und -Kommentare erstellen, oder sich unangemessen verhalten.", + "moderation.report_abuse_form.invalid": "Ungültige Argumente", + "moderation.report_abuse_form.already_reported": "Du hast diesen Inhalt bereits gemeldet", + "moderation.abuse_category": "Kategorie", + "moderation.abuse_category.placeholder": "Eine Kategorie auswählen", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Illegaler Inhalt", + "moderation.abuse_category.other_violations": "Andere Verstöße gegen die Plattformregeln", + "moderation.report_remarks": "Anmerkungen", + "moderation.report_remarks.placeholder": "Bitte stelle Details über den von dir gemeldeten Missbrauch bereit.", + "moderation.submit_report": "Meldung absenden", + "moderation.reporting_failed": "Kann die neue Missbrauchsmeldung nicht absenden: %v", + "moderation.reported_thank_you": "Danke für deine Meldung. Die Administration wurde darüber in Kenntnis gesetzt.", + "repo.form.cannot_create": "Alle Orte, wo du Repositorys erstellen kannst, haben die Obergrenze an Repositorys erreicht.", + "repo.issue_indexer.title": "Issue-Indexer", + "watch.list.none": "Niemand beobachtet dieses Repo.", + "followers.incoming.list.self.none": "Niemand folgt deinem Profil.", + "followers.outgoing.list.self.none": "Du folgst niemanden.", + "followers.outgoing.list.none": "%s folgt niemanden.", + "stars.list.none": "Niemand hat dieses Repo favorisiert.", + "followers.incoming.list.none": "Niemand folgt diesem Benutzer.", + "editor.textarea.tab_hint": "Zeile bereits eingerückt. Drücke nochmals Tab oder Escape um den Editor zu verlassen.", + "editor.textarea.shift_tab_hint": "Keine Einrückung auf dieser Zeile. Drücke nochmals Shift + Tab oder Escape um den Editor zu verlassen.", + "admin.dashboard.cleanup_offline_runners": "Aufräumen der offline Runner", + "settings.visibility.description": "Die Profilsichtbarkeit beeinflusst die Möglichkeit anderer, auf deine nicht-privaten Repositorys zuzugreifen. Erfahre mehr", + "avatar.constraints_hint": "Individuelles Profilbild darf %[1]s in der Größe nicht überschreiten, und nicht größer als %[2]dx%[3]d Pixel sein", + "repo.diff.commit.next-short": "Nächste", + "repo.diff.commit.previous-short": "Vorherige", + "profile.edit.link": "Profil bearbeiten", + "feed.atom.link": "Atom-Feed", + "keys.ssh.link": "SSH-Schlüssel", + "keys.gpg.link": "GPG-Schlüssel", + "profile.actions.tooltip": "Mehr Aktionen" } diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index ec5c313a90..b1c98e4551 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -4,6 +4,12 @@ "home.explore_repos": "Explore repositories", "home.explore_users": "Explore users", "home.explore_orgs": "Explore organizations", + "stars.list.none": "No one starred this repo.", + "watch.list.none": "No one is watching this repo.", + "followers.incoming.list.self.none": "No one is following your profile.", + "followers.incoming.list.none": "No one is following this user.", + "followers.outgoing.list.self.none": "You are not following anyone.", + "followers.outgoing.list.none": "%s isn't following anyone.", "relativetime.now": "now", "relativetime.future": "in future", "relativetime.mins": { @@ -46,6 +52,8 @@ "one": "wants to merge %[1]d commit from %[2]s into %[3]s", "other": "wants to merge %[1]d commits from %[2]s into %[3]s" }, + "repo.form.cannot_create": "All spaces in which you can create repositories have reached the limit of repositories.", + "repo.issue_indexer.title": "Issue Indexer", "search.milestone_kind": "Search milestones…", "incorrect_root_url": "This Forgejo instance is configured to be served on \"%s\". You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.", "themes.names.forgejo-auto": "Forgejo (follow system theme)", @@ -55,6 +63,11 @@ "alert.asset_load_failed": "Failed to load asset files from {path}. Please make sure the asset files can be accessed.", "alert.range_error": " must be a number between %[1]s and %[2]s.", "install.invalid_lfs_path": "Unable to create the LFS root at the specified path: %[1]s", + "profile.actions.tooltip": "More actions", + "profile.edit.link": "Edit profile", + "feed.atom.link": "Atom feed", + "keys.ssh.link": "SSH keys", + "keys.gpg.link": "GPG keys", "admin.config.moderation_config": "Moderation configuration", "moderation.report_abuse": "Report abuse", "moderation.report_content": "Report content", @@ -81,6 +94,13 @@ "mail.actions.run_info_previous_status": "Previous Run's Status: %[1]s", "mail.actions.run_info_ref": "Branch: %[1]s (%[2]s)", "mail.actions.run_info_trigger": "Triggered because: %[1]s by: %[2]s", + "repo.diff.commit.next-short": "Next", + "repo.diff.commit.previous-short": "Prev", "discussion.locked": "This discussion has been locked. Commenting is limited to contributors.", + "editor.textarea.tab_hint": "Line already indented. Press Tab again or Escape to leave the editor.", + "editor.textarea.shift_tab_hint": "No indentation on this line. Press Shift + Tab again or Escape to leave the editor.", + "admin.dashboard.cleanup_offline_runners": "Cleanup offline runners", + "settings.visibility.description": "Profile visibility affects others' ability to access your non-private repositories. Learn more", + "avatar.constraints_hint": "Custom avatar may not exceed %[1]s in size or be larger than %[2]dx%[3]d pixels", "meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it." } diff --git a/options/locale_next/locale_fi-FI.json b/options/locale_next/locale_fi-FI.json index b9e6208440..cb26d76e66 100644 --- a/options/locale_next/locale_fi-FI.json +++ b/options/locale_next/locale_fi-FI.json @@ -20,7 +20,7 @@ "themes.names.forgejo-dark": "Forgejo, tumma", "alert.range_error": " täytyy olla numero välillä %[1]s ja %[2]s.", "alert.asset_load_failed": "Staattisen tiedoston lataus kohteesta {path} epäonnistui. Varmista, että staattisiin tiedostoihin pääsee käsiksi.", - "install.invalid_lfs_path": "LFS juurta ei voitu luoda polkuun: %[1]s", + "install.invalid_lfs_path": "LFS-juurta ei voitu luoda polkuun: %[1]s", "mail.actions.not_successful_run_subject": "Työnkulku %[1]s epäonnistui tietovarastossa %[2]s", "mail.actions.not_successful_run": "Työnkulku %[1]s epäonnistui tietovarastossa %[2]s", "discussion.locked": "Tämä keskustelu on lukittu. Kommentointi on rajoitettu avustajille.", @@ -57,7 +57,7 @@ "one": "%d viikko sitten", "other": "%d viikkoa sitten" }, - "meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it. Päivitä tämä käännös, jos luet tämän viestin.", + "meta.last_line": "Thank you for translating Forgejo! Päivitä tämä käännös, jos luet tämän viestin.", "relativetime.2months": "kaksi kuukautta sitten", "mail.actions.successful_run_after_failure_subject": "Työnkulku %[1]s palautettu tietovarastoon %[2]s", "mail.actions.successful_run_after_failure": "Työnkulku %[1]s palautettu tietovarastoon %[2]s", diff --git a/options/locale_next/locale_fil.json b/options/locale_next/locale_fil.json index 649f9d7acb..884a7b44eb 100644 --- a/options/locale_next/locale_fil.json +++ b/options/locale_next/locale_fil.json @@ -21,7 +21,7 @@ "alert.asset_load_failed": "Nabigong i-load ang mga asset file mula sa {path}. Siguraduhin na maa-access ang mga asset file.", "install.invalid_lfs_path": "Nabigong gawin ang LFS root sa tinakdang path: %[1]s", "alert.range_error": " dapat ay numero sa pagitan ng %[1]s at %[2]s.", - "meta.last_line": "Sayori... I love you. — MC from Doki Doki Literature Club", + "meta.last_line": "Every day, I imagine a future where I can be with you. In my hand is a pen that will write a poem of me and you. The ink flows down into a dark puddle... Just move your hand, write the way into his heart. But in this world of infinite choices. What will it take just to find that special day? Have I found everybody a fun assignment to do today? When you're here, everything that we do is fun for them anyway... When I can't even read my own feelings. What good are words when a smile says it all? And if this world won't write me an ending... What will it take just for me to have it all? Does my pen only write bitter words for those who are dear to me? Is it love if I take you, or is it love if I set you free? The ink flows down into a dark puddle... How can I write love into reality? If I can't hear the sound of your heartbeat What do you call love in your reality? And in your reality, if I don't know how to love you... I'll leave you be.", "mail.actions.successful_run_after_failure": "Na-recover ang workflow na %[1]s sa repositoryong %[2]s", "mail.actions.not_successful_run": "Nabigo ang workflow na %[1]s sa repositoryong %[2]s", "mail.actions.run_info_previous_status": "Nakaraang Status ng Run: %[1]s", @@ -64,5 +64,43 @@ }, "discussion.locked": "Naka-kandado ang pag-uusap na ito. Nilimitahan ang pagkomento sa mga tagatulong.", "relativetime.1month": "nakaraang buwan", - "relativetime.1year": "nakaraang taon" + "relativetime.1year": "nakaraang taon", + "moderation.report_abuse_form.details": "Dapat gamitin ang form na ito upang mag-ulat ng mga user na gumagawa ng mga spam na profile, repositoryo, isyu, komento, o kumikilos nang hindi naaangkop.", + "admin.config.moderation_config": "Pagsasaayos ng Moderation", + "moderation.report_abuse": "Mag-ulat ng pang aabuso", + "moderation.report_content": "Iulat ng nilalaman", + "moderation.report_abuse_form.header": "Mag-ulat ng pang aabuso sa tagapangasiwa", + "moderation.report_abuse_form.invalid": "Hindi wasto ang mga argumento", + "moderation.report_abuse_form.already_reported": "Inulat mo na ang nilalaman na ito", + "moderation.abuse_category": "Kategorya", + "moderation.abuse_category.placeholder": "Pumili ng kategorya", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Ilegal na nilalaman", + "moderation.abuse_category.other_violations": "Mga ibang paglabag sa mga patakaran ng platform", + "moderation.report_remarks": "Mga pahayag", + "moderation.report_remarks.placeholder": "Mangyaring magbigay ng mga detalye tungkol sa pang aabuso na inuulat mo.", + "moderation.submit_report": "I-submit ang ulat", + "moderation.reporting_failed": "Hindi ma-submit ang bagong ulat sa pang aabuso: %v", + "moderation.reported_thank_you": "Salamat sa iyong ulat. Naipaalam na ito sa administrasyon.", + "repo.form.cannot_create": "Naabot na ng lahat ng mga espasyo kung saan ka makakagawa ng mga repositoryo ang limitasyon ng mga repositoryo.", + "stars.list.none": "Wala pang nag-star ng repositoryong ito.", + "followers.incoming.list.self.none": "Walang sumusubaybay sa iyong profile.", + "repo.issue_indexer.title": "Indexer ng Isyu", + "watch.list.none": "Wala pang nanonood sa repositoryong ito.", + "followers.incoming.list.none": "Wala pang sumusunod sa user na ito.", + "followers.outgoing.list.self.none": "Hindi ka sumusunod ng anumang tao.", + "followers.outgoing.list.none": "Hindi sinusundan ni %s ang sinuman.", + "editor.textarea.tab_hint": "Naka-indent na ang linya. Pindutin ulit ang Tab o Escape para umalis sa editor.", + "editor.textarea.shift_tab_hint": "Walang indentation sa linyang ito. Pindutin ang Shift + Tab ulit o Escape para umalis sa editor.", + "admin.dashboard.cleanup_offline_runners": "Linisin ang mga offline na runner", + "settings.visibility.description": "Maaapektuhan ng visibility ng profile ang kakayahan ng iba na i-access ang iyong mga hindi pribadong repositoryo. Matuto pa", + "avatar.constraints_hint": "Hindi maaaring lumagpas sa laking %[1]s o mas malaki sa %[2]dx%[3]d pixel ang custom na avatar", + "repo.diff.commit.next-short": "Susunod", + "repo.diff.commit.previous-short": "Nakaraan", + "profile.edit.link": "I-edit ang profile", + "feed.atom.link": "Atom feed", + "keys.ssh.link": "Mga SSH key", + "keys.gpg.link": "Mga GPG key", + "profile.actions.tooltip": "Higit pang mga aksyon" } diff --git a/options/locale_next/locale_fr-FR.json b/options/locale_next/locale_fr-FR.json index cff682b10a..da26d56107 100644 --- a/options/locale_next/locale_fr-FR.json +++ b/options/locale_next/locale_fr-FR.json @@ -6,7 +6,7 @@ }, "repo.pulls.title_desc": { "one": "veut fusionner %[1]d commit depuis %[2]s vers %[3]s", - "many": "souhaite fusionner %[1]d révision(s) depuis %[2]s vers %[3]s", + "many": "veut fusionner %[1]d commits depuis %[2]s vers %[3]s", "other": "" }, "search.milestone_kind": "Recherche dans les jalons…", @@ -65,5 +65,43 @@ "error.not_found.title": "Page non trouvée", "relativetime.1month": "le mois dernier", "incorrect_root_url": "Cette instance Forgejo est configuré pour être servi sur \"%s\". Vous êtes actuellement en train de regarder Forgejo avec une URL différente, ce qui pourrait casser certaines parties de cette application. L'URL canonique est controllée par les administrateurs Forgejo grâce au paramètre ROOT_URL dans le app.ini.", - "meta.last_line": "Merci de traduire Forgejo ! Cette ligne n'est pas vue par les utilisateurs mais sert à d'autres fins dans la gestion de la traduction. Vous pouvez mettre une fun fact dans la traduction au lieu de la traduire. Miaou." + "meta.last_line": "Merci de traduire Forgejo ! Cette ligne n'est pas vue par les utilisateurs mais sert à d'autres fins dans la gestion de la traduction. Vous pouvez mettre une fun fact dans la traduction au lieu de la traduire. Miaou.", + "mail.actions.successful_run_after_failure": "Workflow %[1]s récupéré dans le dépôt %[2]s", + "mail.actions.successful_run_after_failure_subject": "Workflow %[1]s récupéré dans le dépôt %[2]s", + "mail.actions.not_successful_run_subject": "Workflow %[1]s a raté dans le dépôt %[2]s", + "mail.actions.not_successful_run": "Workflow %[1]s raté dans le dépôt %[2]s", + "mail.actions.run_info_cur_status": "Statut de cette exécution : %[1]s (vient de se mettre à jour de %[2]s)", + "mail.actions.run_info_previous_status": "Précédent statut de l'éxecution : %[1]s", + "moderation.reported_thank_you": "Merci pour votre signalement. L'administration a été mise au courant.", + "admin.config.moderation_config": "Configuration de la modération", + "moderation.report_content": "Contenu du rapport", + "moderation.report_abuse": "Signaler un rapport", + "moderation.report_abuse_form.header": "Signaler le rapport à l'administrateur", + "moderation.report_abuse_form.details": "Ce formulaire devrait être utilisé pour signaler des utilisateurs qui spam des comptes, des dépôts, des questions, des commentaires ou qui se comportent de façon inappropriée.", + "moderation.report_abuse_form.invalid": "Arguments invalides", + "moderation.report_abuse_form.already_reported": "Vous avez déjà signalisé ce contenu", + "moderation.abuse_category": "Catégorie", + "moderation.abuse_category.placeholder": "Sélectionnez une catégorie", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "logiciel malveillant", + "moderation.abuse_category.illegal_content": "Contenu illégal", + "moderation.abuse_category.other_violations": "Autres infractions des règles de la plateforme", + "moderation.report_remarks": "Remarques", + "moderation.report_remarks.placeholder": "S'il vous plaît fournissez quelques détails en rapport avec l'abus que vous signalez.", + "moderation.submit_report": "Soumettre le signalement", + "moderation.reporting_failed": "Impossible de soumettre le nouveau signalement : %v", + "followers.incoming.list.self.none": "Personne ne suit votre profile.", + "followers.incoming.list.none": "Personne ne suit cet utilisateur.", + "followers.outgoing.list.self.none": "Vous ne suivez personne.", + "followers.outgoing.list.none": "%s ne vous suit plus.", + "repo.issue_indexer.title": "Indexeur de problèmes", + "stars.list.none": "Personne n'a mis d'étoiles sur ce dépôt.", + "watch.list.none": "Personne ne consulte ce dépôt.", + "repo.form.cannot_create": "Tous les espaces dans lesquels vous pouvez créer des dépôts ont atteint la limite de dépôts.", + "admin.dashboard.cleanup_offline_runners": "Nettoyer les exécuteurs hors ligne", + "mail.actions.run_info_trigger": "Déclenché parce que : %[1]s par : %[2]s", + "settings.visibility.description": "La visibilité du profil affecte la capacité des autres à accéder à vos dépôts non-privés. Voir plus", + "editor.textarea.shift_tab_hint": "Pas d'indentation sur cette ligne. Appuyez sur Maj + Tab une nouvelle fois ou sur Échap pour quitter l'éditeur.", + "avatar.constraints_hint": "L'avatar personnalisé ne doit pas dépasser une taille de %[1]s ou être plus grand que %[2]dx%[3]d pixels", + "editor.textarea.tab_hint": "Ligne déjà indentée. Appuyez sur Tab une nouvelle fois ou sur Échap pour quitter l'éditeur." } diff --git a/options/locale_next/locale_gl.json b/options/locale_next/locale_gl.json index 0967ef424b..e83a2ecbd9 100644 --- a/options/locale_next/locale_gl.json +++ b/options/locale_next/locale_gl.json @@ -1 +1,39 @@ -{} +{ + "relativetime.now": "agora", + "relativetime.future": "máis adiante", + "repo.form.cannot_create": "Todos os espazos onde podes crear repositorios chegaron ao límite de repositorios creados.", + "followers.outgoing.list.self.none": "Non esas seguindo a ninguén.", + "followers.outgoing.list.none": "%s non está seguindo a ninguén.", + "relativetime.1day": "onte", + "relativetime.2days": "hai dous días", + "relativetime.1week": "a semana pasada", + "relativetime.2weeks": "hai dúas semanas", + "relativetime.1month": "hai un mes", + "relativetime.2months": "hai dous meses", + "relativetime.1year": "hai un ano", + "relativetime.2years": "hai dous anos", + "relativetime.mins": { + "one": "hai %d minuto", + "other": "hai %d minutos" + }, + "relativetime.hours": { + "one": "hai %d hora", + "other": "hai %d horas" + }, + "relativetime.days": { + "one": "hai %d día", + "other": "hai %d días" + }, + "relativetime.weeks": { + "one": "hai %d semana", + "other": "hai %d semanas" + }, + "relativetime.months": { + "one": "hai %d mes", + "other": "hai %d meses" + }, + "relativetime.years": { + "one": "hai %d ano", + "other": "hai %d anos" + } +} diff --git a/options/locale_next/locale_it-IT.json b/options/locale_next/locale_it-IT.json index 8bd6d811a0..8464d6244e 100644 --- a/options/locale_next/locale_it-IT.json +++ b/options/locale_next/locale_it-IT.json @@ -15,5 +15,83 @@ "home.welcome.no_activity": "Nessun'attività", "home.explore_repos": "Esplora i repositori", "home.explore_users": "Esplora l'utenza", - "home.explore_orgs": "Esplora le organizzazioni" + "home.explore_orgs": "Esplora le organizzazioni", + "mail.actions.successful_run_after_failure_subject": "Il flusso di lavoro %[1] ha ripreso a funzionare nel repositorio %[2]s", + "mail.actions.not_successful_run_subject": "Il flusso di lavoro %[1]s è fallito nel repositorio %[2]s", + "relativetime.future": "nel futuro", + "relativetime.days": { + "one": "ieri", + "many": "%d giorni fa", + "other": "%d giorni fa" + }, + "relativetime.1day": "ieri", + "repo.form.cannot_create": "Tutti gli spazi in cui puoi creare repositori hanno raggiunto il limite di repositori.", + "discussion.locked": "Questa discussione è stata bloccata. Solo i contributori possono commentare.", + "relativetime.hours": { + "one": "un'ora fa", + "many": "%d ore fa", + "other": "%d ore fa" + }, + "relativetime.2years": "due anni fa", + "relativetime.now": "adesso", + "relativetime.weeks": { + "one": "una settimana fa", + "many": "%d settimane fa", + "other": "%d settimane fa" + }, + "relativetime.months": { + "one": "un mese fa", + "many": "%d mesi fa", + "other": "%d mesi fa" + }, + "relativetime.years": { + "one": "un anno fa", + "many": "%d anni fa", + "other": "%d anni fa" + }, + "repo.issue_indexer.title": "Indicizzatore delle segnalazioni", + "admin.config.moderation_config": "Impostazioni di moderazione", + "moderation.report_abuse": "Segnala abuso", + "moderation.report_content": "Segnala contenuto", + "moderation.report_abuse_form.already_reported": "Hai già segnalato questo contenuto", + "moderation.abuse_category": "Categoria", + "moderation.abuse_category.placeholder": "Seleziona una categoria", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Contenuti illegali", + "moderation.abuse_category.other_violations": "Altre violazioni delle regole della piattaforma", + "moderation.report_remarks": "Note aggiuntive", + "moderation.report_remarks.placeholder": "Aggiungi dettagli riguardanti l'abuso che stai segnalando.", + "moderation.submit_report": "Invia segnalazione", + "error.not_found.title": "Pagina non trovata", + "themes.names.forgejo-auto": "Forgejo (segui le impostazioni di sistema)", + "stars.list.none": "Nessuno ha messo una stella a questo repo.", + "watch.list.none": "Nessuno sta osservando questo repo.", + "followers.incoming.list.self.none": "Nessuno sta seguendo il tuo profilo.", + "followers.incoming.list.none": "Nessuno sta seguendo questo utente.", + "followers.outgoing.list.self.none": "Non segui nessuno.", + "followers.outgoing.list.none": "%s non sta seguendo nessuno.", + "relativetime.2days": "due giorni fa", + "relativetime.2weeks": "due settimane fa", + "relativetime.1week": "la settimana scorsa", + "relativetime.1month": "il mese scorso", + "relativetime.2months": "due mesi fa", + "relativetime.1year": "l'anno scorso", + "moderation.report_abuse_form.header": "Segnala abuso all'amministratore", + "moderation.report_abuse_form.details": "Questo modulo dovrebbe essere utilizzato per segnalare utenti che creano profili, repositori, segnalazioni o commenti spam o che si comportano in modo non adeguato.", + "moderation.report_abuse_form.invalid": "Argomenti non validi", + "moderation.reporting_failed": "Impossibile inviare segnalazione: %v", + "moderation.reported_thank_you": "Grazie per la segnalazione. L'amministratore è stato avvertito.", + "mail.actions.run_info_ref": "Ramo: %[1]s (%[2]s)", + "alert.asset_load_failed": "Impossibile caricare i file di risorsa da {path}. Controlla che i file di risorsa siano accessibili.", + "install.invalid_lfs_path": "Non è possibile creare una root LFS nel percorso specificato: %[1]s", + "home.welcome.activity_hint": "Non c'è nulla nel tuo feed. Le tue azioni e le attività dei repositori che segui verranno mostrate qui.", + "relativetime.mins": { + "one": "un minuto fa", + "many": "%d minuti fa", + "other": "%d minuti fa" + }, + "editor.textarea.tab_hint": "Linea già indentata. Premi di nuovo Tab o Esc per uscire dall'editor.", + "repo.diff.commit.previous-short": "Precedente", + "meta.last_line": "Ambaraba cicci cocco." } diff --git a/options/locale_next/locale_lv-LV.json b/options/locale_next/locale_lv-LV.json index 66224cd567..f71cfa227d 100644 --- a/options/locale_next/locale_lv-LV.json +++ b/options/locale_next/locale_lv-LV.json @@ -72,5 +72,38 @@ "one": "pirms %d nedēļas", "other": "pirms %d nedēļām" }, - "relativetime.1month": "iepriekšējā mēnesī" + "relativetime.1month": "iepriekšējā mēnesī", + "moderation.abuse_category.other_violations": "Citi noteikumu pārkāpumi", + "moderation.reported_thank_you": "Paldies par ziņojumu! Pārvaldītājiem ir darīts zināms par to.", + "admin.config.moderation_config": "Satura pārraudzības konfigurācija", + "moderation.report_abuse": "Ziņot par pārkāpumu", + "moderation.report_content": "Ziņot par saturu", + "moderation.report_abuse_form.header": "Ziņot pārvaldītājam par pārkāpumu", + "moderation.report_abuse_form.details": "Šī veidlapa ir izmantojama, lai ziņotu par lietotājiem, kuri izveido mēstuļošanas profilus, glabātavas, pieteikumus un piebildes vai uzvedas nepienācīgi.", + "moderation.report_abuse_form.invalid": "Nederīgi argumenti", + "moderation.report_abuse_form.already_reported": "Tu jau ziņoji par šo saturu", + "moderation.abuse_category": "Kategorija", + "moderation.abuse_category.placeholder": "Atlasīt kategoriju", + "moderation.abuse_category.spam": "Mēstuļošana", + "moderation.abuse_category.malware": "Ļaunatūra", + "moderation.abuse_category.illegal_content": "Nelikumīgs saturs", + "moderation.report_remarks": "Piezīmes", + "moderation.report_remarks.placeholder": "Lūgums sniegt informāciju par pārkāpumu, par ko ziņo.", + "moderation.submit_report": "Iesniegt ziņojumu", + "moderation.reporting_failed": "Nevar iesniegt jauno ziņojumu par pārkāpumu: %v", + "repo.form.cannot_create": "Visas vietas, kurās vari izveidot glabātavas, ir sasniegušas glabātavu skaita ierobežojumu.", + "repo.issue_indexer.title": "Pieteikumu indeksētājs", + "watch.list.none": "Neviens nevēro šo glabātavu.", + "followers.incoming.list.none": "Neviens neseko šim lietotājam.", + "followers.outgoing.list.self.none": "Tu nevienam neseko.", + "followers.outgoing.list.none": "%s nevienam neseko.", + "stars.list.none": "Neviens šo glabātavu nav atzīmējis ar zvaigzni.", + "followers.incoming.list.self.none": "Neviens neseko Tavam profilam.", + "editor.textarea.tab_hint": "Rinda jau ir ar atkāpi. Spied Tab vēlreiz vai Escape, lai izietu no redaktora!", + "editor.textarea.shift_tab_hint": "Šajā rindā nav atkāpes. Spied Shift + Tab vēlreiz vai Escape, lai izietu no redaktora!", + "admin.dashboard.cleanup_offline_runners": "Notīrīt bezsaistes izpildītājus", + "settings.visibility.description": "Profila redzamība ietekmē iespēju citiem piekļūt Tavām glabātavām, kas nav privātas. Uzzināt vairāk", + "avatar.constraints_hint": "Pielāgots profila attēls nevar pārsniegt %[1]s vai būt lielāks par %[2]dx%[3]d pikseļiem", + "repo.diff.commit.next-short": "Nāk.", + "repo.diff.commit.previous-short": "Iepr." } diff --git a/options/locale_next/locale_nds.json b/options/locale_next/locale_nds.json index 52c3194bfd..24268e2082 100644 --- a/options/locale_next/locale_nds.json +++ b/options/locale_next/locale_nds.json @@ -64,5 +64,43 @@ "one": "vör %d Dag", "other": "vör %d Dagen" }, - "relativetime.2days": "vörgüstern" + "relativetime.2days": "vörgüstern", + "moderation.report_abuse": "Missbruuk mellen", + "moderation.report_content": "Inholl mellen", + "moderation.report_abuse_form.invalid": "Ungültige Argumenten", + "moderation.report_abuse_form.already_reported": "Du hest deesen Inholl al mellt", + "moderation.abuse_category": "Deel", + "moderation.abuse_category.placeholder": "Köör een Deel ut", + "moderation.abuse_category.spam": "Oolkert-Tüüg", + "moderation.abuse_category.illegal_content": "Verboden Inholl", + "moderation.abuse_category.other_violations": "Anner Verstöten tegen de Plattfoorms-Regels", + "moderation.report_remarks": "Anmarkens", + "moderation.submit_report": "Mellen ofschicken", + "moderation.reporting_failed": "Kann dat neje Missbruuk-Mellen nich ofschicken: %v", + "moderation.reported_thank_you": "Wees bedankt för dien Mellen. De Sied-Chefs hebben de Naricht daaröver kregen.", + "moderation.report_remarks.placeholder": "Bidde giff mehr Informatioonen över de Missbruuk, wat du mellst, an.", + "admin.config.moderation_config": "Moderatioons-Instellens", + "moderation.report_abuse_form.header": "Missbruuk an de Chef mellen", + "moderation.report_abuse_form.details": "Deeses Formular kann bruukt worden, um Brukers to mellen, wenn se Oolkert-Profilen, -Repositoriums, -Gefallens of -Kommentaren maken of sik unanstännig verhollen.", + "moderation.abuse_category.malware": "Schaa-Waar", + "repo.form.cannot_create": "All Rumen, waar du Repositoriums maken kannst, enthollen al de grootste Tahl vun verlöövt Repositoriums.", + "repo.issue_indexer.title": "Gefallen-Indizerer", + "followers.outgoing.list.none": "%s gaht nüms na.", + "watch.list.none": "Nüms beluurt deeses Repositorium.", + "followers.incoming.list.self.none": "Nüms gaht dienem Profil na.", + "followers.incoming.list.none": "Nüms gaht deesem Bruker na.", + "followers.outgoing.list.self.none": "Du gahst nüms na.", + "stars.list.none": "Nüms hett up deesem Repositorium eenen Steern sett.", + "editor.textarea.tab_hint": "Rieg al inschuven. Drück weer Tab of Esc, um de Bewarker to verlaten.", + "editor.textarea.shift_tab_hint": "Keen Inschuuv in deeser Rieg. Drück weer Umschalt+Tab of Esc, um de Bewarker to verlaten.", + "admin.dashboard.cleanup_offline_runners": "Nich verbunnen Lopers uprümen", + "settings.visibility.description": "De Profil-Sichtbaarkeid maakt daar wat an, of un wo anner Lüü diene nich-privaaten Repositoriums ankieken könen. Mehr unnerhören", + "avatar.constraints_hint": "Dat eegene Kontobill düür nich groter as %[1]s wesen of groter as %[2]d×%[3]d Billtüttels wesen", + "repo.diff.commit.next-short": "Anner", + "repo.diff.commit.previous-short": "Vörig", + "feed.atom.link": "Atom-Schuuv", + "keys.ssh.link": "SSH-Slötels", + "keys.gpg.link": "GPG-Slötels", + "profile.actions.tooltip": "Mehr Aktioonen", + "profile.edit.link": "Profil bewarken" } diff --git a/options/locale_next/locale_nl-NL.json b/options/locale_next/locale_nl-NL.json index 52971e734a..4f109825aa 100644 --- a/options/locale_next/locale_nl-NL.json +++ b/options/locale_next/locale_nl-NL.json @@ -1,11 +1,11 @@ { "repo.pulls.merged_title_desc": { "one": "heeft %[1]d commit van %[2]s samengevoegd in %[3]s %[4]s", - "other": "heeft %[1]d commits samengevoegd van %[2]s naar %[3]s %[4]s" + "other": "heeft %[1]d commits van %[2]s samengevoegd in %[3]s %[4]s" }, "repo.pulls.title_desc": { "one": "wil %[1]d commit van %[2]s samenvoegen in %[3]s", - "other": "wil %[1]d commits van %[2]s samenvoegen met %[3]s" + "other": "wil %[1]d commits van %[2]s samenvoegen in %[3]s" }, "search.milestone_kind": "Zoek mijlpalen…", "home.welcome.no_activity": "Geen activiteit", @@ -13,22 +13,84 @@ "home.explore_repos": "Verken repositories", "home.explore_users": "Verken gebruikers", "home.explore_orgs": "Verken organisaties", - "incorrect_root_url": "Deze Forgejo-instantie is geconfigureerd om geserveerd te worden op \"%s\". U bekijkt Forgejo momenteel via een andere URL, waardoor onderdelen van de applicatie kunnen breken. De canonieke URL kan worden gewijzigd door Forgejo admins via de ROOT_URL instelling in de app.ini.", + "incorrect_root_url": "Deze Forgejo-instantie is geconfigureerd om bereikbaar te zijn op \"%s\". U bekijkt Forgejo momenteel via een andere URL, waardoor onderdelen van de applicatie kunnen breken. De canonieke URL kan worden gewijzigd door Forgejo admins via de ROOT_URL instelling in de app.ini.", "themes.names.forgejo-auto": "Forgejo (volg het systeemthema)", "themes.names.forgejo-light": "Forgejo licht", "themes.names.forgejo-dark": "Forgejo donker", "error.not_found.title": "Pagina niet gevonden", - "alert.asset_load_failed": "Het laden van asset-bestanden van {path} is mislukt. Controleer of de asset-bestanden toegankelijk zijn.", - "install.invalid_lfs_path": "Kan de LFS-root niet aanmaken op het opgegeven pad: %[1]s", + "alert.asset_load_failed": "Het laden van hulp-bestanden vanuit {path} is mislukt. Controleer of de hulp-bestanden toegankelijk zijn.", + "install.invalid_lfs_path": "Kan de LFS-root niet aanmaken op de opgegeven locatie: %[1]s", "alert.range_error": " moet een getal zijn tussen %[1]s en %[2]s.", "meta.last_line": "Wist je dat de paprika, behalve in de bekende kleuren rood, geel, oranje en groen, ook in de kleuren wit, paars, lila, muntgroen en bruin voorkomt.", - "mail.actions.successful_run_after_failure_subject": "Werkstroom %[1]s hersteld in repository %[2]s", - "mail.actions.not_successful_run_subject": "Werkstroom %[1]s mislukt in repository %[2]s", - "mail.actions.successful_run_after_failure": "Werkstroom %[1]s hersteld in repository %[2]s", - "mail.actions.not_successful_run": "Werkstroom %[1]s mislukt in repository %[2]s", + "mail.actions.successful_run_after_failure_subject": "Werkstroom %[1]s hersteld in repositorie %[2]s", + "mail.actions.not_successful_run_subject": "Werkstroom %[1]s mislukt in repositorie %[2]s", + "mail.actions.successful_run_after_failure": "Werkstroom %[1]s hersteld in repositorie %[2]s", + "mail.actions.not_successful_run": "Werkstroom %[1]s mislukt in repositorie %[2]s", "mail.actions.run_info_cur_status": "De status van deze run: %[1]s (zojuist bijgewerkt van %[2]s)", "mail.actions.run_info_previous_status": "Status vorige run: %[1]s", "mail.actions.run_info_ref": "Branch: %[1]s (%[2]s)", "mail.actions.run_info_trigger": "Getriggerd omdat: %[1]s door: %[2]s", - "discussion.locked": "Deze discussie is gesloten. Commentaar is beperkt tot bijdragers." + "discussion.locked": "Deze discussie is afgesloten. Commentaar is alleen mogelijk voor bijdragers.", + "relativetime.now": "nu", + "relativetime.future": "in de toekomst", + "repo.form.cannot_create": "Alle ruimtes waarin u repositories kan maken hebben hun maximum aantal repositories bereikt.", + "moderation.report_content": "Meldt content", + "moderation.report_abuse_form.header": "Meld misbruik bij de beheerder", + "moderation.report_abuse_form.invalid": "Ongeldige argumenten", + "moderation.report_abuse_form.already_reported": "U heeft deze content reeds gerapporteerd", + "moderation.abuse_category": "Categorie", + "moderation.abuse_category.placeholder": "Selecteer een categorie", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.report_abuse_form.details": "Dit formulier dient gebruikt te worden om gebruikers te rapporteren die spamprofielen, -issues, -commentaren aanmaken of zich anderszins misdragen.", + "moderation.submit_report": "Verzend rapport", + "moderation.reporting_failed": "De nieuwe misbruikrapportage kan niet worden ingediend: %v", + "moderation.reported_thank_you": "Dank voor uw melding. Het beheer is hierover nu geïnformeerd.", + "moderation.report_remarks.placeholder": "Geef a.u.b. wat details betreffende het misbruik dat u meldt.", + "followers.outgoing.list.none": "%s volgt niemand.", + "relativetime.1day": "gisteren", + "relativetime.2days": "twee dagen geleden", + "relativetime.1week": "vorige week", + "relativetime.2weeks": "twee weken geleden", + "relativetime.1month": "vorige maand", + "relativetime.2months": "twee maanden geleden", + "relativetime.1year": "vorig jaar", + "relativetime.2years": "twee jaar geleden", + "moderation.abuse_category.illegal_content": "Illegale content", + "moderation.abuse_category.other_violations": "Andere overtreding van platformregels", + "moderation.report_remarks": "Opmerkingen", + "editor.textarea.tab_hint": "Regel springt reeds in. Type Tab nogmaals of Escape om de editor te verlaten.", + "editor.textarea.shift_tab_hint": "Geen inspringing op deze regel. Type Shift + Tab nogmaals of Escape om de editor te verlaten.", + "relativetime.days": { + "one": "%d dag geleden", + "other": "%d dagen geleden" + }, + "relativetime.weeks": { + "one": "%d week geleden", + "other": "%d weken geleden" + }, + "relativetime.months": { + "one": "%d maand geleden", + "other": "%d maanden geleden" + }, + "relativetime.years": { + "one": "%d jaar geleden", + "other": "%d jaren geleden" + }, + "admin.config.moderation_config": "Moderatie configuratie", + "moderation.report_abuse": "Meld misbruik", + "relativetime.mins": { + "one": "%d minuut geleden", + "other": "%d minuten geleden" + }, + "relativetime.hours": { + "one": "%d uur geleden", + "other": "%d uren geleden" + }, + "repo.issue_indexer.title": "Issue indexer", + "stars.list.none": "Niemand heeft deze repo een ster gegeven.", + "watch.list.none": "Niemand houdt deze repo in de gaten.", + "followers.incoming.list.self.none": "Niemand volgt uw profiel.", + "followers.incoming.list.none": "Deze gebruiker wordt door niemand gevolgd.", + "followers.outgoing.list.self.none": "U volgt niemand." } diff --git a/options/locale_next/locale_pt-BR.json b/options/locale_next/locale_pt-BR.json index a1fc84fc90..1a5eca6d34 100644 --- a/options/locale_next/locale_pt-BR.json +++ b/options/locale_next/locale_pt-BR.json @@ -72,5 +72,38 @@ "other": "%d horas atrás" }, "relativetime.2weeks": "duas semanas atrás", - "relativetime.now": "agora" + "relativetime.now": "agora", + "moderation.reported_thank_you": "Agradecemos pela sua denúncia. A administração foi notificada.", + "moderation.report_content": "Denunciar conteúdo", + "admin.config.moderation_config": "Configuração de moderação", + "moderation.report_abuse": "Denunciar abuso", + "moderation.report_abuse_form.header": "Denunciar abuso à administração", + "moderation.report_abuse_form.invalid": "Argumentos inválidos", + "moderation.report_abuse_form.already_reported": "Você já denunciou este conteúdo", + "moderation.abuse_category": "Categoria", + "moderation.abuse_category.placeholder": "Selecione uma categoria", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Conteúdo ilegal", + "moderation.abuse_category.other_violations": "Outras violações de regras da plataforma", + "moderation.report_remarks": "Observações", + "moderation.report_remarks.placeholder": "Por favor, forneça detalhes sobre o abuso que você está denunciando.", + "moderation.submit_report": "Enviar denúncia", + "moderation.reporting_failed": "Não foi possível enviar a denúncia: %v", + "moderation.report_abuse_form.details": "Este formulário deve ser utilizado para denunciar contas com perfis, repositórios, issues e comentários com spam ou que se comportam inapropriadamente.", + "repo.form.cannot_create": "Todos os espaços onde você pode criar repositórios atingiram o limite de repositórios.", + "repo.issue_indexer.title": "Indexador de Issues", + "watch.list.none": "Ninguém está observando este repositório.", + "followers.incoming.list.self.none": "Ninguém está seguindo seu perfil.", + "followers.incoming.list.none": "Ninguém está seguindo este perfil.", + "followers.outgoing.list.none": "%s não está seguindo ninguém.", + "stars.list.none": "Ninguém favoritou este repositório.", + "followers.outgoing.list.self.none": "Você não está seguindo ninguém.", + "editor.textarea.tab_hint": "Linha já indentada. Pressione Tab novamente ou Esc para sair do editor.", + "editor.textarea.shift_tab_hint": "Sem indentação nesta linha. Pressione Shift + Tab novamente ou Esc para sair do editor.", + "admin.dashboard.cleanup_offline_runners": "Limpar runners desconectados", + "avatar.constraints_hint": "Imagem de perfil personalizada não pode exceder %[1]s em tamanho ou ser maior que %[2]dx%[3]d pixels", + "settings.visibility.description": "A visibilidade do perfil afeta a habilidade de acessarem seus repositórios não-privados. Saiba mais", + "repo.diff.commit.next-short": "Próximo", + "repo.diff.commit.previous-short": "Anterior" } diff --git a/options/locale_next/locale_pt-PT.json b/options/locale_next/locale_pt-PT.json index 02d8087314..78e6dc4493 100644 --- a/options/locale_next/locale_pt-PT.json +++ b/options/locale_next/locale_pt-PT.json @@ -72,5 +72,36 @@ "other": "%d horas atrás" }, "relativetime.1week": "semana passada", - "relativetime.1day": "ontem" + "relativetime.1day": "ontem", + "moderation.report_remarks.placeholder": "Por favor forneça alguns pormenores sobre o abuso que está a denunciar.", + "admin.config.moderation_config": "Configuração de moderação", + "moderation.report_abuse": "Denunciar abuso", + "moderation.report_content": "Denunciar conteúdo", + "moderation.report_abuse_form.header": "Denunciar abuso à administração", + "moderation.report_abuse_form.details": "Este formulário deve ser utilizado para denunciar utilizadores que criam perfis, repositórios, questões, comentários de spam ou que se comportam de forma inadequada.", + "moderation.report_abuse_form.invalid": "Argumentos inválidos", + "moderation.report_abuse_form.already_reported": "Já denunciou este conteúdo", + "moderation.abuse_category": "Categoria", + "moderation.abuse_category.placeholder": "Escolha uma categoria", + "moderation.abuse_category.spam": "Spam", + "moderation.abuse_category.malware": "Malware", + "moderation.abuse_category.illegal_content": "Conteúdo ilegal", + "moderation.abuse_category.other_violations": "Outras violações das regras da plataforma", + "moderation.report_remarks": "Observações", + "moderation.submit_report": "Submeter denúncia", + "moderation.reporting_failed": "Não foi possível submeter a nova denúncia de abuso: %v", + "moderation.reported_thank_you": "Obrigado pela sua denúncia. A administração foi informada do facto.", + "repo.form.cannot_create": "Todos os espaços nos quais pode criar repositórios atingiram o limite de repositórios.", + "repo.issue_indexer.title": "Indexador de Questões", + "watch.list.none": "Ninguém está a vigiar este repositório.", + "followers.incoming.list.self.none": "Ninguém está a seguir o seu perfil.", + "followers.incoming.list.none": "Ninguém está a seguir este utilizador.", + "followers.outgoing.list.none": "%s não está a seguir ninguém.", + "followers.outgoing.list.self.none": "Não está a seguir ninguém.", + "editor.textarea.tab_hint": "Linha já indentada. Pressione Tab novamente ou Escape para sair do editor.", + "editor.textarea.shift_tab_hint": "Sem indentação nesta linha. Pressione Shift + Tab novamente ou Escape para sair do editor.", + "stars.list.none": "Ninguém juntou este repositório aos favoritos.", + "admin.dashboard.cleanup_offline_runners": "Limpeza de executores offline", + "settings.visibility.description": "A visibilidade do perfil afecta a capacidade de outros acederem aos seus repositórios não privados. Ler mais", + "avatar.constraints_hint": "O avatar personalizado não pode exceder %[1]s de tamanho ou ser maior do que %[2]dx%[3]d pixéis" } diff --git a/options/locale_next/locale_ru-RU.json b/options/locale_next/locale_ru-RU.json index 86011f8ac9..922e2612af 100644 --- a/options/locale_next/locale_ru-RU.json +++ b/options/locale_next/locale_ru-RU.json @@ -23,7 +23,7 @@ "alert.asset_load_failed": "Не удалось получить ресурсы из {path}. Убедитесь, что файлы ресурсов доступны.", "install.invalid_lfs_path": "Не удалось расположить корень LFS по указанному пути: %[1]s", "alert.range_error": " - число должно быть в диапазоне от %[1]s-%[2]s.", - "meta.last_line": "Triggering CI skip again.", + "meta.last_line": "Unskip..", "mail.actions.not_successful_run_subject": "Провал раб. потока %[1]s в репозитории %[2]s", "mail.actions.successful_run_after_failure_subject": "Возобновление раб. потока %[1]s в репозитории %[2]s", "mail.actions.run_info_ref": "Ветвь: %[1]s (%[2]s)", @@ -72,5 +72,43 @@ "few": "%d месяца назад", "many": "%d месяцев назад" }, - "relativetime.1year": "в прошлом году" + "relativetime.1year": "в прошлом году", + "repo.issue_indexer.title": "Индексатор задач", + "followers.incoming.list.self.none": "На вас никто не подписан.", + "followers.incoming.list.none": "На этого пользователя никто не подписан.", + "followers.outgoing.list.none": "%s ни на кого не подписан.", + "stars.list.none": "Никто не добавил этот репозиторий в избранное.", + "watch.list.none": "Никто не отслеживает этот репозиторий.", + "repo.form.cannot_create": "Во всех пространствах, где вы можете создавать репозитории, достигнуто ограничение количества репозиториев.", + "followers.outgoing.list.self.none": "Вы ни на кого не подписаны.", + "admin.config.moderation_config": "Настройки модерации", + "moderation.abuse_category": "Категория", + "moderation.abuse_category.placeholder": "Выберите категорию", + "moderation.abuse_category.spam": "Спам", + "moderation.abuse_category.malware": "Вредоносное ПО", + "moderation.abuse_category.illegal_content": "Незаконное содержимое", + "moderation.abuse_category.other_violations": "Прочие нарушения правил", + "moderation.report_remarks": "Подробности", + "moderation.report_abuse": "Пожаловаться", + "moderation.report_remarks.placeholder": "Пожалуйста, предоставьте немного подробностей о содержимом, на которое вы жалуетесь.", + "moderation.reporting_failed": "Не удалось отправить жалобу: %v", + "moderation.report_content": "Пожаловаться", + "moderation.report_abuse_form.already_reported": "Вы уже пожаловались на это содержимое", + "moderation.submit_report": "Пожаловаться", + "moderation.reported_thank_you": "Спасибо за ваше сообщение. Администрация оповещена.", + "moderation.report_abuse_form.details": "Через эту форму можно жаловаться на пользователей, распространяющих спам или ведущих себя неадекватно.", + "moderation.report_abuse_form.invalid": "Невалидные аргументы", + "moderation.report_abuse_form.header": "Жалоба администрации", + "editor.textarea.tab_hint": "Отступ уже добавлен. Нажмите Tab снова или Escape, чтобы покинуть редактор.", + "editor.textarea.shift_tab_hint": "В строке нет отступов. Нажмите Shift + Tab снова или Escape, чтобы покинуть редактор.", + "admin.dashboard.cleanup_offline_runners": "Удалить недоступных исполнителей", + "avatar.constraints_hint": "Изображение профиля не может быть более %[1]s и крупнее %[2]dx%[3]d пикселей", + "settings.visibility.description": "Видимость профиля влияет на доступ других до ваших не частных репозиториев. Подробнее", + "repo.diff.commit.previous-short": "Пред.", + "repo.diff.commit.next-short": "След.", + "profile.actions.tooltip": "Показать действия", + "feed.atom.link": "Atom-лента", + "keys.ssh.link": "Ключи SSH", + "keys.gpg.link": "Ключи GPG", + "profile.edit.link": "Изменить профиль" } diff --git a/options/locale_next/locale_sv-SE.json b/options/locale_next/locale_sv-SE.json index dc41e4d6e1..9a8762212c 100644 --- a/options/locale_next/locale_sv-SE.json +++ b/options/locale_next/locale_sv-SE.json @@ -1,5 +1,99 @@ { - "repo.pulls.merged_title_desc": "sammanfogade %[1]d incheckningar från %[2]s in i %[3]s %[4]s", - "repo.pulls.title_desc": "vill sammanfoga %[1]d incheckningar från s[2]s in i %[3]s", - "search.milestone_kind": "Sök milstolpar..." + "repo.pulls.merged_title_desc": { + "one": "sammanfogade %[1]d incheckning från %[2]s in i %[3]s %[4]s", + "other": "sammanfogade %[1]d incheckningar från %[2]s in i %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "vill sammanfoga %[1]d incheckning från s[2]s in i %[3]s", + "other": "vill sammanfoga %[1]d incheckningar från s[2]s in i %[3]s" + }, + "search.milestone_kind": "Sök milstolpar…", + "mail.actions.not_successful_run_subject": "Arbetsflödet %[1]s misslyckades i förrådet %[2]s", + "discussion.locked": "Denna diskussion har låsts. Kommentarer är begränsade till bidragsgivare.", + "relativetime.now": "nu", + "relativetime.1day": "igår", + "relativetime.2days": "för två dagar sedan", + "relativetime.days": { + "one": "%d dag sedan", + "other": "%d dagar sedan" + }, + "admin.dashboard.cleanup_offline_runners": "Städa upp offline runners", + "repo.issue_indexer.title": "Indexerare för utgåvor", + "moderation.reported_thank_you": "Tack för din rapport. Administrationen har gjorts uppmärksam på det.", + "themes.names.forgejo-light": "Forgejo ljus", + "themes.names.forgejo-dark": "Forgejo mörk", + "themes.names.forgejo-auto": "Forgejo (följ systemets tema)", + "moderation.submit_report": "Skicka in rapport", + "moderation.reporting_failed": "Det gick inte att skicka in den nya övergreppsrapporten: %v", + "mail.actions.run_info_cur_status": "Status för denna körning: %[1]s (just uppdaterad från %[2]s)", + "mail.actions.run_info_previous_status": "Status för föregående körning: %[1]s", + "mail.actions.run_info_ref": "Gren: %[1]s (%[2]s)", + "mail.actions.run_info_trigger": "Utlöses på grund av: %[1]s av: %[2]s", + "alert.asset_load_failed": "Misslyckades med att läsa in resursfiler från {path}. Kontrollera att resursfilerna är åtkomliga.", + "install.invalid_lfs_path": "Det gick inte att skapa LFS-roten på den angivna sökvägen: %[1]s", + "alert.range_error": " måste vara ett tal mellan %[1]s och %[2]s.", + "stars.list.none": "Ingen har stjärnmarkerat detta förråd.", + "watch.list.none": "Ingen tittar på det här förrådet.", + "followers.incoming.list.self.none": "Ingen följer din profil.", + "followers.incoming.list.none": "Ingen följer den här användaren.", + "followers.outgoing.list.self.none": "Du följer inte någon.", + "followers.outgoing.list.none": "%s följer inte någon.", + "relativetime.future": "i framtiden", + "relativetime.1week": "förra veckan", + "relativetime.2weeks": "för två veckor sedan", + "relativetime.1month": "senaste månaden", + "relativetime.2months": "för två månader sedan", + "relativetime.1year": "förra året", + "relativetime.2years": "för två år sedan", + "repo.form.cannot_create": "Alla utrymmen där du kan skapa förråd har nått gränsen för antal förråd.", + "incorrect_root_url": "Denna Forgejo-instans är konfigurerad att serveras på \"%s\". Du tittar för närvarande på Forgejo via en annan URL, vilket kan leda till att delar av applikationen bryts. Den kanoniska webbadressen styrs av Forgejo-administratörer via inställningen ROOT_URL i app.ini.", + "error.not_found.title": "Sidan hittades inte", + "moderation.abuse_category.illegal_content": "Olagligt innehåll", + "moderation.abuse_category.other_violations": "Andra överträdelser av plattformsreglerna", + "moderation.report_remarks": "Anmärkningar", + "moderation.report_remarks.placeholder": "Ge några detaljer om det övergrepp som du rapporterar.", + "mail.actions.successful_run_after_failure_subject": "Arbetsflöde %[1]s återställdes i förrådet %[2]s", + "mail.actions.successful_run_after_failure": "Arbetsflöde %[1]s återställdes i förrådet %[2]s", + "mail.actions.not_successful_run": "Arbetsflödet %[1]s misslyckades i förrådet %[2]s", + "editor.textarea.shift_tab_hint": "Ingen indragning på den här raden. Tryck på Shift + Tab igen eller Escape för att lämna redigeringsläget.", + "meta.last_line": "Daniel Nylander heter jag och har översatt Forgejo. Mer information om mig på https://www.danielnylander.se", + "editor.textarea.tab_hint": "Raden är redan indragen. Tryck på Tab igen eller Escape för att lämna redigeringsläget.", + "home.welcome.no_activity": "Ingen aktivitet", + "home.welcome.activity_hint": "Det finns inget i ditt flöde ännu. Dina åtgärder och aktivitet från förråd som du bevakar kommer att visas här.", + "home.explore_repos": "Utforska förråd", + "home.explore_users": "Utforska användare", + "home.explore_orgs": "Utforska organisationer", + "relativetime.mins": { + "one": "%d minut sedan", + "other": "%d minuter sedan" + }, + "relativetime.hours": { + "one": "%d timme sedan", + "other": "%d timmar sedan" + }, + "relativetime.weeks": { + "one": "%d vecka sedan", + "other": "%d veckor sedan" + }, + "relativetime.months": { + "one": "%d månad sedan", + "other": "%d månader sedan" + }, + "relativetime.years": { + "one": "%d år sedan", + "other": "%d år sedan" + }, + "admin.config.moderation_config": "Konfiguration av moderering", + "moderation.report_abuse": "Rapportera missbruk", + "moderation.report_content": "Rapportera innehåll", + "moderation.report_abuse_form.header": "Rapportera missbruk till administratör", + "moderation.report_abuse_form.details": "Detta formulär ska användas för att rapportera användare som skapar skräpprofiler, förråd, problem, kommentarer eller beter sig olämpligt.", + "moderation.report_abuse_form.invalid": "Ogiltiga argument", + "moderation.report_abuse_form.already_reported": "Du har redan rapporterat detta innehåll", + "moderation.abuse_category": "Kategori", + "moderation.abuse_category.placeholder": "Välj en kategori", + "moderation.abuse_category.spam": "Skräppost", + "moderation.abuse_category.malware": "Skadlig kod", + "settings.visibility.description": "Profilens synlighet påverkar andras möjlighet att komma åt dina icke-privata förråd. Läs mer", + "avatar.constraints_hint": "Anpassade avatarer får inte vara större än %[1] eller %[2]dx%[3] bildpunkter" } diff --git a/options/locale_next/locale_uk-UA.json b/options/locale_next/locale_uk-UA.json index 20f68b06ca..33cb5a41a3 100644 --- a/options/locale_next/locale_uk-UA.json +++ b/options/locale_next/locale_uk-UA.json @@ -72,5 +72,43 @@ "one": "%d рік тому", "few": "%d роки тому", "many": "%d років тому" - } + }, + "admin.config.moderation_config": "Конфігурація модерування", + "moderation.abuse_category.placeholder": "Виберіть категорію", + "moderation.report_abuse_form.invalid": "Недійсні аргументи", + "moderation.report_abuse_form.already_reported": "Ви вже скаржилися на цей вміст", + "moderation.report_abuse": "Повідомити про порушення", + "moderation.report_content": "Поскаржитися на вміст", + "moderation.abuse_category.spam": "Спам", + "moderation.report_remarks.placeholder": "Будь ласка, докладно опишіть порушення, про яке ви повідомляєте.", + "moderation.submit_report": "Надіслати скаргу", + "moderation.abuse_category.malware": "Шкідливе ПЗ", + "moderation.abuse_category.illegal_content": "Нелегальний вміст", + "moderation.report_abuse_form.header": "Повідомити адміністраторам про порушення", + "moderation.report_abuse_form.details": "Використовуйте цю форму, щоб повідомити про користувачів, які створюють спам-профілі, репозиторії, задачі, коментарі або поводяться неналежним чином.", + "moderation.abuse_category": "Категорія", + "moderation.abuse_category.other_violations": "Інші порушення правил платформи", + "moderation.reporting_failed": "Не вдалося надіслати повідомлення про порушення: %v", + "moderation.report_remarks": "Подробиці", + "moderation.reported_thank_you": "Дякуємо за ваше повідомлення. Адміністрацію поінформовано про нього.", + "repo.form.cannot_create": "У всіх просторах, де ви можете створювати репозиторії, досягнуто обмеження кількості репозиторіїв.", + "repo.issue_indexer.title": "Індексатор задач", + "followers.incoming.list.self.none": "Ніхто не стежить за вашим профілем.", + "followers.incoming.list.none": "Ніхто не стежить за цим користувачем.", + "followers.outgoing.list.self.none": "Ви ні за ким не стежите.", + "followers.outgoing.list.none": "%s ні за ким не стежить.", + "stars.list.none": "Ніхто не додав цей репозиторій в обрані.", + "watch.list.none": "Ніхто не спостерігає за цим репозиторієм.", + "editor.textarea.tab_hint": "У рядку вже є відступ. Натисніть Tab ще раз або Esc, щоб вийти з редактора.", + "editor.textarea.shift_tab_hint": "У цьому рядку немає відступів. Натисніть Shift + Tab ще раз або Esc, щоб вийти з редактора.", + "admin.dashboard.cleanup_offline_runners": "Очистити неактивні раннери", + "settings.visibility.description": "Видимість профілю впливає на можливість інших користувачів отримати доступ до ваших неприватних репозиторіїв. Дізнатися більше", + "avatar.constraints_hint": "Розмір користувацького аватара не може перевищувати %[1]s або бути більшим за %[2]d×%[3]d пікселів", + "repo.diff.commit.next-short": "Наступний", + "repo.diff.commit.previous-short": "Попередній", + "keys.ssh.link": "Ключі SSH", + "keys.gpg.link": "Ключі GPG", + "profile.edit.link": "Редагувати профіль", + "feed.atom.link": "Стрічка Atom", + "profile.actions.tooltip": "Більше дій" } diff --git a/options/locale_next/locale_zh-CN.json b/options/locale_next/locale_zh-CN.json index dab8832e9a..0f408997bf 100644 --- a/options/locale_next/locale_zh-CN.json +++ b/options/locale_next/locale_zh-CN.json @@ -40,5 +40,43 @@ "relativetime.2years": "两年前", "relativetime.now": "现在", "relativetime.2weeks": "两周前", - "relativetime.days": "%d 天前" + "relativetime.days": "%d 天前", + "moderation.report_abuse_form.details": "此表单用于举报创建垃圾个人信息、仓库、议题、评论或行为不当的用户。", + "admin.config.moderation_config": "审核配置", + "moderation.report_abuse": "举报滥用", + "moderation.report_content": "举报内容", + "moderation.report_abuse_form.header": "向管理员举报滥用", + "moderation.report_abuse_form.invalid": "参数无效", + "moderation.report_abuse_form.already_reported": "您已举报此内容", + "moderation.abuse_category": "类别", + "moderation.abuse_category.placeholder": "选择类别", + "moderation.abuse_category.spam": "垃圾信息", + "moderation.abuse_category.malware": "恶意软件", + "moderation.abuse_category.illegal_content": "非法内容", + "moderation.abuse_category.other_violations": "其他违反平台规则的行为", + "moderation.report_remarks.placeholder": "请提供一些关于您所举报的滥用行为的详细信息。", + "moderation.report_remarks": "备注", + "moderation.submit_report": "提交举报", + "moderation.reporting_failed": "无法提交新的滥用举报:%v", + "moderation.reported_thank_you": "感谢您的举报,管理员已收到通知。", + "repo.form.cannot_create": "您可以创建仓库的所有空间均已达到仓库限制。", + "repo.issue_indexer.title": "议题索引器", + "watch.list.none": "没有人关注这个仓库。", + "followers.incoming.list.none": "没有人关注这位用户。", + "followers.outgoing.list.self.none": "你没有关注任何人。", + "followers.outgoing.list.none": "%s 没有关注任何人。", + "stars.list.none": "没有人点赞这个仓库。", + "followers.incoming.list.self.none": "没有人关注你的个人资料。", + "editor.textarea.tab_hint": "此行已缩进。再次按 Tab 或按 Escape 退出编辑器。", + "editor.textarea.shift_tab_hint": "此行无缩进。再次按 Shift + Tab 或按 Escape 退出编辑器。", + "admin.dashboard.cleanup_offline_runners": "清理离线运行器", + "settings.visibility.description": "个人资料可见性设置会影响他人对您的非私有仓库的访问。了解更多", + "avatar.constraints_hint": "自定义头像大小不得超过 %[1]s,或大于 %[2]d×%[3]d 像素", + "keys.ssh.link": "SSH 密钥", + "keys.gpg.link": "GPG 密钥", + "profile.actions.tooltip": "更多操作", + "repo.diff.commit.next-short": "下个", + "repo.diff.commit.previous-short": "上个", + "feed.atom.link": "Atom 订阅源", + "profile.edit.link": "编辑个人资料" } diff --git a/options/locale_next/locale_zh-TW.json b/options/locale_next/locale_zh-TW.json index 222e8bb332..3ae0b00d2b 100644 --- a/options/locale_next/locale_zh-TW.json +++ b/options/locale_next/locale_zh-TW.json @@ -1,5 +1,77 @@ { "repo.pulls.merged_title_desc": "將 %[1]d 次提交從 %[2]s 合併至 %[3]s %[4]s", "repo.pulls.title_desc": "請求將 %[1]d 次程式碼提交從 %[2]s 合併至 %[3]s", - "search.milestone_kind": "搜尋里程碑…" + "search.milestone_kind": "搜尋里程碑…", + "home.welcome.no_activity": "沒有活動", + "home.welcome.activity_hint": "您的動態摘要目前沒有任何內容。 您對關注的儲存庫所做的操作與活動將會顯示在這裡。", + "stars.list.none": "沒有人標星這個儲存庫。", + "watch.list.none": "沒有人關注這個儲存庫。", + "home.explore_repos": "探索儲存庫", + "home.explore_users": "探索使用者", + "home.explore_orgs": "探索組織", + "alert.range_error": " 必須是一個介於 %[1]s 和 %[2]s 之間的數字。", + "install.invalid_lfs_path": "無法在指定路徑建立 LFS 根目錄:%[1]s", + "relativetime.now": "現在", + "relativetime.future": "未來", + "repo.form.cannot_create": "您可以建立儲存庫的所有空間都已達到儲存庫上限。", + "repo.issue_indexer.title": "問題索引器", + "moderation.abuse_category": "分類", + "moderation.abuse_category.placeholder": "選擇分類", + "moderation.abuse_category.spam": "垃圾訊息", + "moderation.abuse_category.malware": "惡意軟體", + "moderation.abuse_category.illegal_content": "違法內容", + "moderation.report_remarks.placeholder": "請提供您所檢舉濫用行為的相關細節。", + "moderation.report_remarks": "備註", + "moderation.submit_report": "提交檢舉", + "moderation.reporting_failed": "無法提交新的濫用回報:%v", + "moderation.reported_thank_you": "感謝您的回報,管理團隊已收到相關通知。", + "mail.actions.successful_run_after_failure_subject": "儲存庫 %[2]s 中的工作流程 %[1]s 已恢復", + "error.not_found.title": "找不到頁面", + "incorrect_root_url": "這個 Forgejo 實例設定為在 \"%s\" 上提供服務。您目前是透過不同的 URL 存取 Forgejo,這可能會導致部分功能無法正常運作。正式的 URL 是由 Forgejo 管理員透過 app.ini 中的 ROOT_URL 設定所控制。", + "themes.names.forgejo-auto": "Forgejo(遵循系統主題)", + "themes.names.forgejo-light": "Forgejo 淺色", + "themes.names.forgejo-dark": "Forgejo 深色", + "followers.incoming.list.self.none": "沒有人關注您的個人資料。", + "followers.incoming.list.none": "沒有人關注這位使用者。", + "followers.outgoing.list.none": "%s 沒有追蹤任何人。", + "followers.outgoing.list.self.none": "您沒有追蹤任何人。", + "relativetime.mins": "%d 分鐘前", + "relativetime.hours": "%d 小時前", + "relativetime.days": "%d 天前", + "relativetime.weeks": "%d 周前", + "relativetime.months": "%d 個月前", + "relativetime.years": "%d 年前", + "relativetime.1day": "昨天", + "relativetime.2days": "兩天前", + "relativetime.1week": "上週", + "relativetime.2weeks": "兩週前", + "relativetime.1month": "上個月", + "relativetime.2months": "兩個月前", + "relativetime.1year": "去年", + "relativetime.2years": "兩年前", + "moderation.abuse_category.other_violations": "其他違反平台規則的行為", + "mail.actions.not_successful_run_subject": "儲存庫 %[2]s 中的工作流程 %[1]s 已失敗", + "mail.actions.successful_run_after_failure": "儲存庫 %[2]s 中的工作流程 %[1]s 已恢復", + "mail.actions.not_successful_run": "儲存庫 %[2]s 中的工作流程 %[1]s 已失敗", + "mail.actions.run_info_cur_status": "本次執行狀態:%[1]s(剛從 %[2]s 更新)", + "mail.actions.run_info_previous_status": "前一次執行狀態:%[1]s", + "mail.actions.run_info_ref": "分支:%[1]s (%[2]s)", + "mail.actions.run_info_trigger": "觸發原因:%[1]s,由 %[2]s 執行", + "discussion.locked": "此討論已被鎖定。僅限貢獻者留言。", + "alert.asset_load_failed": "無法從 {path} 載入資源檔案。請確保這些資源檔案可以被存取。", + "editor.textarea.tab_hint": "此行已縮排。再次按下 Tab 鍵或按下 Escape 鍵以離開編輯器。", + "editor.textarea.shift_tab_hint": "此行未縮排。請再次按下 Shift + Tab,或按下 Escape 鍵以離開編輯器。", + "admin.config.moderation_config": "審核設定", + "moderation.report_abuse": "回報濫用行為", + "moderation.report_content": "檢舉內容", + "moderation.report_abuse_form.header": "向管理員回報濫用", + "moderation.report_abuse_form.details": "這個表單是用來檢舉用戶建立垃圾帳號、儲存庫、問題、留言,或其他不當行為。", + "moderation.report_abuse_form.invalid": "無效參數", + "moderation.report_abuse_form.already_reported": "您已檢舉此內容", + "meta.last_line": "Rubi-chan? Hai! Nani ga suki? Choko minto yori mo a・na・ta♡ Ayumu-chan? Hai! Nani ga suki? Sutoroberii fureibaa yori mo a・na・ta♡ Shiki-chan! Hai! Nani ga suki? Kukkii and kuriimu yori mo a・na・ta♡ Minna? Hai! Nani ga suki? Mochiron daisuki AiScReam.", + "admin.dashboard.cleanup_offline_runners": "清理離線 runners", + "settings.visibility.description": "個人資料的可見度會影響他人存取您非私人儲存庫的能力。了解更多", + "avatar.constraints_hint": "自定義大頭貼的大小不得超過 %[1]s,且解析度不得大於 %[2]d×%[3]d 像素", + "repo.diff.commit.next-short": "下一個", + "repo.diff.commit.previous-short": "上一個" } diff --git a/package-lock.json b/package-lock.json index 169292caa8..604ff38c18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,15 @@ "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", "@github/text-expander-element": "2.8.0", + "@google/model-viewer": "4.1.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "ansi_up": "6.0.5", "asciinema-player": "3.8.2", - "chart.js": "4.4.9", + "chart.js": "4.5.0", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", - "clippie": "4.1.6", + "clippie": "4.1.7", "css-loader": "7.0.0", "dayjs": "1.11.12", "dropzone": "6.0.0-beta.2", @@ -31,15 +32,15 @@ "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.22", - "mermaid": "11.6.0", + "mermaid": "11.7.0", "mini-css-extract-plugin": "2.9.2", - "minimatch": "10.0.1", + "minimatch": "10.0.3", "monaco-editor": "0.52.2", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", - "postcss": "8.5.2", + "postcss": "8.5.5", "postcss-loader": "8.1.1", - "postcss-nesting": "13.0.1", + "postcss-nesting": "13.0.2", "pretty-ms": "9.0.0", "sortablejs": "1.15.6", "swagger-ui-dist": "5.17.14", @@ -51,56 +52,55 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", - "vue": "3.5.14", + "vue": "3.5.16", "vue-chartjs": "5.3.1", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.99.7", + "webpack": "5.99.9", "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { - "@axe-core/playwright": "4.10.1", + "@axe-core/playwright": "4.10.2", "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", "@playwright/test": "1.52.0", "@stoplight/spectral-cli": "6.15.0", - "@stylistic/eslint-plugin-js": "4.2.0", + "@stylistic/eslint-plugin": "4.4.1", "@stylistic/stylelint-plugin": "3.1.2", - "@typescript-eslint/parser": "8.31.1", - "@vitejs/plugin-vue": "5.2.3", - "@vitest/coverage-v8": "3.1.2", - "@vitest/eslint-plugin": "1.1.43", + "@vitejs/plugin-vue": "5.2.4", + "@vitest/coverage-v8": "3.2.3", + "@vitest/eslint-plugin": "1.2.2", "@vue/test-utils": "2.4.6", - "eslint": "9.25.1", - "eslint-import-resolver-typescript": "4.3.4", + "eslint": "9.28.0", + "eslint-import-resolver-typescript": "4.4.3", "eslint-plugin-array-func": "5.0.2", - "eslint-plugin-import-x": "4.11.0", + "eslint-plugin-import-x": "4.15.1", "eslint-plugin-no-jquery": "3.1.1", "eslint-plugin-no-use-extend-native": "0.7.2", "eslint-plugin-playwright": "2.2.0", - "eslint-plugin-regexp": "2.7.0", + "eslint-plugin-regexp": "2.9.0", "eslint-plugin-sonarjs": "3.0.2", "eslint-plugin-toml": "0.12.0", - "eslint-plugin-unicorn": "59.0.0", + "eslint-plugin-unicorn": "59.0.1", "eslint-plugin-vitest-globals": "1.5.0", - "eslint-plugin-vue": "10.1.0", - "eslint-plugin-vue-scoped-css": "2.9.0", - "eslint-plugin-wc": "2.2.1", + "eslint-plugin-vue": "10.2.0", + "eslint-plugin-vue-scoped-css": "2.10.0", + "eslint-plugin-wc": "3.0.1", "globals": "16.1.0", - "happy-dom": "17.4.6", + "happy-dom": "18.0.0", "license-checker-rseidelsohn": "4.4.2", - "markdownlint-cli": "0.44.0", + "markdownlint-cli": "0.45.0", "postcss-html": "1.8.0", - "sharp": "0.34.1", - "stylelint": "16.19.1", + "sharp": "0.34.2", + "stylelint": "16.20.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.11", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.2.0", "typescript": "5.8.3", - "typescript-eslint": "8.31.1", + "typescript-eslint": "8.34.0", "vite-string-plugin": "1.3.4", - "vitest": "3.1.2" + "vitest": "3.2.3" }, "engines": { "node": ">= 20.0.0" @@ -165,13 +165,13 @@ } }, "node_modules/@axe-core/playwright": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.1.tgz", - "integrity": "sha512-EV5t39VV68kuAfMKqb/RL+YjYKhfuGim9rgIaQ6Vntb2HgaCaau0h98Y3WEUqW1+PbdzxDtDNjFAipbtZuBmEA==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.2.tgz", + "integrity": "sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==", "dev": true, "license": "MPL-2.0", "dependencies": { - "axe-core": "~4.10.2" + "axe-core": "~4.10.3" }, "peerDependencies": { "playwright-core": ">= 1.0.0" @@ -216,12 +216,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -231,18 +231,18 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -438,9 +438,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -457,13 +457,13 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -559,9 +559,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -575,9 +575,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -591,9 +591,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -607,9 +607,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -623,9 +623,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -639,9 +639,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -655,9 +655,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -671,9 +671,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -687,9 +687,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -703,9 +703,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -719,9 +719,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -735,9 +735,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -751,9 +751,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -767,9 +767,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -783,9 +783,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -799,9 +799,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -815,9 +815,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -831,9 +831,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -847,9 +847,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -863,9 +863,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -879,9 +879,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -895,9 +895,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -911,9 +911,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -927,9 +927,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -943,9 +943,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -1021,9 +1021,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1035,17 +1035,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1060,9 +1049,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1070,9 +1059,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1123,17 +1112,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -1168,13 +1146,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1188,19 +1169,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@github/combobox-nav": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.1.tgz", @@ -1229,6 +1223,22 @@ "dom-input-range": "^1.2.0" } }, + "node_modules/@google/model-viewer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.1.0.tgz", + "integrity": "sha512-7WB/jS6wfBfRl/tWhsUUvDMKFE1KlKME97coDLlZQfvJD0nCwjhES1lJ+k7wnmf7T3zMvCfn9mIjM/mgZapuig==", + "license": "Apache-2.0", + "dependencies": { + "@monogrid/gainmap-js": "^3.1.0", + "lit": "^3.2.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "three": "^0.172.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1330,9 +1340,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", "cpu": [ "arm64" ], @@ -1353,9 +1363,9 @@ } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", "cpu": [ "x64" ], @@ -1529,9 +1539,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", "cpu": [ "arm" ], @@ -1552,9 +1562,9 @@ } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", "cpu": [ "arm64" ], @@ -1575,9 +1585,9 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", "cpu": [ "s390x" ], @@ -1598,9 +1608,9 @@ } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", "cpu": [ "x64" ], @@ -1621,9 +1631,9 @@ } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", "cpu": [ "arm64" ], @@ -1644,9 +1654,9 @@ } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", "cpu": [ "x64" ], @@ -1667,9 +1677,9 @@ } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", "cpu": [ "wasm32" ], @@ -1677,7 +1687,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.0" + "@emnapi/runtime": "^1.4.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1686,10 +1696,30 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", "cpu": [ "ia32" ], @@ -1707,9 +1737,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", "cpu": [ "x64" ], @@ -1726,6 +1756,27 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1970,6 +2021,21 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", + "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz", + "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, "node_modules/@mcaptcha/core-glue": { "version": "0.1.0-alpha-5", "resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz", @@ -2022,24 +2088,36 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.4.0.tgz", - "integrity": "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.5.0.tgz", + "integrity": "sha512-AiaN7+VjXC+3BYE+GwNezkpjIcCI2qIMB/K4S2/vMWe0q/XJCBbx5+K7iteuz7VyltX9iAK4FmVTvGc9kjOV4w==", "license": "MIT", "dependencies": { "langium": "3.3.1" } }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz", + "integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", - "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, @@ -2191,9 +2269,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", "cpu": [ "arm" ], @@ -2205,9 +2283,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", "cpu": [ "arm64" ], @@ -2219,9 +2297,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", "cpu": [ "arm64" ], @@ -2233,9 +2311,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", "cpu": [ "x64" ], @@ -2247,9 +2325,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", "cpu": [ "arm64" ], @@ -2261,9 +2339,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", "cpu": [ "x64" ], @@ -2275,9 +2353,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", "cpu": [ "arm" ], @@ -2289,9 +2367,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", "cpu": [ "arm" ], @@ -2303,9 +2381,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", "cpu": [ "arm64" ], @@ -2317,9 +2395,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", "cpu": [ "arm64" ], @@ -2331,9 +2409,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", "cpu": [ "loong64" ], @@ -2345,9 +2423,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", "cpu": [ "ppc64" ], @@ -2359,9 +2437,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", "cpu": [ "riscv64" ], @@ -2373,9 +2451,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", "cpu": [ "riscv64" ], @@ -2387,9 +2465,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", "cpu": [ "s390x" ], @@ -2401,9 +2479,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", "cpu": [ "x64" ], @@ -2415,9 +2493,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", "cpu": [ "x64" ], @@ -2429,9 +2507,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", "cpu": [ "arm64" ], @@ -2443,9 +2521,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", "cpu": [ "ia32" ], @@ -2457,9 +2535,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", "cpu": [ "x64" ], @@ -2679,17 +2757,6 @@ "node": "^12.20 || >=14.13" } }, - "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@stoplight/spectral-core/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2988,15 +3055,18 @@ "node": "^12.20 || >=14.13" } }, - "node_modules/@stylistic/eslint-plugin-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-4.2.0.tgz", - "integrity": "sha512-MiJr6wvyzMYl/wElmj8Jns8zH7Q1w8XoVtm+WM6yDaTrfxryMyb8n0CMxt82fo42RoLIfxAEtM6tmQVxqhk0/A==", + "node_modules/@stylistic/eslint-plugin": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", + "integrity": "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ==", "dev": true, "license": "MIT", "dependencies": { + "@typescript-eslint/utils": "^8.32.1", "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0" + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3005,6 +3075,19 @@ "eslint": ">=9.0.0" } }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@stylistic/stylelint-plugin": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.2.tgz", @@ -3055,10 +3138,20 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/codemirror": { - "version": "5.60.15", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", - "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "version": "5.60.16", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", + "integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==", "license": "MIT", "dependencies": { "@types/tern": "*" @@ -3327,6 +3420,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/es-aggregate-error": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", @@ -3409,9 +3509,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", - "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3437,8 +3537,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@types/unist": { "version": "2.0.11", @@ -3454,22 +3553,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", - "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3479,22 +3585,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", - "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "engines": { @@ -3509,15 +3625,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", - "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3527,17 +3665,34 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", - "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3552,9 +3707,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", - "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", "dev": true, "license": "MIT", "engines": { @@ -3566,20 +3721,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", - "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3592,6 +3749,23 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3609,16 +3783,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", - "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3633,13 +3807,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", - "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3650,10 +3824,38 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.1.tgz", + "integrity": "sha512-dd7yIp1hfJFX9ZlVLQRrh/Re9WMUHHmF9hrKD1yIvxcyNr2BhQ3xc1upAVhy8NijadnCswAxWQu8MkkSMC1qXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.1.tgz", + "integrity": "sha512-EzUPcMFtDVlo5yrbzMqUsGq3HnLXw+3ZOhSd7CUaDmbTtnrzM+RO2ntw2dm2wjbbc5djWj3yX0wzbbg8pLhx8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.1.tgz", + "integrity": "sha512-nB+dna3q4kOleKFcSZJ/wDXIsAd1kpMO9XrVAt8tG3RDWJ6vi+Ic6bpz4cmg5tWNeCfHEY4KuqJCB+pKejPEmQ==", "cpu": [ "arm64" ], @@ -3665,9 +3867,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", - "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.1.tgz", + "integrity": "sha512-aKWHCrOGaCGwZcekf3TnczQoBxk5w//W3RZ4EQyhux6rKDwBPgDU9Y2yGigCV1Z+8DWqZgVGQi+hdpnlSy3a1w==", "cpu": [ "x64" ], @@ -3679,9 +3881,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", - "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.1.tgz", + "integrity": "sha512-4dIEMXrXt0UqDVgrsUd1I+NoIzVQWXy/CNhgpfS75rOOMK/4Abn0Mx2M2gWH4Mk9+ds/ASAiCmqoUFynmMY5hA==", "cpu": [ "x64" ], @@ -3693,9 +3895,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", - "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.1.tgz", + "integrity": "sha512-vtvS13IXPs1eE8DuS/soiosqMBeyh50YLRZ+p7EaIKAPPeevRnA9G/wu/KbVt01ZD5qiGjxS+CGIdVC7I6gTOw==", "cpu": [ "arm" ], @@ -3707,9 +3909,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", - "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.1.tgz", + "integrity": "sha512-BfdnN6aZ7NcX8djW8SR6GOJc+K+sFhWRF4vJueVE0vbUu5N1bLnBpxJg1TGlhSyo+ImC4SR0jcNiKN0jdoxt+A==", "cpu": [ "arm" ], @@ -3721,9 +3923,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", - "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.1.tgz", + "integrity": "sha512-Jhge7lFtH0QqfRz2PyJjJXWENqywPteITd+nOS0L6AhbZli+UmEyGBd2Sstt1c+l9C+j/YvKTl9wJo9PPmsFNg==", "cpu": [ "arm64" ], @@ -3735,9 +3937,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.1.tgz", + "integrity": "sha512-ofdK/ow+ZSbSU0pRoB7uBaiRHeaAOYQFU5Spp87LdcPL/P1RhbCTMSIYVb61XWzsVEmYKjHFtoIE0wxP6AFvrA==", "cpu": [ "arm64" ], @@ -3749,9 +3951,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", - "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.1.tgz", + "integrity": "sha512-eC8SXVn8de67HacqU7PoGdHA+9tGbqfEdD05AEFRAB81ejeQtNi5Fx7lPcxpLH79DW0BnMAHau3hi4RVkHfSCw==", "cpu": [ "ppc64" ], @@ -3763,9 +3965,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", - "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.1.tgz", + "integrity": "sha512-fIkwvAAQ41kfoGWfzeJ33iLGShl0JEDZHrMnwTHMErUcPkaaZRJYjQjsFhMl315NEQ4mmTlC+2nfK/J2IszDOw==", "cpu": [ "riscv64" ], @@ -3777,9 +3979,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", - "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.1.tgz", + "integrity": "sha512-RAAszxImSOFLk44aLwnSqpcOdce8sBcxASledSzuFAd8Q5ZhhVck472SisspnzHdc7THCvGXiUeZ2hOC7NUoBQ==", "cpu": [ "riscv64" ], @@ -3791,9 +3993,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", - "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.1.tgz", + "integrity": "sha512-QoP9vkY+THuQdZi05bA6s6XwFd6HIz3qlx82v9bTOgxeqin/3C12Ye7f7EOD00RQ36OtOPWnhEMMm84sv7d1XQ==", "cpu": [ "s390x" ], @@ -3805,9 +4007,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", - "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.1.tgz", + "integrity": "sha512-/p77cGN/h9zbsfCseAP5gY7tK+7+DdM8fkPfr9d1ye1fsF6bmtGbtZN6e/8j4jCZ9NEIBBkT0GhdgixSelTK9g==", "cpu": [ "x64" ], @@ -3819,9 +4021,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.1.tgz", + "integrity": "sha512-wInTqT3Bu9u50mDStEig1v8uxEL2Ht+K8pir/YhyyrM5ordJtxoqzsL1vR/CQzOJuDunUTrDkMM0apjW/d7/PA==", "cpu": [ "x64" ], @@ -3833,9 +4035,9 @@ ] }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", - "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.1.tgz", + "integrity": "sha512-eNwqO5kUa+1k7yFIircwwiniKWA0UFHo2Cfm8LYgkh9km7uMad+0x7X7oXbQonJXlqfitBTSjhA0un+DsHIrhw==", "cpu": [ "wasm32" ], @@ -3843,16 +4045,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.9" + "@napi-rs/wasm-runtime": "^0.2.11" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", - "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.1.tgz", + "integrity": "sha512-Eaz1xMUnoa2mFqh20mPqSdbYl6crnk8HnIXDu6nsla9zpgZJZO8w3c1gvNN/4Eb0RXRq3K9OG6mu8vw14gIqiA==", "cpu": [ "arm64" ], @@ -3864,9 +4066,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.1.tgz", + "integrity": "sha512-H/+d+5BGlnEQif0gnwWmYbYv7HJj563PUKJfn8PlmzF8UmF+8KxdvXdwCsoOqh4HHnENnoLrav9NYBrv76x1wQ==", "cpu": [ "ia32" ], @@ -3878,9 +4080,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.1.tgz", + "integrity": "sha512-rS86wI4R6cknYM3is3grCb/laE8XBEbpWAMSIPjYfmYp75KL5dT87jXF2orDa4tQYg5aajP5G8Fgh34dRyR+Rw==", "cpu": [ "x64" ], @@ -3892,9 +4094,9 @@ ] }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", - "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", "engines": { @@ -3906,15 +4108,16 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.2.tgz", - "integrity": "sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.3.tgz", + "integrity": "sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", @@ -3929,8 +4132,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.2", - "vitest": "3.1.2" + "@vitest/browser": "3.2.3", + "vitest": "3.2.3" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3949,13 +4152,15 @@ } }, "node_modules/@vitest/eslint-plugin": { - "version": "1.1.43", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.43.tgz", - "integrity": "sha512-OLoUMO67Yg+kr7E6SjF5+Qvl2f6uNJ7ImQYnXT8WgnPiZE41ZQBsnzn70jehXrhFVadphHs2smk+yl0TFKLV5Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.2.2.tgz", + "integrity": "sha512-R8NwW+VxyKqVGcMfYsUbdThQyMbtNcoeg+jJeTgMHqWdFdcS0nrODAQXhkplvWzgd7jIJ+GQeydGqFLibsxMxg==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.24.0" + }, "peerDependencies": { - "@typescript-eslint/utils": ">= 8.24.0", "eslint": ">= 8.57.0", "typescript": ">= 5.0.0", "vitest": "*" @@ -3970,14 +4175,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", - "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", + "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -3986,13 +4192,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", - "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", + "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.2", + "@vitest/spy": "3.2.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -4001,7 +4207,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -4013,9 +4219,9 @@ } }, "node_modules/@vitest/mocker/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -4040,9 +4246,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", + "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", "dev": true, "license": "MIT", "dependencies": { @@ -4053,27 +4259,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", - "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", + "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.2", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.3", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", - "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", + "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", + "@vitest/pretty-format": "3.2.3", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -4092,26 +4299,26 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", - "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", + "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", - "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", + "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.2", + "@vitest/pretty-format": "3.2.3", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -4120,39 +4327,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz", - "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz", + "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.27.2", - "@vue/shared": "3.5.14", + "@vue/shared": "3.5.16", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz", - "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz", + "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-core": "3.5.16", + "@vue/shared": "3.5.16" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz", - "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz", + "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.27.2", - "@vue/compiler-core": "3.5.14", - "@vue/compiler-dom": "3.5.14", - "@vue/compiler-ssr": "3.5.14", - "@vue/shared": "3.5.14", + "@vue/compiler-core": "3.5.16", + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.3", @@ -4168,92 +4375,64 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/@vue/compiler-sfc/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz", - "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz", + "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-dom": "3.5.16", + "@vue/shared": "3.5.16" } }, "node_modules/@vue/reactivity": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", - "integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", + "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.14" + "@vue/shared": "3.5.16" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz", - "integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz", + "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/reactivity": "3.5.16", + "@vue/shared": "3.5.16" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz", - "integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz", + "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.14", - "@vue/runtime-core": "3.5.14", - "@vue/shared": "3.5.14", + "@vue/reactivity": "3.5.16", + "@vue/runtime-core": "3.5.16", + "@vue/shared": "3.5.16", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz", - "integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz", + "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16" }, "peerDependencies": { - "vue": "3.5.14" + "vue": "3.5.16" } }, "node_modules/@vue/shared": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz", - "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz", + "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -4493,9 +4672,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4676,18 +4855,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4790,6 +4971,35 @@ "node": ">=4" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -4860,9 +5070,10 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -4914,14 +5125,23 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -4935,9 +5155,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "funding": [ { "type": "opencollective", @@ -4954,8 +5174,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -5037,9 +5257,9 @@ } }, "node_modules/cacheable": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.9.0.tgz", - "integrity": "sha512-8D5htMCxPDUULux9gFzv30f04Xo3wCnik0oOxKoRTPIBoqA7HtOcJ87uBhQTs3jCfZZTrUBGsYIZOgE0ZRgMAg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.0.tgz", + "integrity": "sha512-SSgQTAnhd7WlJXnGlIi4jJJOiHzgnM5wRMEPaXAU4kECTAMpBoYKoZ9i5zHmclIEZbxcu3j7yY/CF8DTmwIsHg==", "dev": true, "license": "MIT", "dependencies": { @@ -5048,9 +5268,9 @@ } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", - "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", + "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", "dev": true, "license": "MIT", "dependencies": { @@ -5126,9 +5346,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", "funding": [ { "type": "opencollective", @@ -5212,9 +5432,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", - "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -5370,9 +5590,9 @@ } }, "node_modules/clippie": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.1.6.tgz", - "integrity": "sha512-1M3xZRNWcVwRkh3i2XcVYFjVtfC6FCLmIpk5s54uT3+jkBa25KRE3dB0Fgkt/YLoeR7AABWkVTlb0WziQYGgaw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.1.7.tgz", + "integrity": "sha512-l8BmUmWqOt4mpxeSflcfODJJOU+DsE5atWj82k1zsxd2X82haz2+g12PJPMOt6e1Cs4/ZnOWdRAV+nY9n2o1Rg==", "license": "BSD-2-Clause" }, "node_modules/cliui": { @@ -5554,13 +5774,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.0" }, "funding": { "type": "opencollective", @@ -6358,9 +6578,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", - "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6576,9 +6796,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", - "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6662,6 +6882,23 @@ "node": ">=14" } }, + "node_modules/editorconfig/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/editorconfig/node_modules/minimatch": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", @@ -6679,9 +6916,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.153", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.153.tgz", - "integrity": "sha512-4bwluTFwjXZ0/ei1qDpHDGzVveuBfx4wiZ9VQ8j/30+T2JxSF2TfZ00d1X+wNMeDyUdZXgLkJFbarJdAMtd+/w==", + "version": "1.5.171", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", + "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -6755,9 +6992,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -6765,18 +7002,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "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.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -6788,21 +7025,24 @@ "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.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "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.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "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", @@ -6811,7 +7051,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -6821,18 +7061,18 @@ } }, "node_modules/es-aggregate-error": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz", - "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", + "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.24.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "globalthis": "^1.0.3", + "globalthis": "^1.0.4", "has-property-descriptors": "^1.0.2", "set-function-name": "^2.0.2" }, @@ -6930,9 +7170,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -6942,31 +7182,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/esbuild-loader": { @@ -7022,9 +7262,9 @@ } }, "node_modules/eslint": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", - "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7032,10 +7272,10 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -7098,41 +7338,45 @@ "eslint": ">=6.0.0" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "node_modules/eslint-import-context": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.8.tgz", + "integrity": "sha512-bq+F7nyc65sKpZGT09dY0S0QrOnQtuDVIfyTGQ8uuvtMIF7oHp6CEP3mouN0rrnYF3Jqo6Ke0BfU/5wASZue1w==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.1.1" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } } }, "node_modules/eslint-import-resolver-typescript": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.4.tgz", - "integrity": "sha512-buzw5z5VtiQMysYLH9iW9BV04YyZebsw+gPi+c4FCjfS9i6COYOrEWw9t3m3wA9PFBfqcBCqWf32qrXLbwafDw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.3.tgz", + "integrity": "sha512-elVDn1eWKFrWlzxlWl9xMt8LltjKl161Ix50JFC50tHXI5/TRP32SNEqlJ/bo/HV+g7Rou/tlPQU2AcRtIhrOg==", "dev": true, "license": "ISC", "dependencies": { - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.3" + "stable-hash-x": "^0.1.1", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" }, "engines": { "node": "^16.17.0 || >=18.6.0" @@ -7168,23 +7412,21 @@ } }, "node_modules/eslint-plugin-import-x": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.11.0.tgz", - "integrity": "sha512-NAaYY49342gj09QGvwnFFl5KcD5aLzjAz97Lo+upnN8MzjEGSIlmL5sxCYGqtIeMjw8fSRDFZIp2xjRLT+yl4Q==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.15.1.tgz", + "integrity": "sha512-JfVpNg1qMkPD66iaSgmMoSYeUCGS8UFSm3GwHV0IbuV3Knar/SyK5qqCct9+AxoMIzaM+KSO7KK5pOeOkC/3GQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.31.0", + "@typescript-eslint/types": "^8.33.1", "comment-parser": "^1.4.1", - "debug": "^4.4.0", - "eslint-import-resolver-node": "^0.3.9", - "get-tsconfig": "^4.10.0", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.7", "is-glob": "^4.0.3", "minimatch": "^9.0.3 || ^10.0.1", - "semver": "^7.7.1", - "stable-hash": "^0.0.5", - "tslib": "^2.8.1", - "unrs-resolver": "^1.7.0" + "semver": "^7.7.2", + "stable-hash-x": "^0.1.1", + "unrs-resolver": "^1.7.10" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7193,7 +7435,17 @@ "url": "https://opencollective.com/eslint-plugin-import-x" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } } }, "node_modules/eslint-plugin-no-jquery": { @@ -7261,9 +7513,9 @@ } }, "node_modules/eslint-plugin-regexp": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.7.0.tgz", - "integrity": "sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.9.0.tgz", + "integrity": "sha512-9WqJMnOq8VlE/cK+YAo9C9YHhkOtcEtEk9d12a+H7OSZFwlpI6stiHmYPGa2VE0QhTzodJyhlyprUaXDZLgHBw==", "dev": true, "license": "MIT", "dependencies": { @@ -7303,6 +7555,23 @@ "eslint": "^8.0.0 || ^9.0.0" } }, + "node_modules/eslint-plugin-sonarjs/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-sonarjs/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7319,6 +7588,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/eslint-plugin-sonarjs/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-toml": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/eslint-plugin-toml/-/eslint-plugin-toml-0.12.0.tgz", @@ -7342,9 +7624,9 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "59.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.0.tgz", - "integrity": "sha512-7IEeqkymGa7tr6wTWS4DolfXnfcE3QjcD0g7I+qCfV5GPMvVsFsLT7zTIYvnudqwAm5nWekdGIOTTXA93Sz9Ow==", + "version": "59.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz", + "integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7376,6 +7658,33 @@ "eslint": ">=9.22.0" } }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint-plugin-vitest-globals": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.5.0.tgz", @@ -7384,9 +7693,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-vue": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.1.0.tgz", - "integrity": "sha512-/VTiJ1eSfNLw6lvG9ENySbGmcVvz6wZ9nA7ZqXlLBY2RkaF15iViYKxglWiIch12KiLAj0j1iXPYU6W4wTROFA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.2.0.tgz", + "integrity": "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7406,9 +7715,9 @@ } }, "node_modules/eslint-plugin-vue-scoped-css": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.9.0.tgz", - "integrity": "sha512-zXeKtEUpfk3PlsgKnr9/2U8K2xcsCV1M9hXWRhKbl3wipVowGXfHrhqUzHFVWNAHzEQv0DCDXGFWrmsGFqhGGA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.10.0.tgz", + "integrity": "sha512-oH2NY7XFHF3EGOotvuPdnhB0x4uOmjoRoWZVfMnJ2PILDKVgZgM8WZ0rhDlh+fsr9jO9P8CAXO5/9s9v/GZNhg==", "dev": true, "license": "MIT", "dependencies": { @@ -7433,23 +7742,23 @@ } }, "node_modules/eslint-plugin-wc": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-2.2.1.tgz", - "integrity": "sha512-KstLqGmyQz088DvFlDYHg0sHih+w2QeulreCi1D1ftr357klO2zqHdG/bbnNMmuQdVFDuNkopNIyNhmG0XCT/g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-3.0.1.tgz", + "integrity": "sha512-0p1wkSlA2Ue3FA4qW+5LZ+15sy0p1nUyVl1eyBMLq4rtN1LtE9IdI49BXNWMz8N8bM/y7Ulx8SWGAni5f8XO5g==", "dev": true, "license": "MIT", "dependencies": { "is-valid-element-name": "^1.0.0", - "js-levenshtein-esm": "^1.2.0" + "js-levenshtein-esm": "^2.0.0" }, "peerDependencies": { "eslint": ">=8.40.0" } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7464,9 +7773,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7477,9 +7786,9 @@ } }, "node_modules/eslint/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -7500,17 +7809,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7532,15 +7830,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7629,9 +7927,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -8060,9 +8358,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -8111,17 +8409,6 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8276,17 +8563,18 @@ } }, "node_modules/happy-dom": { - "version": "17.4.6", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.6.tgz", - "integrity": "sha512-OEV1hDe9i2rFr66+WZNiwy1S8rAJy6bRXmXql68YJDjdfHBRbN76om+qVh68vQACf6y5Bcr90e/oK53RQxsDdg==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.0.tgz", + "integrity": "sha512-o3p2Axi1EdIfMaOUulDzO/5yXzLLV0g/54eLPVrkt3u20r3yOuOenHpyp2clAJ0eHMc+HyE139ulQxl+8pEJIw==", "dev": true, "license": "MIT", "dependencies": { - "webidl-conversions": "^7.0.0", + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/has-bigints": { @@ -8388,9 +8676,9 @@ } }, "node_modules/hookified": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.9.0.tgz", - "integrity": "sha512-2yEEGqphImtKIe1NXWEhu6yD3hlFR4Mxk4Mtp3XEyScpSt4pQ4ymmXA1zzxZpj99QkFK+nN0nzjeb2+RUi/6CQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.9.1.tgz", + "integrity": "sha512-u3pxtGhKjcSXnGm1CX6aXS9xew535j3lkOCegbA6jdyh0BaAjTbXI4aslKstCr6zUNtoCxFGFKwjbSHdGrMB8g==", "dev": true, "license": "MIT" }, @@ -8523,6 +8811,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -8992,6 +9286,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9049,6 +9356,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-proto-prop": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-3.0.1.tgz", @@ -9385,6 +9698,23 @@ "node": ">=14" } }, + "node_modules/js-beautify/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/js-beautify/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -9433,9 +9763,9 @@ } }, "node_modules/js-levenshtein-esm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", - "integrity": "sha512-fzreKVq1eD7eGcQr7MtRpQH94f8gIfhdrc7yeih38xh684TNMK9v5aAu2wxfIRMk/GpAJRrzcirMAPIaSDaByQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-2.0.0.tgz", + "integrity": "sha512-1n4LEPOL4wRXY8rOQcuA7Iuaphe5xCMayvufCzlLAi+hRsnBRDbSS6XPuV58CBVJxj5D9ApFLyjQ7KzFToyHBw==", "dev": true, "license": "MIT" }, @@ -9748,6 +10078,15 @@ "npm": ">=8" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -9776,6 +10115,37 @@ "uc.micro": "^2.0.0" } }, + "node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz", + "integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz", + "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -9881,9 +10251,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, @@ -9974,52 +10344,52 @@ } }, "node_modules/markdownlint": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.37.4.tgz", - "integrity": "sha512-u00joA/syf3VhWh6/ybVFkib5Zpj2e5KB/cfCei8fkSRuums6nyisTWGqjTWIOFoFwuXoTBQQiqlB4qFKp8ncQ==", + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.38.0.tgz", + "integrity": "sha512-xaSxkaU7wY/0852zGApM8LdlIfGCW8ETZ0Rr62IQtAnUMlMuifsg09vWJcNYeL4f0anvr8Vo4ZQar8jGpV0btQ==", "dev": true, "license": "MIT", "dependencies": { - "markdown-it": "14.1.0", - "micromark": "4.0.1", - "micromark-core-commonmark": "2.0.2", - "micromark-extension-directive": "3.0.2", + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.1" + "micromark-util-types": "2.0.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.44.0.tgz", - "integrity": "sha512-ZJTAONlvF9NkrIBltCdW15DxN9UTbPiKMEqAh2EU2gwIFlrCMavyCEPPO121cqfYOrLUJWW8/XKWongstmmTeQ==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.45.0.tgz", + "integrity": "sha512-GiWr7GfJLVfcopL3t3pLumXCYs8sgWppjIA1F/Cc3zIMgD3tmkpyZ1xkm1Tej8mw53B93JsDjgA3KOftuYcfOw==", "dev": true, "license": "MIT", "dependencies": { "commander": "~13.1.0", - "glob": "~10.4.5", - "ignore": "~7.0.3", + "glob": "~11.0.2", + "ignore": "~7.0.4", "js-yaml": "~4.1.0", "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", - "markdownlint": "~0.37.4", - "minimatch": "~9.0.5", + "markdown-it": "~14.1.0", + "markdownlint": "~0.38.0", + "minimatch": "~10.0.1", "run-con": "~1.3.2", - "smol-toml": "~1.3.1" + "smol-toml": "~1.3.4" }, "bin": { "markdownlint": "markdownlint.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/markdownlint-cli/node_modules/commander": { @@ -10033,36 +10403,55 @@ } }, "node_modules/markdownlint-cli/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/markdownlint-cli/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/markdownlint-cli/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/markdownlint-cli/node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -10070,17 +10459,28 @@ "dev": true, "license": "MIT" }, - "node_modules/markdownlint-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/markdownlint-cli/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", "dev": true, "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/markdownlint-cli/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10162,14 +10562,14 @@ } }, "node_modules/mermaid": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz", - "integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.7.0.tgz", + "integrity": "sha512-/1/5R0rt0Z1Ak0CuznAnCF3HtQgayRXUz6SguzOwN4L+DuCobz0UxnQ+ZdTSZ3AugKVVh78tiVmsHpHWV25TCw==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.0.4", "@iconify/utils": "^2.1.33", - "@mermaid-js/parser": "^0.4.0", + "@mermaid-js/parser": "^0.5.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", @@ -10178,7 +10578,7 @@ "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.11", "dayjs": "^1.11.13", - "dompurify": "^3.2.4", + "dompurify": "^3.2.5", "katex": "^0.16.9", "khroma": "^2.1.0", "lodash-es": "^4.17.21", @@ -10196,9 +10596,9 @@ "license": "MIT" }, "node_modules/mermaid/node_modules/marked": { - "version": "15.0.11", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.11.tgz", - "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -10208,9 +10608,9 @@ } }, "node_modules/micromark": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", - "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "dev": true, "funding": [ { @@ -10244,9 +10644,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", - "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "dev": true, "funding": [ { @@ -10279,9 +10679,9 @@ } }, "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", "dev": true, "license": "MIT", "dependencies": { @@ -10337,9 +10737,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -10727,9 +11127,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "dev": true, "funding": [ { @@ -10808,12 +11208,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -11632,9 +12032,9 @@ } }, "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "funding": [ { "type": "opencollective", @@ -11651,7 +12051,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11853,9 +12253,9 @@ } }, "node_modules/postcss-nesting": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", - "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", "funding": [ { "type": "github", @@ -11868,7 +12268,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, @@ -11880,9 +12280,9 @@ } }, "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", - "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", "funding": [ { "type": "github", @@ -12058,6 +12458,16 @@ "dev": true, "license": "Unlicense" }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -12189,6 +12599,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/read-package-json/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-package-json/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/read-package-json/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -12683,9 +13110,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12704,18 +13131,18 @@ } }, "node_modules/seroval": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.1.tgz", - "integrity": "sha512-F+T9EQPdLzgdewgxnBh4mSc+vde+EOkU6dC9BDuu/bfGb+UyUlqM6t8znFCTPQSuai/ZcfFg0gu79h+bVW2O0w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.1.tgz", - "integrity": "sha512-dOlUoiI3fgZbQIcj6By+l865pzeWdP3XCSLdI3xlKnjCk5983yLWPsXytFOUI0BUZKG9qwqbj78n9yVcVwUqaQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", + "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", "license": "MIT", "engines": { "node": ">=10" @@ -12786,16 +13213,16 @@ } }, "node_modules/sharp": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", - "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" + "detect-libc": "^2.0.4", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -12804,8 +13231,8 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", @@ -12815,15 +13242,16 @@ "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" } }, "node_modules/shebang-command": { @@ -13024,14 +13452,14 @@ } }, "node_modules/solid-js": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.6.tgz", - "integrity": "sha512-PoasAJvLk60hRtOTe9ulvALOdLjjqxuxcGZRolBQqxOnXrBXHGzqMT4ijNhGsDAYdOgEa8ZYaAE94PSldrFSkA==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz", + "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", "license": "MIT", "dependencies": { "csstype": "^3.1.0", - "seroval": "^1.1.0", - "seroval-plugins": "^1.1.0" + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" } }, "node_modules/sortablejs": { @@ -13161,12 +13589,15 @@ "spdx-ranges": "^2.0.0" } }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "node_modules/stable-hash-x": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.1.1.tgz", + "integrity": "sha512-l0x1D6vhnsNUGPFVDx45eif0y6eedVC8nm5uACTrVFJFtl2mLRW17aWtVyxFCpn5t94VUPkjU8vSLwIuwwqtJQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } }, "node_modules/stackback": { "version": "0.0.2", @@ -13193,6 +13624,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -13335,6 +13780,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -13343,9 +13801,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.19.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.19.1.tgz", - "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==", + "version": "16.20.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.20.0.tgz", + "integrity": "sha512-B5Myu9WRxrgKuLs3YyUXLP2H0mrbejwNxPmyADlACWwFsrL8Bmor/nTSh4OMae5sHjOz6gkSeccQH34gM4/nAw==", "dev": true, "funding": [ { @@ -13369,15 +13827,15 @@ "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", - "debug": "^4.3.7", + "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.0.8", + "file-entry-cache": "^10.1.0", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^7.0.3", + "ignore": "^7.0.4", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.36.0", @@ -13449,9 +13907,9 @@ } }, "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", - "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "dev": true, "funding": [ { @@ -13468,8 +13926,8 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/stylelint/node_modules/@csstools/selector-specificity": { @@ -13495,74 +13953,38 @@ "postcss-selector-parser": "^7.0.0" } }, - "node_modules/stylelint/node_modules/balanced-match": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", - "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", - "dev": true, - "license": "MIT" - }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.0.tgz", - "integrity": "sha512-Et/ex6smi3wOOB+n5mek+Grf7P2AxZR5ueqRUvAAn4qkyatXi3cUC1cuQXVkX0VlzBVsN4BkWJFmY/fYiRTdww==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz", + "integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.9" + "flat-cache": "^6.1.10" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.9.tgz", - "integrity": "sha512-DUqiKkTlAfhtl7g78IuwqYM+YqvT+as0mY+EVk6mfimy19U79pJCzDZQsnqk3Ou/T6hFXWLGbwbADzD/c8Tydg==", + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.10.tgz", + "integrity": "sha512-B6/v1f0NwjxzmeOhzfXPGWpKBVA207LS7lehaVKQnFrVktcFRfkzjZZ2gwj2i1TkEUMQht7ZMJbABUT5N+V1Nw==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^1.9.0", + "cacheable": "^1.10.0", "flatted": "^3.3.3", - "hookified": "^1.8.2" + "hookified": "^1.9.1" } }, "node_modules/stylelint/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/stylelint/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/stylelint/node_modules/postcss-safe-parser": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", @@ -13673,6 +14095,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -13930,22 +14367,22 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/terser": { - "version": "5.39.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz", - "integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -14011,6 +14448,23 @@ "node": ">=18" } }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/test-exclude/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -14076,6 +14530,13 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.172.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz", + "integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==", + "license": "MIT", + "peer": true + }, "node_modules/throttle-debounce": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", @@ -14105,9 +14566,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14122,9 +14583,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -14150,9 +14611,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -14170,9 +14631,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -14411,15 +14872,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", - "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz", + "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.31.1", - "@typescript-eslint/parser": "8.31.1", - "@typescript-eslint/utils": "8.31.1" + "@typescript-eslint/eslint-plugin": "8.34.0", + "@typescript-eslint/parser": "8.34.0", + "@typescript-eslint/utils": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -14494,9 +14955,9 @@ } }, "node_modules/unrs-resolver": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", - "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.1.tgz", + "integrity": "sha512-4AZVxP05JGN6DwqIkSP4VKLOcwQa5l37SWHF/ahcuqBMbfxbpN1L1QKafEhWCziHhzKex9H/AR09H0OuVyU+9g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -14504,26 +14965,28 @@ "napi-postinstall": "^0.2.2" }, "funding": { - "url": "https://github.com/sponsors/JounQin" + "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.7.2", - "@unrs/resolver-binding-darwin-x64": "1.7.2", - "@unrs/resolver-binding-freebsd-x64": "1.7.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-musl": "1.7.2", - "@unrs/resolver-binding-wasm32-wasi": "1.7.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" + "@unrs/resolver-binding-android-arm-eabi": "1.9.1", + "@unrs/resolver-binding-android-arm64": "1.9.1", + "@unrs/resolver-binding-darwin-arm64": "1.9.1", + "@unrs/resolver-binding-darwin-x64": "1.9.1", + "@unrs/resolver-binding-freebsd-x64": "1.9.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.1", + "@unrs/resolver-binding-linux-x64-musl": "1.9.1", + "@unrs/resolver-binding-wasm32-wasi": "1.9.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.1" } }, "node_modules/update-browserslist-db": { @@ -14705,17 +15168,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", - "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", + "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -14735,16 +15198,16 @@ "license": "BSD-2-Clause" }, "node_modules/vite/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -14784,43 +15247,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vite/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/vite/node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -14830,56 +15264,58 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" } }, "node_modules/vitest": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", - "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", + "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.2", - "@vitest/mocker": "3.1.2", - "@vitest/pretty-format": "^3.1.2", - "@vitest/runner": "3.1.2", - "@vitest/snapshot": "3.1.2", - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.3", + "@vitest/mocker": "3.2.3", + "@vitest/pretty-format": "^3.2.3", + "@vitest/runner": "3.2.3", + "@vitest/snapshot": "3.2.3", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.2", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.3", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14895,8 +15331,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.2", - "@vitest/ui": "3.1.2", + "@vitest/browser": "3.2.3", + "@vitest/ui": "3.2.3", "happy-dom": "*", "jsdom": "*" }, @@ -14934,6 +15370,19 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest/node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -14991,16 +15440,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz", - "integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz", + "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.14", - "@vue/compiler-sfc": "3.5.14", - "@vue/runtime-dom": "3.5.14", - "@vue/server-renderer": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-sfc": "3.5.16", + "@vue/runtime-dom": "3.5.16", + "@vue/server-renderer": "3.5.16", + "@vue/shared": "3.5.16" }, "peerDependencies": { "typescript": "*" @@ -15090,9 +15539,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -15103,19 +15552,15 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.99.7", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", - "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==", + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -15235,9 +15680,9 @@ } }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/webpack/node_modules/eslint-scope": { @@ -15263,9 +15708,9 @@ } }, "node_modules/webpack/node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -15291,12 +15736,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -15573,15 +16012,15 @@ } }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 81b7e6c3ad..3d71e94cd3 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,15 @@ "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", "@github/text-expander-element": "2.8.0", + "@google/model-viewer": "4.1.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "ansi_up": "6.0.5", "asciinema-player": "3.8.2", - "chart.js": "4.4.9", + "chart.js": "4.5.0", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", - "clippie": "4.1.6", + "clippie": "4.1.7", "css-loader": "7.0.0", "dayjs": "1.11.12", "dropzone": "6.0.0-beta.2", @@ -30,15 +31,15 @@ "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.22", - "mermaid": "11.6.0", + "mermaid": "11.7.0", "mini-css-extract-plugin": "2.9.2", - "minimatch": "10.0.1", + "minimatch": "10.0.3", "monaco-editor": "0.52.2", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", - "postcss": "8.5.2", + "postcss": "8.5.5", "postcss-loader": "8.1.1", - "postcss-nesting": "13.0.1", + "postcss-nesting": "13.0.2", "pretty-ms": "9.0.0", "sortablejs": "1.15.6", "swagger-ui-dist": "5.17.14", @@ -50,56 +51,55 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", - "vue": "3.5.14", + "vue": "3.5.16", "vue-chartjs": "5.3.1", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.99.7", + "webpack": "5.99.9", "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { - "@axe-core/playwright": "4.10.1", + "@axe-core/playwright": "4.10.2", "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", "@playwright/test": "1.52.0", "@stoplight/spectral-cli": "6.15.0", - "@stylistic/eslint-plugin-js": "4.2.0", + "@stylistic/eslint-plugin": "4.4.1", "@stylistic/stylelint-plugin": "3.1.2", - "@typescript-eslint/parser": "8.31.1", - "@vitejs/plugin-vue": "5.2.3", - "@vitest/coverage-v8": "3.1.2", - "@vitest/eslint-plugin": "1.1.43", + "@vitejs/plugin-vue": "5.2.4", + "@vitest/coverage-v8": "3.2.3", + "@vitest/eslint-plugin": "1.2.2", "@vue/test-utils": "2.4.6", - "eslint": "9.25.1", - "eslint-import-resolver-typescript": "4.3.4", + "eslint": "9.28.0", + "eslint-import-resolver-typescript": "4.4.3", "eslint-plugin-array-func": "5.0.2", - "eslint-plugin-import-x": "4.11.0", + "eslint-plugin-import-x": "4.15.1", "eslint-plugin-no-jquery": "3.1.1", "eslint-plugin-no-use-extend-native": "0.7.2", "eslint-plugin-playwright": "2.2.0", - "eslint-plugin-regexp": "2.7.0", + "eslint-plugin-regexp": "2.9.0", "eslint-plugin-sonarjs": "3.0.2", - "eslint-plugin-unicorn": "59.0.0", "eslint-plugin-toml": "0.12.0", + "eslint-plugin-unicorn": "59.0.1", "eslint-plugin-vitest-globals": "1.5.0", - "eslint-plugin-vue": "10.1.0", - "eslint-plugin-vue-scoped-css": "2.9.0", - "eslint-plugin-wc": "2.2.1", + "eslint-plugin-vue": "10.2.0", + "eslint-plugin-vue-scoped-css": "2.10.0", + "eslint-plugin-wc": "3.0.1", "globals": "16.1.0", - "happy-dom": "17.4.6", + "happy-dom": "18.0.0", "license-checker-rseidelsohn": "4.4.2", - "markdownlint-cli": "0.44.0", + "markdownlint-cli": "0.45.0", "postcss-html": "1.8.0", - "sharp": "0.34.1", - "stylelint": "16.19.1", + "sharp": "0.34.2", + "stylelint": "16.20.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.11", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.2.0", "typescript": "5.8.3", - "typescript-eslint": "8.31.1", + "typescript-eslint": "8.34.0", "vite-string-plugin": "1.3.4", - "vitest": "3.1.2" + "vitest": "3.2.3" }, "browserslist": [ "defaults" diff --git a/release-notes-published/11.0.2.md b/release-notes-published/11.0.2.md new file mode 100644 index 0000000000..a1a8549984 --- /dev/null +++ b/release-notes-published/11.0.2.md @@ -0,0 +1,33 @@ + + + + +## Release notes + +- Features + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7986) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7991)): feat: make Forgejo Actions server logs less noisy +- Bug fixes + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8155) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8167)): fix: do not fail when release or wiki is set in `/repos/migrate` API + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7976) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7985)): fix: ignore expired artifacts for quota calculation + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7979) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7983)): fix: pull request cross references + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7883) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7886)): fix: quote reply in Chromium + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7775) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7779)): fix: make hash pattern more strict +- Included for completeness but not worth a release note + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8112) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8120)): fix: remove download attribute from external assets + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8110): Update bleve to v2.5.2 with changes made in backport of 2.5.0 + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8094) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8095)): fix: show membership of limited orgs + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8059): Update dependency go to v1.24.3 (v11.0/forgejo) + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8057): chore: drop unused `@typescript-eslint/parser` package + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8021) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8022)): chore(cleanup): suppress non actionable XORM warnings + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7987) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8000)): fix: aggregate deleted team as ghost team + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7925) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7937)): fix(ui): center footer links + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7894) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7903)): fix(ui): fix force-push compare line layout + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7884) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7887)): fix: parse `change-id` in the git commit header + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7885): Update module github.com/blevesearch/bleve/v2 to v2.5.1 (v11.0/forgejo) - abandoned + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7746) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7871)): fix(ui): improve force-push compare line layout + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7640) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7869)): fix: Remove "create branch" button on mirrored repos + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7858): Update module github.com/msteinert/pam/v2 to v2.1.0 (v11.0/forgejo) + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7817) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7821)): fix: replace ß with ss in normalizeUserName + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7784) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7786)): fix(api): document `is_system_webhook` field + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7773) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7774)): fix: remove artificial delay for PR update + diff --git a/release-notes-published/12.0.0.md b/release-notes-published/12.0.0.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/release-notes/6813.md b/release-notes/6813.md new file mode 100644 index 0000000000..bd9da8dc39 --- /dev/null +++ b/release-notes/6813.md @@ -0,0 +1 @@ +Reimplemented editor Tab key handling with accessibility safeguards. Balance having the editor work as expected by developers (with Tab key affecting indentation) while also not impeding keyboard navigation. diff --git a/release-notes/8035.md b/release-notes/8035.md new file mode 100644 index 0000000000..541fe35277 --- /dev/null +++ b/release-notes/8035.md @@ -0,0 +1 @@ +The `forgejo docs` command is deprecated and CLI errors are now displayed on stderr instead of stdout. These breaking changes happened because the package used to parse the command line arguments was [upgraded from v2 to v3](https://cli.urfave.org/migrate-v2-to-v3/). A [separate project was initiated](https://github.com/urfave/cli-docs) to re-implement the `docs` command, but it is not yet production ready. diff --git a/renovate.json b/renovate.json index 3903e316db..7796490544 100644 --- a/renovate.json +++ b/renovate.json @@ -8,14 +8,13 @@ ], "baseBranches": [ "$default", - "/^v7\\.\\d+/forgejo$/", "/^v11\\.\\d+/forgejo$/" ], "postUpdateOptions": ["gomodTidy", "gomodUpdateImportPaths", "npmDedupe"], "prConcurrentLimit": 10, "osvVulnerabilityAlerts": true, "automergeStrategy": "squash", - "labels": ["dependency-upgrade","test/not-needed"], + "labels": ["dependency-upgrade", "test/not-needed"], "packageRules": [ { "description": "Require approval for python minor version", @@ -37,8 +36,7 @@ "description": "Schedule some deps less frequently", "matchPackageNames": [ "code.forgejo.org/f3/gof3/**", - "github.com/google/pprof", - "github.com/golangci/misspell/cmd/misspell" + "github.com/google/pprof" ], "extends": ["schedule:quarterly"] }, @@ -145,21 +143,19 @@ "matchPackageNames": ["monaco-editor"], "minimumReleaseAge": "30 days" }, + { + "description": "Disable indirect updates for stable branches", + "matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"], + "matchManagers": ["gomod"], + "matchUpdateTypes": ["major", "minor", "patch", "digest"], + "matchDepTypes": ["indirect"], + "enabled": false + }, { "description": "Require approval for stable branches (must be last rule to override all others)", "matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"], "dependencyDashboardApproval": true, "schedule": ["at any time"] } - ], - "customManagers": [ - { - "description": "Update deps inside Makefile", - "customType": "regex", - "fileMatch": ["^Makefile$"], - "matchStrings": [ - " \\?= (?.+?)@(?.+?) # renovate: datasource=(?.+?)(?: packageName=(?.+?))?( versioning=(?.+?))?\\s" - ] - } ] } diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index a15fa4fd1e..c0af750d7b 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -51,11 +51,11 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext, log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) // if md5 not match, delete the chunk if reqMd5String != chunkMd5String { - checkErr = fmt.Errorf("md5 not match") + checkErr = errors.New("md5 not match") } } if writtenSize != contentSize { - checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size")) + checkErr = errors.Join(checkErr, errors.New("contentSize not match body size")) } if checkErr != nil { if err := st.Delete(storagePath); err != nil { @@ -261,7 +261,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st return fmt.Errorf("save merged file error: %v", err) } if written != artifact.FileCompressedSize { - return fmt.Errorf("merged file size is not equal to chunk length") + return errors.New("merged file size is not equal to chunk length") } defer func() { diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index bbd5ce860f..7263cf13bb 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -12,6 +12,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" "hash" "math/big" @@ -121,7 +122,7 @@ func verifyTimestamp(req *http.Request) error { } if diff > maxTimeDifference { - return fmt.Errorf("time difference") + return errors.New("time difference") } return nil @@ -190,7 +191,7 @@ func getAuthorizationData(req *http.Request) ([]byte, error) { tmp := make([]string, len(valueList)) for k, v := range valueList { if k > len(tmp) { - return nil, fmt.Errorf("invalid X-Ops-Authorization headers") + return nil, errors.New("invalid X-Ops-Authorization headers") } tmp[k-1] = v } @@ -267,7 +268,7 @@ func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error { } if !slices.Equal(out[skip:], data) { - return fmt.Errorf("could not verify signature") + return errors.New("could not verify signature") } return nil diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 5276dd5706..191a4aa455 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -4,6 +4,7 @@ package container import ( + "bytes" "errors" "fmt" "io" @@ -62,9 +63,6 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { if h.ContentType != "" { resp.Header().Set("Content-Type", h.ContentType) } - if h.ContentLength != 0 { - resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10)) - } if h.UploadUUID != "" { resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID) } @@ -72,17 +70,29 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { resp.Header().Set("Docker-Content-Digest", h.ContentDigest) resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest)) } + if h.ContentLength >= 0 { + resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10)) + } resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0") resp.WriteHeader(h.Status) } func jsonResponse(ctx *context.Context, status int, obj any) { - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: status, - ContentType: "application/json", - }) - if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil { + // Buffer the JSON content first to calculate correct Content-Length + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(obj); err != nil { log.Error("JSON encode: %v", err) + return + } + + setResponseHeaders(ctx.Resp, &containerHeaders{ + Status: status, + ContentType: "application/json", + ContentLength: int64(buf.Len()), + }) + + if _, err := buf.WriteTo(ctx.Resp); err != nil { + log.Error("JSON write: %v", err) } } @@ -691,33 +701,30 @@ func DeleteManifest(ctx *context.Context) { func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { serveDirectReqParams := make(url.Values) serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType)) - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams) + s, u, pf, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams) if err != nil { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } apiError(ctx, http.StatusInternalServerError, err) return } - headers := &containerHeaders{ - ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), - ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), - ContentLength: pfd.Blob.Size, - Status: http.StatusOK, + opts := &context.ServeHeaderOptions{ + ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), + RedirectStatusCode: http.StatusTemporaryRedirect, + AdditionalHeaders: map[string][]string{ + "Docker-Distribution-Api-Version": {"registry/2.0"}, + }, } - if u != nil { - headers.Status = http.StatusTemporaryRedirect - headers.Location = u.String() - - setResponseHeaders(ctx.Resp, headers) - return + if d := pfd.Properties.GetByName(container_module.PropertyDigest); d != "" { + opts.AdditionalHeaders["Docker-Content-Digest"] = []string{d} + opts.AdditionalHeaders["ETag"] = []string{fmt.Sprintf(`"%s"`, d)} } - defer s.Close() - - setResponseHeaders(ctx.Resp, headers) - if _, err := io.Copy(ctx.Resp, s); err != nil { - log.Error("Error whilst copying content to response: %v", err) - } + helper.ServePackageFile(ctx, s, u, pf, opts) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery @@ -725,7 +732,7 @@ func GetTagList(ctx *context.Context) { image := ctx.Params("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiErrorDefined(ctx, errNameUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/container/container_test.go b/routers/api/packages/container/container_test.go new file mode 100644 index 0000000000..2ed38d846d --- /dev/null +++ b/routers/api/packages/container/container_test.go @@ -0,0 +1,124 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetResponseHeaders(t *testing.T) { + t.Run("Content-Length for empty content", func(t *testing.T) { + recorder := httptest.NewRecorder() + + setResponseHeaders(recorder, &containerHeaders{ + Status: http.StatusOK, + ContentLength: 0, // Empty blob + ContentDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }) + + assert.Equal(t, "0", recorder.Header().Get("Content-Length")) + assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", recorder.Header().Get("Docker-Content-Digest")) + assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version")) + assert.Equal(t, http.StatusOK, recorder.Code) + }) + + t.Run("Content-Length for non-empty content", func(t *testing.T) { + recorder := httptest.NewRecorder() + + setResponseHeaders(recorder, &containerHeaders{ + Status: http.StatusOK, + ContentLength: 1024, + ContentDigest: "sha256:abcd1234", + }) + + assert.Equal(t, "1024", recorder.Header().Get("Content-Length")) + assert.Equal(t, "sha256:abcd1234", recorder.Header().Get("Docker-Content-Digest")) + }) + + t.Run("All headers set correctly", func(t *testing.T) { + recorder := httptest.NewRecorder() + + setResponseHeaders(recorder, &containerHeaders{ + Status: http.StatusAccepted, + ContentLength: 512, + ContentDigest: "sha256:test123", + ContentType: "application/vnd.oci.image.manifest.v1+json", + Location: "/v2/test/repo/blobs/uploads/uuid123", + Range: "0-511", + UploadUUID: "uuid123", + }) + + assert.Equal(t, "512", recorder.Header().Get("Content-Length")) + assert.Equal(t, "sha256:test123", recorder.Header().Get("Docker-Content-Digest")) + assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", recorder.Header().Get("Content-Type")) + assert.Equal(t, "/v2/test/repo/blobs/uploads/uuid123", recorder.Header().Get("Location")) + assert.Equal(t, "0-511", recorder.Header().Get("Range")) + assert.Equal(t, "uuid123", recorder.Header().Get("Docker-Upload-Uuid")) + assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version")) + assert.Equal(t, `"sha256:test123"`, recorder.Header().Get("ETag")) + assert.Equal(t, http.StatusAccepted, recorder.Code) + }) +} + +// TestResponseHeadersForEmptyBlobs tests the core fix for ORAS empty blob support +func TestResponseHeadersForEmptyBlobs(t *testing.T) { + t.Run("Content-Length set for empty blob", func(t *testing.T) { + recorder := httptest.NewRecorder() + + // This tests the main fix: empty blobs should have Content-Length: 0 + setResponseHeaders(recorder, &containerHeaders{ + Status: http.StatusOK, + ContentLength: 0, // Empty blob (like empty config in ORAS artifacts) + ContentDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }) + + // The key fix: Content-Length should be set even for 0-byte blobs + assert.Equal(t, "0", recorder.Header().Get("Content-Length")) + assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", recorder.Header().Get("Docker-Content-Digest")) + assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version")) + assert.Equal(t, `"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`, recorder.Header().Get("ETag")) + assert.Equal(t, http.StatusOK, recorder.Code) + }) + + t.Run("Content-Length set for regular blob", func(t *testing.T) { + recorder := httptest.NewRecorder() + + setResponseHeaders(recorder, &containerHeaders{ + Status: http.StatusOK, + ContentLength: 1024, + ContentDigest: "sha256:abcd1234", + }) + + assert.Equal(t, "1024", recorder.Header().Get("Content-Length")) + assert.Equal(t, "sha256:abcd1234", recorder.Header().Get("Docker-Content-Digest")) + }) + + t.Run("All headers set correctly", func(t *testing.T) { + recorder := httptest.NewRecorder() + + setResponseHeaders(recorder, &containerHeaders{ + Status: http.StatusAccepted, + ContentLength: 512, + ContentDigest: "sha256:test123", + ContentType: "application/vnd.oci.image.manifest.v1+json", + Location: "/v2/test/repo/blobs/uploads/uuid123", + Range: "0-511", + UploadUUID: "uuid123", + }) + + assert.Equal(t, "512", recorder.Header().Get("Content-Length")) + assert.Equal(t, "sha256:test123", recorder.Header().Get("Docker-Content-Digest")) + assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", recorder.Header().Get("Content-Type")) + assert.Equal(t, "/v2/test/repo/blobs/uploads/uuid123", recorder.Header().Get("Location")) + assert.Equal(t, "0-511", recorder.Header().Get("Range")) + assert.Equal(t, "uuid123", recorder.Header().Get("Docker-Upload-Uuid")) + assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version")) + assert.Equal(t, `"sha256:test123"`, recorder.Header().Get("ETag")) + assert.Equal(t, http.StatusAccepted, recorder.Code) + }) +} diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 6e116e050d..b84b902d2b 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -155,7 +155,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -182,7 +182,7 @@ func DeletePackageFile(ctx *context.Context) { return pv, pf, nil }() if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go index 99c0867bbb..f9b91d9a09 100644 --- a/routers/api/packages/helper/helper.go +++ b/routers/api/packages/helper/helper.go @@ -39,16 +39,9 @@ func LogAndProcessError(ctx *context.Context, status int, obj any, cb func(strin } } -// Serves the content of the package file +// ServePackageFile Serves the content of the package file // If the url is set it will redirect the request, otherwise the content is copied to the response. func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) { - if u != nil { - ctx.Redirect(u.String()) - return - } - - defer s.Close() - var opts *context.ServeHeaderOptions if len(forceOpts) > 0 { opts = forceOpts[0] @@ -59,5 +52,12 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf } } + if u != nil { + ctx.Redirect(u.String(), opts.RedirectStatusCode) + return + } + + defer s.Close() + ctx.ServeContent(s, opts) } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index ea04a7b42e..30737f91dd 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -11,6 +11,7 @@ import ( "encoding/hex" "encoding/xml" "errors" + "fmt" "io" "net/http" "path/filepath" @@ -61,6 +62,12 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +// buildPackageID creates a package ID from group and artifact ID +// Refer to https://maven.apache.org/pom.html#Maven_Coordinates +func buildPackageID(groupID, artifactID string) string { + return fmt.Sprintf("%s:%s", groupID, artifactID) +} + // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { handlePackageFile(ctx, true) @@ -88,7 +95,7 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { func serveMavenMetadata(ctx *context.Context, params parameters) { // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - packageName := params.GroupID + "-" + params.ArtifactID + packageName := buildPackageID(params.GroupID, params.ArtifactID) pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -119,8 +126,8 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) ext := strings.ToLower(filepath.Ext(params.Filename)) if isChecksumExtension(ext) { @@ -150,7 +157,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID + packageName := buildPackageID(params.GroupID, params.ArtifactID) pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) if err != nil { @@ -169,9 +176,9 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename = filename[:len(filename)-len(ext)] } - pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) + pf, err := packages_model.GetFileForVersionByNameMatchCase(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -247,7 +254,7 @@ func UploadPackageFile(ctx *context.Context) { return } - packageName := params.GroupID + "-" + params.ArtifactID + packageName := buildPackageID(params.GroupID, params.ArtifactID) mavenUploadLock.CheckIn(packageName) defer mavenUploadLock.CheckOut(packageName) @@ -283,9 +290,9 @@ func UploadPackageFile(ctx *context.Context) { apiError(ctx, http.StatusInternalServerError, err) return } - pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) + pf, err := packages_model.GetFileForVersionByNameMatchCase(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -339,7 +346,7 @@ func UploadPackageFile(ctx *context.Context) { if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) - if err != nil && err != packages_model.ErrPackageNotExist { + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusInternalServerError, err) return } diff --git a/routers/api/shared/middleware.go b/routers/api/shared/middleware.go index f56acbe1bf..7d537f1ef9 100644 --- a/routers/api/shared/middleware.go +++ b/routers/api/shared/middleware.go @@ -30,7 +30,6 @@ func Middlewares() (stack []any) { return append(stack, context.APIContexter(), - checkDeprecatedAuthMethods, // Get user from session if logged in. apiAuth(buildAuthGroup()), verifyAuthWithOptions(&common.VerifyOptions{ @@ -127,13 +126,6 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC } } -// check for and warn against deprecated authentication options -func checkDeprecatedAuthMethods(ctx *context.APIContext) { - if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" { - ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.") - } -} - func securityHeaders() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go index 2d9cd3b1bb..b84fbe05fa 100644 --- a/routers/api/v1/activitypub/reqsignature.go +++ b/routers/api/v1/activitypub/reqsignature.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "database/sql" "encoding/pem" + "errors" "fmt" "net/http" "net/url" @@ -29,7 +30,7 @@ import ( func decodePublicKeyPem(pubKeyPem string) ([]byte, error) { block, _ := pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { - return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") + return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") } return block.Bytes, nil diff --git a/routers/api/v1/admin/quota_rule.go b/routers/api/v1/admin/quota_rule.go index ea188107fa..c2bc6843e4 100644 --- a/routers/api/v1/admin/quota_rule.go +++ b/routers/api/v1/admin/quota_rule.go @@ -4,7 +4,7 @@ package admin import ( - "fmt" + "errors" "net/http" quota_model "forgejo.org/models/quota" @@ -83,7 +83,7 @@ func CreateQuotaRule(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateQuotaRuleOptions) if form.Limit == nil { - ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", fmt.Errorf("[Limit]: Required")) + ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", errors.New("[Limit]: Required")) return } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 32e1dd414d..8aa67b3b0a 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -140,7 +140,6 @@ func CreateUser(ctx *context.APIContext) { user_model.IsErrEmailAlreadyUsed(err) || db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || - validation.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) || db.IsErrNamePatternNotAllowed(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -197,7 +196,7 @@ func EditUser(ctx *context.APIContext) { // If either LoginSource or LoginName is given, the other must be present too. if form.SourceID != nil || form.LoginName != nil { if form.SourceID == nil || form.LoginName == nil { - ctx.Error(http.StatusUnprocessableEntity, "LoginSourceAndLoginName", fmt.Errorf("source_id and login_name must be specified together")) + ctx.Error(http.StatusUnprocessableEntity, "LoginSourceAndLoginName", errors.New("source_id and login_name must be specified together")) return } } @@ -226,7 +225,7 @@ func EditUser(ctx *context.APIContext) { if form.Email != nil { if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { switch { - case validation.IsErrEmailCharIsNotSupported(err), validation.IsErrEmailInvalid(err): + case validation.IsErrEmailInvalid(err): ctx.Error(http.StatusBadRequest, "EmailInvalid", err) case user_model.IsErrEmailAlreadyUsed(err): ctx.Error(http.StatusBadRequest, "EmailUsed", err) @@ -305,7 +304,7 @@ func DeleteUser(ctx *context.APIContext) { // admin should not delete themself if ctx.ContextUser.ID == ctx.Doer.ID { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself")) + ctx.Error(http.StatusUnprocessableEntity, "", errors.New("you cannot delete yourself")) return } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1919664cce..bf08bdd249 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -22,8 +22,6 @@ // // Security: // - BasicAuth : -// - Token : -// - AccessToken : // - AuthorizationHeaderToken : // - SudoParam : // - SudoHeader : @@ -32,16 +30,6 @@ // SecurityDefinitions: // BasicAuth: // type: basic -// Token: -// type: apiKey -// name: token -// in: query -// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead. -// AccessToken: -// type: apiKey -// name: access_token -// in: query -// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead. // AuthorizationHeaderToken: // type: apiKey // name: Authorization @@ -1184,6 +1172,10 @@ func Routes() *web.Route { }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) + m.Group("/runs", func() { + m.Get("", repo.ListActionRuns) + m.Get("/{run_id}", repo.GetActionRun) + }) m.Group("/workflows", func() { m.Group("/{workflowname}", func() { @@ -1318,6 +1310,7 @@ func Routes() *web.Route { m.Get("/refs", repo.GetGitAllRefs) m.Get("/refs/*", repo.GetGitRefs) m.Get("/trees/{sha}", repo.GetTree) + m.Get("/blobs", repo.GetBlobs) m.Get("/blobs/{sha}", repo.GetBlob) m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Group("/notes/{sha}", func() { diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index fcf2c6b412..87bc27be63 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -398,7 +398,7 @@ func Edit(ctx *context.APIContext) { ctx.Org.Organization.Email = "" } else { if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), *form.Email); err != nil { - if validation.IsErrEmailInvalid(err) || validation.IsErrEmailCharIsNotSupported(err) { + if validation.IsErrEmailInvalid(err) { ctx.Error(http.StatusUnprocessableEntity, "ReplacePrimaryEmailAddress", err) } else { ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index a39d4836e1..dbc4933de6 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -5,6 +5,7 @@ package repo import ( "errors" + "fmt" "net/http" actions_model "forgejo.org/models/actions" @@ -694,3 +695,161 @@ func DispatchWorkflow(ctx *context.APIContext) { ctx.JSON(http.StatusNoContent, nil) } } + +// ListActionRuns return a filtered list of ActionRun +func ListActionRuns(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository ListActionRuns + // --- + // summary: List a repository's action runs + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results, default maximum page size is 50 + // type: integer + // - name: event + // in: query + // description: Returns workflow run triggered by the specified events. For example, `push`, `pull_request` or `workflow_dispatch`. + // type: array + // items: + // type: string + // - name: status + // in: query + // description: | + // Returns workflow runs with the check run status or conclusion that is specified. For example, a conclusion can be success or a status can be in_progress. Only Forgejo Actions can set a status of waiting, pending, or requested. + // type: array + // items: + // type: string + // enum: [unknown, waiting, running, success, failure, cancelled, skipped, blocked] + // - name: run_number + // in: query + // description: | + // Returns the workflow run associated with the run number. + // type: integer + // format: int64 + // - name: head_sha + // in: query + // description: Only returns workflow runs that are associated with the specified head_sha. + // type: string + // responses: + // "200": + // "$ref": "#/responses/ActionRunList" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + + statusStrs := ctx.FormStrings("status") + statuses := make([]actions_model.Status, len(statusStrs)) + for i, s := range statusStrs { + if status, exists := actions_model.StatusFromString(s); exists { + statuses[i] = status + } else { + ctx.Error(http.StatusBadRequest, "StatusFromString", fmt.Sprintf("unknown status: %s", s)) + return + } + } + + runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, &actions_model.FindRunJobOptions{ + ListOptions: utils.GetListOptions(ctx), + OwnerID: ctx.Repo.Owner.ID, + RepoID: ctx.Repo.Repository.ID, + Events: ctx.FormStrings("event"), + Statuses: statuses, + RunNumber: ctx.FormInt64("run_number"), + CommitSHA: ctx.FormString("head_sha"), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListActionRuns", err) + return + } + + res := new(api.ListActionRunResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionRun, len(runs)) + for i, r := range runs { + if err := r.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + return + } + cr := convert.ToActionRun(ctx, r, ctx.Doer) + res.Entries[i] = cr + } + + ctx.JSON(http.StatusOK, &res) +} + +// GetActionRun get one action instance +func GetActionRun(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id} repository ActionRun + // --- + // summary: Get an action run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: run_id + // in: path + // description: id of the action run + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActionRun" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + run, err := actions_model.GetRunByID(ctx, ctx.ParamsInt64(":run_id")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetRunById", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetRunByID", err) + } + return + } + + // Action runs lives in its own table, therefore we check that the + // run with the requested ID is owned by the repository + if ctx.Repo.Repository.ID != run.RepoID { + ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist) + return + } + + if err := run.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToActionRun(ctx, run, ctx.Doer)) +} diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index 8ed57d4787..63baec2025 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -5,11 +5,54 @@ package repo import ( "net/http" + "strings" "forgejo.org/services/context" files_service "forgejo.org/services/repository/files" ) +// GetBlobs gets multiple blobs of a repository. +func GetBlobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/git/blobs repository GetBlobs + // --- + // summary: Gets multiplbe blobs of a repository. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: shas + // in: query + // description: a comma separated list of blob-sha (mind the overall URL-length limit of ~2,083 chars) + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/GitBlobList" + // "400": + // "$ref": "#/responses/error" + + shas := ctx.FormString("shas") + if len(shas) == 0 { + ctx.Error(http.StatusBadRequest, "", "shas not provided") + return + } + + if blobs, err := files_service.GetBlobsBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, strings.Split(shas, ",")); err != nil { + ctx.Error(http.StatusBadRequest, "", err) + } else { + ctx.JSON(http.StatusOK, blobs) + } +} + // GetBlob get the blob of a repository file. func GetBlob(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/git/blobs/{sha} repository GetBlob @@ -30,12 +73,12 @@ func GetBlob(ctx *context.APIContext) { // required: true // - name: sha // in: path - // description: sha of the commit + // description: sha of the blob to retrieve // type: string // required: true // responses: // "200": - // "$ref": "#/responses/GitBlobResponse" + // "$ref": "#/responses/GitBlob" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 7474df4fd9..7c9593d625 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -6,7 +6,6 @@ package repo import ( "errors" - "fmt" "net/http" "forgejo.org/models" @@ -151,7 +150,7 @@ func DeleteBranch(ctx *context.APIContext) { } if ctx.Repo.Repository.IsMirror { - ctx.Error(http.StatusForbidden, "IsMirrored", fmt.Errorf("can not delete branch of an mirror repository")) + ctx.Error(http.StatusForbidden, "IsMirrored", errors.New("can not delete branch of an mirror repository")) return } @@ -160,9 +159,9 @@ func DeleteBranch(ctx *context.APIContext) { case git.IsErrBranchNotExist(err): ctx.NotFound(err) case errors.Is(err, repo_service.ErrBranchIsDefault): - ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) + ctx.Error(http.StatusForbidden, "DefaultBranch", errors.New("can not delete default branch")) case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) + ctx.Error(http.StatusForbidden, "IsProtectedBranch", errors.New("branch protected")) default: ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index dd1d7b3a9a..549fe9fae0 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -480,6 +480,8 @@ func ChangeFiles(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" // "413": // "$ref": "#/responses/quotaExceeded" // "422": @@ -584,6 +586,8 @@ func CreateFile(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" // "413": // "$ref": "#/responses/quotaExceeded" // "422": @@ -684,6 +688,8 @@ func UpdateFile(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" // "413": // "$ref": "#/responses/quotaExceeded" // "422": @@ -692,7 +698,7 @@ func UpdateFile(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions) if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) + ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", errors.New("repo is empty")) return } @@ -757,11 +763,19 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { ctx.Error(http.StatusForbidden, "Access", err) return } - if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || - models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { + if git_model.IsErrBranchAlreadyExists(err) || + models.IsErrFilenameInvalid(err) || + models.IsErrSHAOrCommitIDNotProvided(err) || + models.IsErrFilePathInvalid(err) || + models.IsErrRepoFileAlreadyExists(err) { ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) return } + if models.IsErrCommitIDDoesNotMatch(err) || + models.IsErrSHADoesNotMatch(err) { + ctx.Error(http.StatusConflict, "Conflict", err) + return + } if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) return diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 5495c4a6ba..442e109843 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -121,6 +121,12 @@ func SearchIssues(ctx *context.APIContext) { // description: Number of items per page // type: integer // minimum: 0 + // - name: sort + // in: query + // description: Type of sort + // type: string + // enum: [relevance, latest, oldest, recentupdate, leastupdate, mostcomment, leastcomment, nearduedate, farduedate] + // default: latest // responses: // "200": // "$ref": "#/responses/IssueList" @@ -276,7 +282,7 @@ func SearchIssues(ctx *context.APIContext) { IsClosed: isClosed, IncludedAnyLabelIDs: includedAnyLabels, MilestoneIDs: includedMilestones, - SortBy: issue_indexer.SortByCreatedDesc, + SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc), } if since != 0 { @@ -305,9 +311,10 @@ func SearchIssues(ctx *context.APIContext) { } } - // FIXME: It's unsupported to sort by priority repo when searching by indexer, - // it's indeed an regression, but I think it is worth to support filtering by indexer first. - _ = ctx.FormInt64("priority_repo_id") + priorityRepoID := ctx.FormInt64("priority_repo_id") + if priorityRepoID > 0 { + searchOpt.PriorityRepoID = optional.Some(priorityRepoID) + } ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index 85af1149ff..3b2935305c 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -5,7 +5,7 @@ package repo import ( - "fmt" + "errors" "net/http" "reflect" @@ -352,12 +352,12 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) labelNames = append(labelNames, rv.String()) default: ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string") - return nil, nil, fmt.Errorf("invalid label") + return nil, nil, errors.New("invalid label") } } if len(labelIDs) > 0 && len(labelNames) > 0 { ctx.Error(http.StatusBadRequest, "InvalidLabels", "labels should be an array of strings or integers") - return nil, nil, fmt.Errorf("invalid labels") + return nil, nil, errors.New("invalid labels") } if len(labelNames) > 0 { repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 7d88b1b2cd..61875b577c 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" "time" @@ -116,7 +117,7 @@ func ListTrackedTimes(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) + ctx.Error(http.StatusForbidden, "", errors.New("query by user not allowed; not enough rights")) return } } @@ -437,7 +438,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { } if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID { - ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) + ctx.Error(http.StatusForbidden, "", errors.New("query by user not allowed; not enough rights")) return } @@ -545,7 +546,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) + ctx.Error(http.StatusForbidden, "", errors.New("query by user not allowed; not enough rights")) return } } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index d0ab5e270e..a848a950db 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -123,12 +123,12 @@ func Migrate(ctx *context.APIContext) { gitServiceType := convert.ToGitServiceType(form.Service) if form.Mirror && setting.Mirror.DisableNewPull { - ctx.Error(http.StatusForbidden, "MirrorsGlobalDisabled", fmt.Errorf("the site administrator has disabled the creation of new pull mirrors")) + ctx.Error(http.StatusForbidden, "MirrorsGlobalDisabled", errors.New("the site administrator has disabled the creation of new pull mirrors")) return } if setting.Repository.DisableMigrations { - ctx.Error(http.StatusForbidden, "MigrationsGlobalDisabled", fmt.Errorf("the site administrator has disabled migrations")) + ctx.Error(http.StatusForbidden, "MigrationsGlobalDisabled", errors.New("the site administrator has disabled migrations")) return } @@ -224,9 +224,8 @@ func Migrate(ctx *context.APIContext) { HasWiki: &opts.Wiki, } - // only enabling wiki could return an error - if err = updateRepoUnits(ctx, repoOpt); err != nil { - log.Error("Failed to enable wiki on %s/%s repo. %w", repoOwner.Name, form.RepoName, err) + if err = updateRepoUnits(ctx, repoOwner.Name, repo, repoOpt); err != nil { + log.Error("Failed to update units on %s/%s repo. %w", repoOwner.Name, form.RepoName, err) } } diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index 87903d9f36..f3ceeaeacf 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" @@ -63,7 +64,7 @@ func GetNote(ctx *context.APIContext) { func getNote(ctx *context.APIContext, identifier string) { if ctx.Repo.GitRepo == nil { - ctx.InternalServerError(fmt.Errorf("no open git repo")) + ctx.InternalServerError(errors.New("no open git repo")) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 8456dcff59..c9dda124de 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -71,7 +71,7 @@ func ListPullRequests(ctx *context.APIContext) { // in: query // description: Type of sort // type: string - // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority] + // enum: [oldest, recentupdate, recentclose, leastupdate, mostcomment, leastcomment, priority] // - name: milestone // in: query // description: ID of the milestone @@ -1050,11 +1050,11 @@ func MergePullRequest(ctx *context.APIContext) { if err := repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, pr, headRepo); err != nil { switch { case errors.Is(err, repo_service.ErrBranchIsDefault): - ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("the head branch is the default branch")) + ctx.Error(http.StatusForbidden, "DefaultBranch", errors.New("the head branch is the default branch")) case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("the head branch is protected")) + ctx.Error(http.StatusForbidden, "IsProtectedBranch", errors.New("the head branch is protected")) case errors.Is(err, util.ErrPermissionDenied): - ctx.Error(http.StatusForbidden, "HeadBranch", fmt.Errorf("insufficient permission to delete head branch")) + ctx.Error(http.StatusForbidden, "HeadBranch", errors.New("insufficient permission to delete head branch")) default: ctx.Error(http.StatusInternalServerError, "DeleteBranchAfterMerge", err) } @@ -1194,10 +1194,17 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) return nil, nil, nil, "", "" } + baseBranchRef := baseBranch + if baseIsBranch { + baseBranchRef = git.BranchPrefix + baseBranch + } else if baseIsTag { + baseBranchRef = git.TagPrefix + baseBranch + } + // Check if head branch is valid. - headIsCommit := headGitRepo.IsBranchExist(headBranch) - headIsBranch := headGitRepo.IsTagExist(headBranch) - headIsTag := headGitRepo.IsCommitExist(baseBranch) + headIsCommit := ctx.Repo.GitRepo.IsCommitExist(headBranch) + headIsBranch := ctx.Repo.GitRepo.IsBranchExist(headBranch) + headIsTag := ctx.Repo.GitRepo.IsTagExist(headBranch) if !headIsCommit && !headIsBranch && !headIsTag { // Check if headBranch is short sha commit hash if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil { @@ -1209,18 +1216,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } } - baseBranchRef := baseBranch - if baseIsBranch { - baseBranchRef = git.BranchPrefix + baseBranch - } else if baseIsTag { - baseBranchRef = git.TagPrefix + baseBranch - } headBranchRef := headBranch - if headIsBranch { - headBranchRef = headBranch - } else if headIsTag { - headBranchRef = headBranch - } compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranchRef, headBranchRef, false, false) if err != nil { diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index aa4c7318a2..830a62bf54 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" "strings" @@ -581,7 +582,7 @@ func SubmitPullReview(ctx *context.APIContext) { } if review.Type != issues_model.ReviewTypePending { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted")) + ctx.Error(http.StatusUnprocessableEntity, "", errors.New("only a pending review can be submitted")) return } @@ -593,7 +594,7 @@ func SubmitPullReview(ctx *context.APIContext) { // if review stay pending return if reviewType == issues_model.ReviewTypePending { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending")) + ctx.Error(http.StatusUnprocessableEntity, "", errors.New("review stay pending")) return } @@ -634,7 +635,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.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed")) + ctx.Error(http.StatusUnprocessableEntity, "", errors.New("approve your own pull is not allowed")) return -1, true } reviewType = issues_model.ReviewTypeApprove @@ -643,7 +644,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.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed")) + ctx.Error(http.StatusUnprocessableEntity, "", errors.New("reject your own pull is not allowed")) return -1, true } reviewType = issues_model.ReviewTypeReject diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 336741b932..0bf958b523 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" @@ -226,7 +227,7 @@ func CreateRelease(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateReleaseOption) if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) + ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", errors.New("repo is empty")) return } rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 50f628e035..3d6a40e9ab 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -5,6 +5,7 @@ package repo import ( + "errors" "fmt" "net/http" "slices" @@ -646,7 +647,7 @@ func Edit(ctx *context.APIContext) { return } - if err := updateRepoUnits(ctx, opts); err != nil { + if err := updateRepoUnits(ctx, ctx.Repo.Owner.Name, ctx.Repo.Repository, opts); err != nil { return } @@ -723,7 +724,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err visibilityChanged = repo.IsPrivate != *opts.Private // 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 := fmt.Errorf("cannot change private repository to public") + err := errors.New("cannot change private repository to public") ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err) return err } @@ -778,10 +779,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err } // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings -func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { - owner := ctx.Repo.Owner - repo := ctx.Repo.Repository - +func updateRepoUnits(ctx *context.APIContext, owner string, repo *repo_model.Repository, opts api.EditRepoOption) error { var units []repo_model.RepoUnit var deleteUnitTypes []unit_model.Type @@ -794,12 +792,12 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { // Check that values are valid if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) { - err := fmt.Errorf("External tracker URL not valid") + err := errors.New("External tracker URL not valid") ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err) return err } if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { - err := fmt.Errorf("External tracker URL format not valid") + err := errors.New("External tracker URL format not valid") ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err) return err } @@ -870,7 +868,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { // Check that values are valid if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) { - err := fmt.Errorf("External wiki URL not valid") + err := errors.New("External wiki URL not valid") ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL") return err } @@ -1044,7 +1042,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } } - log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name) + log.Trace("Repository advanced settings updated: %s/%s", owner, repo.Name) return nil } @@ -1054,7 +1052,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e // archive / un-archive if opts.Archived != nil { if repo.IsMirror { - err := fmt.Errorf("repo is a mirror, cannot archive/un-archive") + err := errors.New("repo is a mirror, cannot archive/un-archive") ctx.Error(http.StatusUnprocessableEntity, err.Error(), err) return err } diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index 3b6cb4d3f2..72cfeaf902 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -238,7 +238,7 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil) - return fmt.Errorf("user does not have permissions to do this") + return errors.New("user does not have permissions to do this") } if accept { diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index bf7e2cc0c3..cd4832e15f 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -231,11 +231,18 @@ type swaggerGitTreeResponse struct { Body api.GitTreeResponse `json:"body"` } -// GitBlobResponse -// swagger:response GitBlobResponse -type swaggerGitBlobResponse struct { +// GitBlob +// swagger:response GitBlob +type swaggerGitBlob struct { // in: body - Body api.GitBlobResponse `json:"body"` + Body api.GitBlob `json:"body"` +} + +// GitBlobList +// swagger:response GitBlobList +type swaggerGitBlobList struct { + // in: body + Body []api.GitBlob `json:"body"` } // Commit @@ -455,3 +462,17 @@ type swaggerSyncForkInfo struct { // in:body Body []api.SyncForkInfo `json:"body"` } + +// ActionRunList +// swagger:response ActionRunList +type swaggerActionRunList struct { + // in:body + Body api.ListActionRunResponse `json:"body"` +} + +// ActionRun +// swagger:response ActionRun +type swaggerActionRun struct { + // in:body + Body api.ActionRun `json:"body"` +} diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index 7b18ea97b0..03d8d14b90 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -75,14 +75,11 @@ func AddEmail(ctx *context.APIContext) { if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { if user_model.IsErrEmailAlreadyUsed(err) { ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email) - } else if validation.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) { + } else if validation.IsErrEmailInvalid(err) { email := "" if typedError, ok := err.(validation.ErrEmailInvalid); ok { email = typedError.Email } - if typedError, ok := err.(validation.ErrEmailCharIsNotSupported); ok { - email = typedError.Email - } errMsg := fmt.Sprintf("Email address %q invalid", email) ctx.Error(http.StatusUnprocessableEntity, "", errMsg) diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index 1581358b66..886e33b205 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -4,6 +4,7 @@ package user import ( + "errors" "fmt" "net/http" "strings" @@ -143,7 +144,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.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) return } @@ -298,7 +299,7 @@ func DeleteGPGKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) return } diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 7119de9b2e..d8b5dfdfe9 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -5,7 +5,7 @@ package user import ( std_ctx "context" - "fmt" + "errors" "net/http" asymkey_model "forgejo.org/models/asymkey" @@ -209,7 +209,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.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } @@ -285,7 +285,7 @@ func DeletePublicKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index 5359a54899..65a8994405 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -5,6 +5,7 @@ package utils import ( gocontext "context" + "errors" "fmt" "net/http" @@ -50,7 +51,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string { // GetGitRefs return git references based on filter func GetGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, string, error) { if ctx.Repo.GitRepo == nil { - return nil, "", fmt.Errorf("no open git repo found in context") + return nil, "", errors.New("no open git repo found in context") } if len(filter) > 0 { filter = "refs/" + filter diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index d882845008..fc4b3293ac 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -16,6 +16,7 @@ import ( "forgejo.org/modules/setting" api "forgejo.org/modules/structs" "forgejo.org/modules/util" + "forgejo.org/modules/validation" webhook_module "forgejo.org/modules/webhook" "forgejo.org/services/context" webhook_service "forgejo.org/services/webhook" @@ -93,6 +94,10 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") return false } + if !validation.IsValidURL(form.Config["url"]) { + ctx.Error(http.StatusUnprocessableEntity, "", "Invalid url") + return false + } return true } @@ -322,6 +327,10 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6 func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool { if form.Config != nil { if url, ok := form.Config["url"]; ok { + if !validation.IsValidURL(url) { + ctx.Error(http.StatusUnprocessableEntity, "", "Invalid url") + return false + } w.URL = url } if ct, ok := form.Config["content_type"]; ok { diff --git a/routers/api/v1/utils/hook_test.go b/routers/api/v1/utils/hook_test.go new file mode 100644 index 0000000000..3d0e6db079 --- /dev/null +++ b/routers/api/v1/utils/hook_test.go @@ -0,0 +1,86 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "net/http" + "testing" + + "forgejo.org/models/unittest" + "forgejo.org/modules/structs" + "forgejo.org/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestTestHookValidation(t *testing.T) { + unittest.PrepareTestEnv(t) + + t.Run("Test Validation", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadGitRepo(t, ctx) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "gitea", + Config: map[string]string{ + "content_type": "json", + "url": "https://example.com/webhook", + }, + }) + assert.Equal(t, 0, ctx.Resp.WrittenStatus()) // not written yet + }) + + t.Run("Test Validation with invalid URL", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadGitRepo(t, ctx) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "gitea", + Config: map[string]string{ + "content_type": "json", + "url": "example.com/webhook", + }, + }) + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + }) + + t.Run("Test Validation with invalid webhook type", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadGitRepo(t, ctx) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "unknown", + Config: map[string]string{ + "content_type": "json", + "url": "example.com/webhook", + }, + }) + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + }) + + t.Run("Test Validation with empty content type", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadGitRepo(t, ctx) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "unknown", + Config: map[string]string{ + "url": "https://example.com/webhook", + }, + }) + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + }) +} diff --git a/routers/api/v1/utils/main_test.go b/routers/api/v1/utils/main_test.go new file mode 100644 index 0000000000..f243572436 --- /dev/null +++ b/routers/api/v1/utils/main_test.go @@ -0,0 +1,21 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "testing" + + "forgejo.org/models/unittest" + "forgejo.org/modules/setting" + webhook_service "forgejo.org/services/webhook" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + SetUp: func() error { + setting.LoadQueueSettings() + return webhook_service.Init() + }, + }) +} diff --git a/routers/common/db.go b/routers/common/db.go index 0f78d8debc..ec31ced1bf 100644 --- a/routers/common/db.go +++ b/routers/common/db.go @@ -5,7 +5,7 @@ package common import ( "context" - "fmt" + "errors" "time" "forgejo.org/models/db" @@ -24,7 +24,7 @@ func InitDBEngine(ctx context.Context) (err error) { for i := 0; i < setting.Database.DBConnectRetries; i++ { select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization") + return errors.New("Aborted due to shutdown:\nin retry ORM engine initialization") default: } log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries) diff --git a/routers/private/manager_process.go b/routers/private/manager_process.go index 87447da2be..e60ed04879 100644 --- a/routers/private/manager_process.go +++ b/routers/private/manager_process.go @@ -122,7 +122,7 @@ func writeProcess(out io.Writer, process *process_module.Process, indent string, if stack.Count > 1 { _, _ = fmt.Fprintf(sb, "* %d", stack.Count) } - _, _ = fmt.Fprintf(sb, "\n") + _, _ = fmt.Fprintln(sb) indent += "| " if len(stack.Labels) > 0 { _, _ = fmt.Fprintf(sb, "%sLabels: %q:%q", indent, stack.Labels[0].Name, stack.Labels[0].Value) @@ -132,7 +132,7 @@ func writeProcess(out io.Writer, process *process_module.Process, indent string, _, _ = fmt.Fprintf(sb, ", %q:%q", label.Name, label.Value) } } - _, _ = fmt.Fprintf(sb, "\n") + _, _ = fmt.Fprintln(sb) } _, _ = fmt.Fprintf(sb, "%sStack:\n", indent) indent += " " diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 33b3d43571..964326291e 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -186,7 +186,7 @@ func NewUserPost(ctx *context.Context) { case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form) - case validation.IsErrEmailInvalid(err), validation.IsErrEmailCharIsNotSupported(err): + case validation.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form) case db.IsErrNameReserved(err): @@ -195,9 +195,6 @@ func NewUserPost(ctx *context.Context) { case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) - case db.IsErrNameCharsNotAllowed(err): - ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form) default: ctx.ServerError("CreateUser", err) } @@ -316,6 +313,9 @@ func editUserCommon(ctx *context.Context) { ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) + ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize + ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth + ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight } // EditUser show editing user page @@ -410,7 +410,7 @@ func EditUserPost(ctx *context.Context) { if form.Email != "" { if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { switch { - case validation.IsErrEmailCharIsNotSupported(err), validation.IsErrEmailInvalid(err): + case validation.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form) case user_model.IsErrEmailAlreadyUsed(err): diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 9b5364c813..dbb6665398 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -552,9 +552,6 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us case user_model.IsErrCooldownPeriod(err): ctx.Data["Err_UserName"] = true ctx.RenderWithErr(ctx.Locale.Tr("form.username_claiming_cooldown", err.(user_model.ErrCooldownPeriod).ExpireTime.Format(time.RFC1123Z)), tpl, form) - case validation.IsErrEmailCharIsNotSupported(err): - ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) case validation.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) @@ -767,7 +764,7 @@ func ActivatePost(ctx *context.Context) { ctx.HTML(http.StatusOK, TplActivate) return } - if !user.ValidatePassword(password) { + if !user.ValidatePassword(ctx, password) { ctx.Data["IsPasswordInvalid"] = true ctx.HTML(http.StatusOK, TplActivate) return diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index aa599bd252..e8e5d2c54b 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -225,7 +225,7 @@ func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, ser idToken := &oauth2.OIDCToken{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()), - Issuer: setting.AppURL, + Issuer: strings.TrimSuffix(setting.AppURL, "/"), Audience: []string{app.ClientID}, Subject: fmt.Sprint(grant.UserID), }, @@ -409,7 +409,7 @@ func IntrospectOAuth(ctx *context.Context) { if err == nil && app != nil { response.Active = true response.Scope = grant.Scope - response.Issuer = setting.AppURL + response.Issuer = strings.TrimSuffix(setting.AppURL, "/") response.Audience = []string{app.ClientID} response.Subject = fmt.Sprint(grant.UserID) } @@ -669,6 +669,7 @@ func GrantApplicationOAuth(ctx *context.Context) { // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities func OIDCWellKnown(ctx *context.Context) { ctx.Data["SigningKey"] = oauth2.DefaultSigningKey + ctx.Data["Issuer"] = strings.TrimSuffix(setting.AppURL, "/") ctx.JSONTemplate("user/auth/oidc_wellknown") } diff --git a/routers/web/auth/oauth_test.go b/routers/web/auth/oauth_test.go index 487b551d6c..9782711dd0 100644 --- a/routers/web/auth/oauth_test.go +++ b/routers/web/auth/oauth_test.go @@ -51,6 +51,7 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) { // Scopes: openid oidcToken := createAndParseToken(t, grants[0]) + assert.Equal(t, "https://try.gitea.io", oidcToken.RegisteredClaims.Issuer) assert.Empty(t, oidcToken.Name) assert.Empty(t, oidcToken.PreferredUsername) assert.Empty(t, oidcToken.Profile) @@ -67,6 +68,7 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) { // Scopes: openid profile email oidcToken = createAndParseToken(t, grants[0]) + assert.Equal(t, "https://try.gitea.io", oidcToken.RegisteredClaims.Issuer) assert.Equal(t, "User Five", oidcToken.Name) assert.Equal(t, "user5", oidcToken.PreferredUsername) assert.Equal(t, "https://try.gitea.io/user5", oidcToken.Profile) diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index b12dea84ea..fcb2155953 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -4,6 +4,7 @@ package auth import ( + "errors" "fmt" "net/http" "net/url" @@ -55,13 +56,13 @@ func allowedOpenIDURI(uri string) (err error) { } } // must match one of this or be refused - return fmt.Errorf("URI not allowed by whitelist") + return errors.New("URI not allowed by whitelist") } // A blacklist match expliclty forbids for _, pat := range setting.Service.OpenIDBlacklist { if pat.MatchString(uri) { - return fmt.Errorf("URI forbidden by blacklist") + return errors.New("URI forbidden by blacklist") } } diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 2ef030cb92..9b5804b976 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "forgejo.org/models/asymkey" + "forgejo.org/models/user" "forgejo.org/modules/base" "forgejo.org/modules/templates" "forgejo.org/services/context" @@ -65,6 +67,19 @@ func Tmpl(ctx *context.Context) { ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) + userNonZero := &user.User{ID: 1} + ctx.Data["TrustedVerif"] = &asymkey.ObjectVerification{Verified: true, Reason: asymkey.NotSigned, SigningUser: userNonZero, TrustStatus: "trusted"} + ctx.Data["UntrustedVerif"] = &asymkey.ObjectVerification{Verified: true, Reason: asymkey.NotSigned, SigningUser: userNonZero, TrustStatus: "untrusted"} + ctx.Data["UnmatchedVerif"] = &asymkey.ObjectVerification{Verified: true, Reason: asymkey.NotSigned, SigningUser: userNonZero, TrustStatus: ""} + ctx.Data["WarnVerif"] = &asymkey.ObjectVerification{Verified: false, Warning: true, Reason: asymkey.NotSigned, SigningUser: userNonZero} + ctx.Data["UnknownVerif"] = &asymkey.ObjectVerification{Verified: false, Warning: false, Reason: asymkey.NotSigned, SigningUser: userNonZero} + userUnknown := &user.User{ID: 0} + ctx.Data["TrustedVerifUnk"] = &asymkey.ObjectVerification{Verified: true, Reason: asymkey.NotSigned, SigningUser: userUnknown, TrustStatus: "trusted"} + ctx.Data["UntrustedVerifUnk"] = &asymkey.ObjectVerification{Verified: true, Reason: asymkey.NotSigned, SigningUser: userUnknown, TrustStatus: "untrusted"} + ctx.Data["UnmatchedVerifUnk"] = &asymkey.ObjectVerification{Verified: true, Reason: asymkey.NotSigned, SigningUser: userUnknown, TrustStatus: ""} + ctx.Data["WarnVerifUnk"] = &asymkey.ObjectVerification{Verified: false, Warning: true, Reason: asymkey.NotSigned, SigningUser: userUnknown} + ctx.Data["UnknownVerifUnk"] = &asymkey.ObjectVerification{Verified: false, Warning: false, Reason: asymkey.NotSigned, SigningUser: userUnknown} + if ctx.Req.Method == "POST" { _ = ctx.Req.ParseForm() ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 01e663a672..7b09c92ee5 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -25,7 +25,7 @@ import ( "forgejo.org/services/context" "github.com/gorilla/feeds" - "github.com/jaytaylor/html2text" + "github.com/inbucket/html2text" ) func toBranchLink(ctx *context.Context, act *activities_model.Action) string { diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index 2f1438b9d0..87b5247599 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -20,7 +20,7 @@ func SSHInfo(rw http.ResponseWriter, req *http.Request) { return } rw.Header().Set("content-type", "text/json;charset=UTF-8") - _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) + _, err := rw.Write([]byte(`{"type":"agit","version":1}`)) if err != nil { log.Error("fail to write result: err: %v", err) rw.WriteHeader(http.StatusInternalServerError) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index a3823565ed..8f14f8899c 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -175,10 +175,12 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor return } - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) + if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to NewTruncatedReader: %v", err) } else { - if profileContent, err := markdown.RenderString(&markup.RenderContext{ + defer rc.Close() + + if profileContent, err := markdown.RenderReader(&markup.RenderContext{ Ctx: ctx, GitRepo: profileGitRepo, Links: markup.Links{ @@ -188,7 +190,7 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, Metas: map[string]string{"mode": "document"}, - }, bytes); err != nil { + }, rc); err != nil { log.Error("failed to RenderString: %v", err) } else { ctx.Data["ProfileReadme"] = profileContent diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 284f406413..c83242754b 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -50,6 +50,9 @@ func Settings(ctx *context.Context) { ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod + ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize + ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth + ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight err := shared_user.LoadHeaderCount(ctx) if err != nil { diff --git a/routers/web/repo/action_aggregator_test.go b/routers/web/repo/action_aggregator_test.go index e1c6d37fd0..94e6d506c5 100644 --- a/routers/web/repo/action_aggregator_test.go +++ b/routers/web/repo/action_aggregator_test.go @@ -94,6 +94,14 @@ func reqReview(t int64, name string, delReq bool) *issue_model.Comment { return c } +func ghostReqReview(t, id int64) *issue_model.Comment { + c := testComment(t) + c.Type = issue_model.CommentTypeReviewRequest + c.AssigneeTeam = organization.NewGhostTeam() + c.AssigneeTeamID = id + return c +} + func reqReviewList(t int64, del bool, names ...string) *issue_model.Comment { req := []issue_model.RequestReviewTarget{} for _, name := range names { @@ -179,7 +187,7 @@ func (kase *testCase) doTest(t *testing.T) { if len(after) != len(issue.Comments) { t.Logf("Expected %v comments, got %v", len(after), len(issue.Comments)) - t.Logf("Comments got after combination:") + t.Log("Comments got after combination:") for c := 0; c < len(issue.Comments); c++ { cmt := issue.Comments[c] t.Logf("%v %v %v\n", cmt.Type, cmt.CreatedUnix, cmt.Content) @@ -588,6 +596,27 @@ func TestCombineReviewRequests(t *testing.T) { reqReviewList(121, true, "titi", "toto-team"), }, }, + + // Ghost. + { + name: "ghost reviews", + beforeCombined: []*issue_model.Comment{ + reqReview(1, "titi", false), + ghostReqReview(2, 50), + ghostReqReview(3, 51), + ghostReqReview(4, 50), + }, + afterCombined: []*issue_model.Comment{ + { + PosterID: 1, + Type: issue_model.CommentTypeReviewRequest, + CreatedUnix: timeutil.TimeStamp(1), + AddedRequestReview: []issue_model.RequestReviewTarget{ + createReqReviewTarget("titi"), {Team: organization.NewGhostTeam()}, + }, + }, + }, + }, } for _, kase := range kases { diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 364bbf34a8..260468f207 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -517,7 +517,7 @@ func Cancel(ctx *context_module.Context) { return err } if n == 0 { - return fmt.Errorf("job has changed, try again") + return errors.New("job has changed, try again") } continue } diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index ccdd59f2dd..f4cc2a2cea 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -82,19 +82,19 @@ func RefBlame(ctx *context.Context) { return } - ctx.Data["NumLinesSet"] = true - ctx.Data["NumLines"], err = blob.GetBlobLineCount() - if err != nil { - ctx.ServerError("GetBlobLineCount", err) - return - } - result, err := performBlame(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormBool("bypass-blame-ignore")) if err != nil { ctx.ServerError("performBlame", err) return } + ctx.Data["NumLinesSet"] = true + numLines := 0 + for _, p := range result.Parts { + numLines += len(p.Lines) + } + ctx.Data["NumLines"] = numLines + ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index af8a838fc9..0fe52bfb48 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -70,11 +70,6 @@ func Branches(ctx *context.Context) { ctx.ServerError("LoadBranches", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { - for key := range commitStatuses { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) - } - } commitStatus := make(map[string]*git_model.CommitStatus) for commitID, cs := range commitStatuses { diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 89463d9d03..f3192266ad 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -16,7 +16,6 @@ import ( "forgejo.org/models/db" git_model "forgejo.org/models/git" repo_model "forgejo.org/models/repo" - unit_model "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/base" "forgejo.org/modules/charset" @@ -84,7 +83,7 @@ func Commits(ctx *context.Context) { ctx.ServerError("CommitsByRange", err) return } - ctx.Data["Commits"] = processGitCommits(ctx, commits) + ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository) ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name @@ -202,7 +201,7 @@ func SearchCommits(ctx *context.Context) { return } ctx.Data["CommitCount"] = len(commits) - ctx.Data["Commits"] = processGitCommits(ctx, commits) + ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository) ctx.Data["Keyword"] = query if all { @@ -267,7 +266,7 @@ func FileHistory(ctx *context.Context) { } } - ctx.Data["Commits"] = processGitCommits(ctx, commits) + ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository) ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name @@ -375,9 +374,6 @@ func Diff(ctx *context.Context) { if err != nil { log.Error("GetLatestCommitStatus: %v", err) } - if !ctx.Repo.CanRead(unit_model.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, statuses) - } ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses) ctx.Data["CommitStatuses"] = statuses @@ -456,20 +452,6 @@ func RawDiff(ctx *context.Context) { } } -func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) []*git_model.SignCommitWithStatuses { - commits := git_model.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository) - if !ctx.Repo.CanRead(unit_model.TypeActions) { - for _, commit := range commits { - if commit.Status == nil { - continue - } - commit.Status.HideActionsURL(ctx) - git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) - } - } - return commits -} - func SetCommitNotes(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CommitNotesForm) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index f5826cf249..de2e29ab9f 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -654,7 +654,7 @@ func PrepareCompareDiff( return false } - commits := processGitCommits(ctx, ci.CompareInfo.Commits) + commits := git_model.ParseCommitsWithStatus(ctx, ci.CompareInfo.Commits, ctx.Repo.Repository) ctx.Data["Commits"] = commits ctx.Data["CommitCount"] = len(commits) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index b97c268ae2..a34e3b7c78 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -348,11 +348,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.ServerError("GetIssuesAllCommitStatus", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { - for key := range commitStatuses { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) - } - } if err := issues.LoadAttributes(ctx); err != nil { ctx.ServerError("issues.LoadAttributes", err) @@ -1313,7 +1308,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use } // Special user that can't have associated contributions and permissions in the repo. - if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() { + if poster.IsSystem() || poster.IsAPServerActor() { return roleDescriptor, nil } @@ -1698,7 +1693,7 @@ func ViewIssue(ctx *context.Context) { return } ghostMilestone := &issues_model.Milestone{ - ID: -1, + ID: issues_model.GhostMilestoneID, Name: ctx.Locale.TrString("repo.issues.deleted_milestone"), } if comment.OldMilestoneID > 0 && comment.OldMilestone == nil { @@ -1799,15 +1794,6 @@ func ViewIssue(ctx *context.Context) { ctx.ServerError("LoadPushCommits", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { - for _, commit := range comment.Commits { - if commit.Status == nil { - continue - } - commit.Status.HideActionsURL(ctx) - git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) - } - } } else if comment.Type == issues_model.CommentTypeAddTimeManual || comment.Type == issues_model.CommentTypeStopTracking || comment.Type == issues_model.CommentTypeDeleteTimeManual { @@ -2789,7 +2775,7 @@ func SearchIssues(ctx *context.Context) { IncludedAnyLabelIDs: includedAnyLabels, MilestoneIDs: includedMilestones, ProjectID: projectID, - SortBy: issue_indexer.SortByCreatedDesc, + SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc), } if since != 0 { @@ -2818,9 +2804,10 @@ func SearchIssues(ctx *context.Context) { } } - // FIXME: It's unsupported to sort by priority repo when searching by indexer, - // it's indeed an regression, but I think it is worth to support filtering by indexer first. - _ = ctx.FormInt64("priority_repo_id") + priorityRepoID := ctx.FormInt64("priority_repo_id") + if priorityRepoID > 0 { + searchOpt.PriorityRepoID = optional.Some(priorityRepoID) + } ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { @@ -2958,7 +2945,7 @@ func ListIssues(ctx *context.Context) { IsPull: isPull, IsClosed: isClosed, ProjectID: projectID, - SortBy: issue_indexer.SortByCreatedDesc, + SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc), } if since != 0 { searchOpt.UpdatedAfterUnix = optional.Some(since) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index bb89e30d54..fd18646211 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -10,13 +10,16 @@ import ( "errors" "fmt" "html" + "html/template" "net/http" "net/url" + "path" "strconv" "strings" "forgejo.org/models" activities_model "forgejo.org/models/activities" + asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/db" git_model "forgejo.org/models/git" issues_model "forgejo.org/models/issues" @@ -28,11 +31,13 @@ import ( "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/base" + "forgejo.org/modules/charset" "forgejo.org/modules/emoji" "forgejo.org/modules/git" "forgejo.org/modules/gitrepo" issue_template "forgejo.org/modules/issue/template" "forgejo.org/modules/log" + "forgejo.org/modules/markup" "forgejo.org/modules/optional" "forgejo.org/modules/setting" "forgejo.org/modules/structs" @@ -498,6 +503,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -508,6 +514,12 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) ctx.Data["NumCommits"] = len(compareInfo.Commits) ctx.Data["NumFiles"] = compareInfo.NumFiles + commitIDs := map[string]bool{} + for _, commit := range compareInfo.Commits { + commitIDs[commit.ID.String()] = true + } + ctx.Data["CommitIDs"] = commitIDs + if len(compareInfo.Commits) != 0 { sha := compareInfo.Commits[0].ID.String() commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll) @@ -515,9 +527,6 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) ctx.ServerError("GetLatestCommitStatus", err) return nil } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) - } if len(commitStatuses) != 0 { ctx.Data["LatestCommitStatuses"] = commitStatuses @@ -581,9 +590,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.ServerError("GetLatestCommitStatus", err) return nil } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) - } if len(commitStatuses) > 0 { ctx.Data["LatestCommitStatuses"] = commitStatuses @@ -597,6 +603,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -607,6 +614,13 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["NumCommits"] = len(compareInfo.Commits) ctx.Data["NumFiles"] = compareInfo.NumFiles + + commitIDs := map[string]bool{} + for _, commit := range compareInfo.Commits { + commitIDs[commit.ID.String()] = true + } + ctx.Data["CommitIDs"] = commitIDs + return compareInfo } @@ -665,6 +679,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C } ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -677,9 +692,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.ServerError("GetLatestCommitStatus", err) return nil } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) - } if len(commitStatuses) > 0 { ctx.Data["LatestCommitStatuses"] = commitStatuses @@ -745,6 +757,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -769,6 +782,13 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["NumCommits"] = len(compareInfo.Commits) ctx.Data["NumFiles"] = compareInfo.NumFiles + + commitIDs := map[string]bool{} + for _, commit := range compareInfo.Commits { + commitIDs[commit.ID.String()] = true + } + ctx.Data["CommitIDs"] = commitIDs + return compareInfo } @@ -847,7 +867,7 @@ func ViewPullCommits(ctx *context.Context) { ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - commits := processGitCommits(ctx, prInfo.Commits) + commits := git_model.ParseCommitsWithStatus(ctx, prInfo.Commits, ctx.Repo.Repository) ctx.Data["Commits"] = commits ctx.Data["CommitCount"] = len(commits) @@ -928,7 +948,78 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit - if willShowSpecifiedCommit || willShowSpecifiedCommitRange { + if willShowSpecifiedCommit { + commitID := specifiedEndCommit + + ctx.Data["CommitID"] = commitID + + var prevCommit, curCommit, nextCommit *git.Commit + + // Iterate in reverse to properly map "previous" and "next" buttons + for i := len(prInfo.Commits) - 1; i >= 0; i-- { + commit := prInfo.Commits[i] + + if curCommit != nil { + nextCommit = commit + break + } + + if commit.ID.String() == commitID { + curCommit = commit + } else { + prevCommit = commit + } + } + + if curCommit == nil { + ctx.ServerError("Repo.GitRepo.viewPullFiles", git.ErrNotExist{ID: commitID}) + return + } + + ctx.Data["Commit"] = curCommit + if prevCommit != nil { + ctx.Data["PrevCommitLink"] = path.Join(ctx.Repo.RepoLink, "pulls", strconv.FormatInt(issue.Index, 10), "commits", prevCommit.ID.String()) + } + if nextCommit != nil { + ctx.Data["NextCommitLink"] = path.Join(ctx.Repo.RepoLink, "pulls", strconv.FormatInt(issue.Index, 10), "commits", nextCommit.ID.String()) + } + + statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) + if err != nil { + log.Error("GetLatestCommitStatus: %v", err) + } + + ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses) + ctx.Data["CommitStatuses"] = statuses + + verification := asymkey_model.ParseCommitWithSignature(ctx, curCommit) + ctx.Data["Verification"] = verification + ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, curCommit) + + note := &git.Note{} + err = git.GetNote(ctx, ctx.Repo.GitRepo, specifiedEndCommit, note) + if err == nil { + ctx.Data["NoteCommit"] = note.Commit + ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) + ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)), + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + if err != nil { + ctx.ServerError("RenderCommitMessage", err) + return + } + } + + endCommitID = commitID + startCommitID = prInfo.MergeBase + ctx.Data["IsShowingAllCommits"] = false + } else if willShowSpecifiedCommitRange { if len(specifiedEndCommit) > 0 { endCommitID = specifiedEndCommit } else { @@ -939,6 +1030,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi } else { startCommitID = prInfo.MergeBase } + ctx.Data["IsShowingAllCommits"] = false } else { endCommitID = headCommitID @@ -946,10 +1038,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi ctx.Data["IsShowingAllCommits"] = true } - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["AfterCommitID"] = endCommitID ctx.Data["BeforeCommitID"] = startCommitID + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name fileOnly := ctx.FormBool("file-only") diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 3c923c2c5e..493787ad8b 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -693,9 +693,6 @@ func SearchRepo(ctx *context.Context) { ctx.JSON(http.StatusInternalServerError, nil) return } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, latestCommitStatuses) - } results := make([]*repo_service.WebSearchRepository, len(repos)) for i, repo := range repos { diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 2e9c34e8a7..b9cb86bd08 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -342,6 +342,20 @@ func LFSFileGet(ctx *context.Context) { ctx.Data["IsVideoFile"] = true case st.IsAudio(): ctx.Data["IsAudioFile"] = true + case st.Is3DModel(): + ctx.Data["Is3DModelFile"] = true + switch { + case st.IsGLB(): + ctx.Data["IsGLBFile"] = true + case st.IsSTL(): + ctx.Data["IsSTLFile"] = true + case st.IsGLTF(): + ctx.Data["IsGLTFFile"] = true + case st.IsOBJ(): + ctx.Data["IsOBJFile"] = true + case st.Is3MF(): + ctx.Data["Is3MFFile"] = true + } case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()): ctx.Data["IsImageFile"] = true } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 6e4f0f4829..6f35e19880 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -23,6 +23,7 @@ import ( "forgejo.org/modules/base" "forgejo.org/modules/git" "forgejo.org/modules/indexer/code" + "forgejo.org/modules/indexer/issues" "forgejo.org/modules/indexer/stats" "forgejo.org/modules/lfs" "forgejo.org/modules/log" @@ -63,6 +64,9 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval + ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize + ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth + ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) ctx.Data["SigningKeyAvailable"] = len(signing) > 0 @@ -149,11 +153,9 @@ func UnitsPost(ctx *context.Context) { }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { - var wikiPermissions repo_model.UnitAccessMode + wikiPermissions := repo_model.UnitAccessModeUnset if form.GloballyWriteableWiki { wikiPermissions = repo_model.UnitAccessModeWrite - } else { - wikiPermissions = repo_model.UnitAccessModeRead } units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, @@ -775,6 +777,8 @@ func SettingsPost(ctx *context.Context) { return } code.UpdateRepoIndexer(ctx.Repo.Repository) + case "issues": + issues.UpdateRepoIndexer(ctx, ctx.Repo.Repository.ID) default: ctx.NotFound("", nil) return diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index 8b37eb92cb..3a81b85e4c 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -15,6 +15,7 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "forgejo.org/modules/web" "forgejo.org/services/context" "forgejo.org/services/contexttest" @@ -25,23 +26,8 @@ import ( "github.com/stretchr/testify/require" ) -func createSSHAuthorizedKeysTmpPath(t *testing.T) func() { - tmpDir := t.TempDir() - - oldPath := setting.SSH.RootPath - setting.SSH.RootPath = tmpDir - - return func() { - setting.SSH.RootPath = oldPath - } -} - func TestAddReadOnlyDeployKey(t *testing.T) { - if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil { - defer deferable() - } else { - return - } + defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())() unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys") @@ -65,11 +51,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) { } func TestAddReadWriteOnlyDeployKey(t *testing.T) { - if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil { - defer deferable() - } else { - return - } + defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())() unittest.PrepareTestEnv(t) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 6d4d9e47e2..0caa196e25 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -175,6 +175,9 @@ func ParseHookEvent(form forms.WebhookCoreForm) *webhook_module.HookEvent { Wiki: form.Wiki, Repository: form.Repository, Package: form.Package, + ActionRunFailure: form.ActionFailure, + ActionRunRecover: form.ActionRecover, + ActionRunSuccess: form.ActionSuccess, }, BranchFilter: form.BranchFilter, } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 219de722d2..bb3e1388a8 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -1,5 +1,6 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2023 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo @@ -368,9 +369,6 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { if err != nil { log.Error("GetLatestCommitStatus: %v", err) } - if !ctx.Repo.CanRead(unit_model.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, statuses) - } ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses) ctx.Data["LatestCommitStatuses"] = statuses @@ -441,8 +439,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { - if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { - _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) + if rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err == nil { + _, warnings := issue_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize) if len(warnings) > 0 { ctx.Data["FileWarning"] = strings.Join(warnings, "\n") } @@ -626,6 +624,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsVideoFile"] = true case fInfo.st.IsAudio(): ctx.Data["IsAudioFile"] = true + case fInfo.st.Is3DModel(): + ctx.Data["Is3DModelFile"] = true + switch { + case fInfo.st.IsGLB(): + ctx.Data["IsGLBFile"] = true + case fInfo.st.IsSTL(): + ctx.Data["IsSTLFile"] = true + case fInfo.st.IsGLTF(): + ctx.Data["IsGLTFFile"] = true + case fInfo.st.IsOBJ(): + ctx.Data["IsOBJFile"] = true + case fInfo.st.Is3MF(): + ctx.Data["Is3MFFile"] = true + } case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): ctx.Data["IsImageFile"] = true ctx.Data["CanCopyContent"] = true @@ -1240,6 +1252,7 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp func Watchers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.watchers") ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers") + ctx.Data["CardsNoneMsg"] = ctx.Tr("watch.list.none") ctx.Data["PageIsWatchers"] = true RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) { @@ -1251,6 +1264,7 @@ func Watchers(ctx *context.Context) { func Stars(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.stargazers") ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers") + ctx.Data["CardsNoneMsg"] = ctx.Tr("stars.list.none") ctx.Data["PageIsStargazers"] = true RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) { return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts) diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 9a21ac21a3..1b5265978a 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -393,7 +393,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } - ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) + ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commitsHistory, ctx.Repo.Repository) pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 5873df8a24..379e23cce4 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -67,6 +67,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{ UserID: ctx.ContextUser.ID, + IncludeLimited: ctx.IsSigned, IncludePrivate: showPrivate, }) if err != nil { diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 2a93221c8f..d980fa393a 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -16,7 +16,6 @@ import ( activities_model "forgejo.org/models/activities" asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/db" - git_model "forgejo.org/models/git" issues_model "forgejo.org/models/issues" "forgejo.org/models/organization" repo_model "forgejo.org/models/repo" @@ -611,11 +610,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.ServerError("GetIssuesLastCommitStatus", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { - for key := range commitStatuses { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) - } - } // ------------------------------- // Fill stats to post to ctx.Data. diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 9fa71add57..fdca1a2fdd 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -13,10 +13,8 @@ import ( activities_model "forgejo.org/models/activities" "forgejo.org/models/db" - git_model "forgejo.org/models/git" issues_model "forgejo.org/models/issues" repo_model "forgejo.org/models/repo" - "forgejo.org/models/unit" "forgejo.org/modules/base" "forgejo.org/modules/log" "forgejo.org/modules/optional" @@ -311,11 +309,6 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.ServerError("GetIssuesAllCommitStatus", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { - for key := range commitStatuses { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) - } - } ctx.Data["CommitLastStatus"] = lastStatus ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["Issues"] = issues diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 5132b1da5c..78dd6c5e7c 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -1,5 +1,6 @@ // Copyright 2015 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2023 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package user @@ -69,17 +70,6 @@ func userProfile(ctx *context.Context) { ctx.Data["OpenGraphURL"] = ctx.ContextUser.HTMLURL() ctx.Data["OpenGraphDescription"] = ctx.ContextUser.Description - // prepare heatmap data - if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserHeatmapDataByUser", err) - return - } - ctx.Data["HeatmapData"] = data - ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) - } - profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) defer profileClose() @@ -170,11 +160,32 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["Cards"] = followers total = int(numFollowers) ctx.Data["CardsTitle"] = ctx.TrN(total, "user.followers.title.one", "user.followers.title.few") + if ctx.IsSigned && ctx.ContextUser.ID == ctx.Doer.ID { + ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.incoming.list.self.none") + } else { + ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.incoming.list.none") + } case "following": ctx.Data["Cards"] = following total = int(numFollowing) ctx.Data["CardsTitle"] = ctx.TrN(total, "user.following.title.one", "user.following.title.few") + if ctx.IsSigned && ctx.ContextUser.ID == ctx.Doer.ID { + ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.self.none") + } else { + ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.none", ctx.ContextUser.Name) + } case "activity": + // prepare heatmap data + if setting.Service.EnableUserHeatmap { + data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserHeatmapDataByUser", err) + return + } + ctx.Data["HeatmapData"] = data + ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) + } + date := ctx.FormString("date") pagingNum = setting.UI.FeedPagingNum items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ @@ -253,10 +264,12 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb total = int(count) case "overview": - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) + if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to NewTruncatedReader: %v", err) } else { - if profileContent, err := markdown.RenderString(&markup.RenderContext{ + defer rc.Close() + + if profileContent, err := markdown.RenderReader(&markup.RenderContext{ Ctx: ctx, GitRepo: profileGitRepo, Links: markup.Links{ @@ -269,7 +282,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, Metas: map[string]string{"mode": "document"}, - }, bytes); err != nil { + }, rc); err != nil { log.Error("failed to RenderString: %v", err) } else { ctx.Data["ProfileReadme"] = profileContent diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index b421d28424..1dfcc90e35 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -57,7 +57,7 @@ func AccountPost(ctx *context.Context) { return } - if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) { + if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(ctx, form.OldPassword) { ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) } else if form.Password != form.Retype { ctx.Flash.Error(ctx.Tr("form.password_not_match")) @@ -212,7 +212,7 @@ func EmailPost(ctx *context.Context) { loadAccountData(ctx) ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) - } else if validation.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) { + } else if validation.IsErrEmailInvalid(err) { loadAccountData(ctx) ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 94d32b730f..935efd7ba7 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -5,7 +5,7 @@ package setting import ( - "fmt" + "errors" "net/http" asymkey_model "forgejo.org/models/asymkey" @@ -80,7 +80,7 @@ func KeysPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "gpg": if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) return } @@ -161,7 +161,7 @@ func KeysPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "ssh": if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } @@ -205,7 +205,7 @@ func KeysPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "verify_ssh": if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } @@ -242,7 +242,7 @@ func DeleteKey(ctx *context.Context) { switch ctx.FormString("type") { case "gpg": if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) return } if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil { @@ -252,7 +252,7 @@ func DeleteKey(ctx *context.Context) { } case "ssh": if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index fe6ffb802d..400ee71f08 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -51,6 +51,9 @@ func Profile(ctx *context.Context) { ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod ctx.Data["CommonPronouns"] = commonPronouns + ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize + ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth + ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight ctx.HTML(http.StatusOK, tplSettingsProfile) } @@ -63,6 +66,9 @@ func ProfilePost(ctx *context.Context) { ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod ctx.Data["CommonPronouns"] = commonPronouns + ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize + ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth + ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) diff --git a/routers/web/web.go b/routers/web/web.go index f8a13dab7e..4b39f22f7d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1510,7 +1510,10 @@ func registerRoutes(m *web.Route) { m.Group("/commits", func() { m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits) m.Get("/list", context.RepoRef(), repo.GetPullCommits) - m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) + m.Group("/{sha:[a-f0-9]{4,40}}", func() { + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) + m.Post("/reviews/submit", context.RepoMustNotBeArchived(), web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) + }) }) m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest) m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) diff --git a/services/actions/auth.go b/services/actions/auth.go index 4dc86a35f3..98b618aeba 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -4,6 +4,7 @@ package actions import ( + "errors" "fmt" "net/http" "strings" @@ -80,7 +81,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { parts := strings.SplitN(h, " ", 2) if len(parts) != 2 { log.Error("split token failed: %s", h) - return 0, fmt.Errorf("split token failed") + return 0, errors.New("split token failed") } return TokenToTaskID(parts[1]) @@ -100,7 +101,7 @@ func TokenToTaskID(token string) (int64, error) { c, ok := parsedToken.Claims.(*actionsClaims) if !parsedToken.Valid || !ok { - return 0, fmt.Errorf("invalid token claim") + return 0, errors.New("invalid token claim") } return c.TaskID, nil diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index fde5286e60..918be0f185 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -126,3 +126,9 @@ func CleanupLogs(ctx context.Context) error { log.Info("Removed %d logs", count) return nil } + +// CleanupOfflineRunners removes offline runners +func CleanupOfflineRunners(ctx context.Context, duration time.Duration, globalOnly bool) error { + olderThan := timeutil.TimeStampNow().AddDuration(-duration) + return actions_model.DeleteOfflineRunners(ctx, olderThan, globalOnly) +} diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 1fffa6852f..755fa648dc 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" "fmt" "path" @@ -50,7 +51,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("GetPushEventPayload: %w", err) } if payload.HeadCommit == nil { - return fmt.Errorf("head commit is missing in event payload") + return errors.New("head commit is missing in event payload") } sha = payload.HeadCommit.ID case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestMilestone: @@ -64,9 +65,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("GetPullRequestEventPayload: %w", err) } if payload.PullRequest == nil { - return fmt.Errorf("pull request is missing in event payload") + return errors.New("pull request is missing in event payload") } else if payload.PullRequest.Head == nil { - return fmt.Errorf("head of pull request is missing in event payload") + return errors.New("head of pull request is missing in event payload") } sha = payload.PullRequest.Head.Sha case webhook_module.HookEventRelease: diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index e55714a2c3..e240c996b5 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -345,6 +345,14 @@ func handleWorkflows( Status: actions_model.StatusWaiting, } + if workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content)); err == nil { + notifications, err := workflow.Notifications() + if err != nil { + log.Error("Notifications: %w", err) + } + run.NotifyEmail = notifications + } + need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer) if err != nil { log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err) @@ -366,8 +374,11 @@ func handleWorkflows( jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars)) if err != nil { - log.Error("jobparser.Parse: %v", err) - continue + run.Status = actions_model.StatusFailure + log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err) + jobs = []*jobparser.SingleWorkflow{{ + Name: dwf.EntryName, + }} } // cancel running jobs if the event is push or pull_request_sync diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 9bfa7d3e1d..cf8b29ead7 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -4,7 +4,9 @@ package actions import ( + "bytes" "context" + "errors" "fmt" "time" @@ -17,6 +19,7 @@ import ( webhook_module "forgejo.org/modules/webhook" "github.com/nektos/act/pkg/jobparser" + act_model "github.com/nektos/act/pkg/model" "xorm.io/builder" ) @@ -139,6 +142,16 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) return err } + workflow, err := act_model.ReadWorkflow(bytes.NewReader(cron.Content)) + if err != nil { + return err + } + notifications, err := workflow.Notifications() + if err != nil { + return err + } + run.NotifyEmail = notifications + // Parse the workflow specification from the cron schedule workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars)) if err != nil { @@ -205,7 +218,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. if n == 0 { - return fmt.Errorf("job has changed, try again") + return errors.New("job has changed, try again") } // Continue with the next job. diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go new file mode 100644 index 0000000000..7073985252 --- /dev/null +++ b/services/actions/schedule_tasks_test.go @@ -0,0 +1,121 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package actions + +import ( + "testing" + + actions_model "forgejo.org/models/actions" + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + webhook_module "forgejo.org/modules/webhook" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateScheduleTask(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2}) + + assertConstant := func(t *testing.T, cron *actions_model.ActionSchedule, run *actions_model.ActionRun) { + t.Helper() + assert.Equal(t, cron.Title, run.Title) + assert.Equal(t, cron.RepoID, run.RepoID) + assert.Equal(t, cron.OwnerID, run.OwnerID) + assert.Equal(t, cron.WorkflowID, run.WorkflowID) + assert.Equal(t, cron.TriggerUserID, run.TriggerUserID) + assert.Equal(t, cron.Ref, run.Ref) + assert.Equal(t, cron.CommitSHA, run.CommitSHA) + assert.Equal(t, cron.Event, run.Event) + assert.Equal(t, cron.EventPayload, run.EventPayload) + assert.Equal(t, cron.ID, run.ScheduleID) + assert.Equal(t, actions_model.StatusWaiting, run.Status) + } + + assertMutable := func(t *testing.T, expected, run *actions_model.ActionRun) { + t.Helper() + assert.Equal(t, expected.NotifyEmail, run.NotifyEmail) + } + + testCases := []struct { + name string + cron actions_model.ActionSchedule + want []actions_model.ActionRun + }{ + { + name: "simple", + cron: actions_model.ActionSchedule{ + Title: "scheduletitle1", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: "some.yml", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", + Content: []byte( + ` +name: test +on: push +jobs: + job2: + runs-on: ubuntu-latest + steps: + - run: true +`), + }, + want: []actions_model.ActionRun{ + { + Title: "scheduletitle1", + NotifyEmail: false, + }, + }, + }, + { + name: "enable-email-notifications is true", + cron: actions_model.ActionSchedule{ + Title: "scheduletitle2", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: "some.yml", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", + Content: []byte( + ` +name: test +enable-email-notifications: true +on: push +jobs: + job2: + runs-on: ubuntu-latest + steps: + - run: true +`), + }, + want: []actions_model.ActionRun{ + { + Title: "scheduletitle2", + NotifyEmail: true, + }, + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.NoError(t, CreateScheduleTask(t.Context(), &testCase.cron)) + require.Equal(t, len(testCase.want), unittest.GetCount(t, actions_model.ActionRun{RepoID: repo.ID})) + for _, expected := range testCase.want { + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{Title: expected.Title}) + assertConstant(t, &testCase.cron, run) + assertMutable(t, &expected, run) + } + unittest.AssertSuccessfulDelete(t, actions_model.ActionRun{RepoID: repo.ID}) + }) + } +} diff --git a/services/actions/task.go b/services/actions/task.go index 4e885f69cc..bb319c7d05 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" "fmt" actions_model "forgejo.org/models/actions" @@ -183,7 +184,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task } else if !has { return nil, util.ErrNotExist } else if runnerID != task.RunnerID { - return nil, fmt.Errorf("invalid runner for task") + return nil, errors.New("invalid runner for task") } if task.Status.IsDone() { diff --git a/services/actions/workflows.go b/services/actions/workflows.go index 7ec7c3abed..fbba3fd667 100644 --- a/services/actions/workflows.go +++ b/services/actions/workflows.go @@ -111,6 +111,11 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette return nil, nil, err } + notifications, err := wf.Notifications() + if err != nil { + return nil, nil, err + } + run := &actions_model.ActionRun{ Title: title, RepoID: repo.ID, @@ -125,6 +130,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette EventPayload: string(p), TriggerEvent: string(webhook.HookEventWorkflowDispatch), Status: actions_model.StatusWaiting, + NotifyEmail: notifications, } vars, err := actions_model.GetVariablesOfRun(ctx, run) diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go index 365bd7faf6..b6f763842b 100644 --- a/services/attachment/attachment.go +++ b/services/attachment/attachment.go @@ -51,7 +51,7 @@ func NewExternalAttachment(ctx context.Context, attach *repo_model.Attachment) ( if attach.ExternalURL == "" { return nil, fmt.Errorf("attachment %s should have a external url", attach.Name) } - if !validation.IsValidExternalURL(attach.ExternalURL) { + if !validation.IsValidReleaseAssetURL(attach.ExternalURL) { return nil, repo_model.ErrInvalidExternalURL{ExternalURL: attach.ExternalURL} } diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go index d3cbb8aa60..e776ccbbed 100644 --- a/services/auth/httpsign.go +++ b/services/auth/httpsign.go @@ -134,7 +134,7 @@ func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) { // Check if it's really a ssh certificate cert, ok := pk.(*ssh.Certificate) if !ok { - return nil, fmt.Errorf("no certificate found") + return nil, errors.New("no certificate found") } c := &ssh.CertChecker{ @@ -153,7 +153,7 @@ func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) { // check the CA of the cert if !c.IsUserAuthority(cert.SignatureKey) { - return nil, fmt.Errorf("CA check failed") + return nil, errors.New("CA check failed") } // Create a verifier @@ -191,7 +191,7 @@ func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) { } // No public key matching a principal in the certificate is registered in gitea - return nil, fmt.Errorf("no valid principal found") + return nil, errors.New("no valid principal found") } // doVerify iterates across the provided public keys attempting the verify the current request against each key in turn diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index e6d556d10b..093940aa18 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -121,18 +121,6 @@ func (o *OAuth2) Name() string { // representing whether the token exists or not func parseToken(req *http.Request) (string, bool) { _ = req.ParseForm() - if !setting.DisableQueryAuthToken { - // Check token. - if token := req.Form.Get("token"); token != "" { - return token, true - } - // Check access token. - if token := req.Form.Get("access_token"); token != "" { - return token, true - } - } else if req.Form.Get("token") != "" || req.Form.Get("access_token") != "" { - log.Warn("API token sent in query string but DISABLE_QUERY_AUTH_TOKEN=true") - } // check header token if auHead := req.Header.Get("Authorization"); auHead != "" { diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go index 7c18540a10..b1d8eae6ae 100644 --- a/services/auth/source/db/authenticate.go +++ b/services/auth/source/db/authenticate.go @@ -50,7 +50,7 @@ func Authenticate(ctx context.Context, user *user_model.User, login, password st if !user.IsPasswordSet() { return nil, ErrUserPasswordNotSet{UID: user.ID, Name: user.Name} - } else if !user.ValidatePassword(password) { + } else if !user.ValidatePassword(ctx, password) { return nil, ErrUserPasswordInvalid{UID: user.ID, Name: user.Name} } diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go index fba1fd8a01..b060b6b746 100644 --- a/services/auth/source/oauth2/token.go +++ b/services/auth/source/oauth2/token.go @@ -4,6 +4,7 @@ package oauth2 import ( + "errors" "fmt" "time" @@ -51,12 +52,12 @@ func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) { return nil, err } if !parsedToken.Valid { - return nil, fmt.Errorf("invalid token") + return nil, errors.New("invalid token") } var token *Token var ok bool if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid { - return nil, fmt.Errorf("invalid token") + return nil, errors.New("invalid token") } return token, nil } diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index 51a14edd9a..cbfe3bd54e 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -32,7 +32,7 @@ func Init() error { shared_automerge.PRAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler) if shared_automerge.PRAutoMergeQueue == nil { - return fmt.Errorf("unable to create pr_auto_merge queue") + return errors.New("unable to create pr_auto_merge queue") } go graceful.GetManager().RunWithCancel(shared_automerge.PRAutoMergeQueue) return nil @@ -107,6 +107,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { return } if !exists { + log.Trace("GetScheduledMergeByPullID found nothing for PR %d", pullID) return } @@ -204,6 +205,10 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { return } + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { + log.Error("DeleteScheduledAutoMerge[%d]: %v", pr.ID, err) + } + if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil { log.Error("pull_service.Merge: %v", err) // FIXME: if merge failed, we should display some error message to the pull request page. diff --git a/services/context/api.go b/services/context/api.go index 0a3cb04399..e9f67c720d 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -6,6 +6,7 @@ package context import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -365,12 +366,12 @@ func RepoRefForAPI(next http.Handler) http.Handler { ctx := GetAPIContext(req) if ctx.Repo.Repository.IsEmpty { - ctx.NotFound(fmt.Errorf("repository is empty")) + ctx.NotFound(errors.New("repository is empty")) return } if ctx.Repo.GitRepo == nil { - ctx.InternalServerError(fmt.Errorf("no open git repo")) + ctx.InternalServerError(errors.New("no open git repo")) return } diff --git a/services/context/api_test.go b/services/context/api_test.go index 610f15ed5a..4bc89939ca 100644 --- a/services/context/api_test.go +++ b/services/context/api_test.go @@ -9,13 +9,14 @@ import ( "testing" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenAPILinks(t *testing.T) { - setting.AppURL = "http://localhost:3000/" + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")() kases := map[string][]string{ "api/v1/repos/jerrykan/example-repo/issues?state=all": { `; rel="next"`, diff --git a/services/context/base.go b/services/context/base.go index 0275ea8a99..dc3d226bb0 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -250,7 +250,7 @@ func (b *Base) PlainText(status int, text string) { // Redirect redirects the request func (b *Base) Redirect(location string, status ...int) { code := http.StatusSeeOther - if len(status) == 1 { + if len(status) == 1 && status[0] > 0 { code = status[0] } diff --git a/services/context/base_test.go b/services/context/base_test.go index 868ac00f8b..9e058d8f24 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -9,11 +9,13 @@ import ( "testing" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" ) func TestRedirect(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")() req, _ := http.NewRequest("GET", "/", nil) cases := []struct { @@ -34,6 +36,7 @@ func TestRedirect(t *testing.T) { cleanup() has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy" assert.Equal(t, c.keep, has, "url = %q", c.url) + assert.Equal(t, http.StatusSeeOther, resp.Code) } req, _ = http.NewRequest("GET", "/", nil) @@ -45,3 +48,24 @@ func TestRedirect(t *testing.T) { assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) assert.Equal(t, http.StatusNoContent, resp.Code) } + +func TestRedirectOptionalStatus(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")() + req, _ := http.NewRequest("GET", "/", nil) + + cases := []struct { + expected int + actual int + }{ + {expected: 303}, + {http.StatusTemporaryRedirect, 307}, + {http.StatusPermanentRedirect, 308}, + } + for _, c := range cases { + resp := httptest.NewRecorder() + b, cleanup := NewBaseContext(resp, req) + b.Redirect("/", c.actual) + cleanup() + assert.Equal(t, c.expected, resp.Code) + } +} diff --git a/services/convert/action.go b/services/convert/action.go new file mode 100644 index 0000000000..703c1f1261 --- /dev/null +++ b/services/convert/action.go @@ -0,0 +1,49 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package convert + +import ( + "context" + + actions_model "forgejo.org/models/actions" + access_model "forgejo.org/models/perm/access" + user_model "forgejo.org/models/user" + api "forgejo.org/modules/structs" +) + +// ToActionRun convert actions_model.User to api.ActionRun +// the run needs all attributes loaded +func ToActionRun(ctx context.Context, run *actions_model.ActionRun, doer *user_model.User) *api.ActionRun { + if run == nil { + return nil + } + + permissionInRepo, _ := access_model.GetUserRepoPermission(ctx, run.Repo, doer) + + return &api.ActionRun{ + ID: run.ID, + Title: run.Title, + Repo: ToRepo(ctx, run.Repo, permissionInRepo), + WorkflowID: run.WorkflowID, + Index: run.Index, + TriggerUser: ToUser(ctx, run.TriggerUser, doer), + ScheduleID: run.ScheduleID, + PrettyRef: run.PrettyRef(), + IsRefDeleted: run.IsRefDeleted, + CommitSHA: run.CommitSHA, + IsForkPullRequest: run.IsForkPullRequest, + NeedApproval: run.NeedApproval, + ApprovedBy: run.ApprovedBy, + Event: run.Event.Event(), + EventPayload: run.EventPayload, + TriggerEvent: run.TriggerEvent, + Status: run.Status.String(), + Started: run.Started.AsTime(), + Stopped: run.Stopped.AsTime(), + Created: run.Created.AsTime(), + Updated: run.Updated.AsTime(), + Duration: run.Duration(), + HTMLURL: run.HTMLURL(), + } +} diff --git a/services/cron/setting.go b/services/cron/setting.go index 7fd4c4e1d8..2db6c15370 100644 --- a/services/cron/setting.go +++ b/services/cron/setting.go @@ -46,6 +46,13 @@ type CleanupHookTaskConfig struct { NumberToKeep int } +// CleanupOfflineRunnersConfig represents a cron task with settings to clean up offline-runner +type CleanupOfflineRunnersConfig struct { + BaseConfig + OlderThan time.Duration + GlobalScopeOnly bool +} + // GetSchedule returns the schedule for the base config func (b *BaseConfig) GetSchedule() string { return b.Schedule diff --git a/services/cron/tasks_actions.go b/services/cron/tasks_actions.go index a7fd3cd0bc..2cd484fa69 100644 --- a/services/cron/tasks_actions.go +++ b/services/cron/tasks_actions.go @@ -5,6 +5,7 @@ package cron import ( "context" + "time" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" @@ -20,6 +21,7 @@ func initActionsTasks() { registerCancelAbandonedJobs() registerScheduleTasks() registerActionsCleanup() + registerOfflineRunnersCleanup() } func registerStopZombieTasks() { @@ -74,3 +76,22 @@ func registerActionsCleanup() { return actions_service.Cleanup(ctx) }) } + +func registerOfflineRunnersCleanup() { + RegisterTaskFatal("cleanup_offline_runners", &CleanupOfflineRunnersConfig{ + BaseConfig: BaseConfig{ + Enabled: false, + RunAtStart: false, + Schedule: "@midnight", + }, + GlobalScopeOnly: true, + OlderThan: time.Hour * 24, + }, func(ctx context.Context, _ *user_model.User, cfg Config) error { + c := cfg.(*CleanupOfflineRunnersConfig) + return actions_service.CleanupOfflineRunners( + ctx, + c.OlderThan, + c.GlobalScopeOnly, + ) + }) +} diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go index 04a3680ff5..465a3fc7c0 100644 --- a/services/doctor/authorizedkeys.go +++ b/services/doctor/authorizedkeys.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" "os" "path/filepath" @@ -77,7 +78,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e fPath, "forgejo admin regenerate keys", "forgejo doctor check --run authorized-keys --fix") - return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "forgejo admin regenerate keys" or "forgejo doctor check --run authorized-keys --fix"`) + return errors.New(`authorized_keys is out of date and should be regenerated with "forgejo admin regenerate keys" or "forgejo doctor check --run authorized-keys --fix"`) } logger.Warn("authorized_keys is out of date. Attempting rewrite...") err = asymkey_model.RewriteAllPublicKeys(ctx) diff --git a/services/doctor/lfs.go b/services/doctor/lfs.go index fed127de5d..fe858605f4 100644 --- a/services/doctor/lfs.go +++ b/services/doctor/lfs.go @@ -5,7 +5,7 @@ package doctor import ( "context" - "fmt" + "errors" "time" "forgejo.org/modules/log" @@ -27,7 +27,7 @@ func init() { func garbageCollectLFSCheck(ctx context.Context, logger log.Logger, autofix bool) error { if !setting.LFS.StartServer { - return fmt.Errorf("LFS support is disabled") + return errors.New("LFS support is disabled") } if err := repository.GarbageCollectLFSMetaObjects(ctx, repository.GarbageCollectLFSMetaObjectsOptions{ diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go index f5d29b5ce5..5672313181 100644 --- a/services/externalaccount/link.go +++ b/services/externalaccount/link.go @@ -5,7 +5,7 @@ package externalaccount import ( "context" - "fmt" + "errors" user_model "forgejo.org/models/user" @@ -23,7 +23,7 @@ type Store interface { func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error { gothUser := store.Get("linkAccountGothUser") if gothUser == nil { - return fmt.Errorf("not in LinkAccount session") + return errors.New("not in LinkAccount session") } return LinkAccountToUser(ctx, user, gothUser.(goth.User)) diff --git a/services/f3/driver/asset.go b/services/f3/driver/asset.go deleted file mode 100644 index c9d2ecdf2f..0000000000 --- a/services/f3/driver/asset.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright Earl Warren -// Copyright Loïc Dachary -// SPDX-License-Identifier: MIT - -package driver - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "os" - - "forgejo.org/models/db" - repo_model "forgejo.org/models/repo" - user_model "forgejo.org/models/user" - "forgejo.org/modules/storage" - "forgejo.org/modules/timeutil" - "forgejo.org/services/attachment" - - "code.forgejo.org/f3/gof3/v3/f3" - f3_id "code.forgejo.org/f3/gof3/v3/id" - f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3" - "code.forgejo.org/f3/gof3/v3/tree/generic" - f3_util "code.forgejo.org/f3/gof3/v3/util" - "github.com/google/uuid" -) - -var _ f3_tree.ForgeDriverInterface = &issue{} - -type asset struct { - common - - forgejoAsset *repo_model.Attachment - sha string - contentType string - downloadFunc f3.DownloadFuncType -} - -func (o *asset) SetNative(asset any) { - o.forgejoAsset = asset.(*repo_model.Attachment) -} - -func (o *asset) GetNativeID() string { - return fmt.Sprintf("%d", o.forgejoAsset.ID) -} - -func (o *asset) NewFormat() f3.Interface { - node := o.GetNode() - return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind()) -} - -func (o *asset) ToFormat() f3.Interface { - if o.forgejoAsset == nil { - return o.NewFormat() - } - - return &f3.ReleaseAsset{ - Common: f3.NewCommon(o.GetNativeID()), - Name: o.forgejoAsset.Name, - ContentType: o.contentType, - Size: o.forgejoAsset.Size, - DownloadCount: o.forgejoAsset.DownloadCount, - Created: o.forgejoAsset.CreatedUnix.AsTime(), - SHA256: o.sha, - DownloadURL: o.forgejoAsset.DownloadURL(), - DownloadFunc: o.downloadFunc, - } -} - -func (o *asset) FromFormat(content f3.Interface) { - asset := content.(*f3.ReleaseAsset) - o.forgejoAsset = &repo_model.Attachment{ - ID: f3_util.ParseInt(asset.GetID()), - Name: asset.Name, - Size: asset.Size, - DownloadCount: asset.DownloadCount, - CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()), - CustomDownloadURL: asset.DownloadURL, - } - o.contentType = asset.ContentType - o.sha = asset.SHA256 - o.downloadFunc = asset.DownloadFunc -} - -func (o *asset) Get(ctx context.Context) bool { - node := o.GetNode() - o.Trace("%s", node.GetID()) - - id := node.GetID().Int64() - - asset, err := repo_model.GetAttachmentByID(ctx, id) - if repo_model.IsErrAttachmentNotExist(err) { - return false - } - if err != nil { - panic(fmt.Errorf("asset %v %w", id, err)) - } - - o.forgejoAsset = asset - - path := o.forgejoAsset.RelativePath() - - { - f, err := storage.Attachments.Open(path) - if err != nil { - panic(err) - } - hasher := sha256.New() - if _, err := io.Copy(hasher, f); err != nil { - panic(fmt.Errorf("io.Copy to hasher: %v", err)) - } - o.sha = hex.EncodeToString(hasher.Sum(nil)) - } - - o.downloadFunc = func() io.ReadCloser { - o.Trace("download %s from copy stored in temporary file %s", o.forgejoAsset.DownloadURL, path) - f, err := os.Open(path) - if err != nil { - panic(err) - } - return f - } - return true -} - -func (o *asset) Patch(ctx context.Context) { - o.Trace("%d", o.forgejoAsset.ID) - if _, err := db.GetEngine(ctx).ID(o.forgejoAsset.ID).Cols("name").Update(o.forgejoAsset); err != nil { - panic(fmt.Errorf("UpdateAssetCols: %v %v", o.forgejoAsset, err)) - } -} - -func (o *asset) Put(ctx context.Context) f3_id.NodeID { - node := o.GetNode() - o.Trace("%s", node.GetID()) - - uploader, err := user_model.GetAdminUser(ctx) - if err != nil { - panic(fmt.Errorf("GetAdminUser %w", err)) - } - - o.forgejoAsset.UploaderID = uploader.ID - o.forgejoAsset.RepoID = f3_tree.GetProjectID(o.GetNode()) - o.forgejoAsset.ReleaseID = f3_tree.GetReleaseID(o.GetNode()) - o.forgejoAsset.UUID = uuid.New().String() - - download := o.downloadFunc() - defer download.Close() - - _, err = attachment.NewAttachment(ctx, o.forgejoAsset, download, o.forgejoAsset.Size) - if err != nil { - panic(err) - } - - o.Trace("asset created %d", o.forgejoAsset.ID) - return f3_id.NewNodeID(o.forgejoAsset.ID) -} - -func (o *asset) Delete(ctx context.Context) { - node := o.GetNode() - o.Trace("%s", node.GetID()) - - if err := repo_model.DeleteAttachment(ctx, o.forgejoAsset, true); err != nil { - panic(err) - } -} - -func newAsset() generic.NodeDriverInterface { - return &asset{} -} diff --git a/services/f3/driver/assets.go b/services/f3/driver/assets.go deleted file mode 100644 index 106d5029f3..0000000000 --- a/services/f3/driver/assets.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright Earl Warren -// Copyright Loïc Dachary -// SPDX-License-Identifier: MIT - -package driver - -import ( - "context" - "fmt" - - repo_model "forgejo.org/models/repo" - - f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3" - "code.forgejo.org/f3/gof3/v3/tree/generic" -) - -type assets struct { - container -} - -func (o *assets) ListPage(ctx context.Context, page int) generic.ChildrenSlice { - if page > 1 { - return generic.NewChildrenSlice(0) - } - - releaseID := f3_tree.GetReleaseID(o.GetNode()) - - release, err := repo_model.GetReleaseByID(ctx, releaseID) - if err != nil { - panic(fmt.Errorf("GetReleaseByID %v %w", releaseID, err)) - } - - if err := release.LoadAttributes(ctx); err != nil { - panic(fmt.Errorf("error while listing assets: %v", err)) - } - - return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(release.Attachments...)...) -} - -func newAssets() generic.NodeDriverInterface { - return &assets{} -} diff --git a/services/f3/driver/attachment.go b/services/f3/driver/attachment.go new file mode 100644 index 0000000000..64c188d6e0 --- /dev/null +++ b/services/f3/driver/attachment.go @@ -0,0 +1,185 @@ +// Copyright Earl Warren +// Copyright Loïc Dachary +// SPDX-License-Identifier: MIT + +package driver + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + + "forgejo.org/models/db" + repo_model "forgejo.org/models/repo" + user_model "forgejo.org/models/user" + "forgejo.org/modules/storage" + "forgejo.org/modules/timeutil" + forgejo_attachment "forgejo.org/services/attachment" + + "code.forgejo.org/f3/gof3/v3/f3" + f3_id "code.forgejo.org/f3/gof3/v3/id" + f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3" + "code.forgejo.org/f3/gof3/v3/tree/generic" + f3_util "code.forgejo.org/f3/gof3/v3/util" + "github.com/google/uuid" +) + +var _ f3_tree.ForgeDriverInterface = &issue{} + +type attachment struct { + common + + forgejoAttachment *repo_model.Attachment + sha string + contentType string + downloadFunc f3.DownloadFuncType +} + +func (o *attachment) SetNative(attachment any) { + o.forgejoAttachment = attachment.(*repo_model.Attachment) +} + +func (o *attachment) GetNativeID() string { + return fmt.Sprintf("%d", o.forgejoAttachment.ID) +} + +func (o *attachment) NewFormat() f3.Interface { + node := o.GetNode() + return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind()) +} + +func (o *attachment) ToFormat() f3.Interface { + if o.forgejoAttachment == nil { + return o.NewFormat() + } + + return &f3.Attachment{ + Common: f3.NewCommon(o.GetNativeID()), + Name: o.forgejoAttachment.Name, + ContentType: o.contentType, + Size: o.forgejoAttachment.Size, + DownloadCount: o.forgejoAttachment.DownloadCount, + Created: o.forgejoAttachment.CreatedUnix.AsTime(), + SHA256: o.sha, + DownloadURL: o.forgejoAttachment.DownloadURL(), + DownloadFunc: o.downloadFunc, + } +} + +func (o *attachment) FromFormat(content f3.Interface) { + attachment := content.(*f3.Attachment) + o.forgejoAttachment = &repo_model.Attachment{ + ID: f3_util.ParseInt(attachment.GetID()), + Name: attachment.Name, + Size: attachment.Size, + DownloadCount: attachment.DownloadCount, + CreatedUnix: timeutil.TimeStamp(attachment.Created.Unix()), + CustomDownloadURL: attachment.DownloadURL, + } + o.contentType = attachment.ContentType + o.sha = attachment.SHA256 + o.downloadFunc = attachment.DownloadFunc +} + +func (o *attachment) Get(ctx context.Context) bool { + node := o.GetNode() + o.Trace("%s", node.GetID()) + + id := node.GetID().Int64() + + attachment, err := repo_model.GetAttachmentByID(ctx, id) + if repo_model.IsErrAttachmentNotExist(err) { + return false + } + if err != nil { + panic(fmt.Errorf("attachment %v %w", id, err)) + } + + o.forgejoAttachment = attachment + + path := o.forgejoAttachment.RelativePath() + + { + f, err := storage.Attachments.Open(path) + if err != nil { + panic(err) + } + hasher := sha256.New() + if _, err := io.Copy(hasher, f); err != nil { + panic(fmt.Errorf("io.Copy to hasher: %v", err)) + } + o.sha = hex.EncodeToString(hasher.Sum(nil)) + } + + o.downloadFunc = func() io.ReadCloser { + o.Trace("download %s from copy stored in temporary file %s", o.forgejoAttachment.DownloadURL, path) + f, err := os.Open(path) + if err != nil { + panic(err) + } + return f + } + return true +} + +func (o *attachment) Patch(ctx context.Context) { + o.Trace("%d", o.forgejoAttachment.ID) + if _, err := db.GetEngine(ctx).ID(o.forgejoAttachment.ID).Cols("name").Update(o.forgejoAttachment); err != nil { + panic(fmt.Errorf("UpdateAttachmentCols: %v %v", o.forgejoAttachment, err)) + } +} + +func (o *attachment) Put(ctx context.Context) f3_id.NodeID { + node := o.GetNode() + o.Trace("%s", node.GetID()) + + uploader, err := user_model.GetAdminUser(ctx) + if err != nil { + panic(fmt.Errorf("GetAdminUser %w", err)) + } + + attachable := f3_tree.GetAttachable(o.GetNode()) + attachableID := f3_tree.GetAttachableID(o.GetNode()) + + switch attachable.GetKind() { + case f3_tree.KindRelease: + o.forgejoAttachment.ReleaseID = attachableID + case f3_tree.KindComment: + o.forgejoAttachment.CommentID = attachableID + case f3_tree.KindIssue, f3_tree.KindPullRequest: + o.forgejoAttachment.IssueID = attachableID + default: + panic(fmt.Errorf("unexpected type %s", attachable.GetKind())) + } + + o.forgejoAttachment.UploaderID = uploader.ID + o.forgejoAttachment.RepoID = f3_tree.GetProjectID(o.GetNode()) + o.forgejoAttachment.UUID = uuid.New().String() + + download := o.downloadFunc() + defer download.Close() + + _, err = forgejo_attachment.NewAttachment(ctx, o.forgejoAttachment, download, o.forgejoAttachment.Size) + if err != nil { + panic(err) + } + + o.Trace("attachment created %d", o.forgejoAttachment.ID) + return f3_id.NewNodeID(o.forgejoAttachment.ID) +} + +func (o *attachment) Delete(ctx context.Context) { + node := o.GetNode() + o.Trace("%s", node.GetID()) + + if err := repo_model.DeleteAttachment(ctx, o.forgejoAttachment, true); err != nil { + panic(err) + } +} + +func newAttachment() generic.NodeDriverInterface { + return &attachment{} +} diff --git a/services/f3/driver/attachments.go b/services/f3/driver/attachments.go new file mode 100644 index 0000000000..392afda52c --- /dev/null +++ b/services/f3/driver/attachments.go @@ -0,0 +1,79 @@ +// Copyright Earl Warren +// Copyright Loïc Dachary +// SPDX-License-Identifier: MIT + +package driver + +import ( + "context" + "fmt" + + issues_model "forgejo.org/models/issues" + repo_model "forgejo.org/models/repo" + + f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3" + "code.forgejo.org/f3/gof3/v3/tree/generic" +) + +type attachments struct { + container +} + +func (o *attachments) ListPage(ctx context.Context, page int) generic.ChildrenSlice { + if page > 1 { + return generic.NewChildrenSlice(0) + } + + attachable := f3_tree.GetAttachable(o.GetNode()) + attachableID := f3_tree.GetAttachableID(o.GetNode()) + + var attachments []*repo_model.Attachment + + switch attachable.GetKind() { + case f3_tree.KindRelease: + release, err := repo_model.GetReleaseByID(ctx, attachableID) + if err != nil { + panic(fmt.Errorf("GetReleaseByID %v %w", attachableID, err)) + } + + if err := release.LoadAttributes(ctx); err != nil { + panic(fmt.Errorf("error while listing attachments: %v", err)) + } + + attachments = release.Attachments + + case f3_tree.KindComment: + comment, err := issues_model.GetCommentByID(ctx, attachableID) + if err != nil { + panic(fmt.Errorf("GetCommentByID %v %w", attachableID, err)) + } + + if err := comment.LoadAttachments(ctx); err != nil { + panic(fmt.Errorf("error while listing attachments: %v", err)) + } + + attachments = comment.Attachments + + case f3_tree.KindIssue, f3_tree.KindPullRequest: + repoID := f3_tree.GetProjectID(o.GetNode()) + issue, err := issues_model.GetIssueByIndex(ctx, repoID, attachableID) + if err != nil { + panic(fmt.Errorf("GetIssueByID %v %w", attachableID, err)) + } + + if err := issue.LoadAttachments(ctx); err != nil { + panic(fmt.Errorf("error while listing attachments: %v", err)) + } + + attachments = issue.Attachments + + default: + panic(fmt.Errorf("unexpected type %s", attachable.GetKind())) + } + + return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(attachments...)...) +} + +func newAttachments() generic.NodeDriverInterface { + return &attachments{} +} diff --git a/services/f3/driver/reaction.go b/services/f3/driver/reaction.go index 74c50b9d13..b959206074 100644 --- a/services/f3/driver/reaction.go +++ b/services/f3/driver/reaction.go @@ -89,7 +89,7 @@ func (o *reaction) Patch(ctx context.Context) { } func (o *reaction) Put(ctx context.Context) f3_id.NodeID { - o.Error("%v", o.forgejoReaction.User) + o.Trace("%v", o.forgejoReaction.User) sess := db.GetEngine(ctx) @@ -110,7 +110,7 @@ func (o *reaction) Put(ctx context.Context) f3_id.NodeID { panic(fmt.Errorf("unexpected type %v", reactionable.GetKind())) } - o.Error("%v", o.forgejoReaction) + o.Trace("%v", o.forgejoReaction) if _, err := sess.Insert(o.forgejoReaction); err != nil { panic(err) diff --git a/services/f3/driver/repository.go b/services/f3/driver/repository.go index e7f4e43723..3cd9aa7f2e 100644 --- a/services/f3/driver/repository.go +++ b/services/f3/driver/repository.go @@ -72,7 +72,7 @@ func (o *repository) upsert(ctx context.Context) f3_id.NodeID { return f3_id.NewNodeID(o.f.Name) } -func (o *repository) SetFetchFunc(fetchFunc func(ctx context.Context, destination string, internalRefs []string)) { +func (o *repository) SetFetchFunc(fetchFunc func(ctx context.Context, destination, internalRef string)) { o.f.FetchFunc = fetchFunc } @@ -93,10 +93,16 @@ func (o *repository) GetRepositoryPushURL() string { return o.getURL() } -func (o *repository) GetRepositoryInternalRefs() []string { - return []string{} +func (o *repository) GetRepositoryInternalRef() string { + return "" } +func (o *repository) GetPullRequestBranch(pr *f3.PullRequestBranch) *f3.PullRequestBranch { + panic("") +} +func (o *repository) CreatePullRequestBranch(pr *f3.PullRequestBranch) {} +func (o *repository) DeletePullRequestBranch(pr *f3.PullRequestBranch) {} + func newRepository(_ context.Context) generic.NodeDriverInterface { r := &repository{ f: &f3.Repository{}, diff --git a/services/f3/driver/tree.go b/services/f3/driver/tree.go index ff927df9d4..fe11b15f6e 100644 --- a/services/f3/driver/tree.go +++ b/services/f3/driver/tree.go @@ -49,10 +49,10 @@ func (o *treeDriver) Factory(ctx context.Context, kind f3_kind.Kind) generic.Nod return newComments() case f3_tree.KindComment: return newComment() - case f3_tree.KindAssets: - return newAssets() - case f3_tree.KindAsset: - return newAsset() + case f3_tree.KindAttachments: + return newAttachments() + case f3_tree.KindAttachment: + return newAttachment() case f3_tree.KindLabels: return newLabels() case f3_tree.KindLabel: diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go index 3d6e219ffc..a3b719d1a7 100644 --- a/services/federation/federation_service.go +++ b/services/federation/federation_service.go @@ -5,6 +5,7 @@ package federation import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -46,7 +47,7 @@ func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int return http.StatusInternalServerError, "Wrong FederationHost", err } if !activity.IsNewer(federationHost.LatestActivity) { - return http.StatusNotAcceptable, "Activity out of order.", fmt.Errorf("Activity already processed") + return http.StatusNotAcceptable, "Activity out of order.", errors.New("Activity already processed") } actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName)) if err != nil { @@ -210,6 +211,11 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI return nil, nil, err } + inbox, err := url.ParseRequestURI(person.Inbox.GetLink().String()) + if err != nil { + return nil, nil, err + } + newUser := user.User{ LowerName: strings.ToLower(name), Name: name, @@ -226,6 +232,7 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI federatedUser := user.FederatedUser{ ExternalID: personID.ID, FederationHostID: federationHostID, + InboxPath: inbox.Path, NormalizedOriginalURL: personID.AsURI(), } diff --git a/services/feed/action.go b/services/feed/action.go index a2cd0551a3..7d179bd1c8 100644 --- a/services/feed/action.go +++ b/services/feed/action.go @@ -39,6 +39,24 @@ func NewNotifier() notify_service.Notifier { return &actionNotifier{} } +func notifyAll(ctx context.Context, action *activities_model.Action) error { + _, err := activities_model.NotifyWatchers(ctx, action) + if err != nil { + return err + } + return err + // return federation_service.NotifyActivityPubFollowers(ctx, out) +} + +func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error { + _, err := activities_model.NotifyWatchersActions(ctx, acts) + if err != nil { + return err + } + return nil + // return federation_service.NotifyActivityPubFollowers(ctx, out) +} + func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) { if err := issue.LoadPoster(ctx); err != nil { log.Error("issue.LoadPoster: %v", err) @@ -50,7 +68,7 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue } repo := issue.Repo - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: issue.Poster.ID, ActUser: issue.Poster, OpType: activities_model.ActionCreateIssue, @@ -91,7 +109,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model } // Notify watchers for whatever action comes in, ignore if no action type. - if err := activities_model.NotifyWatchers(ctx, act); err != nil { + if err := notifyAll(ctx, act); err != nil { log.Error("NotifyWatchers: %v", err) } } @@ -127,7 +145,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode } // Notify watchers for whatever action comes in, ignore if no action type. - if err := activities_model.NotifyWatchers(ctx, act); err != nil { + if err := notifyAll(ctx, act); err != nil { log.Error("NotifyWatchers: %v", err) } } @@ -146,7 +164,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: pull.Issue.Poster.ID, ActUser: pull.Issue.Poster, OpType: activities_model.ActionCreatePullRequest, @@ -160,7 +178,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. } func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionRenameRepo, @@ -174,7 +192,7 @@ func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model. } func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionTransferRepo, @@ -188,7 +206,7 @@ func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_mode } func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -201,7 +219,7 @@ func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_mod } func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -266,13 +284,13 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model actions = append(actions, action) } - if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil { + if err := notifyAllActions(ctx, actions); err != nil { log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err) } } func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionMergePullRequest, @@ -286,7 +304,7 @@ func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.Us } func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionAutoMergePullRequest, @@ -304,7 +322,7 @@ func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_ if len(review.OriginalAuthor) > 0 { reviewerName = review.OriginalAuthor } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionPullReviewDismissed, @@ -342,7 +360,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use opType = activities_model.ActionDeleteBranch } - if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err = notifyAll(ctx, &activities_model.Action{ ActUserID: pusher.ID, ActUser: pusher, OpType: opType, @@ -362,7 +380,7 @@ func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -381,7 +399,7 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -405,7 +423,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncPush, @@ -420,7 +438,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model } func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncCreate, @@ -434,7 +452,7 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use } func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncDelete, @@ -452,7 +470,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release log.Error("LoadAttributes: %v", err) return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: rel.PublisherID, ActUser: rel.Publisher, OpType: activities_model.ActionPublishRelease, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 4a46c9cc5f..bb81e939b0 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -277,6 +277,9 @@ type WebhookCoreForm struct { Wiki bool Repository bool Package bool + ActionFailure bool + ActionRecover bool + ActionSuccess bool Active bool BranchFilter string `binding:"GlobPattern"` AuthorizationHeader string diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go index 67fb64cabf..ae08f65f23 100644 --- a/services/forms/user_form_test.go +++ b/services/forms/user_form_test.go @@ -9,18 +9,14 @@ import ( auth_model "forgejo.org/models/auth" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = nil + defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, nil)() form := RegisterForm{} @@ -28,12 +24,7 @@ func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io")} + defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io")})() tt := []struct { email string @@ -50,12 +41,7 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")} + defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")})() tt := []struct { email string @@ -78,13 +64,7 @@ func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = nil - setting.Service.EmailDomainBlockList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")} + defer test.MockVariableValue(&setting.Service.EmailDomainBlockList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")})() tt := []struct { email string diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 989f69d4f4..6835dfbf36 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -440,11 +440,29 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int { if err != nil { return 0 } - lineCount, err := blob.GetBlobLineCount() + reader, err := blob.DataAsync() if err != nil { return 0 } - return lineCount + defer reader.Close() + buf := make([]byte, 32*1024) + count := 1 + lineSep := []byte{'\n'} + + c, err := reader.Read(buf) + if c == 0 && err == io.EOF { + return 0 + } + for { + count += bytes.Count(buf[:c], lineSep) + switch { + case err == io.EOF: + return count + case err != nil: + return count + } + c, err = reader.Read(buf) + } } // Diff represents a difference between two git trees. diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 3d3c8432c4..695b177b8b 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -712,6 +712,8 @@ func TestGetDiffFull(t *testing.T) { assert.Equal(t, ".gitattributes", diff.Files[0].Name) assert.Equal(t, "24139dae656713ba861751fb2c2ac38839349a7a", diff.Files[0].NameHash) + assert.Len(t, diff.Files[0].Sections, 2) + assert.Equal(t, 4, diff.Files[0].Sections[1].Lines[0].SectionInfo.LeftIdx) }) } diff --git a/services/issue/comments.go b/services/issue/comments.go index 2993d38003..2cac900d41 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -5,6 +5,7 @@ package issue import ( "context" + "errors" "fmt" "forgejo.org/models/db" @@ -18,7 +19,7 @@ import ( // CreateRefComment creates a commit reference comment to issue. func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, commitSHA string) error { if len(commitSHA) == 0 { - return fmt.Errorf("cannot create reference with empty commit SHA") + return errors.New("cannot create reference with empty commit SHA") } // Check if same reference from same commit has already existed. diff --git a/services/issue/issue.go b/services/issue/issue.go index 1ec41476b4..7071a912b0 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -6,6 +6,7 @@ package issue import ( "context" + "errors" "fmt" "time" @@ -345,13 +346,13 @@ func SetIssueUpdateDate(ctx context.Context, issue *issues_model.Issue, updated return err } if !perm.IsAdmin() && !perm.IsOwner() { - return fmt.Errorf("user needs to have admin or owner right") + return errors.New("user needs to have admin or owner right") } // A simple guard against potential inconsistent calls updatedUnix := timeutil.TimeStamp(updated.Unix()) if updatedUnix < issue.CreatedUnix || updatedUnix > timeutil.TimeStampNow() { - return fmt.Errorf("unallowed update date") + return errors.New("unallowed update date") } issue.UpdatedUnix = updatedUnix diff --git a/services/issue/milestone.go b/services/issue/milestone.go index 3fa7083812..a561bf8eee 100644 --- a/services/issue/milestone.go +++ b/services/issue/milestone.go @@ -5,6 +5,7 @@ package issue import ( "context" + "errors" "fmt" "forgejo.org/models/db" @@ -47,7 +48,7 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is return fmt.Errorf("HasMilestoneByRepoID: %w", err) } if !has { - return fmt.Errorf("HasMilestoneByRepoID: issue doesn't exist") + return errors.New("HasMilestoneByRepoID: issue doesn't exist") } } diff --git a/services/issue/pull.go b/services/issue/pull.go index b0a0c47d88..2eef1fbfa8 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -43,8 +43,6 @@ type ReviewRequestNotifier struct { } func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { - files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} - if pr.IsWorkInProgress(ctx) { return nil, nil } @@ -72,18 +70,17 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, return nil, err } - var data string - for _, file := range files { + var rules []*issues_model.CodeOwnerRule + for _, file := range []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} { if blob, err := commit.GetBlobByPath(file); err == nil { - data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) + rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize) if err == nil { + rules, _ = issues_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize) break } } } - rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data) - // get the mergebase mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) if err != nil { diff --git a/services/lfs/server.go b/services/lfs/server.go index 8b2d5e66e1..17e6d0eec7 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -595,15 +595,15 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo claims, claimsOk := token.Claims.(*Claims) if !token.Valid || !claimsOk { - return nil, fmt.Errorf("invalid token claim") + return nil, errors.New("invalid token claim") } if claims.RepoID != target.ID { - return nil, fmt.Errorf("invalid token claim") + return nil, errors.New("invalid token claim") } if mode == perm.AccessModeWrite && claims.Op != "upload" { - return nil, fmt.Errorf("invalid token claim") + return nil, errors.New("invalid token claim") } u, err := user_model.GetUserByID(ctx, claims.UserID) @@ -616,12 +616,12 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) { if authorization == "" { - return nil, fmt.Errorf("no token") + return nil, errors.New("no token") } parts := strings.SplitN(authorization, " ", 2) if len(parts) != 2 { - return nil, fmt.Errorf("no token") + return nil, errors.New("no token") } tokenSHA := parts[1] switch strings.ToLower(parts[0]) { @@ -630,7 +630,7 @@ func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Rep case "token": return handleLFSToken(ctx, tokenSHA, target, mode) } - return nil, fmt.Errorf("token not found") + return nil, errors.New("token not found") } func requireAuth(ctx *context.Context) { diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go index 7c63603a98..09763e164e 100644 --- a/services/mailer/mail_actions.go +++ b/services/mailer/mail_actions.go @@ -23,19 +23,24 @@ func MailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Statu return nil } - if run.TriggerUser.Email != "" && run.TriggerUser.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { - if err := sendMailActionRun(run.TriggerUser, run, priorStatus, lastRun); err != nil { - return err - } + if !run.NotifyEmail { + return nil } - if run.Repo.Owner.Email != "" && run.Repo.Owner.Email != run.TriggerUser.Email && run.Repo.Owner.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { - if err := sendMailActionRun(run.Repo.Owner, run, priorStatus, lastRun); err != nil { - return err - } + user := run.TriggerUser + // this happens e.g. when this is a scheduled run + if user.IsSystem() { + user = run.Repo.Owner + } + if user.IsSystem() || user.Email == "" { + return nil } - return nil + if user.EmailNotificationsPreference == user_model.EmailNotificationsDisabled { + return nil + } + + return sendMailActionRun(user, run, priorStatus, lastRun) } func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error { diff --git a/services/mailer/mail_actions_now_done_test.go b/services/mailer/mail_actions_now_done_test.go index 0d832f2b36..6a01ea7631 100644 --- a/services/mailer/mail_actions_now_done_test.go +++ b/services/mailer/mail_actions_now_done_test.go @@ -4,42 +4,53 @@ package mailer import ( + "slices" "testing" actions_model "forgejo.org/models/actions" "forgejo.org/models/db" + organization_model "forgejo.org/models/organization" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" "forgejo.org/modules/setting" + "forgejo.org/modules/test" notify_service "forgejo.org/services/notify" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func getActionsNowDoneTestUsers(t *testing.T) []*user_model.User { +func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User { t.Helper() - newTriggerUser := new(user_model.User) - newTriggerUser.Name = "new_trigger_user" - newTriggerUser.Language = "en_US" - newTriggerUser.IsAdmin = false - newTriggerUser.Email = "new_trigger_user@example.com" - newTriggerUser.LastLoginUnix = 1693648327 - newTriggerUser.CreatedUnix = 1693648027 - newTriggerUser.EmailNotificationsPreference = user_model.EmailNotificationsEnabled - require.NoError(t, user_model.CreateUser(db.DefaultContext, newTriggerUser)) + user := new(user_model.User) + user.Name = name + user.Language = "en_US" + user.IsAdmin = false + user.Email = email + user.LastLoginUnix = 1693648327 + user.CreatedUnix = 1693648027 + opts := user_model.CreateUserOverwriteOptions{ + AllowCreateOrganization: optional.Some(true), + EmailNotificationsPreference: ¬ifications, + } + require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts)) + return user +} - newOwner := new(user_model.User) - newOwner.Name = "new_owner" - newOwner.Language = "en_US" - newOwner.IsAdmin = false - newOwner.Email = "new_owner@example.com" - newOwner.LastLoginUnix = 1693648329 - newOwner.CreatedUnix = 1693648029 - newOwner.EmailNotificationsPreference = user_model.EmailNotificationsEnabled - require.NoError(t, user_model.CreateUser(db.DefaultContext, newOwner)) - - return []*user_model.User{newTriggerUser, newOwner} +func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User { + t.Helper() + org := new(organization_model.Organization) + org.Name = name + org.Language = "en_US" + org.IsAdmin = false + // contact email for the organization, for display purposes but otherwise not used as of v12 + org.Email = email + org.LastLoginUnix = 1693648327 + org.CreatedUnix = 1693648027 + org.Email = email + require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner)) + return (*user_model.User)(org) } func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) { @@ -49,98 +60,181 @@ func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) { func TestActionRunNowDoneNotificationMail(t *testing.T) { ctx := t.Context() - users := getActionsNowDoneTestUsers(t) - defer CleanUpUsers(ctx, users) - triggerUser := users[0] - ownerUser := users[1] + defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)() + + actionsUser := user_model.NewActionsUser() + require.NotEmpty(t, actionsUser.Email) repo := repo_model.Repository{ Name: "some repo", Description: "rockets are cool", - Owner: ownerUser, - OwnerID: ownerUser.ID, } // Do some funky stuff with the action run's ids: // The run with the larger ID finished first. // This is odd but something that must work. - run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} - run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} + run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} + run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} + + assignUsers := func(triggerUser, owner *user_model.User) { + for _, run := range []*actions_model.ActionRun{run1, run2} { + run.TriggerUser = triggerUser + run.TriggerUserID = triggerUser.ID + run.NotifyEmail = true + } + repo.Owner = owner + repo.OwnerID = owner.ID + } notify_service.RegisterNotifier(NewNotifier()) + orgOwner := getActionsNowDoneTestUser(t, "org_owner", "org_owner@example.com", "disabled") + defer CleanUpUsers(ctx, []*user_model.User{orgOwner}) + t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) { + user := getActionsNowDoneTestUser(t, "new_user", "new_user@example.com", "enabled") + defer CleanUpUsers(ctx, []*user_model.User{user}) + assignUsers(user, user) defer MockMailSettings(func(msgs ...*Message) { assert.Fail(t, "no mail should be sent") })() notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) }) - t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { - mailSentToOwner := false - mailSentToTriggerUser := false + t.Run("WorkflowEnableEmailNotificationIsFalse", func(t *testing.T) { + user := getActionsNowDoneTestUser(t, "new_user1", "new_user1@example.com", "enabled") + defer CleanUpUsers(ctx, []*user_model.User{user}) + assignUsers(user, user) defer MockMailSettings(func(msgs ...*Message) { - assert.LessOrEqual(t, len(msgs), 2) - for _, msg := range msgs { - switch msg.To { - case triggerUser.EmailTo(): - assert.False(t, mailSentToTriggerUser, "sent mail twice") - mailSentToTriggerUser = true - case ownerUser.EmailTo(): - assert.False(t, mailSentToOwner, "sent mail twice") - mailSentToOwner = true - default: - assert.Fail(t, "sent mail to unknown sender", msg.To) - } - assert.Contains(t, msg.Body, triggerUser.HTMLURL()) - assert.Contains(t, msg.Body, triggerUser.Name) - // what happened - assert.Contains(t, msg.Body, "failed") - // new status of run - assert.Contains(t, msg.Body, "failure") - // prior status of this run - assert.Contains(t, msg.Body, "waiting") - assertTranslatedLocaleMailActionsNowDone(t, msg.Body) - } + assert.Fail(t, "no mail should be sent") })() - notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) - assert.True(t, mailSentToOwner) - assert.True(t, mailSentToTriggerUser) + run2.NotifyEmail = false + notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) }) - t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { - mailSentToOwner := false - mailSentToTriggerUser := false - defer MockMailSettings(func(msgs ...*Message) { - assert.LessOrEqual(t, len(msgs), 2) - for _, msg := range msgs { - switch msg.To { - case triggerUser.EmailTo(): - assert.False(t, mailSentToTriggerUser, "sent mail twice") - mailSentToTriggerUser = true - case ownerUser.EmailTo(): - assert.False(t, mailSentToOwner, "sent mail twice") - mailSentToOwner = true - default: - assert.Fail(t, "sent mail to unknown sender", msg.To) - } - assert.Contains(t, msg.Body, triggerUser.HTMLURL()) - assert.Contains(t, msg.Body, triggerUser.Name) - // what happened - assert.Contains(t, msg.Body, "recovered") - // old status of run - assert.Contains(t, msg.Body, "failure") - // new status of run - assert.Contains(t, msg.Body, "success") - // prior status of this run - assert.Contains(t, msg.Body, "running") - assertTranslatedLocaleMailActionsNowDone(t, msg.Body) - } - })() - assert.NotNil(t, setting.MailService) + for _, testCase := range []struct { + name string + triggerUser *user_model.User + owner *user_model.User + expected string + expectMail bool + }{ + { + // if the action is assigned a trigger user in a repository + // owned by a regular user, the mail is sent to the trigger user + name: "RegularTriggerUser", + triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user0", "new_trigger_user0@example.com", user_model.EmailNotificationsEnabled), + owner: getActionsNowDoneTestUser(t, "new_owner0", "new_owner0@example.com", user_model.EmailNotificationsEnabled), + expected: "trigger", + expectMail: true, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // in a repository owned by a regular user, the mail is sent to + // the user that owns the repository + name: "SystemTriggerUserAndRegularOwner", + triggerUser: actionsUser, + owner: getActionsNowDoneTestUser(t, "new_owner1", "new_owner1@example.com", user_model.EmailNotificationsEnabled), + expected: "owner", + expectMail: true, + }, + { + // if the action is assigned a trigger user with disabled notifications in a repository + // owned by a regular user, no mail is sent + name: "RegularTriggerUserNotificationsDisabled", + triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user2", "new_trigger_user2@example.com", user_model.EmailNotificationsDisabled), + owner: getActionsNowDoneTestUser(t, "new_owner2", "new_owner2@example.com", user_model.EmailNotificationsEnabled), + expectMail: false, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // owned by a regular user with disabled notifications, no mail is sent + name: "SystemTriggerUserAndRegularOwnerNotificationsDisabled", + triggerUser: actionsUser, + owner: getActionsNowDoneTestUser(t, "new_owner3", "new_owner3@example.com", user_model.EmailNotificationsDisabled), + expectMail: false, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // in a repository owned by an organization with an email contact, the mail is sent to + // this email contact + name: "SystemTriggerUserAndOrgOwner", + triggerUser: actionsUser, + owner: getActionsNowDoneTestOrg(t, "new_org1", "new_org_owner0@example.com", orgOwner), + expected: "owner", + expectMail: true, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // in a repository owned by an organization without an email contact, no mail is sent + name: "SystemTriggerUserAndNoMailOrgOwner", + triggerUser: actionsUser, + owner: getActionsNowDoneTestOrg(t, "new_org2", "", orgOwner), + expectMail: false, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + assignUsers(testCase.triggerUser, testCase.owner) + defer CleanUpUsers(ctx, slices.DeleteFunc([]*user_model.User{testCase.triggerUser, testCase.owner}, func(user *user_model.User) bool { + return user.IsSystem() + })) - notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) - assert.True(t, mailSentToOwner) - assert.True(t, mailSentToTriggerUser) - }) + t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { + mailSent := false + defer MockMailSettings(func(msgs ...*Message) { + assert.Len(t, msgs, 1) + msg := msgs[0] + assert.False(t, mailSent, "sent mail twice") + expectedEmail := testCase.triggerUser.Email + if testCase.expected == "owner" { // otherwise "trigger" + expectedEmail = testCase.owner.Email + } + require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender") + mailSent = true + assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL()) + assert.Contains(t, msg.Body, testCase.triggerUser.Name) + // what happened + assert.Contains(t, msg.Body, "failed") + // new status of run + assert.Contains(t, msg.Body, "failure") + // prior status of this run + assert.Contains(t, msg.Body, "waiting") + assertTranslatedLocaleMailActionsNowDone(t, msg.Body) + })() + require.NotNil(t, setting.MailService) + + notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) + assert.Equal(t, testCase.expectMail, mailSent) + }) + + t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { + mailSent := false + defer MockMailSettings(func(msgs ...*Message) { + assert.Len(t, msgs, 1) + msg := msgs[0] + assert.False(t, mailSent, "sent mail twice") + expectedEmail := testCase.triggerUser.Email + if testCase.expected == "owner" { // otherwise "trigger" + expectedEmail = testCase.owner.Email + } + require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender") + mailSent = true + assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL()) + assert.Contains(t, msg.Body, testCase.triggerUser.Name) + // what happened + assert.Contains(t, msg.Body, "recovered") + // old status of run + assert.Contains(t, msg.Body, "failure") + // new status of run + assert.Contains(t, msg.Body, "success") + // prior status of this run + assert.Contains(t, msg.Body, "running") + })() + require.NotNil(t, setting.MailService) + + notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) + assert.Equal(t, testCase.expectMail, mailSent) + }) + }) + } } diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index a2a871d3c3..5375133415 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -6,6 +6,7 @@ package mailer import ( "bytes" "context" + "errors" "fmt" "net/url" @@ -39,7 +40,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod if err != nil && !user_model.IsErrUserNotExist(err) { return err } else if user != nil && user.ProhibitLogin { - return fmt.Errorf("login is prohibited for the invited user") + return errors.New("login is prohibited for the invited user") } inviteRedirect := url.QueryEscape(fmt.Sprintf("/org/invite/%s", invite.Token)) diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 7fe565b2e1..afbcb8064e 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -494,8 +494,7 @@ func Test_createReference(t *testing.T) { func TestFromDisplayName(t *testing.T) { template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}") require.NoError(t, err) - setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template} - defer func() { setting.MailService = nil }() + defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromDisplayNameFormatTemplate: template})() tests := []struct { userDisplayName string @@ -525,15 +524,9 @@ func TestFromDisplayName(t *testing.T) { t.Run("template with all available vars", func(t *testing.T) { template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])") require.NoError(t, err) - setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template} - oldAppName := setting.AppName - setting.AppName = "Code IT" - oldDomain := setting.Domain - setting.Domain = "code.it" - defer func() { - setting.AppName = oldAppName - setting.Domain = oldDomain - }() + defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromDisplayNameFormatTemplate: template})() + defer test.MockVariableValue(&setting.AppName, "Code IT")() + defer test.MockVariableValue(&setting.Domain, "code.it")() assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"})) }) diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index 4561240df5..d8646d9ddd 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/tls" + "errors" "fmt" "hash/fnv" "io" @@ -28,7 +29,7 @@ import ( notify_service "forgejo.org/services/notify" ntlmssp "github.com/Azure/go-ntlmssp" - "github.com/jaytaylor/html2text" + "github.com/inbucket/html2text" "gopkg.in/gomail.v2" ) @@ -176,7 +177,7 @@ func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { if len(fromServer) == 0 { - return nil, fmt.Errorf("ntlm ChallengeMessage is empty") + return nil, errors.New("ntlm ChallengeMessage is empty") } authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded) return authenticateMessage, err @@ -264,7 +265,7 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { canAuth, options := client.Extension("AUTH") if len(opts.User) > 0 { if !canAuth { - return fmt.Errorf("SMTP server does not support AUTH, but credentials provided") + return errors.New("SMTP server does not support AUTH, but credentials provided") } var auth smtp.Auth diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go index 47e5d5d175..5e9cbe3e99 100644 --- a/services/mailer/main_test.go +++ b/services/mailer/main_test.go @@ -8,6 +8,7 @@ import ( "testing" "forgejo.org/models/db" + organization_model "forgejo.org/models/organization" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" @@ -51,6 +52,11 @@ func MockMailSettings(send func(msgs ...*Message)) func() { func CleanUpUsers(ctx context.Context, users []*user_model.User) { for _, u := range users { - db.DeleteByID[user_model.User](ctx, u.ID) + if u.IsOrganization() { + organization_model.DeleteOrganization(ctx, (*organization_model.Organization)(u)) + } else { + db.DeleteByID[user_model.User](ctx, u.ID) + db.DeleteByBean(ctx, &user_model.EmailAddress{UID: u.ID}) + } } } diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index b5fcd78cb7..2f1b1e738c 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -5,7 +5,7 @@ package markup import ( "context" - "fmt" + "errors" "forgejo.org/models/perm/access" "forgejo.org/models/repo" @@ -55,7 +55,7 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, err } if !perms.CanRead(unit.TypeCode) { - return nil, fmt.Errorf("cannot access repository code") + return nil, errors.New("cannot access repository code") } gitRepo, err := gitrepo.OpenRepository(ctx, repo) diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 55adad9685..7887dacdb1 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -766,7 +766,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model issue := issues_model.Issue{ RepoID: g.repo.ID, Repo: g.repo, - Title: prTitle, + Title: util.TruncateRunes(prTitle, 255), Index: pr.Number, Content: pr.Content, MilestoneID: milestoneID, diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index ac0d3bcf7a..f54f682c47 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -99,6 +99,7 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, passw // Only use basic auth if token is blank and password is NOT // Basic auth will fail with empty strings, but empty token will allow anonymous public API usage if token == "" && password != "" { + //nolint // SA1019 gitlab.NewBasicAuthClient is deprecated: GitLab recommends against using this authentication method gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient())) } @@ -213,7 +214,7 @@ func (g *GitlabDownloader) GetTopics() ([]string, error) { if err != nil { return nil, err } - return gr.TagList, err + return gr.Topics, err } // GetMilestones returns milestones diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go index 6780ad2923..bf0d063ca4 100644 --- a/services/migrations/gogs_test.go +++ b/services/migrations/gogs_test.go @@ -25,7 +25,7 @@ func TestGogsDownloadRepo(t *testing.T) { resp, err := http.Get("https://try.gogs.io/lunnytest/TESTREPO") if err != nil || resp.StatusCode/100 != 2 { // skip and don't run test - t.Skipf("visit test repo failed, ignored") + t.Skip("visit test repo failed, ignored") return } diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 81d1c203fe..61630d9c6d 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -6,6 +6,7 @@ package migrations import ( "context" + "errors" "fmt" "net" "net/url" @@ -227,7 +228,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base if cloneURL.Scheme == "file" || cloneURL.Scheme == "" { if cloneAddrURL.Scheme != "file" && cloneAddrURL.Scheme != "" { - return fmt.Errorf("repo info has changed from external to local filesystem") + return errors.New("repo info has changed from external to local filesystem") } } diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 6d871ad5ff..514b7c3969 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -5,7 +5,7 @@ package mirror import ( "context" - "fmt" + "errors" quota_model "forgejo.org/models/quota" repo_model "forgejo.org/models/repo" @@ -31,7 +31,7 @@ func doMirrorSync(ctx context.Context, req *SyncRequest) { } } -var errLimit = fmt.Errorf("reached limit") +var errLimit = errors.New("reached limit") // Update checks and updates mirror repositories. func Update(ctx context.Context, pullLimit, pushLimit int) error { @@ -70,7 +70,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { // Check we've not been cancelled select { case <-ctx.Done(): - return fmt.Errorf("aborted") + return errors.New("aborted") default: } diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go index 9435887a46..dd66c7d74e 100644 --- a/services/packages/alpine/repository.go +++ b/services/packages/alpine/repository.go @@ -258,7 +258,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package privPem, _ := pem.Decode([]byte(priv)) if privPem == nil { - return fmt.Errorf("failed to decode private key pem") + return errors.New("failed to decode private key pem") } privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) diff --git a/services/packages/auth.go b/services/packages/auth.go index ab2c347bc9..205125cf8b 100644 --- a/services/packages/auth.go +++ b/services/packages/auth.go @@ -4,6 +4,7 @@ package packages import ( + "errors" "fmt" "net/http" "strings" @@ -53,7 +54,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, auth_model.AccessTokenSc parts := strings.SplitN(h, " ", 2) if len(parts) != 2 { log.Error("split token failed: %s", h) - return 0, "", fmt.Errorf("split token failed") + return 0, "", errors.New("split token failed") } token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (any, error) { @@ -68,7 +69,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, auth_model.AccessTokenSc c, ok := token.Claims.(*packageClaims) if !token.Valid || !ok { - return 0, "", fmt.Errorf("invalid token claim") + return 0, "", errors.New("invalid token claim") } return c.UserID, c.Scope, nil diff --git a/services/packages/packages.go b/services/packages/packages.go index 1232e5914f..418ceab798 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -127,12 +127,12 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all OwnerID: pvci.Owner.ID, Type: pvci.PackageType, Name: pvci.Name, - LowerName: strings.ToLower(pvci.Name), + LowerName: packages_model.ResolvePackageName(pvci.Name, pvci.PackageType), SemverCompatible: pvci.SemverCompatible, } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { + if errors.Is(err, packages_model.ErrDuplicatePackage) { packageCreated = false } else { log.Error("Error inserting package: %v", err) @@ -208,7 +208,7 @@ func AddFileToExistingPackage(ctx context.Context, pvi *PackageInfo, pfci *Packa // This method skips quota checks and should only be used for system-managed packages. func AddFileToPackageVersionInternal(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, error) { return addFileToPackageWrapper(ctx, func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) { - return addFileToPackageVersionUnchecked(ctx, pv, pfci) + return addFileToPackageVersionUnchecked(ctx, pv, pfci, "") }) } @@ -261,10 +261,10 @@ func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVers return nil, nil, false, err } - return addFileToPackageVersionUnchecked(ctx, pv, pfci) + return addFileToPackageVersionUnchecked(ctx, pv, pfci, pvi.PackageType) } -func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) { +func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo, packageType packages_model.Type) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) { log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename) pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data)) @@ -304,7 +304,7 @@ func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.Pa VersionID: pv.ID, BlobID: pb.ID, Name: pfci.Filename, - LowerName: strings.ToLower(pfci.Filename), + LowerName: packages_model.ResolvePackageName(pfci.Filename, packageType), CompositeKey: pfci.CompositeKey, IsLead: pfci.IsLead, } diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go index f3b6f5148a..26f34be2bc 100644 --- a/services/packages/rpm/repository.go +++ b/services/packages/rpm/repository.go @@ -26,10 +26,10 @@ import ( "forgejo.org/modules/util" packages_service "forgejo.org/services/packages" + "code.forgejo.org/forgejo/go-rpmutils" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/sassoftware/go-rpmutils" ) // GetOrCreateRepositoryVersion gets or creates the internal repository package diff --git a/services/pull/check.go b/services/pull/check.go index d038b3d829..6002e2ae26 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -28,6 +28,7 @@ import ( "forgejo.org/modules/timeutil" asymkey_service "forgejo.org/services/asymkey" notify_service "forgejo.org/services/notify" + shared_automerge "forgejo.org/services/shared/automerge" ) // prPatchCheckerQueue represents a queue to handle update pull request tests @@ -170,7 +171,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer // checkAndUpdateStatus checks if pull request is possible to leaving checking status, // and set to be either conflict or mergeable. -func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) { +func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) bool { // If status has not been changed to conflict by testPatch then we are mergeable if pr.Status == issues_model.PullRequestStatusChecking { pr.Status = issues_model.PullRequestStatusMergeable @@ -184,12 +185,15 @@ func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) { if has { log.Trace("Not updating status for %-v as it is due to be rechecked", pr) - return + return false } if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil { log.Error("Update[%-v]: %v", pr, err) + return false } + + return true } // getMergeCommit checks if a pull request has been merged @@ -339,15 +343,22 @@ func handler(items ...string) []string { } func testPR(id int64) { - pullWorkingPool.CheckIn(fmt.Sprint(id)) - defer pullWorkingPool.CheckOut(fmt.Sprint(id)) ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id)) defer finished() + if pr, updated := testPRProtected(ctx, id); pr != nil && updated { + shared_automerge.AddToQueueIfMergeable(ctx, pr) + } +} + +func testPRProtected(ctx context.Context, id int64) (*issues_model.PullRequest, bool) { + pullWorkingPool.CheckIn(fmt.Sprint(id)) + defer pullWorkingPool.CheckOut(fmt.Sprint(id)) + pr, err := issues_model.GetPullRequestByID(ctx, id) if err != nil { log.Error("Unable to GetPullRequestByID[%d] for testPR: %v", id, err) - return + return nil, false } log.Trace("Testing %-v", pr) @@ -357,12 +368,12 @@ func testPR(id int64) { if pr.HasMerged { log.Trace("%-v is already merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID) - return + return nil, false } if manuallyMerged(ctx, pr) { log.Trace("%-v is manually merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID) - return + return nil, false } if err := TestPatch(pr); err != nil { @@ -371,9 +382,10 @@ func testPR(id int64) { if err := pr.UpdateCols(ctx, "status"); err != nil { log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err) } - return + return nil, false } - checkAndUpdateStatus(ctx, pr) + + return pr, checkAndUpdateStatus(ctx, pr) } // CheckPRsForBaseBranch check all pulls with baseBrannch @@ -395,7 +407,7 @@ func Init() error { prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", handler) if prPatchCheckerQueue == nil { - return fmt.Errorf("unable to create pr_patch_checker queue") + return errors.New("unable to create pr_patch_checker queue") } go graceful.GetManager().RunWithCancel(prPatchCheckerQueue) diff --git a/services/pull/merge.go b/services/pull/merge.go index 9b0d632377..f69f8a87b4 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -6,6 +6,7 @@ package pull import ( "context" + "errors" "fmt" "net/url" "os" @@ -13,6 +14,7 @@ import ( "regexp" "strconv" "strings" + "unicode" "forgejo.org/models" "forgejo.org/models/db" @@ -168,6 +170,41 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil) } +func AddCommitMessageTrailer(message, tailerKey, tailerValue string) string { + trailerLine := tailerKey + ": " + tailerValue + message = strings.ReplaceAll(message, "\r\n", "\n") + message = strings.ReplaceAll(message, "\r", "\n") + if strings.Contains(message, "\n"+trailerLine+"\n") || strings.HasSuffix(message, "\n"+trailerLine) { + return message + } + + if !strings.HasSuffix(message, "\n") { + message += "\n" + } + lastNewLine := strings.LastIndexByte(message[:len(message)-1], '\n') + keyEnd := -1 + if lastNewLine != -1 { + keyEnd = strings.IndexByte(message[lastNewLine:], ':') + if keyEnd != -1 { + keyEnd += lastNewLine + } + } + var lastLineKey string + if lastNewLine != -1 && keyEnd != -1 { + lastLineKey = message[lastNewLine+1 : keyEnd] + } + + isLikelyTrailerLine := lastLineKey != "" && unicode.IsUpper(rune(lastLineKey[0])) && strings.Contains(message, "-") + for i := 0; isLikelyTrailerLine && i < len(lastLineKey); i++ { + r := rune(lastLineKey[i]) + isLikelyTrailerLine = unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' + } + if !strings.HasSuffix(message, "\n\n") && !isLikelyTrailerLine { + message += "\n" + } + return message + trailerLine +} + // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { @@ -518,13 +555,13 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) if len(commitID) != objectFormat.FullLength() { - return fmt.Errorf("Wrong commit ID") + return errors.New("Wrong commit ID") } commit, err := baseGitRepo.GetCommit(commitID) if err != nil { if git.IsErrNotExist(err) { - return fmt.Errorf("Wrong commit ID") + return errors.New("Wrong commit ID") } return err } @@ -535,7 +572,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use return err } if !ok { - return fmt.Errorf("Wrong commit ID") + return errors.New("Wrong commit ID") } pr.MergedCommitID = commitID @@ -548,7 +585,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use if merged, err = pr.SetMerged(ctx); err != nil { return err } else if !merged { - return fmt.Errorf("SetMerged failed") + return errors.New("SetMerged failed") } return nil }); err != nil { diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 1c6f734a25..f655224c5e 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -5,7 +5,6 @@ package pull import ( "fmt" - "strings" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" @@ -67,10 +66,8 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { // add trailer - if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) { - message += fmt.Sprintf("\nCo-authored-by: %s", sig.String()) - } - message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String()) + message = AddCommitMessageTrailer(message, "Co-authored-by", sig.String()) + message = AddCommitMessageTrailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used } cmdCommit := git.NewCommand(ctx, "commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). diff --git a/services/pull/merge_test.go b/services/pull/merge_test.go index 6df6f55d46..2a26759956 100644 --- a/services/pull/merge_test.go +++ b/services/pull/merge_test.go @@ -65,3 +65,28 @@ func Test_expandDefaultMergeMessage(t *testing.T) { }) } } + +func TestAddCommitMessageTailer(t *testing.T) { + // add tailer for empty message + assert.Equal(t, "\n\nTest-tailer: TestValue", AddCommitMessageTrailer("", "Test-tailer", "TestValue")) + + // add tailer for message without newlines + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nNot tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nNot tailer: xxx", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nNotTailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nNotTailer: xxx", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nnot-tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nnot-tailer: xxx", "Test-tailer", "TestValue")) + + // add tailer for message with one EOL + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n", "Test-tailer", "TestValue")) + + // add tailer for message with two EOLs + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\n", "Test-tailer", "TestValue")) + + // add tailer for message with existing tailer (won't duplicate) + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nTest-tailer: TestValue", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nTest-tailer: TestValue\n", AddCommitMessageTrailer("title\n\nTest-tailer: TestValue\n", "Test-tailer", "TestValue")) + + // add tailer for message with existing tailer and different value (will append) + assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTrailer("title\n\nTest-tailer: v1", "Test-tailer", "v2")) + assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTrailer("title\n\nTest-tailer: v1\n", "Test-tailer", "v2")) +} diff --git a/services/pull/review.go b/services/pull/review.go index 18c27de5a7..c740328e4c 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -6,6 +6,7 @@ package pull import ( "context" + "errors" "fmt" "io" "regexp" @@ -387,7 +388,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string, } if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { - return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request") + return nil, errors.New("not need to dismiss this review because it's type is not Approve or change request") } // load data for notify @@ -397,7 +398,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string, // Check if the review's repoID is the one we're currently expecting. if review.Issue.RepoID != repoID { - return nil, fmt.Errorf("reviews's repository is not the same as the one we expect") + return nil, errors.New("reviews's repository is not the same as the one we expect") } issue := review.Issue diff --git a/services/pull/update.go b/services/pull/update.go index 470abbd6dd..563c11fff3 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -5,6 +5,7 @@ package pull import ( "context" + "errors" "fmt" git_model "forgejo.org/models/git" @@ -22,7 +23,7 @@ import ( func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error { if pr.Flow == issues_model.PullRequestFlowAGit { // TODO: update of agit flow pull request's head branch is unsupported - return fmt.Errorf("update of agit flow pull request's head branch is unsupported") + return errors.New("update of agit flow pull request's head branch is unsupported") } pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) diff --git a/services/release/release.go b/services/release/release.go index f0682c4dca..90eb1320ed 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -161,17 +161,17 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, msg string, for _, attachmentChange := range attachmentChanges { if attachmentChange.Action != "add" { - return fmt.Errorf("can only create new attachments when creating release") + return errors.New("can only create new attachments when creating release") } switch attachmentChange.Type { case "attachment": if attachmentChange.UUID == "" { - return fmt.Errorf("new attachment should have a uuid") + return errors.New("new attachment should have a uuid") } addAttachmentUUIDs.Add(attachmentChange.UUID) case "external": if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" { - return fmt.Errorf("new external attachment should have a name and external url") + return errors.New("new external attachment should have a name and external url") } _, err = attachment.NewExternalAttachment(gitRepo.Ctx, &repo_model.Attachment{ @@ -186,7 +186,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, msg string, } default: if attachmentChange.Type == "" { - return fmt.Errorf("missing attachment type") + return errors.New("missing attachment type") } return fmt.Errorf("unknown attachment type: '%q'", attachmentChange.Type) } @@ -280,7 +280,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo addAttachmentUUIDs.Add(attachmentChange.UUID) case "external": if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" { - return fmt.Errorf("new external attachment should have a name and external url") + return errors.New("new external attachment should have a name and external url") } _, err := attachment.NewExternalAttachment(ctx, &repo_model.Attachment{ Name: attachmentChange.Name, @@ -294,13 +294,13 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo } default: if attachmentChange.Type == "" { - return fmt.Errorf("missing attachment type") + return errors.New("missing attachment type") } return fmt.Errorf("unknown attachment type: %q", attachmentChange.Type) } case "delete": if attachmentChange.UUID == "" { - return fmt.Errorf("attachment deletion should have a uuid") + return errors.New("attachment deletion should have a uuid") } delAttachmentUUIDs.Add(attachmentChange.UUID) case "update": @@ -308,7 +308,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo updateAttachments.Add(attachmentChange) default: if attachmentChange.Action == "" { - return fmt.Errorf("missing attachment action") + return errors.New("missing attachment action") } return fmt.Errorf("unknown attachment action: %q", attachmentChange.Action) } diff --git a/services/release/release_test.go b/services/release/release_test.go index bcc713019b..f03b4d42b8 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -6,7 +6,6 @@ package release import ( "strings" "testing" - "time" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -14,6 +13,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/gitrepo" + "forgejo.org/modules/test" "forgejo.org/services/attachment" _ "forgejo.org/models/actions" @@ -219,7 +219,7 @@ func TestRelease_Update(t *testing.T) { release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.1.1") require.NoError(t, err) releaseCreatedUnix := release.CreatedUnix - time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp + test.SleepTillNextSecond() release.Note = "Changed note" require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{})) release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID) @@ -243,7 +243,7 @@ func TestRelease_Update(t *testing.T) { release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.2.1") require.NoError(t, err) releaseCreatedUnix = release.CreatedUnix - time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp + test.SleepTillNextSecond() release.Title = "Changed title" require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{})) release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID) @@ -267,7 +267,7 @@ func TestRelease_Update(t *testing.T) { release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.3.1") require.NoError(t, err) releaseCreatedUnix = release.CreatedUnix - time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp + test.SleepTillNextSecond() release.Title = "Changed title" release.Note = "Changed note" require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{})) @@ -412,7 +412,7 @@ func TestRelease_createTag(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, release.CreatedUnix) releaseCreatedUnix := release.CreatedUnix - time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp + test.SleepTillNextSecond() release.Note = "Changed note" _, err = createTag(db.DefaultContext, gitRepo, release, "") require.NoError(t, err) @@ -435,7 +435,7 @@ func TestRelease_createTag(t *testing.T) { _, err = createTag(db.DefaultContext, gitRepo, release, "") require.NoError(t, err) releaseCreatedUnix = release.CreatedUnix - time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp + test.SleepTillNextSecond() release.Title = "Changed title" _, err = createTag(db.DefaultContext, gitRepo, release, "") require.NoError(t, err) @@ -458,7 +458,7 @@ func TestRelease_createTag(t *testing.T) { _, err = createTag(db.DefaultContext, gitRepo, release, "") require.NoError(t, err) releaseCreatedUnix = release.CreatedUnix - time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp + test.SleepTillNextSecond() release.Title = "Changed title" release.Note = "Changed note" _, err = createTag(db.DefaultContext, gitRepo, release, "") diff --git a/services/repository/branch.go b/services/repository/branch.go index d5b68c1fa7..bc739825a5 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -251,7 +251,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, // For other batches, it will hit optimization 4. if len(branchNames) != len(commitIDs) { - return fmt.Errorf("branchNames and commitIDs length not match") + return errors.New("branchNames and commitIDs length not match") } return db.WithTx(ctx, func(ctx context.Context) error { diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go index ad4cc400cb..1805bd5960 100644 --- a/services/repository/contributors_graph.go +++ b/services/repository/contributors_graph.go @@ -111,7 +111,7 @@ func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_mode var cachedStats map[string]*ContributorData return cachedStats, json.Unmarshal([]byte(v), &cachedStats) default: - return nil, fmt.Errorf("unexpected type in cache detected") + return nil, errors.New("unexpected type in cache detected") } } diff --git a/services/repository/create_test.go b/services/repository/create_test.go index 7eb3c0f805..0a6c34b6fe 100644 --- a/services/repository/create_test.go +++ b/services/repository/create_test.go @@ -147,3 +147,13 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { } require.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") } + +func TestCreateRepository(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + r, err := CreateRepositoryDirectly(db.DefaultContext, user, user, CreateRepoOptions{Name: "repo-last"}) + require.NoError(t, err) + require.NotNil(t, r.Topics) + require.Empty(t, r.Topics) +} diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go index 838891813e..0e88a29230 100644 --- a/services/repository/files/cherry_pick.go +++ b/services/repository/files/cherry_pick.go @@ -5,6 +5,7 @@ package files import ( "context" + "errors" "fmt" "strings" @@ -88,7 +89,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod } if conflict { - return nil, fmt.Errorf("failed to merge due to conflicts") + return nil, errors.New("failed to merge due to conflicts") } } else { description := fmt.Sprintf("CherryPick %s onto %s", right, opts.OldBranch) @@ -98,7 +99,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod } if conflict { - return nil, fmt.Errorf("failed to merge due to conflicts") + return nil, errors.New("failed to merge due to conflicts") } treeHash, err = t.WriteTree() diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 5f7dd38303..3d2217df18 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -250,8 +250,25 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref return contentsResponse, nil } -// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash. -func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { +// GetBlobsBySHA gets multiple GitBlobs of a repository by sha hash. +func GetBlobsBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, shas []string) ([]*api.GitBlob, error) { + if len(shas) > setting.API.MaxResponseItems { + shas = shas[:setting.API.MaxResponseItems] + } + + blobs := make([]*api.GitBlob, 0, len(shas)) + for _, sha := range shas { + blob, err := GetBlobBySHA(ctx, repo, gitRepo, sha) + if err != nil { + return nil, err + } + blobs = append(blobs, blob) + } + return blobs, nil +} + +// GetBlobBySHA get the GitBlob of a repository using a sha hash. +func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlob, error) { gitBlob, err := gitRepo.GetBlob(sha) if err != nil { return nil, err @@ -263,7 +280,7 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git return nil, err } } - return &api.GitBlobResponse{ + return &api.GitBlob{ SHA: gitBlob.ID.String(), URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()), Size: gitBlob.Size(), diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index e55b840660..8fc8f56b4f 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -192,7 +192,7 @@ func TestGetBlobBySHA(t *testing.T) { defer gitRepo.Close() gbr, err := GetBlobBySHA(db.DefaultContext, repo, gitRepo, "65f1bf27bc3bf70f64657658635e66094edbcb4d") - expectedGBR := &api.GitBlobResponse{ + expectedGBR := &api.GitBlob{ Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK", Encoding: "base64", URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", @@ -202,3 +202,43 @@ func TestGetBlobBySHA(t *testing.T) { require.NoError(t, err) assert.Equal(t, expectedGBR, gbr) } + +func TestGetBlobsBySHA(t *testing.T) { + unittest.PrepareTestEnv(t) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + require.NoError(t, err) + defer gitRepo.Close() + + gbr, err := GetBlobsBySHA(db.DefaultContext, repo, gitRepo, []string{ + "ea82fc8777a24b07c26b3a4bf4e2742c03733eab", // Home.md + "6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", // line.svg + "26f842bcad37fa40a1bb34cbb5ee219ee35d863d", // test.xml + }) + expectedGBR := []*api.GitBlob{ + { + Content: "IyBIb21lIHBhZ2UKClRoaXMgaXMgdGhlIGhvbWUgcGFnZSEK", + Encoding: "base64", + URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/ea82fc8777a24b07c26b3a4bf4e2742c03733eab", + SHA: "ea82fc8777a24b07c26b3a4bf4e2742c03733eab", + Size: 36, + }, + { + Content: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZwogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHdpZHRoPSIxMjgiCiAgIGhlaWdodD0iMTI4IgogICB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+CgogIDxsaW5lIHgxPSIwIiB5MT0iNyIgeDI9IjEwIiB5Mj0iNyIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+", + Encoding: "base64", + URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", + SHA: "6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", + Size: 246, + }, + { + Content: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHRlc3Q+VGhpcyBpcyBYTUw8L3Rlc3Q+Cg==", + Encoding: "base64", + URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/26f842bcad37fa40a1bb34cbb5ee219ee35d863d", + SHA: "26f842bcad37fa40a1bb34cbb5ee219ee35d863d", + Size: 64, + }, + } + require.NoError(t, err) + assert.Equal(t, expectedGBR, gbr) +} diff --git a/services/repository/files/file.go b/services/repository/files/file.go index 810c60163d..ef9a87dbcf 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -5,7 +5,7 @@ package files import ( "context" - "fmt" + "errors" "net/url" "strings" "time" @@ -50,10 +50,10 @@ func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index in // GetFileCommitResponse Constructs a FileCommitResponse from a Commit object func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) { if repo == nil { - return nil, fmt.Errorf("repo cannot be nil") + return nil, errors.New("repo cannot be nil") } if commit == nil { - return nil, fmt.Errorf("commit cannot be nil") + return nil, errors.New("commit cannot be nil") } commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String())) commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.Tree.ID.String())) diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index b3aadbc6cb..64d3e5887d 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -6,6 +6,7 @@ package files import ( "bytes" "context" + "errors" "fmt" "io" "os" @@ -368,7 +369,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { // GetBranchCommit Gets the commit object of the given branch func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) { if t.gitRepo == nil { - return nil, fmt.Errorf("repository has not been cloned") + return nil, errors.New("repository has not been cloned") } return t.gitRepo.GetBranchCommit(branch) } @@ -376,7 +377,7 @@ func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, // GetCommit Gets the commit object of the given commit ID func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) { if t.gitRepo == nil { - return nil, fmt.Errorf("repository has not been cloned") + return nil, errors.New("repository has not been cloned") } return t.gitRepo.GetCommit(commitID) } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 5e8834c6de..8fb9644fa4 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -193,28 +193,34 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } if hasOldBranch { - // Get the commit of the original branch - commit, err := t.GetBranchCommit(opts.OldBranch) + // Get the current commit of the original branch + actualBaseCommit, err := t.GetBranchCommit(opts.OldBranch) if err != nil { return nil, err // Couldn't get a commit for the branch } - // Assigned LastCommitID in opts if it hasn't been set - if opts.LastCommitID == "" { - opts.LastCommitID = commit.ID.String() - } else { - lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID) + var lastKnownCommit git.ObjectID // when nil, the sha provided in the opts.Files must match the current blob-sha + if opts.OldBranch != opts.NewBranch { + // when creating a new branch, ignore if a file has been changed in the meantime + // (such changes will visible when doing the merge) + lastKnownCommit = actualBaseCommit.ID + } else if opts.LastCommitID != "" { + lastKnownCommit, err = t.gitRepo.ConvertToGitID(opts.LastCommitID) if err != nil { return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %w", err) } - opts.LastCommitID = lastCommitID.String() } for _, file := range opts.Files { - if err := handleCheckErrors(file, commit, opts); err != nil { + if err := handleCheckErrors(file, actualBaseCommit, lastKnownCommit); err != nil { return nil, err } } + + if opts.LastCommitID == "" { + // needed for t.CommitTree + opts.LastCommitID = actualBaseCommit.ID.String() + } } contentStore := lfs.NewContentStore() @@ -277,9 +283,9 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // handles the check for various issues for ChangeRepoFiles -func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error { +func handleCheckErrors(file *ChangeRepoFile, actualBaseCommit *git.Commit, lastKnownCommit git.ObjectID) error { if file.Operation == "update" || file.Operation == "delete" { - fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + fromEntry, err := actualBaseCommit.GetTreeEntryByPath(file.Options.fromTreePath) if err != nil { return err } @@ -292,22 +298,22 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep CurrentSHA: fromEntry.ID.String(), } } - } else if opts.LastCommitID != "" { - // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw - // an error, but only if we aren't creating a new branch. - if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { - if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil { + } else if lastKnownCommit != nil { + if actualBaseCommit.ID.String() != lastKnownCommit.String() { + // If a lastKnownCommit was given and it doesn't match the actualBaseCommit, + // check if the file has been changed in between + if changed, err := actualBaseCommit.FileChangedSinceCommit(file.Options.treePath, lastKnownCommit.String()); err != nil { return err } else if changed { return models.ErrCommitIDDoesNotMatch{ - GivenCommitID: opts.LastCommitID, - CurrentCommitID: opts.LastCommitID, + GivenCommitID: lastKnownCommit.String(), + CurrentCommitID: actualBaseCommit.ID.String(), } } - // The file wasn't modified, so we are good to delete it + // The file wasn't modified, so we are good to update it } } else { - // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits + // When updating a file, a lastKnownCommit or SHA needs to be given to make sure other commits // haven't been made. We throw an error if one wasn't provided. return models.ErrSHAOrCommitIDNotProvided{} } @@ -322,7 +328,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep subTreePath := "" for index, part := range treePathParts { subTreePath = path.Join(subTreePath, part) - entry, err := commit.GetTreeEntryByPath(subTreePath) + entry, err := actualBaseCommit.GetTreeEntryByPath(subTreePath) if err != nil { if git.IsErrNotExist(err) { // Means there is no item with that name, so we're good diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go index 227dd1850e..6de241e4d4 100644 --- a/services/repository/fork_test.go +++ b/services/repository/fork_test.go @@ -11,6 +11,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,7 +37,7 @@ func TestForkRepository(t *testing.T) { assert.False(t, repo_model.IsErrReachLimitOfRepo(err)) // change AllowForkWithoutMaximumLimit to false for the test - setting.Repository.AllowForkWithoutMaximumLimit = false + defer test.MockVariableValue(&setting.Repository.AllowForkWithoutMaximumLimit, false)() // user has reached maximum limit of repositories user.MaxRepoCreation = 0 fork2, err := ForkRepositoryAndUpdates(git.DefaultContext, user, user, ForkRepoOptions{ diff --git a/services/repository/generate.go b/services/repository/generate.go index 9aeb057c3d..e23e294de1 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -43,12 +43,8 @@ type expansion struct { var defaultTransformers = []transformer{ {Name: "SNAKE", Transform: xstrings.ToSnakeCase}, {Name: "KEBAB", Transform: xstrings.ToKebabCase}, - // as of xstrings v1.5.0 the CAMEL & PASCAL workarounds are no longer necessary - // and can be removed https://codeberg.org/forgejo/forgejo/pulls/4050 - {Name: "CAMEL", Transform: func(str string) string { - return xstrings.FirstRuneToLower(xstrings.ToCamelCase(str)) - }}, - {Name: "PASCAL", Transform: xstrings.ToCamelCase}, + {Name: "CAMEL", Transform: xstrings.ToCamelCase}, + {Name: "PASCAL", Transform: xstrings.ToPascalCase}, {Name: "LOWER", Transform: strings.ToLower}, {Name: "UPPER", Transform: strings.ToUpper}, {Name: "TITLE", Transform: util.ToTitleCase}, diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index b0f97d0ffb..2eb3a55e96 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -65,3 +65,30 @@ func TestFileNameSanitize(t *testing.T) { assert.Equal(t, "_", fileNameSanitize("\u0000")) assert.Equal(t, "目标", fileNameSanitize("目标")) } + +func TestTransformers(t *testing.T) { + input := "Foo_Forgejo-BAR" + + tests := []struct { + name string + expected string + }{ + {"SNAKE", "foo_forgejo_bar"}, + {"KEBAB", "foo-forgejo-bar"}, + {"CAMEL", "fooForgejoBar"}, + {"PASCAL", "FooForgejoBar"}, + {"LOWER", "foo_forgejo-bar"}, + {"UPPER", "FOO_FORGEJO-BAR"}, + {"TITLE", "Foo_forgejo-Bar"}, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tranform := defaultTransformers[i] + assert.Equal(t, tt.name, tranform.Name) + + got := tranform.Transform(input) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/services/repository/push.go b/services/repository/push.go index 53574a7d93..eaedd80e1f 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -66,7 +66,7 @@ func PushUpdates(opts []*repo_module.PushUpdateOptions) error { for _, opt := range opts { if opt.IsNewRef() && opt.IsDelRef() { - return fmt.Errorf("Old and new revisions are both NULL") + return errors.New("Old and new revisions are both NULL") } } diff --git a/services/repository/repository.go b/services/repository/repository.go index a2620740b1..41f3a96dd1 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -6,6 +6,7 @@ package repository import ( "context" + "errors" "fmt" "forgejo.org/models/db" @@ -72,10 +73,10 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN if ok, err := organization.CanCreateOrgRepo(ctx, owner.ID, authUser.ID); err != nil { return nil, err } else if !ok { - return nil, fmt.Errorf("cannot push-create repository for org") + return nil, errors.New("cannot push-create repository for org") } } else if authUser.ID != owner.ID { - return nil, fmt.Errorf("cannot push-create repository for another user") + return nil, errors.New("cannot push-create repository for another user") } } diff --git a/services/shared/automerge/automerge.go b/services/shared/automerge/automerge.go index 1dc309f4b3..be7b2f6eb4 100644 --- a/services/shared/automerge/automerge.go +++ b/services/shared/automerge/automerge.go @@ -21,9 +21,9 @@ import ( var PRAutoMergeQueue *queue.WorkerPoolQueue[string] func addToQueue(pr *issues_model.PullRequest, sha string) { - log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) + log.Trace("Adding pullID: %d to the automerge queue with sha %s", pr.ID, sha) if err := PRAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil { - log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err) + log.Error("Error adding pullID: %d to the automerge queue %v", pr.ID, err) } } @@ -43,32 +43,29 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m return nil } -// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) { if pull == nil || pull.HasMerged || !pull.CanAutoMerge() { return } - if err := pull.LoadBaseRepo(ctx); err != nil { - log.Error("LoadBaseRepo: %v", err) - return + commitID := pull.HeadCommitID + if commitID == "" { + commitID = getCommitIDFromRefName(ctx, pull) } - gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) - if err != nil { - log.Error("OpenRepository: %v", err) - return - } - defer gitRepo.Close() - commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) - if err != nil { - log.Error("GetRefCommitID: %v", err) + if commitID == "" { return } addToQueue(pull, commitID) } +var AddToQueueIfMergeable = func(ctx context.Context, pull *issues_model.PullRequest) { + if pull.Status == issues_model.PullRequestStatusMergeable { + StartPRCheckAndAutoMerge(ctx, pull) + } +} + func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { @@ -118,3 +115,24 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model. return pulls, nil } + +func getCommitIDFromRefName(ctx context.Context, pull *issues_model.PullRequest) string { + if err := pull.LoadBaseRepo(ctx); err != nil { + log.Error("LoadBaseRepo: %v", err) + return "" + } + + gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) + if err != nil { + log.Error("OpenRepository: %v", err) + return "" + } + defer gitRepo.Close() + commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) + if err != nil { + log.Error("GetRefCommitID: %v", err) + return "" + } + + return commitID +} diff --git a/services/task/task.go b/services/task/task.go index 3181fc79d7..f030bdb38c 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -5,6 +5,7 @@ package task import ( "context" + "errors" "fmt" admin_model "forgejo.org/models/admin" @@ -41,7 +42,7 @@ func Run(ctx context.Context, t *admin_model.Task) error { func Init() error { taskQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "task", handler) if taskQueue == nil { - return fmt.Errorf("unable to create task queue") + return errors.New("unable to create task queue") } go graceful.GetManager().RunWithCancel(taskQueue) return nil diff --git a/services/user/user_test.go b/services/user/user_test.go index b229301a1a..4678d3bc9a 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -205,13 +205,13 @@ func TestRenameUser(t *testing.T) { unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "user_rename"}) // The granularity of created_unix is a second. - time.Sleep(time.Second) + test.SleepTillNextSecond() require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-2")) unittest.AssertExistsIf(t, false, &user_model.Redirect{LowerName: "user_rename"}) unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"}) setting.Service.MaxUserRedirects = 2 - time.Sleep(time.Second) + test.SleepTillNextSecond() require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-3")) unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"}) unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-2"}) diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 0c7c039f10..23aca80345 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -6,6 +6,7 @@ package webhook import ( "context" "crypto/tls" + "errors" "fmt" "io" "net/http" @@ -218,7 +219,7 @@ func Init() error { hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler) if hookQueue == nil { - return fmt.Errorf("unable to create webhook_sender queue") + return errors.New("unable to create webhook_sender queue") } go graceful.GetManager().RunWithCancel(hookQueue) diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 9d5c7e573f..ec53c79c2c 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -207,6 +207,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil } +func (dc dingtalkConvertor) Action(p *api.ActionPayload) (DingtalkPayload, error) { + text, _ := getActionPayloadInfo(p, noneLinkFormatter) + + return createDingtalkPayload(text, text, "view action", p.Run.HTMLURL), nil +} + func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload { return DingtalkPayload{ MsgType: "actionCard", diff --git a/services/webhook/discord.go b/services/webhook/discord.go index d829692aa4..7259c4a995 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -325,6 +325,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil } +func (d discordConvertor) Action(p *api.ActionPayload) (DiscordPayload, error) { + text, color := getActionPayloadInfo(p, noneLinkFormatter) + + return d.createPayload(p.Run.TriggerUser, text, "", p.Run.HTMLURL, color), nil +} + var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{} func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 01b3d07983..57f2362783 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -191,6 +191,12 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) return newFeishuTextPayload(text), nil } +func (fc feishuConvertor) Action(p *api.ActionPayload) (FeishuPayload, error) { + text, _ := getActionPayloadInfo(p, noneLinkFormatter) + + return newFeishuTextPayload(text), nil +} + type feishuConvertor struct{} var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{} diff --git a/services/webhook/general.go b/services/webhook/general.go index 176a9a27f9..c728b6ba1a 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -304,6 +304,25 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w return text, color } +func getActionPayloadInfo(p *api.ActionPayload, linkFormatter linkFormatter) (text string, color int) { + runLink := linkFormatter(p.Run.HTMLURL, p.Run.Title) + repoLink := linkFormatter(p.Run.Repo.HTMLURL, p.Run.Repo.FullName) + + switch p.Action { + case api.HookActionFailure: + text = fmt.Sprintf("%s Action Failed in %s %s", runLink, repoLink, p.Run.PrettyRef) + color = redColor + case api.HookActionRecover: + text = fmt.Sprintf("%s Action Recovered in %s %s", runLink, repoLink, p.Run.PrettyRef) + color = greenColor + case api.HookActionSuccess: + text = fmt.Sprintf("%s Action Succeeded in %s %s", runLink, repoLink, p.Run.PrettyRef) + color = greenColor + } + + return text, color +} + // ToHook convert models.Webhook to api.Hook // This function is not part of the convert package to prevent an import cycle func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go index b321fb3f8c..10c779742d 100644 --- a/services/webhook/general_test.go +++ b/services/webhook/general_test.go @@ -270,6 +270,22 @@ func pullReleaseTestPayload() *api.ReleasePayload { } } +func ActionTestPayload() *api.ActionPayload { + // this is not a complete action payload but enough for testing purposes + return &api.ActionPayload{ + Run: &api.ActionRun{ + Repo: &api.Repository{ + HTMLURL: "http://localhost:3000/test/repo", + Name: "repo", + FullName: "test/repo", + }, + PrettyRef: "main", + HTMLURL: "http://localhost:3000/test/repo/actions/runs/69", + Title: "Build release", + }, + } +} + func pullRequestTestPayload() *api.PullRequestPayload { return &api.PullRequestPayload{ Action: api.HookIssueOpened, @@ -675,3 +691,36 @@ func TestGetIssueCommentPayloadInfo(t *testing.T) { assert.Equal(t, c.color, color, "case %d", i) } } + +func TestGetActionPayloadInfo(t *testing.T) { + p := ActionTestPayload() + + cases := []struct { + action api.HookActionAction + text string + color int + }{ + { + api.HookActionFailure, + "Build release Action Failed in test/repo main", + redColor, + }, + { + api.HookActionSuccess, + "Build release Action Succeeded in test/repo main", + greenColor, + }, + { + api.HookActionRecover, + "Build release Action Recovered in test/repo main", + greenColor, + }, + } + + for i, c := range cases { + p.Action = c.action + text, color := getActionPayloadInfo(p, noneLinkFormatter) + assert.Equal(t, c.text, text, "case %d", i) + assert.Equal(t, c.color, color, "case %d", i) + } +} diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index f1cc9384d3..bdb0c292ab 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -273,6 +273,12 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) { return m.newPayload(text) } +func (m matrixConvertor) Action(p *api.ActionPayload) (MatrixPayload, error) { + text, _ := getActionPayloadInfo(p, htmlLinkFormatter) + + return m.newPayload(text) +} + var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) func getMessageBody(htmlText string) string { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 1ed03afd26..3b35c407e1 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -326,6 +326,23 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) ), nil } +func (m msteamsConvertor) Action(p *api.ActionPayload) (MSTeamsPayload, error) { + title, color := getActionPayloadInfo(p, noneLinkFormatter) + + // TODO: is TriggerUser correct here? + // if you'd like to test these proprietary services, see the discussion on: https://codeberg.org/forgejo/forgejo/pulls/7508 + return createMSTeamsPayload( + p.Run.Repo, + p.Run.TriggerUser, + title, + "", + p.Run.HTMLURL, + color, + // TODO: does this make any sense? + &MSTeamsFact{"Action:", p.Run.Title}, + ), nil +} + func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { facts := make([]MSTeamsFact, 0, 2) if r != nil { diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index e9fd52c940..009efc994f 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -6,6 +6,7 @@ package webhook import ( "context" + actions_model "forgejo.org/models/actions" issues_model "forgejo.org/models/issues" packages_model "forgejo.org/models/packages" "forgejo.org/models/perm" @@ -887,6 +888,45 @@ func (m *webhookNotifier) PackageDelete(ctx context.Context, doer *user_model.Us notifyPackage(ctx, doer, pd, api.HookPackageDeleted) } +func (m *webhookNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) { + source := EventSource{ + Repository: run.Repo, + Owner: run.TriggerUser, + } + + // The doer is the one whose perspective is used to view this ActionRun. + // In the best case we use the user that created the webhook. + // Unfortunately we don't know who that was. + // So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins. + // This is pretty close to perfect. + doer := run.Repo.Owner + + payload := &api.ActionPayload{ + Run: convert.ToActionRun(ctx, run, doer), + LastRun: convert.ToActionRun(ctx, lastRun, doer), + PriorStatus: priorStatus.String(), + } + + if run.Status.IsSuccess() { + payload.Action = api.HookActionSuccess + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunSuccess, payload); err != nil { + log.Error("PrepareWebhooks: %v", err) + } + // send another event when this is a recover + if lastRun != nil && !lastRun.Status.IsSuccess() { + payload.Action = api.HookActionRecover + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunRecover, payload); err != nil { + log.Error("PrepareWebhooks: %v", err) + } + } + } else { + payload.Action = api.HookActionFailure + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunFailure, payload); err != nil { + log.Error("PrepareWebhooks: %v", err) + } + } +} + func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) { source := EventSource{ Repository: pd.Repository, diff --git a/services/webhook/notifier_test.go b/services/webhook/notifier_test.go index b57990a9d8..a810de91c1 100644 --- a/services/webhook/notifier_test.go +++ b/services/webhook/notifier_test.go @@ -6,6 +6,7 @@ package webhook import ( "testing" + actions_model "forgejo.org/models/actions" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" @@ -13,10 +14,12 @@ import ( webhook_model "forgejo.org/models/webhook" "forgejo.org/modules/git" "forgejo.org/modules/json" + "forgejo.org/modules/log" "forgejo.org/modules/repository" "forgejo.org/modules/setting" "forgejo.org/modules/structs" "forgejo.org/modules/test" + webhook_module "forgejo.org/modules/webhook" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -119,3 +122,190 @@ func TestPushCommits(t *testing.T) { assert.Equal(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID) }) } + +func assertActionEqual(t *testing.T, expectedRun *actions_model.ActionRun, actualRun *structs.ActionRun) { + assert.NotNil(t, expectedRun) + assert.NotNil(t, actualRun) + // only test a few things + assert.Equal(t, expectedRun.ID, actualRun.ID) + assert.Equal(t, expectedRun.Status.String(), actualRun.Status) + assert.Equal(t, expectedRun.Index, actualRun.Index) + assert.Equal(t, expectedRun.RepoID, actualRun.Repo.ID) + // convert to unix because of time zones + assert.Equal(t, expectedRun.Stopped.AsTime().Unix(), actualRun.Stopped.Unix()) + assert.Equal(t, expectedRun.Title, actualRun.Title) + assert.Equal(t, expectedRun.WorkflowID, actualRun.WorkflowID) +} + +func TestAction(t *testing.T) { + defer unittest.OverrideFixtures("services/webhook/TestPushCommits")() + require.NoError(t, unittest.PrepareTestDatabase()) + + triggerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: triggerUser.ID}) + + oldSuccessRun := &actions_model.ActionRun{ + ID: 1, + Status: actions_model.StatusSuccess, + Index: 1, + RepoID: repo.ID, + Stopped: 1693648027, + WorkflowID: "some_workflow", + Title: "oldSuccessRun", + TriggerUser: triggerUser, + TriggerUserID: triggerUser.ID, + TriggerEvent: "push", + } + oldSuccessRun.LoadAttributes(db.DefaultContext) + oldFailureRun := &actions_model.ActionRun{ + ID: 1, + Status: actions_model.StatusFailure, + Index: 1, + RepoID: repo.ID, + Stopped: 1693648027, + WorkflowID: "some_workflow", + Title: "oldFailureRun", + TriggerUser: triggerUser, + TriggerUserID: triggerUser.ID, + TriggerEvent: "push", + } + oldFailureRun.LoadAttributes(db.DefaultContext) + newSuccessRun := &actions_model.ActionRun{ + ID: 1, + Status: actions_model.StatusSuccess, + Index: 1, + RepoID: repo.ID, + Stopped: 1693648327, + WorkflowID: "some_workflow", + Title: "newSuccessRun", + TriggerUser: triggerUser, + TriggerUserID: triggerUser.ID, + TriggerEvent: "push", + } + newSuccessRun.LoadAttributes(db.DefaultContext) + newFailureRun := &actions_model.ActionRun{ + ID: 1, + Status: actions_model.StatusFailure, + Index: 1, + RepoID: repo.ID, + Stopped: 1693648327, + WorkflowID: "some_workflow", + Title: "newFailureRun", + TriggerUser: triggerUser, + TriggerUserID: triggerUser.ID, + TriggerEvent: "push", + } + newFailureRun.LoadAttributes(db.DefaultContext) + + t.Run("Successful Run after Nothing", func(t *testing.T) { + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() + + NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, nil) + + // there's only one of these at the time + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType) + + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionSuccess, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newSuccessRun, payloadContent.Run) + assert.Nil(t, payloadContent.LastRun) + }) + + t.Run("Successful Run after Failure", func(t *testing.T) { + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() + + NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, oldFailureRun) + + { + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%oldFailureRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType) + + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionSuccess, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newSuccessRun, payloadContent.Run) + assertActionEqual(t, oldFailureRun, payloadContent.LastRun) + } + { + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_recover' AND payload_content LIKE '%recover%newSuccessRun%oldFailureRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunRecover, hookTask.EventType) + + log.Error("something: %s", hookTask.PayloadContent) + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionRecover, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newSuccessRun, payloadContent.Run) + assertActionEqual(t, oldFailureRun, payloadContent.LastRun) + } + }) + + t.Run("Successful Run after Success", func(t *testing.T) { + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() + + NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, oldSuccessRun) + + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%oldSuccessRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType) + + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionSuccess, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newSuccessRun, payloadContent.Run) + assertActionEqual(t, oldSuccessRun, payloadContent.LastRun) + }) + + t.Run("Failed Run after Nothing", func(t *testing.T) { + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() + + NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, nil) + + // there should only be this one at the time + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType) + + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionFailure, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newFailureRun, payloadContent.Run) + assert.Nil(t, payloadContent.LastRun) + }) + + t.Run("Failed Run after Failure", func(t *testing.T) { + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() + + NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, oldFailureRun) + + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%oldFailureRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType) + + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionFailure, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newFailureRun, payloadContent.Run) + assertActionEqual(t, oldFailureRun, payloadContent.LastRun) + }) + + t.Run("Failed Run after Success", func(t *testing.T) { + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() + + NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, oldSuccessRun) + + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%oldSuccessRun%'")) + assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType) + + var payloadContent structs.ActionPayload + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) + assert.Equal(t, structs.HookActionFailure, payloadContent.Action) + assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus) + assertActionEqual(t, newFailureRun, payloadContent.Run) + assertActionEqual(t, oldSuccessRun, payloadContent.LastRun) + }) +} diff --git a/services/webhook/shared/payloader.go b/services/webhook/shared/payloader.go index 0a6535eddb..e3be4c4b4c 100644 --- a/services/webhook/shared/payloader.go +++ b/services/webhook/shared/payloader.go @@ -36,6 +36,7 @@ type PayloadConvertor[T any] interface { Release(*api.ReleasePayload) (T, error) Wiki(*api.WikiPayload) (T, error) Package(*api.PackagePayload) (T, error) + Action(*api.ActionPayload) (T, error) } func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) { @@ -86,6 +87,8 @@ func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module return convertUnmarshalledJSON(rc.Wiki, data) case webhook_module.HookEventPackage: return convertUnmarshalledJSON(rc.Package, data) + case webhook_module.HookEventActionRunFailure, webhook_module.HookEventActionRunRecover, webhook_module.HookEventActionRunSuccess: + return convertUnmarshalledJSON(rc.Action, data) } var t T return t, fmt.Errorf("newPayload unsupported event: %s", event) diff --git a/services/webhook/slack.go b/services/webhook/slack.go index e854f89c6c..8c61e7ba25 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -142,6 +142,7 @@ func SlackLinkToRef(repoURL, ref string) string { return SlackLinkFormatter(url, refName) } +// TODO: fix spelling to Converter // Create implements payloadConvertor Create method func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) { refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) @@ -311,6 +312,12 @@ func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, erro return s.createPayload(text, nil), nil } +func (s slackConvertor) Action(p *api.ActionPayload) (SlackPayload, error) { + text, _ := getActionPayloadInfo(p, SlackLinkFormatter) + + return s.createPayload(text, nil), nil +} + func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload { return SlackPayload{ Channel: s.Channel, diff --git a/services/webhook/sourcehut/builds.go b/services/webhook/sourcehut/builds.go index bd3eeebc6c..2593afb0b2 100644 --- a/services/webhook/sourcehut/builds.go +++ b/services/webhook/sourcehut/builds.go @@ -190,6 +190,10 @@ func (pc sourcehutConvertor) Package(_ *api.PackagePayload) (graphqlPayload[buil return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported } +func (pc sourcehutConvertor) Action(_ *api.ActionPayload) (graphqlPayload[buildsVariables], error) { + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported +} + // newPayload opens and adjusts the manifest to submit to the builds service // // in case of an error the Error field will be set, to be visible by the end-user under recent deliveries diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index d0abd667f4..e6897f68bc 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -205,6 +205,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro return createTelegramPayload(text), nil } +func (telegramConvertor) Action(p *api.ActionPayload) (TelegramPayload, error) { + text, _ := getActionPayloadInfo(p, htmlLinkFormatter) + + return createTelegramPayload(text), nil +} + func createTelegramPayload(message string) TelegramPayload { return TelegramPayload{ Message: markup.Sanitize(strings.TrimSpace(message)), diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 989b535564..ecbbfcfbd6 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -103,7 +103,7 @@ type EventSource struct { Owner *user_model.User } -// handle delivers hook tasks +// handler delivers hook tasks func handler(items ...int64) []int64 { ctx := graceful.GetManager().HammerContext() diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index c9af09d3e9..15cb8f620c 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -14,6 +14,7 @@ import ( webhook_model "forgejo.org/models/webhook" "forgejo.org/modules/setting" api "forgejo.org/modules/structs" + "forgejo.org/modules/test" webhook_module "forgejo.org/modules/webhook" "forgejo.org/services/convert" @@ -104,7 +105,8 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { func TestWebhookUserMail(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) - setting.Service.NoReplyAddress = "no-reply.com" + defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 323d23aba7..5c765b0754 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -201,6 +201,12 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, return newWechatworkMarkdownPayload(text), nil } +func (wc wechatworkConvertor) Action(p *api.ActionPayload) (WechatworkPayload, error) { + text, _ := getActionPayloadInfo(p, noneLinkFormatter) + + return newWechatworkMarkdownPayload(text), nil +} + type wechatworkConvertor struct{} var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index ff5141a1fd..cb984425af 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -284,9 +284,9 @@ func TestPrepareWikiFileName(t *testing.T) { } if existence != tt.existence { if existence { - t.Errorf("expect to find no escaped file but we detect one") + t.Error("expect to find no escaped file but we detect one") } else { - t.Errorf("expect to find an escaped file but we could not detect one") + t.Error("expect to find an escaped file but we could not detect one") } } assert.Equal(t, tt.wikiPath, newWikiPath) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000..cfd555fa37 --- /dev/null +++ b/shell.nix @@ -0,0 +1,28 @@ +{ + pkgs ? import { }, +}: + +pkgs.mkShell { + name = "forgejo"; + nativeBuildInputs = with pkgs; [ + # generic + git + git-lfs + gnumake + gnused + gnutar + gzip + + # frontend + nodejs + + # backend + gofumpt + sqlite + go + gopls + + # tests + openssh + ]; +} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 7d004dd903..a1ff3d4117 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -208,6 +208,7 @@
+
{{ctx.Locale.Tr "avatar.constraints_hint" (ctx.Locale.TrSize .MaxAvatarFileSize) .MaxAvatarWidth .MaxAvatarHeight}}
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 7ec2ac87b3..6357981549 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -25,7 +25,7 @@ {{template "base/head_style" .}} {{template "custom/header" .}} - + {{template "custom/body_outer_pre" .}}
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index 5490f71784..1c88333520 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -40,8 +40,6 @@ - - @@ -52,16 +50,10 @@

Do not use if there is no strong requirement. Do not use grey/black buttons, they don't work well with dark theme.

- - - - - - diff --git a/templates/devtest/hashbox.tmpl b/templates/devtest/hashbox.tmpl new file mode 100644 index 0000000000..7321c9956d --- /dev/null +++ b/templates/devtest/hashbox.tmpl @@ -0,0 +1,82 @@ +{{template "base/head" .}} + +
+

Hashbox (shabox)

+ +

Unsigned

+ {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + )}} + +

Unknown signature

+
+ {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .UnknownVerif + )}} + {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .UnknownVerifUnk + )}} +
+ +

Trusted

+
+ {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .TrustedVerif + )}} + {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .TrustedVerifUnk + )}} +
+ +

Untrusted

+
+ {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .UntrustedVerif + )}} + {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .UntrustedVerifUnk + )}} +
+ +

Unmatched

+
+ {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .UnmatchedVerif + )}} + {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .UnmatchedVerifUnk + )}} +
+ +

Warning

+
+ {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .WarnVerif + )}} + {{template "repo/shabox" (dict + "sha1" "475e3471b4e8da8776fe7e66a3390c8a30c19f08" + "signature" "true" + "verification" .WarnVerifUnk + )}} +
+
+ +{{template "base/footer" .}} diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 2ef7031aef..562f3f3ae3 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -94,6 +94,7 @@
+
{{ctx.Locale.Tr "avatar.constraints_hint" (ctx.Locale.TrSize .MaxAvatarFileSize) .MaxAvatarWidth .MaxAvatarHeight}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index c9f80259e2..faf0c336a0 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -42,11 +42,8 @@
  • {{ctx.Locale.Tr "org.teams.can_create_org_repo"}}
  • {{end}} - {{if (eq .Team.AccessMode 2)}} -

    {{ctx.Locale.Tr "org.settings.permission"}}

    - {{ctx.Locale.Tr "org.teams.write_permission_desc"}} - {{else if (eq .Team.AccessMode 3)}} -

    {{ctx.Locale.Tr "org.settings.permission"}}

    +

    {{ctx.Locale.Tr "org.settings.permission"}}

    + {{if (eq .Team.AccessMode 3)}} {{ctx.Locale.Tr "org.teams.admin_permission_desc"}} {{else}} diff --git a/templates/repo/commit_header.tmpl b/templates/repo/commit_header.tmpl new file mode 100644 index 0000000000..9604daf2b0 --- /dev/null +++ b/templates/repo/commit_header.tmpl @@ -0,0 +1,355 @@ +{{$class := ""}} +{{if .Commit.Signature}} + {{$class = (print $class " isSigned")}} + {{if .Verification.Verified}} + {{if eq .Verification.TrustStatus "trusted"}} + {{$class = (print $class " isVerified")}} + {{else if eq .Verification.TrustStatus "untrusted"}} + {{$class = (print $class " isVerifiedUntrusted")}} + {{else}} + {{$class = (print $class " isVerifiedUnmatched")}} + {{end}} + {{else if .Verification.Warning}} + {{$class = (print $class " isWarning")}} + {{end}} +{{end}} +
    +
    +

    {{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

    +
    + {{if .PageIsPullFiles}} +
    + + {{svg "octicon-chevron-left"}} {{ctx.Locale.Tr "repo.diff.commit.previous-short"}} + + + {{ctx.Locale.Tr "repo.diff.commit.next-short"}} {{svg "octicon-chevron-right"}} + +
    + {{else if not $.PageIsWiki}} + + {{ctx.Locale.Tr "repo.diff.browse_source"}} + + {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}}{{- /* */ -}} +
    + {{ctx.Locale.Tr "repo.commit.operations"}} + {{svg "octicon-triangle-down" 14 "dropdown icon"}} +
    +
    {{ctx.Locale.Tr "repo.commit.operations"}}
    +
    +
    + {{ctx.Locale.Tr "repo.branch.create_branch_operation"}} +
    +
    + {{ctx.Locale.Tr "repo.tag.create_tag_operation"}} +
    +
    {{ctx.Locale.Tr "repo.commit.revert"}}
    +
    {{ctx.Locale.Tr "repo.commit.cherry-pick"}}
    +
    +
    + +
    +
    +

    + {{template "repo/branch_dropdown" dict "root" . + "noTag" true "disableCreateBranch" true + "branchForm" "branch-dropdown-form" + "branchURLPrefix" (printf "%s/_cherrypick/%s/" $.RepoLink .CommitID) "branchURLSuffix" "" + "setAction" true "submitForm" true}} +
    + + +
    + + +
    +
    +
    +
    + {{ctx.Locale.Tr "repo.branch.new_branch"}} +
    +
    +
    + {{.CsrfTokenHtml}} +
    + +
    +
    + + +
    + +
    + + +
    + +
    +
    +
    +
    + {{ctx.Locale.Tr "repo.tag.create_tag_operation"}} +
    +
    +
    + {{.CsrfTokenHtml}} + +
    + +
    +
    + + +
    + +
    + + +
    + +
    +
    + {{if not .NoteRendered}} +
    + {{ctx.Locale.Tr "repo.diff.git-notes.add"}} +
    + {{end}} +
    +
    + {{end}} + {{end}} +
    +
    + {{if IsMultilineCommitMessage .Commit.Message}} +
    {{RenderCommitBody $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}
    + {{end}} + {{template "repo/commit_load_branches_and_tags" .}} +
    +
    +
    + {{if .Author}} + {{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}} + {{if .Author.FullName}} + {{.Author.FullName}} + {{else}} + {{.Commit.Author.Name}} + {{end}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}} + {{.Commit.Author.Name}} + {{end}} + {{DateUtils.TimeSince .Commit.Author.When}} + {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} + • + {{ctx.Locale.Tr "repo.diff.committed_by"}} + {{if ne .Verification.CommittingUser.ID 0}} + {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mr-2"}} + {{.Commit.Committer.Name}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}} + {{.Commit.Committer.Name}} + {{end}} + {{end}} +
    +
    + {{if .Parents}} +
    + {{ctx.Locale.Tr "repo.diff.parent"}} + {{range .Parents}} + {{if $.PageIsWiki}} + + {{ShortSha .}} + + {{else}} + + {{ShortSha .}} + + {{end}} + {{end}} +
    + {{end}} +
    + {{ctx.Locale.Tr "repo.diff.commit"}} + {{if .PageIsPullFiles}} + {{$commitShaLink := (printf "%s/commit/%s" $.RepoLink (PathEscape .CommitID))}} + + {{ShortSha .CommitID}} + + {{else}} + + {{ShortSha .CommitID}} + + {{end}} +
    +
    +
    +{{if .Commit.Signature}} +
    +
    + {{if .Verification.Verified}} + {{if ne .Verification.SigningUser.ID 0}} + {{svg "gitea-lock" 16 "tw-mr-2"}} + {{if eq .Verification.TrustStatus "trusted"}} + {{ctx.Locale.Tr "repo.commits.signed_by"}}: + {{else if eq .Verification.TrustStatus "untrusted"}} + {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: + {{else}} + {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: + {{end}} + {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}} + {{.Verification.SigningUser.GetDisplayName}} + {{else}} + {{svg "gitea-lock-cog" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.commits.signed_by"}}: + {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}} + {{.Verification.SigningUser.GetDisplayName}} + {{end}} + {{else}} + {{svg "gitea-unlock" 16 "tw-mr-2"}} + {{ctx.Locale.Tr .Verification.Reason}} + {{end}} +
    +
    + {{if .Verification.Verified}} + {{svg "octicon-verified" 16 "tw-mr-2"}} + {{if ne .Verification.SigningUser.ID 0}} + {{if .Verification.SigningSSHKey}} + {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: + {{.Verification.SigningSSHKey.Fingerprint}} + {{else}} + {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: + {{.Verification.SigningKey.PaddedKeyID}} + {{end}} + {{else}} + {{if .Verification.SigningSSHKey}} + {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: + {{.Verification.SigningSSHKey.Fingerprint}} + {{else}} + {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: + {{.Verification.SigningKey.PaddedKeyID}} + {{end}} + {{end}} + {{else if .Verification.Warning}} + {{svg "octicon-unverified" 16 "tw-mr-2"}} + {{if .Verification.SigningSSHKey}} + {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: + {{.Verification.SigningSSHKey.Fingerprint}} + {{else}} + {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: + {{.Verification.SigningKey.PaddedKeyID}} + {{end}} + {{else}} + {{if .Verification.SigningKey}} + {{if ne .Verification.SigningKey.KeyID ""}} + {{svg "octicon-verified" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: + {{.Verification.SigningKey.PaddedKeyID}} + {{end}} + {{end}} + {{if .Verification.SigningSSHKey}} + {{if ne .Verification.SigningSSHKey.Fingerprint ""}} + {{svg "octicon-verified" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: + {{.Verification.SigningSSHKey.Fingerprint}} + {{end}} + {{end}} + {{end}} +
    +
    +{{end}} +{{if .NoteRendered}} +
    + {{svg "octicon-note" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.diff.git-notes"}}: + {{if .NoteAuthor}} + + {{if .NoteAuthor.FullName}} + {{.NoteAuthor.FullName}} + {{else}} + {{.NoteCommit.Author.Name}} + {{end}} + + {{else}} + {{.NoteCommit.Author.Name}} + {{end}} + {{DateUtils.TimeSince .NoteCommit.Author.When}} + {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} +
    + + +
    +
    +
    + {{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}} +
    +
    +

    {{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}

    +
    +
    + {{.CsrfTokenHtml}} + + + +
    +
    +
    + {{end}} +
    +
    +
    {{.NoteRendered | SanitizeHTML}}
    +
    + {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} +
    +
    + {{.CsrfTokenHtml}} + +
    + +
    + +
    + +
    + +
    + {{end}} +{{else if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} +
    +
    + {{.CsrfTokenHtml}} + +
    + +
    + +
    + +
    + +
    +{{end}} + diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl index ffa0e530e8..25402ca2f4 100644 --- a/templates/repo/commit_load_branches_and_tags.tmpl +++ b/templates/repo/commit_load_branches_and_tags.tmpl @@ -1,4 +1,4 @@ -{{if not .PageIsWiki}} +{{if not (or .PageIsWiki .PageIsPullFiles)}}
    - -
    - -
    -
    - {{ctx.Locale.Tr "repo.branch.new_branch"}} -
    -
    -
    - {{.CsrfTokenHtml}} -
    - -
    -
    - - -
    - -
    - - -
    - -
    -
    -
    -
    - {{ctx.Locale.Tr "repo.tag.create_tag_operation"}} -
    -
    -
    - {{.CsrfTokenHtml}} - -
    - -
    -
    - - -
    - -
    - - -
    - -
    -
    - {{if not .NoteRendered}} -
    - {{ctx.Locale.Tr "repo.diff.git-notes.add"}} -
    - {{end}} - - - {{end}} - - {{end}} - - {{if IsMultilineCommitMessage .Commit.Message}} -
    {{RenderCommitBody $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}
    - {{end}} - {{template "repo/commit_load_branches_and_tags" .}} - -
    -
    - {{if .Author}} - {{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}} - {{if .Author.FullName}} - {{.Author.FullName}} - {{else}} - {{.Commit.Author.Name}} - {{end}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}} - {{.Commit.Author.Name}} - {{end}} - {{DateUtils.TimeSince .Commit.Author.When}} - {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} - • - {{ctx.Locale.Tr "repo.diff.committed_by"}} - {{if ne .Verification.CommittingUser.ID 0}} - {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mr-2"}} - {{.Commit.Committer.Name}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}} - {{.Commit.Committer.Name}} - {{end}} - {{end}} -
    -
    - {{if .Parents}} -
    - {{ctx.Locale.Tr "repo.diff.parent"}} - {{range .Parents}} - {{if $.PageIsWiki}} - {{ShortSha .}} - {{else}} - {{ShortSha .}} - {{end}} - {{end}} -
    - {{end}} -
    - {{ctx.Locale.Tr "repo.diff.commit"}} - {{ShortSha .CommitID}} -
    -
    -
    - {{if .Commit.Signature}} -
    -
    - {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "gitea-lock" 16 "tw-mr-2"}} - {{if eq .Verification.TrustStatus "trusted"}} - {{ctx.Locale.Tr "repo.commits.signed_by"}}: - {{else if eq .Verification.TrustStatus "untrusted"}} - {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: - {{else}} - {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: - {{end}} - {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}} - {{.Verification.SigningUser.GetDisplayName}} - {{else}} - {{svg "gitea-lock-cog" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.signed_by"}}: - {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}} - {{.Verification.SigningUser.GetDisplayName}} - {{end}} - {{else}} - {{svg "gitea-unlock" 16 "tw-mr-2"}} - {{ctx.Locale.Tr .Verification.Reason}} - {{end}} -
    -
    - {{if .Verification.Verified}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{if ne .Verification.SigningUser.ID 0}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{else if .Verification.Warning}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{if .Verification.SigningKey}} - {{if ne .Verification.SigningKey.KeyID ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{if .Verification.SigningSSHKey}} - {{if ne .Verification.SigningSSHKey.Fingerprint ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{end}} - {{end}} - {{end}} -
    -
    - {{end}} - {{if .NoteRendered}} -
    - {{svg "octicon-note" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.diff.git-notes"}}: - {{if .NoteAuthor}} - - {{if .NoteAuthor.FullName}} - {{.NoteAuthor.FullName}} - {{else}} - {{.NoteCommit.Author.Name}} - {{end}} - - {{else}} - {{.NoteCommit.Author.Name}} - {{end}} - {{DateUtils.TimeSince .NoteCommit.Author.When}} - {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} -
    - - -
    -
    -
    - {{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}} -
    -
    -

    {{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}

    -
    -
    - {{.CsrfTokenHtml}} - - - -
    -
    -
    - {{end}} -
    -
    -
    {{.NoteRendered | SanitizeHTML}}
    -
    - {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} -
    -
    - {{.CsrfTokenHtml}} - -
    - -
    - -
    - -
    - -
    - {{end}} - {{else if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} -
    -
    - {{.CsrfTokenHtml}} - -
    - -
    - -
    - -
    - -
    - {{end}} + {{template "repo/commit_header" .}} {{template "repo/diff/box" .}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 0e64a1ab1f..69837bfc1a 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -49,6 +49,9 @@ {{.Summary | RenderEmoji $.Context}} {{else}} {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} + {{if $.PageIsPullCommits}} + {{$commitLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} + {{end}} {{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}} {{end}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 961d4c9ce1..6efc812e8f 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -11,7 +11,14 @@ {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} {{end}} - {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} + {{$commitLink := printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} + + {{/* Only link those commits in the file diff view that can actually be reviewed there. + A force push or other rewriting of history will cause a commit to not be + part of the pull request anymore. */}} + {{if index $.root.CommitIDs .ID.String}} + {{$commitLink = printf "%s/pulls/%d/commits/%s" $.comment.Issue.PullRequest.BaseRepo.Link $.comment.Issue.Index (PathEscape .ID.String)}} + {{end}} {{- RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 7249becbab..621fc44bf5 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -10,9 +10,13 @@ {{if .IsDiffCompare}}
    - {{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}} + + {{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}} + ... - {{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}} + + {{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}} +
    {{end}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 7ee8587435..7c07f80c86 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -8,42 +8,48 @@ {{ctx.Locale.Tr "new_repo.title"}}
    - {{template "base/alert" .}} - {{template "repo/create_helper" .}} + {{if or .CanCreateRepo .Orgs}} + {{template "base/alert" .}} + {{template "repo/create_helper" .}} - {{if not .CanCreateRepo}} + {{if and (not .CanCreateRepo) (ne .MaxCreationLimit 0)}} +
    +

    {{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}

    +
    + {{end}} +
    + {{template "repo/create_basic" .}} +
    + +
    + + {{ctx.Locale.Tr "repo.new_from_template"}} + {{ctx.Locale.Tr "repo.new_from_template_description"}} + + {{template "repo/create_from_template" .}} +
    + +
    +
    + {{ctx.Locale.Tr "repo.auto_init"}} + {{template "repo/create_init" .}} +
    + +
    + {{ctx.Locale.Tr "repo.new_advanced"}} +
    {{ctx.Locale.Tr "repo.new_advanced_expand"}} + {{template "repo/create_advanced" .}} +
    +
    +
    + + {{else}}
    -

    {{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}

    + {{ctx.Locale.Tr "repo.form.cannot_create"}}
    {{end}} -
    - {{template "repo/create_basic" .}} -
    - -
    - - {{ctx.Locale.Tr "repo.new_from_template"}} - {{ctx.Locale.Tr "repo.new_from_template_description"}} - - {{template "repo/create_from_template" .}} -
    - -
    -
    - {{ctx.Locale.Tr "repo.auto_init"}} - {{template "repo/create_init" .}} -
    - -
    - {{ctx.Locale.Tr "repo.new_advanced"}} -
    {{ctx.Locale.Tr "repo.new_advanced_expand"}} - {{template "repo/create_advanced" .}} -
    -
    -
    -
    diff --git a/templates/repo/create_basic.tmpl b/templates/repo/create_basic.tmpl index 0396629fef..90545c2769 100644 --- a/templates/repo/create_basic.tmpl +++ b/templates/repo/create_basic.tmpl @@ -2,17 +2,27 @@ {{ctx.Locale.Tr "repo.owner"}}
    {{/* uid id is used by the repo-template code */}} - - - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - {{.ContextUser.ShortName 40}} - + {{if .CanCreateRepo}} + + + {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} + {{.ContextUser.ShortName 40}} + + {{else if .Orgs}} + + + {{ctx.AvatarUtils.Avatar (index .Orgs 0) 28 "mini"}} + {{(index .Orgs 0).ShortName 40}} + + {{end}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
    -
    - {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} - {{.SignedUser.ShortName 40}} -
    + {{if .CanCreateRepo}} +
    + {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} + {{.SignedUser.ShortName 40}} +
    + {{end}} {{range .Orgs}}
    {{ctx.AvatarUtils.Avatar . 28 "mini"}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index e24c880746..f3c0c0989d 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -53,7 +53,7 @@ {{if not .DiffNotAvailable}} {{if and .IsShowingOnlySingleCommit .PageIsPullFiles}}
    -
    {{ctx.Locale.Tr "repo.pulls.showing_only_single_commit" (ShortSha .AfterCommitID)}} - {{ctx.Locale.Tr "repo.pulls.show_all_commits"}}
    +
    {{ctx.Locale.Tr "repo.pulls.showing_only_single_commit" (ShortSha .CommitID)}} - {{ctx.Locale.Tr "repo.pulls.show_all_commits"}}
    {{else if and (not .IsShowingAllCommits) .PageIsPullFiles}}
    @@ -93,6 +93,12 @@ if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden'); {{end}} +
    + {{if .IsShowingOnlySingleCommit}} +
    + {{template "repo/commit_header" .}} +
    + {{end}} {{if .DiffNotAvailable}}

    {{ctx.Locale.Tr "repo.diff.data_not_available"}}

    {{else}} @@ -230,6 +236,7 @@ {{end}}
    {{end}} +
    {{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}} diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl index 13d09babe1..ded7a6c5fc 100644 --- a/templates/repo/diff/new_review.tmpl +++ b/templates/repo/diff/new_review.tmpl @@ -1,17 +1,14 @@
    -
    - {{if $.IsShowingAllCommits}}
    @@ -55,5 +52,4 @@
    - {{end}}
    diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index f9c9eef5aa..49b210f75c 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -11,7 +11,7 @@
    {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}} - {{$shalink := HTMLFormat `%s` $shaurl (ShortSha .SHA)}} + {{$shalink := HTMLFormat `%s` $shaurl (ShortSha .SHA)}} {{if eq .CherryPickType "revert"}} {{ctx.Locale.Tr "repo.editor.revert" $shalink}} {{else}} diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 8646562ca8..6d2f441793 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -4,7 +4,7 @@ {{if $attachments}}
    {{range $attachments}} - {{.Name}} + {{.Name}} {{end}}
    {{end}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index ae50ac4c46..84ba6e5358 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -127,7 +127,7 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}}
    - {{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}} + {{ctx.Locale.Tr "repo.issues.filter_type.all_pull_requests"}} {{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}} {{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}} {{if .PageIsPullList}} diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index 17abe263e7..bdbb9a9daf 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -8,6 +8,7 @@ + {{end}} {{if .PageIsPullList}} {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl index 79085df3ab..8b5094771a 100644 --- a/templates/repo/issue/view_content/attachments.tmpl +++ b/templates/repo/issue/view_content/attachments.tmpl @@ -31,7 +31,7 @@ {{if FilenameIsImage .Name}} {{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}} - {{.Name}} + {{.Name}} {{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 9eb9307f9f..3bc4cd0773 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -1,7 +1,7 @@ {{template "base/alert"}} {{range .Issue.Comments}} {{if call $.ShouldShowCommentType .Type}} - {{$createdStr:= DateUtils.TimeSince .CreatedUnix}} + {{$createdStr := HTMLFormat `%s` .EventTag .HashTag (DateUtils.TimeSince .CreatedUnix)}} +
    + {{ctx.Locale.Tr "repo.settings.event_header_action"}} + + + + + + +
    diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 35fc5e7d1d..d70bf399a5 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -364,7 +364,7 @@ the click will succeed, but the depending interaction won't, although playwright repeatedly tries to find the content. -You can [group statements using toPass]()https://playwright.dev/docs/test-assertions#expecttopass). +You can [group statements using toPass](https://playwright.dev/docs/test-assertions#expecttopass). This code retries the dropdown click until the second item is found. ~~~js diff --git a/tests/e2e/changes.go b/tests/e2e/changes.go index d1d318fd06..5e9238dfa7 100644 --- a/tests/e2e/changes.go +++ b/tests/e2e/changes.go @@ -16,10 +16,15 @@ import ( var ( changesetFiles []string changesetAvailable bool - globalFullRun bool + globalFullRun = false ) func initChangedFiles() { + _, globalFullRun = os.LookupEnv("RUN_ALL") + if globalFullRun { + log.Info("Full run of all tests requested via RUN_ALL environment.") + return + } var changes string changes, changesetAvailable = os.LookupEnv("CHANGED_FILES") // the output of the Action seems to actually contain \n and not a newline literal @@ -44,7 +49,7 @@ func initChangedFiles() { for _, expr := range globalPatterns { fullRunPatterns = append(fullRunPatterns, glob.MustCompile(expr, '.', '/')) } - globalFullRun = false + for _, changedFile := range changesetFiles { for _, pattern := range fullRunPatterns { if pattern.Match(changedFile) { diff --git a/tests/e2e/declare_repos_test.go b/tests/e2e/declare_repos_test.go index f45687651c..93f69faf4c 100644 --- a/tests/e2e/declare_repos_test.go +++ b/tests/e2e/declare_repos_test.go @@ -9,16 +9,23 @@ import ( "testing" "time" + "forgejo.org/models/db" + issues_model "forgejo.org/models/issues" + repo_model "forgejo.org/models/repo" unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/indexer/stats" + "forgejo.org/modules/optional" + "forgejo.org/modules/timeutil" + issue_service "forgejo.org/services/issue" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "xorm.io/xorm/convert" ) // first entry represents filename @@ -29,19 +36,34 @@ type FileChanges struct { Versions []string } +// performs additional repo setup as needed +type SetupRepo func(*user_model.User, *repo_model.Repository) + // put your Git repo declarations in here // feel free to amend the helper function below or use the raw variant directly func DeclareGitRepos(t *testing.T) func() { + now := timeutil.TimeStampNow() + postIssue := func(repo *repo_model.Repository, user *user_model.User, age int64, title, content string) { + issue := &issues_model.Issue{ + RepoID: repo.ID, + PosterID: user.ID, + Title: title, + Content: content, + CreatedUnix: now.Add(-age), + } + require.NoError(t, issue_service.NewIssue(db.DefaultContext, repo, issue, nil, nil, nil)) + } + cleanupFunctions := []func(){ - newRepo(t, 2, "diff-test", []FileChanges{{ + newRepo(t, 2, "diff-test", nil, []FileChanges{{ Filename: "testfile", Versions: []string{"hello", "hallo", "hola", "native", "ubuntu-latest", "- runs-on: ubuntu-latest", "- runs-on: debian-latest"}, - }}), - newRepo(t, 2, "language-stats-test", []FileChanges{{ + }}, nil), + newRepo(t, 2, "language-stats-test", nil, []FileChanges{{ Filename: "main.rs", Versions: []string{"fn main() {", "println!(\"Hello World!\");", "}"}, - }}), - newRepo(t, 2, "mentions-highlighted", []FileChanges{ + }}, nil), + newRepo(t, 2, "mentions-highlighted", nil, []FileChanges{ { Filename: "history1.md", Versions: []string{""}, @@ -52,6 +74,33 @@ func DeclareGitRepos(t *testing.T) func() { Versions: []string{""}, CommitMsg: "Another commit which mentions @user1 in the title\nand @user2 in the text", }, + }, nil), + newRepo(t, 2, "unicode-escaping", nil, []FileChanges{{ + Filename: "a-file", + Versions: []string{"{a}{а}"}, + }}, nil), + newRepo(t, 11, "dependency-test", &tests.DeclarativeRepoOptions{ + UnitConfig: optional.Some(map[unit_model.Type]convert.Conversion{ + unit_model.TypeIssues: &repo_model.IssuesConfig{ + EnableDependencies: true, + }, + }), + }, []FileChanges{}, func(user *user_model.User, repo *repo_model.Repository) { + postIssue(repo, user, 500, "first issue here", "an issue created earlier") + postIssue(repo, user, 400, "second issue here (not 1)", "not the right issue, but in the right repo") + postIssue(repo, user, 300, "third issue here", "depends on things") + postIssue(repo, user, 200, "unrelated issue", "shrug emoji") + postIssue(repo, user, 100, "newest issue", "very new") + }), + newRepo(t, 11, "dependency-test-2", &tests.DeclarativeRepoOptions{ + UnitConfig: optional.Some(map[unit_model.Type]convert.Conversion{ + unit_model.TypeIssues: &repo_model.IssuesConfig{ + EnableDependencies: true, + }, + }), + }, []FileChanges{}, func(user *user_model.User, repo *repo_model.Repository) { + postIssue(repo, user, 450, "right issue", "an issue containing word right") + postIssue(repo, user, 150, "left issue", "an issue containing word left") }), // add your repo declarations here } @@ -63,13 +112,20 @@ func DeclareGitRepos(t *testing.T) func() { } } -func newRepo(t *testing.T, userID int64, repoName string, fileChanges []FileChanges) func() { +func newRepo(t *testing.T, userID int64, repoName string, initOpts *tests.DeclarativeRepoOptions, fileChanges []FileChanges, setup SetupRepo) func() { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) - somerepo, _, cleanupFunc := tests.CreateDeclarativeRepo(t, user, repoName, - []unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues}, nil, - nil, - ) + opts := tests.DeclarativeRepoOptions{} + if initOpts != nil { + opts = *initOpts + } + opts.Name = optional.Some(repoName) + if !opts.EnabledUnits.Has() { + opts.EnabledUnits = optional.Some([]unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues}) + } + somerepo, _, cleanupFunc := tests.CreateDeclarativeRepoWithOptions(t, user, opts) + + var lastCommitID string for _, file := range fileChanges { for i, version := range file.Versions { operation := "update" @@ -104,12 +160,19 @@ func newRepo(t *testing.T, userID int64, repoName string, fileChanges []FileChan Author: time.Now(), Committer: time.Now(), }, + LastCommitID: lastCommitID, }) require.NoError(t, err) assert.NotEmpty(t, resp) + + lastCommitID = resp.Commit.SHA } } + if setup != nil { + setup(user, somerepo) + } + err := stats.UpdateRepoIndexer(somerepo) require.NoError(t, err) diff --git a/tests/e2e/dimmer.test.e2e.ts b/tests/e2e/dimmer.test.e2e.ts index 9ee6f82c07..48084b0e52 100644 --- a/tests/e2e/dimmer.test.e2e.ts +++ b/tests/e2e/dimmer.test.e2e.ts @@ -12,12 +12,13 @@ test.use({user: 'user2'}); test('Dimmed modal', async ({page}) => { await page.goto('/user1'); - await expect(page.locator('.block')).toContainText('Block'); + await expect(page.locator('#action-block')).toContainText('Block'); // Ensure the modal is hidden await expect(page.locator('#block-user')).toBeHidden(); - await page.locator('.block').click(); + await page.locator('.actions .dropdown').click(); + await page.locator('#action-block').click(); // Modal and dimmer should be visible. await expect(page.locator('#block-user')).toBeVisible(); @@ -31,7 +32,8 @@ test('Dimmed modal', async ({page}) => { await save_visual(page); // Open the block modal and make the dimmer visible again. - await page.locator('.block').click(); + await page.locator('.actions .dropdown').click(); + await page.locator('#action-block').click(); await expect(page.locator('#block-user')).toBeVisible(); await expect(page.locator('.ui.dimmer')).toBeVisible(); await expect(page.locator('.ui.dimmer')).toHaveCount(1); diff --git a/tests/e2e/dropdown.test.e2e.ts b/tests/e2e/dropdown.test.e2e.ts new file mode 100644 index 0000000000..5f226f94bb --- /dev/null +++ b/tests/e2e/dropdown.test.e2e.ts @@ -0,0 +1,106 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// templates/shared/user/** +// web_src/js/modules/dropdown.ts +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +test('JS enhanced', async ({page}) => { + await page.goto('/user1'); + + await expect(page.locator('body')).not.toContainClass('no-js'); + const nojsNotice = page.locator('body .full noscript'); + await expect(nojsNotice).toBeHidden(); + + // Open and close by clicking summary + const dropdownSummary = page.locator('details.dropdown summary'); + const dropdownContent = page.locator('details.dropdown ul'); + await expect(dropdownContent).toBeHidden(); + await dropdownSummary.click(); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.click(); + await expect(dropdownContent).toBeHidden(); + + // Close by clicking elsewhere + const elsewhere = page.locator('.username'); + await expect(dropdownContent).toBeHidden(); + await dropdownSummary.click(); + await expect(dropdownContent).toBeVisible(); + await elsewhere.click(); + await expect(dropdownContent).toBeHidden(); + + // Open and close with keypressing + await dropdownSummary.focus(); + await dropdownSummary.press(`Enter`); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.press(`Space`); + await expect(dropdownContent).toBeHidden(); + + await dropdownSummary.press(`Space`); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.press(`Enter`); + await expect(dropdownContent).toBeHidden(); + + await dropdownSummary.press(`Enter`); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.press(`Escape`); + await expect(dropdownContent).toBeHidden(); + + // Open and close by opening a different dropdown + const languageMenu = page.locator('.language-menu'); + await dropdownSummary.click(); + await expect(dropdownContent).toBeVisible(); + await expect(languageMenu).toBeHidden(); + await page.locator('.language.dropdown').click(); + await expect(dropdownContent).toBeHidden(); + await expect(languageMenu).toBeVisible(); +}); + +test('No JS', async ({browser}) => { + const context = await browser.newContext({javaScriptEnabled: false}); + const nojsPage = await context.newPage(); + await nojsPage.goto('/user1'); + + const nojsNotice = nojsPage.locator('body .full noscript'); + await expect(nojsNotice).toBeVisible(); + await expect(nojsPage.locator('body')).toContainClass('no-js'); + + // Open and close by clicking summary + const dropdownSummary = nojsPage.locator('details.dropdown summary'); + const dropdownContent = nojsPage.locator('details.dropdown ul'); + await expect(dropdownContent).toBeHidden(); + await dropdownSummary.click(); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.click(); + await expect(dropdownContent).toBeHidden(); + + // Close by clicking elsewhere (by hitting ::before with increased z-index) + const elsewhere = nojsPage.locator('#navbar'); + await expect(dropdownContent).toBeHidden(); + await dropdownSummary.click(); + await expect(dropdownContent).toBeVisible(); + // eslint-disable-next-line playwright/no-force-option + await elsewhere.click({force: true}); + await expect(dropdownContent).toBeHidden(); + + // Open and close with keypressing + await dropdownSummary.press(`Enter`); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.press(`Space`); + await expect(dropdownContent).toBeHidden(); + + await dropdownSummary.press(`Space`); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.press(`Enter`); + await expect(dropdownContent).toBeHidden(); + + // Escape is not usable w/o JS enhancements + await dropdownSummary.press(`Enter`); + await expect(dropdownContent).toBeVisible(); + await dropdownSummary.press(`Escape`); + await expect(dropdownContent).toBeVisible(); +}); diff --git a/tests/e2e/fixtures/repository.yml b/tests/e2e/fixtures/repository.yml index ac87721d6a..6afbd138e8 100644 --- a/tests/e2e/fixtures/repository.yml +++ b/tests/e2e/fixtures/repository.yml @@ -10,3 +10,4 @@ is_private: true status: 0 lfs_size: 8192 + topics: '[]' diff --git a/tests/e2e/issue-sidebar.test.e2e.ts b/tests/e2e/issue-sidebar.test.e2e.ts index fe2a6cec87..34885d0d5d 100644 --- a/tests/e2e/issue-sidebar.test.e2e.ts +++ b/tests/e2e/issue-sidebar.test.e2e.ts @@ -94,7 +94,7 @@ test.describe('Pull: Toggle WIP', () => { test('Issue: Labels', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - async function submitLabels({page}: { page: Page }) { + async function submitLabels({page}: {page: Page}) { const submitted = page.waitForResponse('/user2/repo1/issues/labels'); await page.locator('textarea').first().click(); // close via unrelated element await submitted; @@ -262,3 +262,91 @@ test('New Issue: Milestone', async ({page}, workerInfo) => { await expect(selectedMilestone).toContainText('No milestone'); await save_visual(page); }); + +test.describe('Dependency dropdown', () => { + test.use({user: 'user11'}); + test('Issue: Dependencies', async ({page}) => { + const response = await page.goto('/user11/dependency-test/issues/3'); + expect(response?.status()).toBe(200); + + const depsBlock = page.locator('.issue-content-right .depending'); + const deleteDepBtn = page.locator('.issue-content-right .depending .delete-dependency-button'); + + const input = page.locator('#new-dependency-drop-list .search'); + const current = page.locator('#new-dependency-drop-list .text').first(); + const menu = page.locator('#new-dependency-drop-list .menu'); + const items = page.locator('#new-dependency-drop-list .menu .item'); + + const confirmDelete = async () => { + const modal = page.locator('.modal.remove-dependency'); + await expect(modal).toBeVisible(); + await expect(modal).toContainText('This will remove the dependency from this issue'); + await modal.locator('button.ok').click(); + }; + + // A kludge to set the dropdown to the *wrong* value so it lets us select the correct one next. + const resetDropdown = async () => { + if (await current.textContent().then((s) => s.includes('#4'))) return; + await input.click(); + await input.fill('unrelated'); + await expect(items.first()).toContainText('unrelated'); + await items.first().click(); + await expect(current).toContainText('#4'); + await input.click(); + }; + + await expect(depsBlock).toBeVisible(); + while (await deleteDepBtn.first().isVisible()) { + await deleteDepBtn.first().click(); // wipe added dependencies from any previously failed tests + await confirmDelete(); + } + await expect(depsBlock).toContainText('No dependencies set'); + + await input.scrollIntoViewIfNeeded(); + await input.click(); + + const first = 'first issue here'; + const second = 'second issue here'; + const newest = 'newest issue'; + + // Without query, it should show issues in the same repo, sorted by date, except current one. + await expect(menu).toBeVisible(); + await expect(items).toHaveCount(4); // 5 issues in this repo, minus current one + await expect(items.first()).toContainText(newest); + await expect(items.last()).toContainText(first); + await resetDropdown(); + + // With query, it should search all repos, but show current repo issues first. + await input.fill('right'); + await expect(items.first()).toContainText(second); + await expect.poll(() => items.count()).toBeGreaterThan(1); // there is an issue in user11/dependency-test-2 containing the word "right" + await resetDropdown(); + + // When entering an issue number, it should always show that one first, then all text matches. + await input.fill('1'); + await expect(items.first()).toContainText(first); + await expect(items.nth(1)).toBeVisible(); + await resetDropdown(); + + // Should behave the same with a prefix + await input.fill('#1'); + await expect(items.first()).toContainText(first); + + // Selecting an issue + await items.first().click(); + await expect(current).toContainText(first); + + // Add dependency + const link = page.locator('.issue-content-right .depending .dependency a.title'); + await page.locator('.issue-content-right .depending button').click(); + await expect(link).toHaveAttribute('href', '/user11/dependency-test/issues/1'); + + // Remove dependency + await expect(deleteDepBtn).toBeVisible(); + await deleteDepBtn.click(); + + await confirmDelete(); + + await expect(depsBlock).toContainText('No dependencies set'); + }); +}); diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index c69c9a7f0c..2b5f0d80a0 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -39,7 +39,7 @@ test('Markdown image preview behaviour', async ({page}, workerInfo) => { await save_visual(page); }); -test('Markdown indentation', async ({page}) => { +test('Markdown indentation via toolbar', async ({page}) => { const initText = `* first\n* second\n* third\n* last`; const response = await page.goto('/user2/repo1/issues/new'); @@ -50,7 +50,6 @@ test('Markdown indentation', async ({page}) => { const indent = page.locator('button[data-md-action="indent"]'); const unindent = page.locator('button[data-md-action="unindent"]'); await textarea.fill(initText); - await textarea.click(); // Tab handling is disabled until pointer event or input. // Indent, then unindent first line await textarea.focus(); @@ -109,6 +108,146 @@ test('Markdown indentation', async ({page}) => { await expect(textarea).toHaveValue(initText); }); +test('markdown indentation with Tab', async ({page}) => { + const initText = `* first\n* second\n* third\n* last`; + + const response = await page.goto('/user2/repo1/issues/new'); + expect(response?.status()).toBe(200); + + const textarea = page.locator('textarea[name=content]'); + const toast = page.locator('.toastify'); + const tab = ' '; + + await textarea.fill(initText); + + await textarea.click(); // Tab handling is disabled until pointer event or input. + + // Indent, then unindent first line + await textarea.focus(); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0)); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`); + await textarea.press('Shift+Tab'); + await expect(textarea).toHaveValue(initText); + + // Attempt unindent again, ensure focus is not immediately lost and toast is shown, but then focus is lost on next attempt. + await expect(toast).toBeHidden(); // toast should not already be there + await textarea.press('Shift+Tab'); + await expect(textarea).toBeFocused(); + await expect(toast).toBeVisible(); + await textarea.press('Shift+Tab'); + await expect(textarea).not.toBeFocused(); + + // Indent lines 2-4 + await textarea.click(); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('\n') + 1, it.value.length)); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}* third\n${tab}* last`); + + // Indent second line while in whitespace, then unindent. + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf(' * third'), it.value.indexOf(' * third'))); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}${tab}* third\n${tab}* last`); + await textarea.press('Shift+Tab'); + await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}* third\n${tab}* last`); + + // Select all and unindent, then lose focus. + await textarea.evaluate((it:HTMLTextAreaElement) => it.select()); + await textarea.press('Shift+Tab'); // Everything is unindented. + await expect(textarea).toHaveValue(initText); + await textarea.press('Shift+Tab'); // Valid, but nothing happens -> switch to "about to lose focus" state. + await expect(textarea).toBeFocused(); + await textarea.press('Shift+Tab'); + await expect(textarea).not.toBeFocused(); + + // Attempt the same with cursor within list element body. + await textarea.focus(); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0)); + await textarea.press('ArrowRight'); + await textarea.press('ArrowRight'); + await textarea.press('Tab'); + // Whole line should be indented. + await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`); + await textarea.press('Shift+Tab'); + + // Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text + const line3 = `* first\n* second\n${tab}* third\n* last`; + const lines23 = `* first\n${tab}* second\n${tab}${tab}* third\n* last`; + await textarea.focus(); + await textarea.fill(line3); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird'))); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(lines23); + await expect(textarea).toHaveJSProperty('selectionStart', lines23.indexOf('cond')); + await expect(textarea).toHaveJSProperty('selectionEnd', lines23.indexOf('hird')); + + // Then unindent twice, erasing all indents. + await textarea.press('Shift+Tab'); + await expect(textarea).toHaveValue(line3); + await textarea.press('Shift+Tab'); + await expect(textarea).toHaveValue(initText); + + // Check that partial indents are cleared + await textarea.focus(); + await textarea.fill(initText); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second'))); + await textarea.pressSequentially(' '); + await textarea.press('Shift+Tab'); + await expect(textarea).toHaveValue(initText); +}); + +test('markdown block quote indentation', async ({page}) => { + const initText = `> first\n> second\n> third\n> last`; + + const response = await page.goto('/user2/repo1/issues/new'); + expect(response?.status()).toBe(200); + + const textarea = page.locator('textarea[name=content]'); + const toast = page.locator('.toastify'); + + await textarea.fill(initText); + + await textarea.click(); // Tab handling is disabled until pointer event or input. + + // Indent, then unindent first line twice (quotes can quote quotes!) + await textarea.focus(); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0)); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`> > first\n> second\n> third\n> last`); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`> > > first\n> second\n> third\n> last`); + await textarea.press('Shift+Tab'); + await textarea.press('Shift+Tab'); + await expect(textarea).toHaveValue(initText); + + // Attempt unindent again. + await expect(toast).toBeHidden(); // toast should not already be there + await textarea.press('Shift+Tab'); + // Nothing happens - quote should not stop being a quote + await expect(textarea).toHaveValue(initText); + // Focus is not immediately lost and toast is shown, + await expect(textarea).toBeFocused(); + await expect(toast).toBeVisible(); + // Focus is lost on next attempt, + await textarea.press('Shift+Tab'); + await expect(textarea).not.toBeFocused(); + + // Indent lines 2-4 + await textarea.click(); + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('\n') + 1, it.value.length)); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`> first\n> > second\n> > third\n> > last`); + + // Select all and unindent, then lose focus. + await textarea.evaluate((it:HTMLTextAreaElement) => it.select()); + await textarea.press('Shift+Tab'); // Everything is unindented. + await expect(textarea).toHaveValue(initText); + await textarea.press('Shift+Tab'); // Valid, but nothing happens -> switch to "about to lose focus" state. + await expect(textarea).toBeFocused(); + await textarea.press('Shift+Tab'); + await expect(textarea).not.toBeFocused(); +}); + test('Markdown list continuation', async ({page}) => { const initText = `* first\n* second`; diff --git a/tests/e2e/org-teams-overview.test.e2e.ts b/tests/e2e/org-teams-overview.test.e2e.ts new file mode 100644 index 0000000000..c3006568cd --- /dev/null +++ b/tests/e2e/org-teams-overview.test.e2e.ts @@ -0,0 +1,116 @@ +// @watch start +// templates/org/team/sidebar.tmpl +// @watch end +/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["assertPermissionsDetails", "assertRestrictedAccess", "assertOwnerPermissions"] }] */ +import {expect, type Page} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +type Permission = 'No access' | 'Write' | 'Read'; + +const UNIT_VALUES = [ + 'Code', + 'Issues', + 'Pull requests', + 'Releases', + 'Wiki', + 'External Wiki', + 'External issues', + 'Projects', + 'Packages', + 'Actions', +] as const; + +type Unit = typeof UNIT_VALUES[number]; + +const assertPermission = async (page: Page, name: Unit, permission: Permission) => { + await expect.soft(page.getByRole('row', {name}).getByRole('cell').nth(1)).toHaveText(permission); +}; + +const testTeamUrl = '/org/org17/teams/test_team'; +const reviewTeamUrl = '/org/org17/teams/review_team'; +const ownersUrl = '/org/org17/teams/owners'; +const adminUrl = '/org/org17/teams/super-user'; + +const cases: Record = { + [testTeamUrl]: {write: ['Issues']}, + [reviewTeamUrl]: {read: ['Code']}, +}; + +const assertOwnerPermissions = async (page: Page, code: number = 200) => { + const response = await page.goto(ownersUrl); + expect(response?.status()).toBe(code); + + await expect(page.getByText('Owners have full access to all repositories and have administrator access to the organization.')).toBeVisible(); +}; + +const assertAdminPermissions = async (page: Page, code: number = 200) => { + const response = await page.goto(adminUrl); + expect(response?.status()).toBe(code); + + await expect(page.getByText('This team grants Administrator access: members can read from, push to and add collaborators to team repositories.')).toBeVisible(); +}; + +const assertRestrictedAccess = async (page: Page, ...urls: string[]) => { + for (const url of urls) { + expect((await page.goto(url))?.status(), 'should not see any details').toBe(404); + } +}; + +const assertPermissionsDetails = async (page: Page, url: (keyof typeof cases)) => { + const response = await page.goto(url); + expect(response?.status()).toBe(200); + + const per = cases[url]; + + for (const unit of UNIT_VALUES) { + if (per.read?.includes(unit)) { + await assertPermission(page, unit, 'Read'); + } else if (per.write?.includes(unit)) { + await assertPermission(page, unit, 'Write'); + } else { + await assertPermission(page, unit, 'No access'); + } + } +}; + +test.describe('Orga team overview', () => { + test.describe('admin', () => { + test.use({user: 'user1'}); + + test('should see all', async ({page}) => { + await assertPermissionsDetails(page, testTeamUrl); + await assertPermissionsDetails(page, reviewTeamUrl); + await assertOwnerPermissions(page); + await assertAdminPermissions(page); + }); + }); + + test.describe('owner', () => { + test.use({user: 'user18'}); + + test('should see all', async ({page}) => { + await assertPermissionsDetails(page, testTeamUrl); + await assertPermissionsDetails(page, reviewTeamUrl); + await assertOwnerPermissions(page); + await assertAdminPermissions(page); + }); + }); + + test.describe('reviewer team', () => { + test.use({user: 'user29'}); + + test('should only see permissions for `reviewer team` and restricted access to other resources', async ({page}) => { + await assertPermissionsDetails(page, reviewTeamUrl); + await assertRestrictedAccess(page, ownersUrl, testTeamUrl, adminUrl); + }); + }); + + test.describe('test_team', () => { + test.use({user: 'user2'}); + + test('should only see permissions for test_team and restricted access to other resources', async ({page}) => { + await assertPermissionsDetails(page, testTeamUrl); + await assertRestrictedAccess(page, ownersUrl, reviewTeamUrl, adminUrl); + }); + }); +}); diff --git a/tests/e2e/pr-review.test.e2e.ts b/tests/e2e/pr-review.test.e2e.ts index b619cdbcd1..577c8bf20a 100644 --- a/tests/e2e/pr-review.test.e2e.ts +++ b/tests/e2e/pr-review.test.e2e.ts @@ -11,7 +11,7 @@ import {save_visual, test} from './utils_e2e.ts'; test.use({user: 'user2'}); -test('PR: Finish review', async ({page}) => { +test('PR: Create review from files', async ({page}) => { const response = await page.goto('/user2/repo1/pulls/5/files'); expect(response?.status()).toBe(200); @@ -22,4 +22,93 @@ test('PR: Finish review', async ({page}) => { await page.locator('#review-box .js-btn-review').click(); await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible(); await save_visual(page); + + await page.locator('.review-box-panel textarea#_combo_markdown_editor_0') + .fill('This is a review'); + await page.locator('.review-box-panel button.btn-submit[value="approve"]').click(); + await page.waitForURL(/.*\/user2\/repo1\/pulls\/5#issuecomment-\d+/); + await save_visual(page); +}); + +test('PR: Create review from commit', async ({page}) => { + const response = await page.goto('/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269'); + expect(response?.status()).toBe(200); + + await page.locator('button.add-code-comment').click(); + const code_comment = page.locator('.comment-code-cloud form textarea.markdown-text-editor'); + await expect(code_comment).toBeVisible(); + + await code_comment.fill('This is a code comment'); + await save_visual(page); + + const start_button = page.locator('.comment-code-cloud form button.btn-start-review'); + // Workaround for #7152, where there might already be a pending review state from previous + // test runs (most likely to happen when debugging tests). + if (await start_button.isVisible({timeout: 100})) { + await start_button.click(); + } else { + await page.locator('.comment-code-cloud form button.btn-add-comment').click(); + } + + await expect(page.locator('.comment-list .comment-container')).toBeVisible(); + + // We need to wait for the review to be processed. Checking the comment counter + // conveniently does that. + await expect(page.locator('#review-box .js-btn-review > span.review-comments-counter')).toHaveText('1'); + + await page.locator('#review-box .js-btn-review').click(); + await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible(); + await save_visual(page); + + await page.locator('.review-box-panel textarea.markdown-text-editor') + .fill('This is a review'); + await page.locator('.review-box-panel button.btn-submit[value="approve"]').click(); + await page.waitForURL(/.*\/user2\/repo1\/pulls\/3#issuecomment-\d+/); + await save_visual(page); + + // In addition to testing the ability to delete comments, this also + // performs clean up. If tests are run for multiple platforms, the data isn't reset + // in-between, and subsequent runs of this test would fail, because when there already is + // a comment, the on-hover button to start a conversation doesn't appear anymore. + await page.goto('/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269'); + await page.locator('.comment-header-right.actions a.context-menu').click(); + + await expect(page.locator('.comment-header-right.actions div.menu').getByText(/Copy link.*/)).toBeVisible(); + // The button to delete a comment will prompt for confirmation using a browser alert. + page.on('dialog', (dialog) => dialog.accept()); + await page.locator('.comment-header-right.actions div.menu .delete-comment').click(); + + await expect(page.locator('.comment-list .comment-container')).toBeHidden(); + await save_visual(page); +}); + +test('PR: Navigate by single commit', async ({page}) => { + const response = await page.goto('/user2/repo1/pulls/3/commits'); + expect(response?.status()).toBe(200); + + await page.locator('tbody.commit-list td.message a').nth(1).click(); + await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/4a357436d925b5c974181ff12a994538ddc5a269/); + await save_visual(page); + + let prevButton = page.locator('.commit-header-buttons').getByText(/Prev/); + let nextButton = page.locator('.commit-header-buttons').getByText(/Next/); + await prevButton.waitFor(); + await nextButton.waitFor(); + + await expect(prevButton).toHaveClass(/disabled/); + await expect(nextButton).not.toHaveClass(/disabled/); + await expect(nextButton).toHaveAttribute('href', '/user2/repo1/pulls/3/commits/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd'); + await nextButton.click(); + + await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/); + await save_visual(page); + + prevButton = page.locator('.commit-header-buttons').getByText(/Prev/); + nextButton = page.locator('.commit-header-buttons').getByText(/Next/); + await prevButton.waitFor(); + await nextButton.waitFor(); + + await expect(prevButton).not.toHaveClass(/disabled/); + await expect(nextButton).toHaveClass(/disabled/); + await expect(prevButton).toHaveAttribute('href', '/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269'); }); diff --git a/tests/e2e/profile_actions.test.e2e.ts b/tests/e2e/profile_actions.test.e2e.ts index a66dc43aab..e27ecf64cf 100644 --- a/tests/e2e/profile_actions.test.e2e.ts +++ b/tests/e2e/profile_actions.test.e2e.ts @@ -2,6 +2,7 @@ // routers/web/user/** // templates/shared/user/** // web_src/js/features/common-global.js +// web_src/js/modules/dropdown.ts // @watch end import {expect} from '@playwright/test'; @@ -9,13 +10,11 @@ import {save_visual, test} from './utils_e2e.ts'; test.use({user: 'user2'}); -test('Follow actions', async ({page}) => { +test('Follow and block actions', async ({page}) => { await page.goto('/user1'); // Check if following and then unfollowing works. - // This checks that the event listeners of - // the buttons aren't disappearing. - const followButton = page.locator('.follow'); + const followButton = page.locator('.primary-action button'); await expect(followButton).toContainText('Follow'); await followButton.click(); await expect(followButton).toContainText('Unfollow'); @@ -23,13 +22,19 @@ test('Follow actions', async ({page}) => { await expect(followButton).toContainText('Follow'); // Simple block interaction. - await expect(page.locator('.block')).toContainText('Block'); + const actionsDropdownBtn = page.locator('.actions .dropdown summary'); + const blockButton = page.locator('#action-block'); + await expect(blockButton).toBeHidden(); - await page.locator('.block').click(); + await actionsDropdownBtn.click(); + await expect(blockButton).toBeVisible(); + await expect(blockButton).toContainText('Block'); + + await blockButton.click(); await expect(page.locator('#block-user')).toBeVisible(); await save_visual(page); await page.locator('#block-user .ok').click(); - await expect(page.locator('.block')).toContainText('Unblock'); + await expect(blockButton).toContainText('Unblock'); await expect(page.locator('#block-user')).toBeHidden(); // Check that following the user yields in a error being shown. @@ -40,6 +45,7 @@ test('Follow actions', async ({page}) => { await save_visual(page); // Unblock interaction. - await page.locator('.block').click(); - await expect(page.locator('.block')).toContainText('Block'); + await actionsDropdownBtn.click(); + await blockButton.click(); + await expect(blockButton).toContainText('Block'); }); diff --git a/tests/e2e/relative-time.test.e2e.ts b/tests/e2e/relative-time.test.e2e.ts new file mode 100644 index 0000000000..ab8bbc19de --- /dev/null +++ b/tests/e2e/relative-time.test.e2e.ts @@ -0,0 +1,35 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// templates/admin/dashboard.tmpl +// web_src/js/webcomponents/relative-time.js +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +test.use({user: 'user1'}); + +test('Relative time after htmx swap', async ({page}, workerInfo) => { + test.skip( + workerInfo.project.name !== 'firefox' && workerInfo.project.name !== 'Mobile Chrome', + 'This is a really slow test, so limit to a subset of client.', + ); + await page.goto('/admin'); + + const relativeTime = page.locator('.admin-dl-horizontal > dd:nth-child(2) > relative-time'); + await expect(relativeTime).toContainText('ago'); + + const body = page.locator('body'); + await body.evaluate( + (element) => + new Promise((resolve) => + element.addEventListener('htmx:afterSwap', () => { + resolve(); + }), + ), + ); + + await expect(relativeTime).toContainText('ago'); +}); diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts index 044e7b93ab..a4303a7320 100644 --- a/tests/e2e/release.test.e2e.ts +++ b/tests/e2e/release.test.e2e.ts @@ -14,78 +14,100 @@ import {validate_form} from './shared/forms.ts'; test.use({user: 'user2'}); -test.describe.configure({ - timeout: 30000, -}); - -test('External Release Attachments', async ({page, isMobile}) => { - test.skip(isMobile); - - // Click "New Release" - await page.goto('/user2/repo2/releases'); - await page.click('.button.small.primary'); - - // Fill out form and create new release - await expect(page).toHaveURL('/user2/repo2/releases/new'); - await validate_form({page}, 'fieldset'); - const textarea = page.locator('input[name=tag_name]'); - await textarea.pressSequentially('2.0'); - await expect(page.locator('input[name=title]')).toHaveValue('2.0'); - await page.click('#add-external-link'); - await page.click('#add-external-link'); - await page.fill('input[name=attachment-new-name-2]', 'Test'); - await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/'); - await page.click('.remove-rel-attach'); - await save_visual(page); - await page.click('.button.small.primary'); - - // Validate release page and click edit - await expect(page).toHaveURL('/user2/repo2/releases'); - await expect(page.locator('.download[open] li')).toHaveCount(3); - - await expect(page.locator('.download[open] li:nth-of-type(1)')).toContainText('Source code (ZIP)'); - await expect(page.locator('.download[open] li:nth-of-type(1) span[data-tooltip-content]')).toHaveAttribute('data-tooltip-content', 'This attachment is automatically generated.'); - await expect(page.locator('.download[open] li:nth-of-type(1) a')).toHaveAttribute('href', '/user2/repo2/archive/2.0.zip'); - await expect(page.locator('.download[open] li:nth-of-type(1) a')).toHaveAttribute('type', 'application/zip'); - - await expect(page.locator('.download[open] li:nth-of-type(2)')).toContainText('Source code (TAR.GZ)'); - await expect(page.locator('.download[open] li:nth-of-type(2) span[data-tooltip-content]')).toHaveAttribute('data-tooltip-content', 'This attachment is automatically generated.'); - await expect(page.locator('.download[open] li:nth-of-type(2) a')).toHaveAttribute('href', '/user2/repo2/archive/2.0.tar.gz'); - await expect(page.locator('.download[open] li:nth-of-type(2) a')).toHaveAttribute('type', 'application/gzip'); - - await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test'); - await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://forgejo.org/'); - await save_visual(page); - await page.locator('.octicon-pencil').first().click(); - - // Validate edit page and edit the release - await expect(page).toHaveURL('/user2/repo2/releases/edit/2.0'); - await validate_form({page}, 'fieldset'); - await expect(page.locator('.attachment_edit:visible')).toHaveCount(2); - await expect(page.locator('.attachment_edit:visible').nth(0)).toHaveValue('Test'); - await expect(page.locator('.attachment_edit:visible').nth(1)).toHaveValue('https://forgejo.org/'); - await page.locator('.attachment_edit:visible').nth(0).fill('Test2'); - await page.locator('.attachment_edit:visible').nth(1).fill('https://gitea.io/'); - await page.click('#add-external-link'); - await expect(page.locator('.attachment_edit:visible')).toHaveCount(4); - await page.locator('.attachment_edit:visible').nth(2).fill('Test3'); - await page.locator('.attachment_edit:visible').nth(3).fill('https://gitea.com/'); - await save_visual(page); - await page.click('.button.small.primary'); - - // Validate release page and click edit - await expect(page).toHaveURL('/user2/repo2/releases'); - await expect(page.locator('.download[open] li')).toHaveCount(4); - await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test2'); - await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://gitea.io/'); - await expect(page.locator('.download[open] li:nth-of-type(4)')).toContainText('Test3'); - await expect(page.locator('.download[open] li:nth-of-type(4) a')).toHaveAttribute('href', 'https://gitea.com/'); - await save_visual(page); - await page.locator('.octicon-pencil').first().click(); - - // Delete release - await expect(page).toHaveURL('/user2/repo2/releases/edit/2.0'); - await page.click('.delete-button'); - await page.click('.button.ok'); - await expect(page).toHaveURL('/user2/repo2/releases'); +test.describe('repo branch protection settings', () => { + test('External Release Attachments', async ({page, isMobile}, workerInfo) => { + test.skip(isMobile || workerInfo.project.name === 'webkit'); + + // Click "New Release" + await page.goto('/user2/repo2/releases'); + await page.click('.button.small.primary'); + + // Fill out form and create new release + await expect(page).toHaveURL('/user2/repo2/releases/new'); + await validate_form({page}, 'fieldset'); + const textarea = page.locator('input[name=tag_name]'); + await textarea.pressSequentially('2.0'); + await expect(page.locator('input[name=title]')).toHaveValue('2.0'); + await page.click('#add-external-link'); + await page.click('#add-external-link'); + await page.fill('input[name=attachment-new-name-2]', 'Test'); + await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/'); + await page.click('.remove-rel-attach'); + await save_visual(page); + await page.click('.button.small.primary'); + + // Validate release page and click edit + await expect(page).toHaveURL('/user2/repo2/releases'); + await expect(page.locator('.download[open] li')).toHaveCount(3); + + await expect(page.locator('.download[open] li:nth-of-type(1)')).toContainText('Source code (ZIP)'); + await expect(page.locator('.download[open] li:nth-of-type(1) span[data-tooltip-content]')).toHaveAttribute('data-tooltip-content', 'This attachment is automatically generated.'); + await expect(page.locator('.download[open] li:nth-of-type(1) a')).toHaveAttribute('href', '/user2/repo2/archive/2.0.zip'); + await expect(page.locator('.download[open] li:nth-of-type(1) a')).toHaveAttribute('type', 'application/zip'); + + await expect(page.locator('.download[open] li:nth-of-type(2)')).toContainText('Source code (TAR.GZ)'); + await expect(page.locator('.download[open] li:nth-of-type(2) span[data-tooltip-content]')).toHaveAttribute('data-tooltip-content', 'This attachment is automatically generated.'); + await expect(page.locator('.download[open] li:nth-of-type(2) a')).toHaveAttribute('href', '/user2/repo2/archive/2.0.tar.gz'); + await expect(page.locator('.download[open] li:nth-of-type(2) a')).toHaveAttribute('type', 'application/gzip'); + + await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test'); + await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://forgejo.org/'); + await save_visual(page); + await page.locator('.octicon-pencil').first().click(); + + // Validate edit page and edit the release + await expect(page).toHaveURL('/user2/repo2/releases/edit/2.0'); + await validate_form({page}, 'fieldset'); + await expect(page.locator('.attachment_edit:visible')).toHaveCount(2); + await expect(page.locator('.attachment_edit:visible').nth(0)).toHaveValue('Test'); + await expect(page.locator('.attachment_edit:visible').nth(1)).toHaveValue('https://forgejo.org/'); + await page.locator('.attachment_edit:visible').nth(0).fill('Test2'); + await page.locator('.attachment_edit:visible').nth(1).fill('https://gitea.io/'); + await page.click('#add-external-link'); + await expect(page.locator('.attachment_edit:visible')).toHaveCount(4); + await page.locator('.attachment_edit:visible').nth(2).fill('Test3'); + await page.locator('.attachment_edit:visible').nth(3).fill('https://gitea.com/'); + await save_visual(page); + await page.click('.button.small.primary'); + + // Validate release page and click edit + await expect(page).toHaveURL('/user2/repo2/releases'); + await expect(page.locator('.download[open] li')).toHaveCount(4); + await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test2'); + await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://gitea.io/'); + await expect(page.locator('.download[open] li:nth-of-type(4)')).toContainText('Test3'); + await expect(page.locator('.download[open] li:nth-of-type(4) a')).toHaveAttribute('href', 'https://gitea.com/'); + await save_visual(page); + await page.locator('.octicon-pencil').first().click(); + }); + + test('Release name equals tag name if created from tag', async ({page}) => { + await page.goto('/user2/repo2/releases/new?tag=v1.1'); + + await expect(page.locator('input[name=title]')).toHaveValue('v1.1'); + }); + + test('Release name equals release name if edit', async ({page, isMobile}) => { + test.skip(isMobile); + + await page.goto('/user2/repo2/releases/new'); + + await page.locator('input[name=title]').pressSequentially('v2.0'); + await page.locator('input[name=tag_name]').pressSequentially('2.0'); + await page.click('.button.small.primary'); + + await page.goto('/user2/repo2/releases/edit/2.0'); + + await expect(page.locator('input[name=title]')).toHaveValue('v2.0'); + }); + + test.afterEach(async ({page}) => { + // Delete release + const response = await page.goto('/user2/repo2/releases/edit/2.0'); + test.skip(response.status() === 404, 'No release to delete'); + + await page.locator('.delete-button').dispatchEvent('click'); + await page.locator('.button.ok').click(); + await expect(page).toHaveURL('/user2/repo2/releases'); + }); }); diff --git a/tests/e2e/repo-code.test.e2e.ts b/tests/e2e/repo-code.test.e2e.ts index 11b710c956..9184c3ce67 100644 --- a/tests/e2e/repo-code.test.e2e.ts +++ b/tests/e2e/repo-code.test.e2e.ts @@ -1,7 +1,13 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + // @watch start -// web_src/js/features/repo-code.js -// web_src/css/repo.css // services/gitdiff/** +// templates/repo/view_file.tmpl +// web_src/css/repo.css +// web_src/css/repo/file-view.css +// web_src/js/features/repo-code.js +// web_src/js/features/repo-unicode-escape.js // @watch end import {expect, type Page} from '@playwright/test'; @@ -16,7 +22,7 @@ async function assertSelectedLines(page: Page, nums: string[]) { .toStrictEqual(nums); // the first line selected has an action button - if (nums.length > 0) await expect(page.locator(`#L${nums[0]} .code-line-button`)).toBeVisible(); + if (nums.length > 0) await expect(page.locator(`.lines-num:has(#L${nums[0]}) .code-line-button`)).toBeVisible(); }; await pageAssertions(); @@ -99,3 +105,25 @@ test.describe('As authenticated user', () => { await save_visual(page); }); }); + +test('Unicode escape highlight', async ({page}) => { + const unselectedBg = 'rgba(0, 0, 0, 0)'; + const selectedBg = 'rgb(255, 237, 213)'; + + const response = await page.goto('/user2/unicode-escaping/src/branch/main/a-file'); + expect(response?.status()).toBe(200); + + await expect(page.locator('.unicode-escape-prompt')).toBeVisible(); + expect(await page.locator('.lines-num').evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(unselectedBg); + expect(await page.locator('.lines-escape').evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(unselectedBg); + expect(await page.locator('.lines-code').evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(unselectedBg); + + await page.locator('#L1').click(); + expect(await page.locator('.lines-num').evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(selectedBg); + expect(await page.locator('.lines-escape').evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(selectedBg); + expect(await page.locator('.lines-code').evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(selectedBg); + + await page.locator('.code-line-button').click(); + await expect(page.locator('.tippy-box .view_git_blame[href$="/a-file#L1"]')).toBeVisible(); + await expect(page.locator('.tippy-box .copy-line-permalink[data-url$="/a-file#L1"]')).toBeVisible(); +}); diff --git a/tests/e2e/user-cards.test.e2e.ts b/tests/e2e/user-cards.test.e2e.ts new file mode 100644 index 0000000000..22c31965d6 --- /dev/null +++ b/tests/e2e/user-cards.test.e2e.ts @@ -0,0 +1,29 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// templates/repo/user_cards.tmpl +// web_src/css/modules/user-cards.css +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +test('Usercards width', async ({page}) => { + await page.goto('/user8?tab=followers'); + + // Regardless of whether cards in a grid or flex mode, they should be ~same + // width. Verifying this relies on fixtures with users that have long website + // link or other content that could push the card width. + const widths = []; + const amount = 3; + + for (let i = 1; i <= amount; i++) { + const card = await page.locator(`.user-cards .card:nth-child(${i})`).boundingBox(); + widths.push(Math.round(card.width)); + } + + for (const width of widths) { + expect(width).toBe(widths[0]); + } +}); diff --git a/tests/e2e/utils_e2e_test.go b/tests/e2e/utils_e2e_test.go index f892d6c518..efa1657cee 100644 --- a/tests/e2e/utils_e2e_test.go +++ b/tests/e2e/utils_e2e_test.go @@ -93,7 +93,10 @@ func createSessions(t testing.TB) { users := []string{ "user1", "user2", + "user11", "user12", + "user18", + "user29", "user40", } diff --git a/tests/e2e/webauthn.test.e2e.ts b/tests/e2e/webauthn.test.e2e.ts index 144f52e374..0b5a6a6c2b 100644 --- a/tests/e2e/webauthn.test.e2e.ts +++ b/tests/e2e/webauthn.test.e2e.ts @@ -40,7 +40,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => // Logout. await expect(async () => { await page.locator('div[aria-label="Profile and settings…"]').click(); - await page.getByText('Sign Out').click(); + await page.getByText('Sign out').click(); }).toPass(); await page.waitForURL(`${workerInfo.project.use.baseURL}/`); diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/logs/refs/heads/branch1 b/tests/gitea-repositories-meta/user2/commitsonpr.git/logs/refs/heads/branch1 index cf96195665..34c9ccecdd 100644 --- a/tests/gitea-repositories-meta/user2/commitsonpr.git/logs/refs/heads/branch1 +++ b/tests/gitea-repositories-meta/user2/commitsonpr.git/logs/refs/heads/branch1 @@ -1 +1,2 @@ 0000000000000000000000000000000000000000 1978192d98bb1b65e11c2cf37da854fbf94bffd6 Gitea 1688672383 +0200 push +1978192d98bb1b65e11c2cf37da854fbf94bffd6 9b93963cf6de4dc33f915bb67f192d099c301f43 Forgejo 1749737639 +0200 push diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/06/c5865eaeaaa2f92e8fce75a281f6272ee68e90 b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/06/c5865eaeaaa2f92e8fce75a281f6272ee68e90 new file mode 100644 index 0000000000..009c9849d9 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/06/c5865eaeaaa2f92e8fce75a281f6272ee68e90 differ diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/2d/65d92dc800c6f448541240c18e82bf36b954bb b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/2d/65d92dc800c6f448541240c18e82bf36b954bb new file mode 100644 index 0000000000..e50c7ad5eb Binary files /dev/null and b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/2d/65d92dc800c6f448541240c18e82bf36b954bb differ diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/9b/93963cf6de4dc33f915bb67f192d099c301f43 b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/9b/93963cf6de4dc33f915bb67f192d099c301f43 new file mode 100644 index 0000000000..41814996f1 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/9b/93963cf6de4dc33f915bb67f192d099c301f43 differ diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/ab/a2b386a1eb0b0273ada0fed4f7d075d6e343c1 b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/ab/a2b386a1eb0b0273ada0fed4f7d075d6e343c1 new file mode 100644 index 0000000000..2d936251cd Binary files /dev/null and b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/ab/a2b386a1eb0b0273ada0fed4f7d075d6e343c1 differ diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/b3/b178fe7c45986f6325aeac1b036c74825ae8f4 b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/b3/b178fe7c45986f6325aeac1b036c74825ae8f4 new file mode 100644 index 0000000000..7852ab9100 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/b3/b178fe7c45986f6325aeac1b036c74825ae8f4 differ diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/c9/cf8a3095808af2425255056e01746fef420801 b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/c9/cf8a3095808af2425255056e01746fef420801 new file mode 100644 index 0000000000..32a51a2837 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/commitsonpr.git/objects/c9/cf8a3095808af2425255056e01746fef420801 differ diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/heads/branch1 b/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/heads/branch1 index 357fc9d6ed..6fc9179bdd 100644 --- a/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/heads/branch1 +++ b/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/heads/branch1 @@ -1 +1 @@ -1978192d98bb1b65e11c2cf37da854fbf94bffd6 +9b93963cf6de4dc33f915bb67f192d099c301f43 diff --git a/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/pull/1/head b/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/pull/1/head index 357fc9d6ed..6fc9179bdd 100644 --- a/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/pull/1/head +++ b/tests/gitea-repositories-meta/user2/commitsonpr.git/refs/pull/1/head @@ -1 +1 @@ -1978192d98bb1b65e11c2cf37da854fbf94bffd6 +9b93963cf6de4dc33f915bb67f192d099c301f43 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG b/tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG new file mode 100644 index 0000000000..6be072ebc0 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG @@ -0,0 +1 @@ +fix: bug diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/FETCH_HEAD b/tests/gitea-repositories-meta/user2/test_action_run_search.git/FETCH_HEAD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD b/tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD new file mode 100644 index 0000000000..b870d82622 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/config b/tests/gitea-repositories-meta/user2/test_action_run_search.git/config new file mode 100644 index 0000000000..515f483629 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/config @@ -0,0 +1,5 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/description b/tests/gitea-repositories-meta/user2/test_action_run_search.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/index b/tests/gitea-repositories-meta/user2/test_action_run_search.git/index new file mode 100644 index 0000000000..cca2ada835 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/index differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/info/exclude b/tests/gitea-repositories-meta/user2/test_action_run_search.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/HEAD b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/HEAD new file mode 100644 index 0000000000..51870db479 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/HEAD @@ -0,0 +1,7 @@ +0000000000000000000000000000000000000000 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745918688 +0200 commit (initial): initial +97f29ee599c373c729132a5c46a046978311e0ee 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745921505 +0200 checkout: moving from master to main +97f29ee599c373c729132a5c46a046978311e0ee 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922102 +0200 checkout: moving from main to bugfix-1 +97f29ee599c373c729132a5c46a046978311e0ee 35c5cddfc19397501ec8f4f7bb808a7c8f04445f User 2 1745922142 +0200 commit: fix: bug +35c5cddfc19397501ec8f4f7bb808a7c8f04445f 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922150 +0200 checkout: moving from bugfix-1 to main +97f29ee599c373c729132a5c46a046978311e0ee 35c5cddfc19397501ec8f4f7bb808a7c8f04445f User 2 1745922198 +0200 checkout: moving from main to bugfix-1 +35c5cddfc19397501ec8f4f7bb808a7c8f04445f 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922272 +0200 checkout: moving from bugfix-1 to main diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 new file mode 100644 index 0000000000..d8977c9d15 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922102 +0200 branch: Created from HEAD +97f29ee599c373c729132a5c46a046978311e0ee 35c5cddfc19397501ec8f4f7bb808a7c8f04445f User 2 1745922142 +0200 commit: fix: bug diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main new file mode 100644 index 0000000000..30a780778b --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745921505 +0200 branch: Created from HEAD diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/05/daa94891793ad0f183273cfb9902b903782e2a b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/05/daa94891793ad0f183273cfb9902b903782e2a new file mode 100644 index 0000000000..741f3a665f Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/05/daa94891793ad0f183273cfb9902b903782e2a differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/18/722d8d68a60af7e66213ef4a15d05dae365f2b b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/18/722d8d68a60af7e66213ef4a15d05dae365f2b new file mode 100644 index 0000000000..cececf6633 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/18/722d8d68a60af7e66213ef4a15d05dae365f2b differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/35/c5cddfc19397501ec8f4f7bb808a7c8f04445f b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/35/c5cddfc19397501ec8f4f7bb808a7c8f04445f new file mode 100644 index 0000000000..3f921384ba Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/35/c5cddfc19397501ec8f4f7bb808a7c8f04445f differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/76/ebce62dde5d6c40cfdda5782ff171f02a618b9 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/76/ebce62dde5d6c40cfdda5782ff171f02a618b9 new file mode 100644 index 0000000000..920604316e Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/76/ebce62dde5d6c40cfdda5782ff171f02a618b9 differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/7f/56afb8a0b9ffbc39e8edd537f252412254d112 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/7f/56afb8a0b9ffbc39e8edd537f252412254d112 new file mode 100644 index 0000000000..4dad68d944 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/7f/56afb8a0b9ffbc39e8edd537f252412254d112 differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8d/6860a6d6bb80661d308b060509ef41f099c390 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8d/6860a6d6bb80661d308b060509ef41f099c390 new file mode 100644 index 0000000000..893f680834 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8d/6860a6d6bb80661d308b060509ef41f099c390 differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8f/2069876727126f91aa756d84caa59f93670dfc b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8f/2069876727126f91aa756d84caa59f93670dfc new file mode 100644 index 0000000000..92663ca712 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8f/2069876727126f91aa756d84caa59f93670dfc differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/97/f29ee599c373c729132a5c46a046978311e0ee b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/97/f29ee599c373c729132a5c46a046978311e0ee new file mode 100644 index 0000000000..9a835a73c4 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/97/f29ee599c373c729132a5c46a046978311e0ee differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a5/3a19de56c7594844459c2951497c15dba8adee b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a5/3a19de56c7594844459c2951497c15dba8adee new file mode 100644 index 0000000000..44b7d5774d Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a5/3a19de56c7594844459c2951497c15dba8adee differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a6/2cdde48ab95bb49294c30cf0971ce2511008c5 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a6/2cdde48ab95bb49294c30cf0971ce2511008c5 new file mode 100644 index 0000000000..e6f7dccf30 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a6/2cdde48ab95bb49294c30cf0971ce2511008c5 differ diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 new file mode 100644 index 0000000000..ae12ad5d79 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 @@ -0,0 +1 @@ +35c5cddfc19397501ec8f4f7bb808a7c8f04445f diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main new file mode 100644 index 0000000000..d46cce7027 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main @@ -0,0 +1 @@ +97f29ee599c373c729132a5c46a046978311e0ee diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 new file mode 100644 index 0000000000..d46cce7027 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 @@ -0,0 +1 @@ +97f29ee599c373c729132a5c46a046978311e0ee diff --git a/tests/integration/README.md b/tests/integration/README.md index d83685388e..a6be7fe72c 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -71,11 +71,11 @@ TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test?multiStatements=true TEST_ ### Run pgsql integration tests Setup a pgsql database inside docker ``` -docker run -e "POSTGRES_DB=test" -e POSTGRES_PASSWORD=postgres -p 5432:5432 --rm --name pgsql postgres:latest #(Ctrl-c to stop the database) +docker run -e "POSTGRES_DB=test" -e POSTGRES_PASSWORD=postgres POSTGRESQL_FSYNC=off POSTGRESQL_EXTRA_FLAGS="-c full_page_writes=off" -p 5432:5432 --rm --name pgsql data.forgejo.org/oci/bitnami/postgresql:16 #(Ctrl-c to stop the database) ``` Start tests based on the database container ``` -TEST_STORAGE_TYPE=local TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-pgsql +TEST_STORAGE_TYPE=local TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make 'test-pgsql#Test' ``` ### Running individual tests diff --git a/tests/integration/actions_notifications_test.go b/tests/integration/actions_notifications_test.go new file mode 100644 index 0000000000..e47cb64b83 --- /dev/null +++ b/tests/integration/actions_notifications_test.go @@ -0,0 +1,88 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "fmt" + "net/url" + "testing" + + actions_model "forgejo.org/models/actions" + auth_model "forgejo.org/models/auth" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/setting" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" +) + +func TestActionNotifications(t *testing.T) { + if !setting.Database.Type.IsSQLite3() { + t.Skip() + } + + testCases := []struct { + name string + treePath string + fileContent string + notifyEmail bool + }{ + { + name: "enabled", + treePath: ".forgejo/workflows/enabled.yml", + fileContent: `name: enabled +on: + push: +enable-email-notifications: true +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 +`, + notifyEmail: true, + }, + { + name: "disabled", + treePath: ".forgejo/workflows/disabled.yml", + fileContent: `name: disabled +on: + push: +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 +`, + notifyEmail: false, + }, + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + apiRepo := createActionsTestRepo(t, token, testCase.name, false) + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", testCase.treePath), testCase.fileContent) + createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts) + + task := runner.fetchTask(t) + actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id}) + actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID}) + actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID}) + assert.Equal(t, testCase.notifyEmail, actionRun.NotifyEmail) + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) + } + }) +} diff --git a/tests/integration/actions_run_now_done_notification_test.go b/tests/integration/actions_run_now_done_notification_test.go index 9a2e118701..d5142096c5 100644 --- a/tests/integration/actions_run_now_done_notification_test.go +++ b/tests/integration/actions_run_now_done_notification_test.go @@ -44,30 +44,35 @@ func (m *mockNotifier) ActionRunNowDone(ctx context.Context, run *actions_model. assert.Equal(m.t, actions_model.StatusSuccess, run.Status) assert.Equal(m.t, actions_model.StatusRunning, priorStatus) assert.Nil(m.t, lastRun) + assert.True(m.t, run.NotifyEmail) case 1: assert.Equal(m.t, m.runID, run.ID) assert.Equal(m.t, actions_model.StatusFailure, run.Status) assert.Equal(m.t, actions_model.StatusRunning, priorStatus) assert.Equal(m.t, m.lastRunID, lastRun.ID) assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status) + assert.True(m.t, run.NotifyEmail) case 2: assert.Equal(m.t, m.runID, run.ID) assert.Equal(m.t, actions_model.StatusCancelled, run.Status) assert.Equal(m.t, actions_model.StatusRunning, priorStatus) assert.Equal(m.t, m.lastRunID, lastRun.ID) assert.Equal(m.t, actions_model.StatusFailure, lastRun.Status) + assert.True(m.t, run.NotifyEmail) case 3: assert.Equal(m.t, m.runID, run.ID) assert.Equal(m.t, actions_model.StatusSuccess, run.Status) assert.Equal(m.t, actions_model.StatusRunning, priorStatus) assert.Equal(m.t, m.lastRunID, lastRun.ID) assert.Equal(m.t, actions_model.StatusCancelled, lastRun.Status) + assert.True(m.t, run.NotifyEmail) case 4: assert.Equal(m.t, m.runID, run.ID) assert.Equal(m.t, actions_model.StatusSuccess, run.Status) assert.Equal(m.t, actions_model.StatusRunning, priorStatus) assert.Equal(m.t, m.lastRunID, lastRun.ID) assert.Equal(m.t, actions_model.StatusSuccess, lastRun.Status) + assert.True(m.t, run.NotifyEmail) default: assert.Fail(m.t, "too many notifications") } @@ -101,6 +106,7 @@ func TestActionNowDoneNotification(t *testing.T) { TreePath: ".forgejo/workflows/dispatch.yml", ContentReader: strings.NewReader( "name: test\n" + + "enable-email-notifications: true\n" + "on: [workflow_dispatch]\n" + "jobs:\n" + " test:\n" + diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go index 3f9e57c41f..859406997e 100644 --- a/tests/integration/actions_runner_test.go +++ b/tests/integration/actions_runner_test.go @@ -101,28 +101,26 @@ func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1 if len(timeout) > 0 { fetchTimeout = timeout[0] } - ddl := time.Now().Add(fetchTimeout) + var task *runnerv1.Task - for time.Now().Before(ddl) { + assert.Eventually(t, func() bool { resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{ TasksVersion: 0, })) require.NoError(t, err) if resp.Msg.Task != nil { task = resp.Msg.Task - break + return true } - time.Sleep(time.Second) - } - assert.NotNil(t, task, "failed to fetch a task") + return false + }, fetchTimeout, time.Millisecond*100, "failed to fetch a task") return task } type mockTaskOutcome struct { - result runnerv1.Result - outputs map[string]string - logRows []*runnerv1.LogRow - execTime time.Duration + result runnerv1.Result + outputs map[string]string + logRows []*runnerv1.LogRow } func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) { @@ -149,7 +147,6 @@ func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTa sentOutputKeys = append(sentOutputKeys, outputKey) assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs) } - time.Sleep(outcome.execTime) resp, err := r.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ State: &runnerv1.TaskState{ Id: task.Id, diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 2d5dd3b957..0e61e05547 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -350,6 +350,87 @@ jobs: }) } +func TestPullRequestWithInvalidWorkflow(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo + session := loginUser(t, "user2") + + // prepare the repository + baseRepo, _, f := tests.CreateDeclarativeRepo(t, user2, "repo-pull-request", + []unit_model.Type{unit_model.TypeActions}, nil, []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".forgejo/workflows/broken.yml", + ContentReader: strings.NewReader(`name: broken +on: +pull_request: +types: + - opened +jobs: +test: +runs-on: docker + - run: true +`), + }, + }) + defer f() + baseGitRepo, err := gitrepo.OpenRepository(t.Context(), baseRepo) + require.NoError(t, err) + defer func() { + baseGitRepo.Close() + }() + + // create the pull request + testEditFileToNewBranch(t, session, "user2", "repo-pull-request", "main", "wip-something", "README.md", "Hello, world 1") + testPullCreate(t, session, "user2", "repo-pull-request", true, "main", "wip-something", "Commit status PR") + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: baseRepo.ID}) + require.NoError(t, pr.LoadIssue(t.Context())) + + // check that one of the status associated with the commit sha matches both + // context & state + checkCommitStatus := func(sha, context string, state api.CommitStatusState) bool { + commitStatuses, _, err := git_model.GetLatestCommitStatus(t.Context(), pr.BaseRepoID, sha, db.ListOptionsAll) + require.NoError(t, err) + for _, commitStatus := range commitStatuses { + if state == commitStatus.State && context == commitStatus.Context { + return true + } + } + return false + } + + var actionRuns []*actions_model.ActionRun + + // wait for ActionRun(s) to be created + require.Eventually(t, func() bool { + actionRuns = make([]*actions_model.ActionRun, 0) + require.NoError(t, db.GetEngine(t.Context()).Where("event=? AND status=? AND repo_id=?", "pull_request", actions_model.StatusFailure, baseRepo.ID).Find(&actionRuns)) + return len(actionRuns) == 1 + }, 30*time.Second, 1*time.Second) + + // verify the expected ActionRuns were created + sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) + require.NoError(t, err) + + // verify the commit status changes to CommitStatusFailure + require.Eventually(t, func() bool { + return checkCommitStatus(sha, "broken.yml / Update README.md (pull_request)", api.CommitStatusFailure) + }, 30*time.Second, 1*time.Second) + + require.Len(t, actionRuns, 1) + actionRun := actionRuns[0] + // verify the expected ActionRunJob was created and is StatusFailure + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, CommitSHA: sha}) + assert.Equal(t, actions_model.StatusFailure, job.Status) + assert.Equal(t, "broken.yml", actionRun.WorkflowID) + assert.Equal(t, sha, actionRun.CommitSHA) + assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent) + event, err := actionRun.GetPullRequestEventPayload() + require.NoError(t, err) + assert.Equal(t, api.HookIssueOpened, event.Action) + }) +} + func TestPullRequestTargetEvent(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go index dc2b86d28b..d6db22700a 100644 --- a/tests/integration/api_actions_artifact_test.go +++ b/tests/integration/api_actions_artifact_test.go @@ -56,7 +56,7 @@ func TestActionsArtifactUploadSingleFile(t *testing.T) { SetHeader("x-actions-results-md5", "XVlf820rMInUi64wmMi6EA==") // base64(md5(body)) MakeRequest(t, req, http.StatusOK) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") // confirm artifact upload req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-single"). @@ -206,7 +206,7 @@ func TestActionsArtifactUploadMultipleFile(t *testing.T) { MakeRequest(t, req, http.StatusOK) } - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") // confirm artifact upload req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName="+testArtifactName). @@ -297,7 +297,7 @@ func TestActionsArtifactUploadWithRetentionDays(t *testing.T) { SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body)) MakeRequest(t, req, http.StatusOK) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") // confirm artifact upload req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days"). diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index 9fa7590620..af0dcb98a9 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -59,7 +59,7 @@ func uploadArtifact(t *testing.T, body string) string { req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") sha := sha256.Sum256([]byte(body)) @@ -113,7 +113,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) { req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") sha := sha256.Sum256([]byte(strings.Repeat("A", 1024))) @@ -158,7 +158,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") sha := sha256.Sum256([]byte(body)) @@ -221,7 +221,7 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") sha := sha256.Sum256([]byte(body)) @@ -286,7 +286,7 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + t.Log("Create artifact confirm") sha := sha256.Sum256([]byte(bodya + bodyb)) diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index 104fdf4f67..9351dd9c20 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -333,11 +333,11 @@ func TestAPICron(t *testing.T) { AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, "28", resp.Header().Get("X-Total-Count")) + assert.Equal(t, "29", resp.Header().Get("X-Total-Count")) var crons []api.Cron DecodeJSON(t, resp, &crons) - assert.Len(t, crons, 28) + assert.Len(t, crons, 29) }) t.Run("Execute", func(t *testing.T) { diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go index 809be572da..3653a859b1 100644 --- a/tests/integration/api_issue_config_test.go +++ b/tests/integration/api_issue_config_test.go @@ -155,7 +155,7 @@ func TestAPIRepoIssueConfigPaths(t *testing.T) { assert.False(t, issueConfig.BlankIssuesEnabled) assert.Empty(t, issueConfig.ContactLinks) - _, err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch) + err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch) require.NoError(t, err) }) } diff --git a/tests/integration/api_issue_templates_test.go b/tests/integration/api_issue_templates_test.go index 49b1a6f277..47ba34198a 100644 --- a/tests/integration/api_issue_templates_test.go +++ b/tests/integration/api_issue_templates_test.go @@ -47,9 +47,7 @@ func TestAPIIssueTemplateList(t *testing.T) { for _, template := range templateCandidates { t.Run(template, func(t *testing.T) { defer tests.PrintCurrentTest(t)() - defer func() { - deleteFileInBranch(user, repo, template, repo.DefaultBranch) - }() + defer deleteFileInBranch(user, repo, template, repo.DefaultBranch) err := createOrReplaceFileInBranch(user, repo, template, repo.DefaultBranch, `--- diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 0e977e9ae7..fb092c91c7 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -56,7 +56,7 @@ func TestPackageContainer(t *testing.T) { return values } - images := []string{"test", "te/st"} + images := []string{"test", "te/st", "oras-artifact"} tags := []string{"latest", "main"} multiTag := "multi" @@ -177,6 +177,90 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version")) }) + t.Run("ORAS Artifact Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + image := "oras-artifact" + url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image) + + // Empty config blob (common in ORAS artifacts) + emptyConfigDigest := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + emptyConfigContent := "" + + // Upload empty config blob + req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, emptyConfigDigest), bytes.NewReader([]byte(emptyConfigContent))). + AddTokenAuth(userToken) + resp := MakeRequest(t, req, http.StatusCreated) + assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, emptyConfigDigest), resp.Header().Get("Location")) + assert.Equal(t, emptyConfigDigest, resp.Header().Get("Docker-Content-Digest")) + + // Verify empty blob exists and has correct Content-Length + req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, emptyConfigDigest)). + AddTokenAuth(userToken) + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "0", resp.Header().Get("Content-Length")) // This was the main fix + assert.Equal(t, emptyConfigDigest, resp.Header().Get("Docker-Content-Digest")) + + // Upload a small data blob (e.g., artifacthub metadata) + artifactData := `{"name":"test-artifact","version":"1.0.0"}` + artifactDigest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(artifactData))) + + req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, artifactDigest), bytes.NewReader([]byte(artifactData))). + AddTokenAuth(userToken) + resp = MakeRequest(t, req, http.StatusCreated) + assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, artifactDigest), resp.Header().Get("Location")) + + // Create OCI artifact manifest + artifactManifest := fmt.Sprintf(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.cncf.artifacthub.config.v1+yaml", + "config": { + "mediaType": "application/vnd.cncf.artifacthub.config.v1+yaml", + "digest": "%s", + "size": %d + }, + "layers": [ + { + "mediaType": "application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml", + "digest": "%s", + "size": %d + } + ] + }`, emptyConfigDigest, len(emptyConfigContent), artifactDigest, len(artifactData)) + + artifactManifestDigest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(artifactManifest))) + + // Upload artifact manifest + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/artifact-v1", url), bytes.NewReader([]byte(artifactManifest))). + AddTokenAuth(userToken). + SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json") + resp = MakeRequest(t, req, http.StatusCreated) + assert.Equal(t, fmt.Sprintf("/v2/%s/%s/manifests/artifact-v1", user.Name, image), resp.Header().Get("Location")) + assert.Equal(t, artifactManifestDigest, resp.Header().Get("Docker-Content-Digest")) + + // Verify manifest can be retrieved + req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/artifact-v1", url)). + AddTokenAuth(userToken). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", resp.Header().Get("Content-Type")) + assert.Equal(t, artifactManifestDigest, resp.Header().Get("Docker-Content-Digest")) + + // Verify package was created with correct metadata + pvs, err := packages_model.GetVersionsByPackageType(db.DefaultContext, user.ID, packages_model.TypeContainer) + require.NoError(t, err) + + found := false + for _, pv := range pvs { + if pv.LowerVersion == "artifact-v1" { + found = true + break + } + } + assert.True(t, found, "ORAS artifact package should be created") + }) + for _, image := range images { t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) { url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image) @@ -430,14 +514,19 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) }) - t.Run("GetManifest", func(t *testing.T) { + t.Run("GetManifest unknown-tag", func(t *testing.T) { defer tests.PrintCurrentTest(t)() req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url)). AddTokenAuth(userToken) MakeRequest(t, req, http.StatusNotFound) + }) - req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)). + t.Run("GetManifest serv indirect", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, false)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)). AddTokenAuth(userToken) resp := MakeRequest(t, req, http.StatusOK) @@ -446,6 +535,25 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, manifestContent, resp.Body.String()) }) + + t.Run("GetManifest serv direct", func(t *testing.T) { + if setting.Packages.Storage.Type != setting.MinioStorageType { + t.Skip("Test skipped for non-Minio-storage.") + return + } + + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, true)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)). + AddTokenAuth(userToken) + resp := MakeRequest(t, req, http.StatusTemporaryRedirect) + + assert.Empty(t, resp.Header().Get("Content-Length")) + assert.NotEmpty(t, resp.Header().Get("Location")) + assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Empty(t, resp.Header().Get("Docker-Content-Digest")) + }) }) } @@ -580,36 +688,76 @@ func TestPackageContainer(t *testing.T) { t.Run("GetTagList", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - cases := []struct { + var cases []struct { URL string ExpectedTags []string ExpectedLink string - }{ - { - URL: fmt.Sprintf("%s/tags/list", url), - ExpectedTags: []string{"latest", "main", "multi"}, - ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), - }, - { - URL: fmt.Sprintf("%s/tags/list?n=0", url), - ExpectedTags: []string{}, - ExpectedLink: "", - }, - { - URL: fmt.Sprintf("%s/tags/list?n=2", url), - ExpectedTags: []string{"latest", "main"}, - ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), - }, - { - URL: fmt.Sprintf("%s/tags/list?last=main", url), - ExpectedTags: []string{"multi"}, - ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), - }, - { - URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url), - ExpectedTags: []string{"main"}, - ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), - }, + } + + if image == "oras-artifact" { + cases = []struct { + URL string + ExpectedTags []string + ExpectedLink string + }{ + { + URL: fmt.Sprintf("%s/tags/list", url), + ExpectedTags: []string{"artifact-v1", "latest", "main", "multi"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + { + URL: fmt.Sprintf("%s/tags/list?n=0", url), + ExpectedTags: []string{}, + ExpectedLink: "", + }, + { + URL: fmt.Sprintf("%s/tags/list?n=2", url), + ExpectedTags: []string{"artifact-v1", "latest"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + { + URL: fmt.Sprintf("%s/tags/list?last=main", url), + ExpectedTags: []string{"multi"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + { + URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url), + ExpectedTags: []string{"main"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + } + } else { + cases = []struct { + URL string + ExpectedTags []string + ExpectedLink string + }{ + { + URL: fmt.Sprintf("%s/tags/list", url), + ExpectedTags: []string{"latest", "main", "multi"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + { + URL: fmt.Sprintf("%s/tags/list?n=0", url), + ExpectedTags: []string{}, + ExpectedLink: "", + }, + { + URL: fmt.Sprintf("%s/tags/list?n=2", url), + ExpectedTags: []string{"latest", "main"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + { + URL: fmt.Sprintf("%s/tags/list?last=main", url), + ExpectedTags: []string{"multi"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + { + URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url), + ExpectedTags: []string{"main"}, + ExpectedLink: fmt.Sprintf(`; rel="next"`, user.Name, image), + }, + } } for _, c := range cases { @@ -636,7 +784,11 @@ func TestPackageContainer(t *testing.T) { var apiPackages []*api.Package DecodeJSON(t, resp, &apiPackages) - assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..." + if image == "oras-artifact" { + assert.Len(t, apiPackages, 5) // "artifact-v1", "latest", "main", "multi", "sha256:..." + } else { + assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..." + } }) t.Run("Delete", func(t *testing.T) { diff --git a/tests/integration/api_packages_goproxy_test.go b/tests/integration/api_packages_goproxy_test.go index 1534fa73cc..1b95da8650 100644 --- a/tests/integration/api_packages_goproxy_test.go +++ b/tests/integration/api_packages_goproxy_test.go @@ -15,6 +15,7 @@ import ( "forgejo.org/models/packages" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" + "forgejo.org/modules/test" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -88,12 +89,12 @@ func TestPackageGo(t *testing.T) { AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusConflict) - time.Sleep(time.Second) - content = createArchive(map[string][]byte{ packageName + "@" + packageVersion2 + "/go.mod": []byte(goModContent), }) + test.SleepTillNextSecond() + req = NewRequestWithBody(t, "PUT", url+"/upload", bytes.NewReader(content)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusCreated) diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go index 438c2f0fb5..9490d52890 100644 --- a/tests/integration/api_packages_maven_test.go +++ b/tests/integration/api_packages_maven_test.go @@ -29,12 +29,12 @@ func TestPackageMaven(t *testing.T) { groupID := "com.gitea" artifactID := "test-project" - packageName := groupID + "-" + artifactID + packageName := groupID + ":" + artifactID packageVersion := "1.0.1" packageDescription := "Test Description" root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID) - filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion) + filename := fmt.Sprintf("%s:%s.jar", packageName, packageVersion) putFile := func(t *testing.T, path, content string, expectedStatus int) { req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)). @@ -249,7 +249,7 @@ func TestPackageMaven(t *testing.T) { partialVersion := packageVersion + "-PARTIAL" putFile(t, fmt.Sprintf("/%s/%s", partialVersion, filename), "test", http.StatusCreated) - pkgUIURL := fmt.Sprintf("/%s/-/packages/maven/%s-%s/%s", user.Name, groupID, artifactID, partialVersion) + pkgUIURL := fmt.Sprintf("/%s/-/packages/maven/%s:%s/%s", user.Name, groupID, artifactID, partialVersion) req := NewRequest(t, "GET", pkgUIURL) resp := MakeRequest(t, req, http.StatusOK) assert.NotContains(t, resp.Body.String(), "Internal server error") diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go index 3690454714..89faa7a67f 100644 --- a/tests/integration/api_packages_rpm_test.go +++ b/tests/integration/api_packages_rpm_test.go @@ -24,8 +24,8 @@ import ( "forgejo.org/modules/util" "forgejo.org/tests" + "code.forgejo.org/forgejo/go-rpmutils" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/sassoftware/go-rpmutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index d5f7960ab1..f25948989a 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -430,6 +430,30 @@ func TestAPIExternalAssetRelease(t *testing.T) { assert.Equal(t, "external", attachment.Type) } +func TestAPIAllowedAPIURLInRelease(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, owner.LowerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + r := createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test") + internalURL := "https://localhost:3003/api/packages/owner/generic/test/1.0.0/test.txt" + + req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=%s", owner.Name, repo.Name, r.ID, url.QueryEscape(internalURL))). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + + var attachment *api.Attachment + DecodeJSON(t, resp, &attachment) + + assert.Equal(t, "test-asset", attachment.Name) + assert.EqualValues(t, 0, attachment.Size) + assert.Equal(t, internalURL, attachment.DownloadURL) + assert.Equal(t, "external", attachment.Type) +} + func TestAPIDuplicateAssetRelease(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/api_repo_actions_test.go b/tests/integration/api_repo_actions_test.go index 43de376421..af2aa10cfd 100644 --- a/tests/integration/api_repo_actions_test.go +++ b/tests/integration/api_repo_actions_test.go @@ -101,3 +101,143 @@ jobs: assert.Len(t, run.Jobs, 2) }) } + +func TestAPIGetListActionRun(t *testing.T) { + defer tests.PrepareTestEnv(t)() + var ( + runIDs = []int64{892, 893, 894} + dbRuns = make(map[int64]*actions_model.ActionRun, 3) + ) + + for _, id := range runIDs { + dbRuns[id] = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: id}) + } + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: dbRuns[runIDs[0]].RepoID}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + token := getUserToken(t, user.LowerName, auth_model.AccessTokenScopeWriteRepository) + + testqueries := []struct { + name string + query string + expectedIDs []int64 + }{ + { + name: "No query parameters", + query: "", + expectedIDs: runIDs, + }, + { + name: "Search for workflow_dispatch events", + query: "?event=workflow_dispatch", + expectedIDs: []int64{894}, + }, + { + name: "Search for multiple events", + query: "?event=workflow_dispatch&event=push", + expectedIDs: []int64{892, 894}, + }, + { + name: "Search for failed status", + query: "?status=failure", + expectedIDs: []int64{893}, + }, + { + name: "Search for multiple statuses", + query: "?status=failure&status=running", + expectedIDs: []int64{893, 894}, + }, + { + name: "Search for num_nr", + query: "?run_number=1", + expectedIDs: []int64{892}, + }, + { + name: "Search for sha", + query: "?head_sha=97f29ee599c373c729132a5c46a046978311e0ee", + expectedIDs: []int64{892, 894}, + }, + } + + for _, tt := range testqueries { + t.Run(tt.name, func(t *testing.T) { + req := NewRequest(t, http.MethodGet, + fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs%s", + repo.OwnerName, repo.Name, tt.query, + ), + ) + req.AddTokenAuth(token) + + res := MakeRequest(t, req, http.StatusOK) + apiRuns := new(api.ListActionRunResponse) + DecodeJSON(t, res, apiRuns) + + assert.Equal(t, int64(len(tt.expectedIDs)), apiRuns.TotalCount) + assert.Len(t, apiRuns.Entries, len(tt.expectedIDs)) + + resultIDs := make([]int64, apiRuns.TotalCount) + for i, run := range apiRuns.Entries { + resultIDs[i] = run.ID + } + + assert.ElementsMatch(t, tt.expectedIDs, resultIDs) + }) + } +} + +func TestAPIGetActionRun(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 63}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + token := getUserToken(t, user.LowerName, auth_model.AccessTokenScopeWriteRepository) + + testqueries := []struct { + name string + runID int64 + expectedStatus int + }{ + { + name: "existing return ok", + runID: 892, + expectedStatus: http.StatusOK, + }, + { + name: "non existing run", + runID: 9876543210, // I hope this run will not exists, else just change it to another. + expectedStatus: http.StatusNotFound, + }, + { + name: "existing run but wrong repo should not be found", + runID: 891, + expectedStatus: http.StatusNotFound, + }, + } + + for _, tt := range testqueries { + t.Run(tt.name, func(t *testing.T) { + req := NewRequest(t, http.MethodGet, + fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", + repo.OwnerName, repo.Name, tt.runID, + ), + ) + req.AddTokenAuth(token) + + res := MakeRequest(t, req, tt.expectedStatus) + + // Only interested in the data if 200 OK + if tt.expectedStatus != http.StatusOK { + return + } + + dbRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: tt.runID}) + apiRun := new(api.ActionRun) + DecodeJSON(t, res, apiRun) + + assert.Equal(t, dbRun.Index, apiRun.Index) + assert.Equal(t, dbRun.Status.String(), apiRun.Status) + assert.Equal(t, dbRun.CommitSHA, apiRun.CommitSHA) + assert.Equal(t, dbRun.TriggerUserID, apiRun.TriggerUser.ID) + }) + } +} diff --git a/tests/integration/api_repo_file_helpers.go b/tests/integration/api_repo_file_helpers.go index 09cf93d8a5..61407bf1bf 100644 --- a/tests/integration/api_repo_file_helpers.go +++ b/tests/integration/api_repo_file_helpers.go @@ -10,6 +10,7 @@ import ( repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" "forgejo.org/modules/git" + "forgejo.org/modules/gitrepo" api "forgejo.org/modules/structs" files_service "forgejo.org/services/repository/files" ) @@ -30,7 +31,12 @@ func createFileInBranch(user *user_model.User, repo *repo_model.Repository, tree return files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts) } -func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName string) (*api.FilesResponse, error) { +func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName string) error { + commitID, err := gitrepo.GetBranchCommitID(git.DefaultContext, repo, branchName) + if err != nil { + return err + } + opts := &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ { @@ -38,16 +44,17 @@ func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, tree TreePath: treePath, }, }, - OldBranch: branchName, - Author: nil, - Committer: nil, + OldBranch: branchName, + Author: nil, + Committer: nil, + LastCommitID: commitID, } - return files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts) + _, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts) + return err } func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error { - _, err := deleteFileInBranch(user, repo, treePath, branchName) - + err := deleteFileInBranch(user, repo, treePath, branchName) if err != nil && !models.IsErrRepoFileDoesNotExist(err) { return err } diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 61deb10c92..878d865aff 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -214,7 +214,7 @@ func TestAPIUpdateFile(t *testing.T) { updateFileOptions.SHA = "badsha" req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &updateFileOptions). AddTokenAuth(token2) - resp = MakeRequest(t, req, http.StatusUnprocessableEntity) + resp = MakeRequest(t, req, http.StatusConflict) expectedAPIError := context.APIError{ Message: "sha does not match [given: " + updateFileOptions.SHA + ", expected: " + correctSHA + "]", URL: setting.API.SwaggerURL, diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go index 1772dec6a6..6b1edd047b 100644 --- a/tests/integration/api_repo_files_change_test.go +++ b/tests/integration/api_repo_files_change_test.go @@ -214,7 +214,7 @@ func TestAPIChangeFiles(t *testing.T) { changeFilesOptions.Files[0].SHA = "badsha" req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions). AddTokenAuth(token2) - resp = MakeRequest(t, req, http.StatusUnprocessableEntity) + resp = MakeRequest(t, req, http.StatusConflict) expectedAPIError := context.APIError{ Message: "sha does not match [given: " + changeFilesOptions.Files[0].SHA + ", expected: " + correctSHA + "]", URL: setting.API.SwaggerURL, diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go index 980fff1e52..a4424a3348 100644 --- a/tests/integration/api_repo_git_blobs_test.go +++ b/tests/integration/api_repo_git_blobs_test.go @@ -37,7 +37,7 @@ func TestAPIReposGitBlobs(t *testing.T) { // Test a public repo that anyone can GET the blob of req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA) resp := MakeRequest(t, req, http.StatusOK) - var gitBlobResponse api.GitBlobResponse + var gitBlobResponse api.GitBlob DecodeJSON(t, resp, &gitBlobResponse) assert.NotNil(t, gitBlobResponse) expectedContent := "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK" diff --git a/tests/integration/api_repo_languages_test.go b/tests/integration/api_repo_languages_test.go index 3572e2a8e1..d9a7f38db1 100644 --- a/tests/integration/api_repo_languages_test.go +++ b/tests/integration/api_repo_languages_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -35,9 +34,6 @@ func TestRepoLanguages(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - // let gitea calculate language stats - time.Sleep(time.Second) - // Save new file to master branch req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/languages") resp = MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go index 078cedf78e..b8ba54f876 100644 --- a/tests/integration/api_repo_lfs_test.go +++ b/tests/integration/api_repo_lfs_test.go @@ -21,6 +21,7 @@ import ( "forgejo.org/modules/json" "forgejo.org/modules/lfs" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -228,9 +229,7 @@ func TestAPILFSBatch(t *testing.T) { t.Run("FileTooBig", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - - oldMaxFileSize := setting.LFS.MaxFileSize - setting.LFS.MaxFileSize = 2 + defer test.MockVariableValue(&setting.LFS.MaxFileSize, 2)() req := newRequest(t, &lfs.BatchRequest{ Operation: "upload", @@ -245,8 +244,6 @@ func TestAPILFSBatch(t *testing.T) { assert.NotNil(t, br.Objects[0].Error) assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code) assert.Equal(t, "Size must be less than or equal to 2", br.Objects[0].Error.Message) - - setting.LFS.MaxFileSize = oldMaxFileSize }) t.Run("AddMeta", func(t *testing.T) { diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index b890f7df43..fd62670eb3 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -376,11 +376,11 @@ func TestAPIRepoMigrate(t *testing.T) { cloneURL, repoName string expectedStatus int }{ - {ctxUserID: 1, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-admin", expectedStatus: http.StatusCreated}, - {ctxUserID: 2, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-own", expectedStatus: http.StatusCreated}, - {ctxUserID: 2, userID: 1, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad", expectedStatus: http.StatusForbidden}, - {ctxUserID: 2, userID: 3, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-org", expectedStatus: http.StatusCreated}, - {ctxUserID: 2, userID: 6, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden}, + {ctxUserID: 1, userID: 2, cloneURL: "https://code.forgejo.org/forgejo/migration-test.git", repoName: "git-admin", expectedStatus: http.StatusCreated}, + {ctxUserID: 2, userID: 2, cloneURL: "https://code.forgejo.org/forgejo/migration-test.git", repoName: "git-own", expectedStatus: http.StatusCreated}, + {ctxUserID: 2, userID: 1, cloneURL: "https://code.forgejo.org/forgejo/migration-test.git", repoName: "git-bad", expectedStatus: http.StatusForbidden}, + {ctxUserID: 2, userID: 3, cloneURL: "https://code.forgejo.org/forgejo/migration-test.git", repoName: "git-org", expectedStatus: http.StatusCreated}, + {ctxUserID: 2, userID: 6, cloneURL: "https://code.forgejo.org/forgejo/migration-test.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden}, {ctxUserID: 2, userID: 3, cloneURL: "https://localhost:3000/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity}, {ctxUserID: 2, userID: 3, cloneURL: "https://10.0.0.1/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity}, } @@ -394,22 +394,10 @@ func TestAPIRepoMigrate(t *testing.T) { CloneAddr: testCase.cloneURL, RepoOwnerID: testCase.userID, RepoName: testCase.repoName, + Wiki: true, }).AddTokenAuth(token) resp := MakeRequest(t, req, NoExpectedStatus) - if resp.Code == http.StatusUnprocessableEntity { - respJSON := map[string]string{} - DecodeJSON(t, resp, &respJSON) - switch respJSON["message"] { - case "Remote visit addressed rate limitation.": - t.Log("test hit github rate limitation") - case "You can not import from disallowed hosts.": - assert.Equal(t, "private-ip", testCase.repoName) - default: - assert.FailNow(t, "unexpected error", "'%v' on url '%s'", respJSON["message"], testCase.cloneURL) - } - } else { - assert.Equal(t, testCase.expectedStatus, resp.Code) - } + require.Equalf(t, testCase.expectedStatus, resp.Code, "unexpected status (may be due to throttling): '%v' on url '%s'", resp.Body.String(), testCase.cloneURL) } } @@ -433,7 +421,7 @@ func testAPIRepoMigrateConflict(t *testing.T, u *url.URL) { require.NoError(t, err) userID := user.ID - cloneURL := "https://github.com/go-gitea/test_repo.git" + cloneURL := "https://code.forgejo.org/forgejo/migration-test.git" req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{ diff --git a/tests/integration/cmd_admin_test.go b/tests/integration/cmd_admin_test.go index 043ecef346..c06f7f7213 100644 --- a/tests/integration/cmd_admin_test.go +++ b/tests/integration/cmd_admin_test.go @@ -4,21 +4,24 @@ package integration import ( + "fmt" "net/url" "testing" + auth_model "forgejo.org/models/auth" "forgejo.org/models/db" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/tests" + "github.com/go-webauthn/webauthn/webauthn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_Cmd_AdminUser(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { - for _, testCase := range []struct { + for i, testCase := range []struct { name string options []string mustChangePassword bool @@ -46,7 +49,7 @@ func Test_Cmd_AdminUser(t *testing.T) { } { t.Run(testCase.name, func(t *testing.T) { defer tests.PrintCurrentTest(t)() - name := "testuser" + name := fmt.Sprintf("testuser%d", i) options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"} options = append(options, testCase.options...) @@ -147,3 +150,41 @@ func Test_Cmd_AdminFirstUser(t *testing.T) { } }) } + +func Test_Cmd_AdminUserResetMFA(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + name := "testuser" + + options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"} + output, err := runMainApp("admin", options...) + require.NoError(t, err) + assert.Contains(t, output, "has been successfully created") + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: name}) + + twoFactor := &auth_model.TwoFactor{ + UID: user.ID, + } + token := twoFactor.GenerateScratchToken() + require.NoError(t, auth_model.NewTwoFactor(t.Context(), twoFactor, token)) + twoFactor, err = auth_model.GetTwoFactorByUID(t.Context(), user.ID) + require.NoError(t, err) + require.NotNil(t, twoFactor) + + authn, err := auth_model.CreateCredential(t.Context(), user.ID, "test", &webauthn.Credential{}) + require.NoError(t, err) + + options = []string{"user", "reset-mfa", "--username", name} + output, err = runMainApp("admin", options...) + require.NoError(t, err) + assert.Contains(t, output, "two-factor authentication settings have been removed") + + _, err = auth_model.GetTwoFactorByUID(t.Context(), user.ID) + require.ErrorContains(t, err, "user not enrolled in 2FA") + + _, err = auth_model.GetWebAuthnCredentialByID(t.Context(), authn.ID) + require.ErrorContains(t, err, "WebAuthn credential does not exist") + + _, err = runMainApp("admin", "user", "delete", "--username", name) + require.NoError(t, err) + }) +} diff --git a/tests/integration/cmd_forgejo_f3_test.go b/tests/integration/cmd_forgejo_f3_test.go index 08f8289e61..19390eec90 100644 --- a/tests/integration/cmd_forgejo_f3_test.go +++ b/tests/integration/cmd_forgejo_f3_test.go @@ -25,7 +25,7 @@ import ( f3_tests "code.forgejo.org/f3/gof3/v3/tree/tests/f3" f3_tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge" "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func runApp(ctx context.Context, args ...string) (string, error) { @@ -33,10 +33,10 @@ func runApp(ctx context.Context, args ...string) (string, error) { ctx = f3_logger.ContextSetLogger(ctx, l) ctx = forgejo.ContextSetNoInit(ctx, true) - app := cli.NewApp() + app := cli.Command{} - app.Writer = l.GetBuffer() - app.ErrWriter = l.GetBuffer() + app.Root().Writer = l.GetBuffer() + app.Root().ErrWriter = l.GetBuffer() defer func() { if r := recover(); r != nil { @@ -48,7 +48,7 @@ func runApp(ctx context.Context, args ...string) (string, error) { app.Commands = []*cli.Command{ forgejo.SubcmdF3Mirror(ctx), } - err := app.Run(args) + err := app.Run(ctx, args) fmt.Println(l.String()) diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index c76055c4a8..55d97a3a1d 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -24,8 +24,8 @@ func Test_CmdKeys(t *testing.T) { wantErr bool expectedOutput string }{ - {"test_empty_1", []string{"--username=git", "--type=test", "--content=test"}, true, ""}, - {"test_empty_2", []string{"-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""}, + {"test_empty_1", []string{"--username=git", "--type=test", "--content=test"}, true, "Command error: internal API error response, status=500, err=public key does not exist [id: 0]\n"}, + {"test_empty_2", []string{"-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, "Command error: internal API error response, status=500, err=public key does not exist [id: 0]\n"}, { "with_key", []string{"-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="}, @@ -44,10 +44,11 @@ func Test_CmdKeys(t *testing.T) { } if tt.wantErr { require.Error(t, err) + assert.Equal(t, tt.expectedOutput, string(exitErr.Stderr)) } else { require.NoError(t, err) + assert.Equal(t, tt.expectedOutput, out) } - assert.Equal(t, tt.expectedOutput, out) }) } }) diff --git a/tests/integration/create_no_session_test.go b/tests/integration/create_no_session_test.go index ed547c5b5a..7a3b902eca 100644 --- a/tests/integration/create_no_session_test.go +++ b/tests/integration/create_no_session_test.go @@ -12,6 +12,7 @@ import ( "forgejo.org/modules/json" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "forgejo.org/routers" "forgejo.org/tests" @@ -53,16 +54,11 @@ func sessionFileExist(t *testing.T, tmpDir, sessionID string) bool { func TestSessionFileCreation(t *testing.T) { defer tests.PrepareTestEnv(t)() - - oldSessionConfig := setting.SessionConfig.ProviderConfig - defer func() { - setting.SessionConfig.ProviderConfig = oldSessionConfig - testWebRoutes = routers.NormalRoutes() - }() + defer test.MockProtect(&setting.SessionConfig.ProviderConfig)() + defer test.MockProtect(&testWebRoutes)() var config session.Options - - err := json.Unmarshal([]byte(oldSessionConfig), &config) + err := json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &config) require.NoError(t, err) config.Provider = "file" diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go index 5b84dae823..bf55bdd8ee 100644 --- a/tests/integration/db_collation_test.go +++ b/tests/integration/db_collation_test.go @@ -7,7 +7,6 @@ package integration import ( "net/http" "testing" - "time" "forgejo.org/models/db" "forgejo.org/modules/setting" @@ -97,7 +96,6 @@ func TestDatabaseCollation(t *testing.T) { defer test.MockVariableValue(&setting.Database.CharsetCollation, "utf8mb4_bin")() require.NoError(t, db.ConvertDatabaseTable()) - time.Sleep(5 * time.Second) r, err := db.CheckCollations(x) require.NoError(t, err) @@ -118,7 +116,6 @@ func TestDatabaseCollation(t *testing.T) { defer test.MockVariableValue(&setting.Database.CharsetCollation, "utf8mb4_general_ci")() require.NoError(t, db.ConvertDatabaseTable()) - time.Sleep(5 * time.Second) r, err := db.CheckCollations(x) require.NoError(t, err) @@ -139,7 +136,6 @@ func TestDatabaseCollation(t *testing.T) { defer test.MockVariableValue(&setting.Database.CharsetCollation, "")() require.NoError(t, db.ConvertDatabaseTable()) - time.Sleep(5 * time.Second) r, err := db.CheckCollations(x) require.NoError(t, err) diff --git a/tests/integration/fixtures/TestAdminDeleteUser/repository.yml b/tests/integration/fixtures/TestAdminDeleteUser/repository.yml index 2c12c7e1de..d71232d834 100644 --- a/tests/integration/fixtures/TestAdminDeleteUser/repository.yml +++ b/tests/integration/fixtures/TestAdminDeleteUser/repository.yml @@ -28,3 +28,4 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + topics: '[]' diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go index bd7a464354..22dd127a4c 100644 --- a/tests/integration/git_push_test.go +++ b/tests/integration/git_push_test.go @@ -27,6 +27,10 @@ import ( func forEachObjectFormat(t *testing.T, f func(t *testing.T, objectFormat git.ObjectFormat)) { for _, objectFormat := range []git.ObjectFormat{git.Sha256ObjectFormat, git.Sha1ObjectFormat} { + if !git.SupportHashSha256 && objectFormat == git.Sha256ObjectFormat { + continue + } + t.Run(objectFormat.Name(), func(t *testing.T) { f(t, objectFormat) }) diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 391d3b6672..e79f6fe802 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -745,9 +745,6 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { // Call API to add Success status for commit t.Run("CreateStatus", addCommitStatus(api.CommitStatusSuccess)) - // wait to let gitea merge stuff - time.Sleep(time.Second) - // test pr status pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) require.NoError(t, err) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index a18e3b0abb..34a975de44 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -19,6 +19,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strconv" "strings" "sync/atomic" @@ -40,6 +41,7 @@ import ( "forgejo.org/routers" "forgejo.org/services/auth/source/remote" gitea_context "forgejo.org/services/context" + "forgejo.org/services/mailer" user_service "forgejo.org/services/user" "forgejo.org/tests" @@ -109,6 +111,9 @@ func runMainAppWithStdin(stdin io.Reader, subcommand string, args ...string) (st "GITEA_WORK_DIR="+setting.AppWorkPath) cmd.Stdin = stdin out, err := cmd.Output() + if ee, ok := err.(*exec.ExitError); ok { + log.Error("%s %v exit on error %s", os.Args[0], args, ee.Stderr) + } return string(out), err } @@ -691,3 +696,9 @@ func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string { doc := NewHTMLParser(t, resp.Body) return doc.Find("head title").Text() } + +func SortMailerMessages(msgs []*mailer.Message) { + slices.SortFunc(msgs, func(a, b *mailer.Message) int { + return strings.Compare(b.To, a.To) + }) +} diff --git a/tests/integration/issue_comment_test.go b/tests/integration/issue_comment_test.go new file mode 100644 index 0000000000..f77bfaa9bd --- /dev/null +++ b/tests/integration/issue_comment_test.go @@ -0,0 +1,369 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "net/http" + "strings" + "testing" + + "forgejo.org/tests" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" +) + +func testIssueCommentChangeEvent(t *testing.T, htmlDoc *HTMLDoc, commentID, badgeOcticon, avatarTitle, avatarLink string, texts, links []string) { + // Check badge octicon + badge := htmlDoc.Find("#issuecomment-" + commentID + " .badge svg." + badgeOcticon) + assert.Equal(t, 1, badge.Length()) + + // Check avatar title + avatarImg := htmlDoc.Find("#issuecomment-" + commentID + " img.avatar") + if len(avatarTitle) == 0 { + assert.Zero(t, avatarImg.Length()) + } else { + assert.Equal(t, 1, avatarImg.Length()) + title, exists := avatarImg.Attr("title") + assert.True(t, exists) + assert.Equal(t, avatarTitle, title) + } + + // Check avatar link + avatarA := htmlDoc.Find("#issuecomment-" + commentID + " a.avatar") + if len(avatarLink) == 0 { + assert.Zero(t, avatarA.Length()) + } else { + assert.Equal(t, 1, avatarA.Length()) + href, exists := avatarA.Attr("href") + assert.True(t, exists) + assert.Equal(t, avatarLink, href) + } + + event := htmlDoc.Find("#issuecomment-" + commentID + " .text") + + // Check text content + for _, text := range texts { + assert.Contains(t, strings.Join(strings.Fields(event.Text()), " "), text) + } + + var ids []string + var hrefs []string + event.Find("a").Each(func(i int, s *goquery.Selection) { + if id, exists := s.Attr("id"); exists { + ids = append(ids, id) + } + if href, exists := s.Attr("href"); exists { + hrefs = append(hrefs, href) + } + }) + + // Check anchors (id) + assert.Equal(t, []string{"event-" + commentID}, ids) + + // Check links (href) + issueCommentLink := "#issuecomment-" + commentID + found := false + for _, link := range links { + if link == issueCommentLink { + found = true + break + } + } + if !found { + links = append(links, issueCommentLink) + } + assert.Equal(t, links, hrefs) +} + +func TestIssueCommentChangeMilestone(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Add milestone + testIssueCommentChangeEvent(t, htmlDoc, "2000", + "octicon-milestone", "User One", "/user1", + []string{"user1 added this to the milestone1 milestone"}, + []string{"/user1", "/user2/repo1/milestone/1"}) + + // Modify milestone + testIssueCommentChangeEvent(t, htmlDoc, "2001", + "octicon-milestone", "User One", "/user1", + []string{"user1 modified the milestone from milestone1 to milestone2"}, + []string{"/user1", "/user2/repo1/milestone/1", "/user2/repo1/milestone/2"}) + + // Remove milestone + testIssueCommentChangeEvent(t, htmlDoc, "2002", + "octicon-milestone", "User One", "/user1", + []string{"user1 removed this from the milestone2 milestone"}, + []string{"/user1", "/user2/repo1/milestone/2"}) + + // Deleted milestone + testIssueCommentChangeEvent(t, htmlDoc, "2003", + "octicon-milestone", "User One", "/user1", + []string{"user1 added this to the (deleted) milestone"}, + []string{"/user1"}) +} + +func TestIssueCommentChangeProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Add project + testIssueCommentChangeEvent(t, htmlDoc, "2010", + "octicon-project", "User One", "/user1", + []string{"user1 added this to the First project project"}, + []string{"/user1", "/user2/repo1/projects/1"}) + + // Modify project + testIssueCommentChangeEvent(t, htmlDoc, "2011", + "octicon-project", "User One", "/user1", + []string{"user1 modified the project from First project to second project"}, + []string{"/user1", "/user2/repo1/projects/1", "/user2/repo1/projects/2"}) + + // Remove project + testIssueCommentChangeEvent(t, htmlDoc, "2012", + "octicon-project", "User One", "/user1", + []string{"user1 removed this from the second project project"}, + []string{"/user1", "/user2/repo1/projects/2"}) + + // Deleted project + testIssueCommentChangeEvent(t, htmlDoc, "2013", + "octicon-project", "User One", "/user1", + []string{"user1 added this to the (deleted) project"}, + []string{"/user1"}) +} + +func TestIssueCommentChangeLabel(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Add multiple labels + testIssueCommentChangeEvent(t, htmlDoc, "2020", + "octicon-tag", "User One", "/user1", + []string{"user1 added the label1 label2 labels "}, + []string{"/user1", "/user2/repo1/issues?labels=1", "/user2/repo1/issues?labels=2"}) + assert.Empty(t, htmlDoc.Find("#issuecomment-2021 .text").Text()) + + // Remove single label + testIssueCommentChangeEvent(t, htmlDoc, "2022", + "octicon-tag", "< Ur Tw ><", "/user2", + []string{"user2 removed the label1 label "}, + []string{"/user2", "/user2/repo1/issues?labels=1"}) + + // Modify labels (add and remove) + testIssueCommentChangeEvent(t, htmlDoc, "2023", + "octicon-tag", "User One", "/user1", + []string{"user1 added label1 and removed label2 labels "}, + []string{"/user1", "/user2/repo1/issues?labels=1", "/user2/repo1/issues?labels=2"}) + assert.Empty(t, htmlDoc.Find("#issuecomment-2024 .text").Text()) + + // Add single label + testIssueCommentChangeEvent(t, htmlDoc, "2025", + "octicon-tag", "< Ur Tw ><", "/user2", + []string{"user2 added the label2 label "}, + []string{"/user2", "/user2/repo1/issues?labels=2"}) + + // Remove multiple labels + testIssueCommentChangeEvent(t, htmlDoc, "2026", + "octicon-tag", "User One", "/user1", + []string{"user1 removed the label1 label2 labels "}, + []string{"/user1", "/user2/repo1/issues?labels=1", "/user2/repo1/issues?labels=2"}) + assert.Empty(t, htmlDoc.Find("#issuecomment-2027 .text").Text()) +} + +func TestIssueCommentChangeAssignee(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Self-assign + testIssueCommentChangeEvent(t, htmlDoc, "2040", + "octicon-person", "User One", "/user1", + []string{"user1 self-assigned this"}, + []string{"/user1"}) + + // Remove other + testIssueCommentChangeEvent(t, htmlDoc, "2041", + "octicon-person", "User One", "/user1", + []string{"user1 was unassigned by user2"}, + []string{"/user1"}) + // []string{"/user1", "/user2"}) + + // Add other + testIssueCommentChangeEvent(t, htmlDoc, "2042", + "octicon-person", "< Ur Tw ><", "/user2", + []string{"user2 was assigned by user1"}, + []string{"/user2"}) + // []string{"/user2", "/user1"}) + + // Self-remove + testIssueCommentChangeEvent(t, htmlDoc, "2043", + "octicon-person", "< Ur Tw ><", "/user2", + []string{"user2 removed their assignment"}, + []string{"/user2"}) +} + +func TestIssueCommentChangeLock(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Lock without reason + testIssueCommentChangeEvent(t, htmlDoc, "2050", + "octicon-lock", "User One", "/user1", + []string{"user1 locked and limited conversation to collaborators"}, + []string{"/user1"}) + + // Unlock + testIssueCommentChangeEvent(t, htmlDoc, "2051", + "octicon-key", "User One", "/user1", + []string{"user1 unlocked this conversation"}, + []string{"/user1"}) + + // Lock with reason + testIssueCommentChangeEvent(t, htmlDoc, "2052", + "octicon-lock", "User One", "/user1", + []string{"user1 locked as Too heated and limited conversation to collaborators"}, + []string{"/user1"}) + + // Unlock + testIssueCommentChangeEvent(t, htmlDoc, "2053", + "octicon-key", "User One", "/user1", + []string{"user1 unlocked this conversation"}, + []string{"/user1"}) +} + +func TestIssueCommentChangePin(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Pin + testIssueCommentChangeEvent(t, htmlDoc, "2060", + "octicon-pin", "User One", "/user1", + []string{"user1 pinned this"}, + []string{"/user1"}) + + // Unpin + testIssueCommentChangeEvent(t, htmlDoc, "2061", + "octicon-pin", "User One", "/user1", + []string{"user1 unpinned this"}, + []string{"/user1"}) +} + +func TestIssueCommentChangeOpen(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Close issue + testIssueCommentChangeEvent(t, htmlDoc, "2070", + "octicon-circle-slash", "User One", "/user1", + []string{"user1 closed this issue"}, + []string{"/user1"}) + + // Reopen issue + testIssueCommentChangeEvent(t, htmlDoc, "2071", + "octicon-dot-fill", "< Ur Tw ><", "/user2", + []string{"user2 reopened this issue"}, + []string{"/user2"}) + + req = NewRequest(t, "GET", "/user2/repo1/pulls/2") + resp = MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + // Close pull request + testIssueCommentChangeEvent(t, htmlDoc, "2072", + "octicon-circle-slash", "User One", "/user1", + []string{"user1 closed this pull request"}, + []string{"/user1"}) + + // Reopen pull request + testIssueCommentChangeEvent(t, htmlDoc, "2073", + "octicon-dot-fill", "< Ur Tw ><", "/user2", + []string{"user2 reopened this pull request"}, + []string{"/user2"}) +} + +func TestIssueCommentChangeIssueReference(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/issues/1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Issue reference from issue + testIssueCommentChangeEvent(t, htmlDoc, "2080", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this issue ", "issue5 #4"}, + []string{"/user1", "/user2/repo1/issues/4", "#issuecomment-2080", "/user2/repo1/issues/4"}) + + // Issue reference from pull + testIssueCommentChangeEvent(t, htmlDoc, "2081", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this issue ", "issue2 #2"}, + []string{"/user1", "/user2/repo1/pulls/2", "#issuecomment-2081", "/user2/repo1/pulls/2"}) + + // Issue reference from issue in different repo + testIssueCommentChangeEvent(t, htmlDoc, "2082", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this issue from org3/repo21", "just a normal issue #1"}, + []string{"/user1", "/org3/repo21/issues/1", "#issuecomment-2082", "/org3/repo21/issues/1"}) + + // Issue reference from pull in different repo + testIssueCommentChangeEvent(t, htmlDoc, "2083", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this issue from user12/repo10 ", "pr2 #1"}, + []string{"/user1", "/user12/repo10/pulls/1", "#issuecomment-2083", "/user12/repo10/pulls/1"}) +} + +func TestIssueCommentChangePullReference(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/pulls/2") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Pull reference from issue + testIssueCommentChangeEvent(t, htmlDoc, "2090", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this pull request ", "issue1 #1"}, + []string{"/user1", "/user2/repo1/issues/1", "#issuecomment-2090", "/user2/repo1/issues/1"}) + + // Pull reference from pull + testIssueCommentChangeEvent(t, htmlDoc, "2091", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this pull request ", "issue2 #2"}, + []string{"/user1", "/user2/repo1/pulls/2", "#issuecomment-2091", "/user2/repo1/pulls/2"}) + + // Pull reference from issue in different repo + testIssueCommentChangeEvent(t, htmlDoc, "2092", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this pull request from org3/repo21", "just a normal issue #1"}, + []string{"/user1", "/org3/repo21/issues/1", "#issuecomment-2092", "/org3/repo21/issues/1"}) + + // Pull reference from pull in different repo + testIssueCommentChangeEvent(t, htmlDoc, "2093", + "octicon-bookmark", "User One", "/user1", + []string{"user1 referenced this pull request from user12/repo10 ", "pr2 #1"}, + []string{"/user1", "/user12/repo10/pulls/1", "#issuecomment-2093", "/user12/repo10/pulls/1"}) +} diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 7944d9fcd6..120fe6534e 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -123,7 +123,6 @@ func TestViewIssuesKeyword(t *testing.T) { Index: 1, }) issues.UpdateIssueIndexer(t.Context(), issue.ID) - time.Sleep(time.Second * 1) const keyword = "first" req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword) @@ -1439,6 +1438,47 @@ func TestIssueCount(t *testing.T) { assert.Contains(t, allCount, "2\u00a0All") } +func TestIssueDefaultValues(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + links := []string{"/user2/repo1/issues", "/user2/repo1/pulls"} + values := []url.Values{ + { + "type": {"created_by"}, + }, + { + "poster": {"1"}, + }, + { + "sort": {"latest"}, + }, + { + "type": {"all"}, + "sort": {"latest"}, + "poster": {"1"}, + }, + { + "type": {"assigned"}, + "sort": {"oldest"}, + "poster": {"1"}, + }, + } + + for _, link := range links { + t.Run(link[13:], func(t *testing.T) { + for _, value := range values { + req := NewRequestf(t, "GET", "%s?%s", link, value.Encode()) + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + for name := range value { + assert.Equal(t, value.Get(name), htmlDoc.GetInputValueByName(name)) + } + } + }) + } +} + func TestIssuePostersSearch(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 556e534ea3..1377d1eb1b 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -301,10 +301,10 @@ func TestSSHPushMirror(t *testing.T) { assert.Eventually(t, func() bool { req = NewRequest(t, "GET", fmt.Sprintf("/%s", pushToRepo.FullName())) - resp = sess.MakeRequest(t, req, http.StatusOK) + resp = sess.MakeRequest(t, req, NoExpectedStatus) htmlDoc = NewHTMLParser(t, resp.Body) - return htmlDoc.Find(".shortsha").Text() == shortSHA + return resp.Code == http.StatusOK && htmlDoc.Find(".shortsha").Text() == shortSHA }, time.Second*30, time.Second) }) diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index a89bb2c510..2b44863ec2 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -632,6 +632,19 @@ func TestSignInOAuthCallbackPKCE(t *testing.T) { }) } +func TestWellKnownDocumentIssuerDoesNotEndWithASlash(t *testing.T) { + defer tests.PrepareTestEnv(t)() + req := NewRequest(t, "GET", "/.well-known/openid-configuration") + resp := MakeRequest(t, req, http.StatusOK) + type response struct { + Issuer string `json:"issuer"` + } + parsed := new(response) + + DecodeJSON(t, resp, parsed) + assert.Equal(t, strings.TrimSuffix(setting.AppURL, "/"), parsed.Issuer) +} + func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) { defer tests.PrepareTestEnv(t)() @@ -697,7 +710,7 @@ func setupMockOIDCServer() *httptest.Server { case "/.well-known/openid-configuration": w.WriteHeader(http.StatusOK) w.Write([]byte(`{ - "issuer": "` + mockServer.URL + `", + "issuer": "` + strings.TrimSuffix(mockServer.URL, "/") + `", "authorization_endpoint": "` + mockServer.URL + `/authorize", "token_endpoint": "` + mockServer.URL + `/token", "userinfo_endpoint": "` + mockServer.URL + `/userinfo" diff --git a/tests/integration/org_profile_test.go b/tests/integration/org_profile_test.go new file mode 100644 index 0000000000..2211a8b3d2 --- /dev/null +++ b/tests/integration/org_profile_test.go @@ -0,0 +1,108 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "strings" + "testing" + + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" + files_service "forgejo.org/services/repository/files" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" +) + +func TestOrgProfile(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + checkReadme := func(t *testing.T, title, readmeFilename string, expectedCount int) { + t.Run(title, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Prepare the test repository + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + + var ops []*files_service.ChangeRepoFile + op := "create" + if readmeFilename != "README.md" { + ops = append(ops, &files_service.ChangeRepoFile{ + Operation: "delete", + TreePath: "README.md", + }) + } else { + op = "update" + } + if readmeFilename != "" { + ops = append(ops, &files_service.ChangeRepoFile{ + Operation: op, + TreePath: readmeFilename, + ContentReader: strings.NewReader("# Hi!\n"), + }) + } + + _, _, f := tests.CreateDeclarativeRepo(t, org3, ".profile", nil, nil, ops) + defer f() + + // Perform the test + req := NewRequest(t, "GET", "/org3") + resp := MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + readmeCount := doc.Find("#readme_profile").Length() + + assert.Equal(t, expectedCount, readmeCount) + }) + } + + checkReadme(t, "No readme", "", 0) + checkReadme(t, "README.md", "README.md", 1) + checkReadme(t, "readme.md", "readme.md", 1) + checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1) + checkReadme(t, "readme.org does not render", "README.org", 0) + + t.Run("readme-size", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Prepare the test repository + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + + _, _, f := tests.CreateDeclarativeRepo(t, org3, ".profile", nil, nil, []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "README.md", + ContentReader: strings.NewReader(`## Lorem ipsum +dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +## Ut enim ad minim veniam +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum`), + }, + }) + defer f() + + t.Run("full", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 500)() + + req := NewRequest(t, "GET", "/org3") + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Ut enim ad minim veniam") + assert.Contains(t, resp.Body.String(), "mollit anim id est laborum") + }) + + t.Run("truncated", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 146)() + + req := NewRequest(t, "GET", "/org3") + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Ut enim ad minim") + assert.NotContains(t, resp.Body.String(), "veniam") + }) + }) + }) +} diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 3ca236a5dd..5abac148ed 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -233,13 +233,7 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { t.Skip() return } - - // enable email confirmation temporarily - defer func(prevVal bool) { - setting.Service.RegisterEmailConfirm = prevVal - }(setting.Service.RegisterEmailConfirm) - setting.Service.RegisterEmailConfirm = true - + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)() defer tests.PrepareTestEnv(t)() org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) diff --git a/tests/integration/patch_status_test.go b/tests/integration/patch_status_test.go index 49eb9c501c..3ce1dc4cb9 100644 --- a/tests/integration/patch_status_test.go +++ b/tests/integration/patch_status_test.go @@ -4,6 +4,7 @@ package integration import ( + "context" "fmt" "net/http" "net/url" @@ -20,7 +21,10 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/optional" + "forgejo.org/modules/test" + pull_service "forgejo.org/services/pull" files_service "forgejo.org/services/repository/files" + shared_automerge "forgejo.org/services/shared/automerge" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -32,10 +36,15 @@ func TestPatchStatus(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) + var objectFormat optional.Option[string] + if git.SupportHashSha256 { + objectFormat = optional.Some("sha256") + } + repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user2, tests.DeclarativeRepoOptions{ AutoInit: optional.Some(true), EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeCode}), - ObjectFormat: optional.Some("sha256"), + ObjectFormat: objectFormat, Files: optional.Some([]*files_service.ChangeRepoFile{ { Operation: "create", @@ -46,6 +55,20 @@ func TestPatchStatus(t *testing.T) { }) defer f() + testAutomergeQueued := func(t *testing.T, pr *issues_model.PullRequest, expected issues_model.PullRequestStatus) { + t.Helper() + + var actual issues_model.PullRequestStatus = -1 + defer test.MockVariableValue(&shared_automerge.AddToQueueIfMergeable, func(ctx context.Context, pull *issues_model.PullRequest) { + actual = pull.Status + })() + + pull_service.AddToTaskQueue(t.Context(), pr) + assert.Eventually(t, func() bool { + return expected == actual + }, time.Second*5, time.Millisecond*200) + } + testRepoFork(t, session, "user2", repo.Name, "org3", "forked-repo") forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "org3", Name: "forked-repo"}) @@ -86,7 +109,9 @@ func TestPatchStatus(t *testing.T) { require.NoError(t, git.NewCommand(t.Context(), "push", "fork", "HEAD:normal").Run(&git.RunOpts{Dir: dstPath})) testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkRepo.OwnerName, forkRepo.Name, "normal", "across repo normal") - test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0")) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0") + test(t, pr) + testAutomergeQueued(t, pr, issues_model.PullRequestStatusMergeable) }) t.Run("Same repository", func(t *testing.T) { @@ -139,7 +164,9 @@ func TestPatchStatus(t *testing.T) { t.Run("Existing", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0")) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0") + test(t, pr) + testAutomergeQueued(t, pr, issues_model.PullRequestStatusConflict) }) t.Run("New", func(t *testing.T) { diff --git a/tests/integration/pull_commit_test.go b/tests/integration/pull_commit_test.go index 90d16d725d..8ca78f8147 100644 --- a/tests/integration/pull_commit_test.go +++ b/tests/integration/pull_commit_test.go @@ -31,3 +31,20 @@ func TestListPullCommits(t *testing.T) { } assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.LastReviewCommitSha) } + +func TestPullCommitLinks(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1/pulls/3/commits") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + commitSha := htmlDoc.Find(".commit-list td.sha a.sha.label").First() + commitShaHref, _ := commitSha.Attr("href") + assert.Equal(t, "/user2/repo1/pulls/3/commits/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commitShaHref) + + commitLink := htmlDoc.Find(".commit-list td.message a").First() + commitLinkHref, _ := commitLink.Attr("href") + assert.Equal(t, "/user2/repo1/pulls/3/commits/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commitLinkHref) +} diff --git a/tests/integration/pull_diff_test.go b/tests/integration/pull_diff_test.go index 70e0d5d33a..6db9fc25d8 100644 --- a/tests/integration/pull_diff_test.go +++ b/tests/integration/pull_diff_test.go @@ -14,22 +14,22 @@ import ( ) func TestPullDiff_CompletePRDiff(t *testing.T) { - doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files", false, []string{"test1.txt", "test10.txt", "test2.txt", "test3.txt", "test4.txt", "test5.txt", "test6.txt", "test7.txt", "test8.txt", "test9.txt"}) + doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files", []string{"test1.txt", "test10.txt", "test2.txt", "test3.txt", "test4.txt", "test5.txt", "test6.txt", "test7.txt", "test8.txt", "test9.txt"}) } func TestPullDiff_SingleCommitPRDiff(t *testing.T) { - doTestPRDiff(t, "/user2/commitsonpr/pulls/1/commits/c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2", true, []string{"test3.txt"}) + doTestPRDiff(t, "/user2/commitsonpr/pulls/1/commits/c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2", []string{"test3.txt"}) } func TestPullDiff_CommitRangePRDiff(t *testing.T) { - doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files/4ca8bcaf27e28504df7bf996819665986b01c847..23576dd018294e476c06e569b6b0f170d0558705", true, []string{"test2.txt", "test3.txt", "test4.txt"}) + doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files/4ca8bcaf27e28504df7bf996819665986b01c847..23576dd018294e476c06e569b6b0f170d0558705", []string{"test2.txt", "test3.txt", "test4.txt"}) } func TestPullDiff_StartingFromBaseToCommitPRDiff(t *testing.T) { - doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files/c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2", true, []string{"test1.txt", "test2.txt", "test3.txt"}) + doTestPRDiff(t, "/user2/commitsonpr/pulls/1/files/c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2", []string{"test1.txt", "test2.txt", "test3.txt"}) } -func doTestPRDiff(t *testing.T, prDiffURL string, reviewBtnDisabled bool, expectedFilenames []string) { +func doTestPRDiff(t *testing.T, prDiffURL string, expectedFilenames []string) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") @@ -52,7 +52,4 @@ func doTestPRDiff(t *testing.T, prDiffURL string, reviewBtnDisabled bool, expect filename, _ := s.Attr("data-old-filename") assert.Equal(t, expectedFilenames[i], filename) }) - - // Ensure the review button is enabled for full PR reviews - assert.Equal(t, reviewBtnDisabled, doc.doc.Find(".js-btn-review").HasClass("disabled")) } diff --git a/tests/integration/pull_files_test.go b/tests/integration/pull_files_test.go new file mode 100644 index 0000000000..eb7072a4e6 --- /dev/null +++ b/tests/integration/pull_files_test.go @@ -0,0 +1,106 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "strings" + "testing" + "time" + + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + "forgejo.org/modules/git" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPullFilesCommitHeader(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Verify commit info", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath()) + require.NoError(t, err) + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit("62fb502a7172d4453f0322a2cc85bddffa57f07a") + require.NoError(t, err) + + req := NewRequest(t, "GET", "/user2/repo1/pulls/5/commits/62fb502a7172d4453f0322a2cc85bddffa57f07a") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + header := htmlDoc.doc.Find("#diff-commit-header") + + summary := header.Find(".commit-header h3") + assert.Equal(t, commit.Summary(), strings.TrimSpace(summary.Text())) + + author := header.Find(".author strong") + assert.Equal(t, commit.Author.Name, author.Text()) + + date, _ := header.Find("#authored-time relative-time").Attr("datetime") + assert.Equal(t, commit.Author.When.Format(time.RFC3339), date) + + sha := header.Find(".commit-header-row .sha.label") + shaHref, _ := sha.Attr("href") + assert.Equal(t, commit.ID.String()[:10], sha.Find(".shortsha").Text()) + assert.Equal(t, "/user2/repo1/commit/62fb502a7172d4453f0322a2cc85bddffa57f07a", shaHref) + }) + + t.Run("Navigation", func(t *testing.T) { + t.Run("No previous on first commit", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/commitsonpr/pulls/1/commits/4ca8bcaf27e28504df7bf996819665986b01c847") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + buttons := htmlDoc.doc.Find(".commit-header-buttons a.small.button") + + assert.Equal(t, 2, buttons.Length(), "expected two buttons in commit header") + + assert.True(t, buttons.First().HasClass("disabled"), "'prev' button should be disabled") + assert.False(t, buttons.Last().HasClass("disabled"), "'next' button should not be disabled") + + href, _ := buttons.Last().Attr("href") + assert.Equal(t, "/user2/commitsonpr/pulls/1/commits/96cef4a7b72b3c208340ae6f0cf55a93e9077c93", href) + }) + + t.Run("No next on last commit", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/commitsonpr/pulls/1/commits/9b93963cf6de4dc33f915bb67f192d099c301f43") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + buttons := htmlDoc.doc.Find(".commit-header-buttons a.small.button") + + assert.Equal(t, 2, buttons.Length(), "expected two buttons in commit header") + + assert.False(t, buttons.First().HasClass("disabled"), "'prev' button should not be disabled") + assert.True(t, buttons.Last().HasClass("disabled"), "'next' button should be disabled") + + href, _ := buttons.First().Attr("href") + assert.Equal(t, "/user2/commitsonpr/pulls/1/commits/2d65d92dc800c6f448541240c18e82bf36b954bb", href) + }) + + t.Run("Both directions on middle commit", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/commitsonpr/pulls/1/commits/c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + buttons := htmlDoc.doc.Find(".commit-header-buttons a.small.button") + + assert.Equal(t, 2, buttons.Length(), "expected two buttons in commit header") + + assert.False(t, buttons.First().HasClass("disabled"), "'prev' button should not be disabled") + assert.False(t, buttons.Last().HasClass("disabled"), "'next' button should not be disabled") + + href, _ := buttons.First().Attr("href") + assert.Equal(t, "/user2/commitsonpr/pulls/1/commits/96cef4a7b72b3c208340ae6f0cf55a93e9077c93", href) + + href, _ = buttons.Last().Attr("href") + assert.Equal(t, "/user2/commitsonpr/pulls/1/commits/23576dd018294e476c06e569b6b0f170d0558705", href) + }) + }) +} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index ad7b80b51a..cca2381fd4 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -585,7 +585,6 @@ func TestPullMergeIndexerNotifier(t *testing.T) { createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull") require.NoError(t, queue.GetManager().FlushAll(t.Context(), 0)) - time.Sleep(time.Second) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ OwnerName: "user2", @@ -624,7 +623,6 @@ func TestPullMergeIndexerNotifier(t *testing.T) { assert.True(t, issue.IsClosed) require.NoError(t, queue.GetManager().FlushAll(t.Context(), 0)) - time.Sleep(time.Second) // search issues again searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) @@ -893,8 +891,6 @@ func testPullAutoMergeAfterCommitStatusSucceed(t *testing.T, ctx APITestContext, }) require.NoError(t, err) - time.Sleep(2 * time.Second) - // approve PR if necessary if approval { // reload PR again @@ -908,8 +904,6 @@ func testPullAutoMergeAfterCommitStatusSucceed(t *testing.T, ctx APITestContext, resp := approveSession.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK) - - time.Sleep(2 * time.Second) } // reload PR again @@ -1090,8 +1084,6 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing. }) require.NoError(t, err) - time.Sleep(2 * time.Second) - // reload pr again pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) assert.False(t, pr.HasMerged) @@ -1104,8 +1096,6 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing. htmlDoc := NewHTMLParser(t, resp.Body) testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK) - time.Sleep(2 * time.Second) - // realod pr again pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) assert.True(t, pr.HasMerged) diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index ed3d3baaf4..603252f45f 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -6,6 +6,7 @@ package integration import ( "fmt" + "io" "net/http" "net/http/httptest" "net/url" @@ -19,6 +20,7 @@ import ( repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" + "forgejo.org/modules/git" "forgejo.org/modules/gitrepo" repo_module "forgejo.org/modules/repository" "forgejo.org/modules/test" @@ -93,16 +95,9 @@ func TestPullView_SelfReviewNotification(t *testing.T) { require.NoError(t, err) // create a new branch to prepare for pull request - _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ - NewBranch: "codeowner-basebranch", - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - TreePath: "README.md", - ContentReader: strings.NewReader("# This is a new project\n"), - }, - }, - }) + err = updateFileInBranch(user2, repo, "README.md", "codeowner-basebranch", + strings.NewReader("# This is a new project\n"), + ) require.NoError(t, err) // Create a pull request. @@ -366,16 +361,9 @@ func TestPullView_CodeOwner(t *testing.T) { defer tests.PrintCurrentTest(t)() // create a new branch to prepare for pull request - _, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ - NewBranch: "codeowner-basebranch", - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - TreePath: "README.md", - ContentReader: strings.NewReader("# This is a new project\n"), - }, - }, - }) + err := updateFileInBranch(user2, repo, "README.md", "codeowner-basebranch", + strings.NewReader("# This is a new project\n"), + ) require.NoError(t, err) // Create a pull request. @@ -400,31 +388,18 @@ func TestPullView_CodeOwner(t *testing.T) { }) // change the default branch CODEOWNERS file to change README.md's codeowner - _, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - TreePath: "CODEOWNERS", - ContentReader: strings.NewReader("README.md @user8\n"), - }, - }, - }) + err := updateFileInBranch(user2, repo, "CODEOWNERS", "", + strings.NewReader("README.md @user8\n"), + ) require.NoError(t, err) t.Run("Second Pull Request", func(t *testing.T) { defer tests.PrintCurrentTest(t)() // create a new branch to prepare for pull request - _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ - NewBranch: "codeowner-basebranch2", - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - TreePath: "README.md", - ContentReader: strings.NewReader("# This is a new project2\n"), - }, - }, - }) + err := updateFileInBranch(user2, repo, "README.md", "codeowner-basebranch2", + strings.NewReader("# This is a new project2\n"), + ) require.NoError(t, err) // Create a pull request. @@ -446,16 +421,9 @@ func TestPullView_CodeOwner(t *testing.T) { require.NoError(t, err) // create a new branch to prepare for pull request - _, err = files_service.ChangeRepoFiles(db.DefaultContext, forkedRepo, user5, &files_service.ChangeRepoFilesOptions{ - NewBranch: "codeowner-basebranch-forked", - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - TreePath: "README.md", - ContentReader: strings.NewReader("# This is a new forked project\n"), - }, - }, - }) + err = updateFileInBranch(user5, forkedRepo, "README.md", "codeowner-basebranch-forked", + strings.NewReader("# This is a new forked project\n"), + ) require.NoError(t, err) session := loginUser(t, "user5") @@ -712,6 +680,7 @@ func TestPullRequestReplyMail(t *testing.T) { called := false defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { assert.Len(t, msgs, 2) + SortMailerMessages(msgs) assert.Equal(t, "user1@example.com", msgs[0].To) assert.Equal(t, "Re: [user2/repo1] issue2 (PR #2)", msgs[0].Subject) assert.Contains(t, msgs[0].Body, "Notification time!") @@ -740,6 +709,7 @@ func TestPullRequestReplyMail(t *testing.T) { called := false defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { assert.Len(t, msgs, 2) + SortMailerMessages(msgs) assert.Equal(t, "user1@example.com", msgs[0].To) assert.Equal(t, "Re: [user2/repo1] issue2 (PR #2)", msgs[0].Subject) assert.Contains(t, msgs[0].Body, "Notification time 2!") @@ -762,3 +732,32 @@ func TestPullRequestReplyMail(t *testing.T) { }) }) } + +func updateFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, newBranch string, content io.ReadSeeker) error { + oldBranch, err := gitrepo.GetDefaultBranch(git.DefaultContext, repo) + if err != nil { + return err + } + + commitID, err := gitrepo.GetBranchCommitID(git.DefaultContext, repo, oldBranch) + if err != nil { + return err + } + + opts := &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: treePath, + ContentReader: content, + }, + }, + OldBranch: oldBranch, + NewBranch: newBranch, + Author: nil, + Committer: nil, + LastCommitID: commitID, + } + _, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts) + return err +} diff --git a/tests/integration/pull_test.go b/tests/integration/pull_test.go index d5321f6ae5..fd9dbf8888 100644 --- a/tests/integration/pull_test.go +++ b/tests/integration/pull_test.go @@ -30,6 +30,43 @@ func TestViewPulls(t *testing.T) { assert.Equal(t, "Search pulls…", placeholder) } +func TestPullViewConversation(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/commitsonpr/pulls/1") + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + t.Run("Commits", func(t *testing.T) { + commitLists := htmlDoc.Find(".timeline-item.commits-list") + assert.Equal(t, 4, commitLists.Length()) + + commits := commitLists.Find(".singular-commit") + assert.Equal(t, 10, commits.Length()) + + // First one has not been affected by a force push, therefore it's still part of the + // PR and should link to the PR-scoped review tab + firstCommit := commits.Eq(0) + firstCommitMessageHref, _ := firstCommit.Find("a.default-link").Attr("href") + firstCommitShaHref, _ := firstCommit.Find("a.sha.label").Attr("href") + assert.Equal(t, "/user2/commitsonpr/pulls/1/commits/4ca8bcaf27e28504df7bf996819665986b01c847", firstCommitMessageHref) + assert.Equal(t, "/user2/commitsonpr/pulls/1/commits/4ca8bcaf27e28504df7bf996819665986b01c847", firstCommitShaHref) + + // The fifth commit has been overwritten by a force push. + // Attempting to view the old one in the review tab won't work: + req := NewRequest(t, "GET", "/user2/commitsonpr/pulls/1/commits/3e64625bd6eb5bcba69ac97de6c8f507402df861") + MakeRequest(t, req, http.StatusNotFound) + + // Therefore, this commit should link to the non-PR commit view instead + fifthCommit := commits.Eq(4) + fifthCommitMessageHref, _ := fifthCommit.Find("a.default-link").Attr("href") + fifthCommitShaHref, _ := fifthCommit.Find("a.sha.label").Attr("href") + assert.Equal(t, "/user2/commitsonpr/commit/3e64625bd6eb5bcba69ac97de6c8f507402df861", fifthCommitMessageHref) + assert.Equal(t, "/user2/commitsonpr/commit/3e64625bd6eb5bcba69ac97de6c8f507402df861", fifthCommitShaHref) + }) +} + func TestPullManuallyMergeWarning(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index e5cf76882c..b5cccc65e8 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -9,7 +9,6 @@ import ( "net/http" "strconv" "testing" - "time" auth_model "forgejo.org/models/auth" "forgejo.org/models/db" @@ -85,9 +84,6 @@ func TestViewReleases(t *testing.T) { session := loginUser(t, "user2") req := NewRequest(t, "GET", "/user2/repo1/releases") session.MakeRequest(t, req, http.StatusOK) - - // if CI is too slow this test fail, so lets wait a bit - time.Sleep(time.Millisecond * 100) } func TestViewReleasesNoLogin(t *testing.T) { diff --git a/tests/integration/repo_commit_test.go b/tests/integration/repo_commit_test.go new file mode 100644 index 0000000000..1e22192cd9 --- /dev/null +++ b/tests/integration/repo_commit_test.go @@ -0,0 +1,84 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "strings" + "testing" + "time" + + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + "forgejo.org/modules/git" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepoCommitHeader(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + t.Run("Verify commit info", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath()) + require.NoError(t, err) + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit("65f1bf27bc3bf70f64657658635e66094edbcb4d") + require.NoError(t, err) + + req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + summary := htmlDoc.Find(".commit-header h3") + assert.Equal(t, commit.Summary(), strings.TrimSpace(summary.Text())) + + author := htmlDoc.Find(".commit-header-row .author strong").First() + assert.Equal(t, commit.Author.Name, author.Text()) + + committer := htmlDoc.Find(".commit-header-row .author strong").Last() + assert.Equal(t, commit.Committer.Name, committer.Text()) + + date, _ := htmlDoc.Find(".commit-header-row #authored-time relative-time").Attr("datetime") + assert.Equal(t, commit.Author.When.Format(time.RFC3339), date) + + sha := htmlDoc.Find(".commit-header-row .sha.label") + assert.Equal(t, commit.ID.String()[:10], sha.Find(".shortsha").Text()) + }) + + t.Run("Verify parent commit ID", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath()) + require.NoError(t, err) + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit("205ac761f3326a7ebe416e8673760016450b5cec") + require.NoError(t, err) + + req := NewRequest(t, "GET", "/user2/repo2/commit/205ac761f3326a7ebe416e8673760016450b5cec") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + shas := htmlDoc.Find(".commit-header-row .sha.label") + assert.Equal(t, 2, shas.Length()) + + parentSha := shas.First() + parentHref, _ := parentSha.Attr("href") + assert.Equal(t, "/user2/repo2/commit/2c54faec6c45d31c1abfaecdab471eac6633738a", parentHref) + + parentID, err := commit.ParentID(0) + require.NoError(t, err) + assert.Equal(t, parentID.String()[:10], parentSha.Find(".shortsha").Text()) + + sha := shas.Last() + assert.Equal(t, commit.ID.String()[:10], sha.Find(".shortsha").Text()) + }) +} diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go index f039b08f06..44987c14b0 100644 --- a/tests/integration/repo_generate_test.go +++ b/tests/integration/repo_generate_test.go @@ -15,6 +15,7 @@ import ( repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" + "forgejo.org/modules/git" "forgejo.org/modules/optional" "forgejo.org/modules/setting" "forgejo.org/modules/test" @@ -43,9 +44,13 @@ func assertRepoCreateForm(t *testing.T, htmlDoc *HTMLDoc, owner *user_model.User // the template menu is loaded client-side, so don't assert the option exists assert.Equal(t, templateID, htmlDoc.GetInputValueByName("repo_template"), "Unexpected repo_template selection") - for _, name := range []string{"issue_labels", "gitignores", "license", "object_format_name"} { + for _, name := range []string{"issue_labels", "gitignores", "license"} { htmlDoc.AssertDropdownHasOptions(t, name) } + + if git.SupportHashSha256 { + htmlDoc.AssertDropdownHasOptions(t, "object_format_name") + } } func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOwnerName, templateRepoName string, user, generateOwner *user_model.User, generateRepoName string) { @@ -126,6 +131,63 @@ func TestRepoCreateForm(t *testing.T) { assertRepoCreateForm(t, htmlDoc, user, "") } +func TestRepoCreateFormRepoLimit(t *testing.T) { + defer tests.PrepareTestEnv(t)() + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + userName := "user2" + session := loginUser(t, userName) + locale := translation.NewLocale("en-US") + cannotCreateTr := locale.Tr("repo.form.cannot_create") + + // Test the case where a user has hit the global max creation limit, but can still create + // a repo in an organization. Because the limit is greater than 0 we also show an alert + // to tell the user they have hit the limit. + t.Run("Limit above zero", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + maxCreationLimit := 1 + creationLimitTr := locale.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) + defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, maxCreationLimit)() + + resp := session.MakeRequest(t, NewRequest(t, "GET", "/repo/create"), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assertRepoCreateForm(t, htmlDoc, org, "") + + alert := htmlDoc.doc.Find("div.ui.negative.message").Text() + assert.Contains(t, alert, creationLimitTr) + }) + + // Test the case where a user has hit the global max creation limit, but can still create + // a repo in an organization. Because the limit is 0 we DO NOT show the alert. + t.Run("Limit is zero", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + maxCreationLimit := 0 + defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, maxCreationLimit)() + + resp := session.MakeRequest(t, NewRequest(t, "GET", "/repo/create"), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assertRepoCreateForm(t, htmlDoc, org, "") + + htmlDoc.AssertElement(t, "div.ui.negative.message", false) + }) + + // Test the case where a user has hit the global max creation limit, and also cannot create + // a repo in any of their orgs. The form isnt shown, and we deisplay an alert telling the user + // they can't create a repo. + t.Run("Global limit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + maxCreationLimit := 0 + defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, maxCreationLimit)() + + session := loginUser(t, "user8") + + resp := session.MakeRequest(t, NewRequest(t, "GET", "/repo/create"), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + alert := htmlDoc.doc.Find("div.ui.negative.message").Text() + assert.Contains(t, alert, cannotCreateTr) + }) +} + func TestRepoGenerate(t *testing.T) { defer tests.PrepareTestEnv(t)() userName := "user1" diff --git a/tests/integration/repo_settings_hook_test.go b/tests/integration/repo_settings_hook_test.go index f7349d4e66..51feecb7fd 100644 --- a/tests/integration/repo_settings_hook_test.go +++ b/tests/integration/repo_settings_hook_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" + "forgejo.org/models/db" + "forgejo.org/models/webhook" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -16,6 +18,8 @@ import ( func TestRepoSettingsHookHistory(t *testing.T) { defer tests.PrepareTestEnv(t)() + _, err := db.GetEngine(t.Context()).ID(2).Cols("is_delivered").Update(&webhook.HookTask{IsDelivered: false}) + require.NoError(t, err) session := loginUser(t, "user2") @@ -26,6 +30,8 @@ func TestRepoSettingsHookHistory(t *testing.T) { doc := NewHTMLParser(t, resp.Body) t.Run("1/delivered", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + html, err := doc.doc.Find(".webhook div[data-tab='request-1']").Html() require.NoError(t, err) assert.Contains(t, html, "Request URL: /matrix-delivered\n") @@ -39,6 +45,8 @@ func TestRepoSettingsHookHistory(t *testing.T) { }) t.Run("2/undelivered", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + html, err := doc.doc.Find(".webhook div[data-tab='request-2']").Html() require.NoError(t, err) assert.Equal(t, "-", strings.TrimSpace(html)) @@ -49,6 +57,8 @@ func TestRepoSettingsHookHistory(t *testing.T) { }) t.Run("3/success", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + html, err := doc.doc.Find(".webhook div[data-tab='request-3']").Html() require.NoError(t, err) assert.Contains(t, html, "Request URL: /matrix-success\n") diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go index d9f4949aed..63cc5332bc 100644 --- a/tests/integration/repo_settings_test.go +++ b/tests/integration/repo_settings_test.go @@ -26,6 +26,7 @@ import ( user_service "forgejo.org/services/user" "forgejo.org/tests" + "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,6 +41,47 @@ func TestRepoSettingsUnits(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } +func TestRepoSettingsAdminOptions(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: "repo1"}) + link := repo.Link() + + hasAdminOpts := func(t *testing.T, doer string, admin bool) { + session := loginUser(t, doer) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/settings", link)) + resp := session.MakeRequest(t, req, http.StatusOK) + html := NewHTMLParser(t, resp.Body) + + elems := html.doc.Find("button[name=request_reindex_type]") + if !admin { + assert.Empty(t, elems.Nodes) + return + } + + values := []string{"code", "issues", "stats"} + if !setting.Indexer.RepoIndexerEnabled { + values = values[1:] + } + elems.Each(func(i int, s *goquery.Selection) { + attr, exists := s.Attr("value") + require.True(t, exists) + assert.Equal(t, values[i], attr) + }) + } + + t.Run("guest", func(t *testing.T) { + hasAdminOpts(t, user.Name, false) + }) + + t.Run("admin", func(t *testing.T) { + hasAdminOpts(t, admin.Name, true) + }) +} + func TestRepoAddMoreUnitsHighlighting(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) @@ -271,11 +313,8 @@ func TestProtectedBranch(t *testing.T) { } func TestRepoFollowing(t *testing.T) { - setting.Federation.Enabled = true defer tests.PrepareTestEnv(t)() - defer func() { - setting.Federation.Enabled = false - }() + defer test.MockVariableValue(&setting.Federation.Enabled, true)() mock := test.NewFederationServerMock() federatedSrv := mock.DistantServer(t) @@ -328,7 +367,7 @@ func TestRepoFollowing(t *testing.T) { isLikeType := activityType == "Like" isCorrectObject := strings.HasSuffix(object, "/api/v1/activitypub/repository-id/1") if !isLikeType || !isCorrectObject { - t.Errorf("Activity is not a like for this repo") + t.Error("Activity is not a like for this repo") } }) } diff --git a/tests/integration/repo_signed_tag_test.go b/tests/integration/repo_signed_tag_test.go index 16d8841304..686690bd19 100644 --- a/tests/integration/repo_signed_tag_test.go +++ b/tests/integration/repo_signed_tag_test.go @@ -25,6 +25,10 @@ import ( ) func TestRepoSSHSignedTags(t *testing.T) { + if git.CheckGitVersionAtLeast("2.34") != nil { + t.Skip("Skipping, does not support SSH signing") + return + } defer tests.PrepareTestEnv(t)() // Preparations diff --git a/tests/integration/repo_starwatch_test.go b/tests/integration/repo_starwatch_test.go index 3bcfa29fe3..85100eca30 100644 --- a/tests/integration/repo_starwatch_test.go +++ b/tests/integration/repo_starwatch_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" ) -func testRepoStarringOrWatching(t *testing.T, action, listURI string) { +func testRepoStarringOrWatching(t *testing.T, action, listURI string, expectEmpty bool) { t.Helper() defer tests.PrepareTestEnv(t)() @@ -50,6 +50,12 @@ func testRepoStarringOrWatching(t *testing.T, action, listURI string) { htmlDoc = NewHTMLParser(t, resp.Body) htmlDoc.AssertElement(t, ".user-cards .list .card > a[href='/user5']", true) + if expectEmpty { + // Verify which user-cards elements are present + htmlDoc.AssertElement(t, ".user-cards > .list", true) + htmlDoc.AssertElement(t, ".user-cards > div", false) + } + // Unstar/unwatch the repo as user5 req = NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/repo1/action/%s", oppositeAction), map[string]string{ "_csrf": GetCSRF(t, session, "/user2/repo1"), @@ -73,15 +79,22 @@ func testRepoStarringOrWatching(t *testing.T, action, listURI string) { // Verify that "user5" is not among the stargazers/watchers htmlDoc = NewHTMLParser(t, resp.Body) - htmlDoc.AssertElement(t, ".user-cards .list .item.ui.segment > a[href='/user5']", false) + htmlDoc.AssertElement(t, ".user-cards .list .item.ui.segment > a[href='/user2']", false) + + if expectEmpty { + // Verify which user-cards elements are present + htmlDoc.AssertElement(t, ".user-cards > .list", false) + htmlDoc.AssertElement(t, ".user-cards > div", true) + } } func TestRepoStarUnstarUI(t *testing.T) { - testRepoStarringOrWatching(t, "star", "stars") + testRepoStarringOrWatching(t, "star", "stars", true) } func TestRepoWatchUnwatchUI(t *testing.T) { - testRepoStarringOrWatching(t, "watch", "watchers") + testRepoStarringOrWatching(t, "watch", "watchers", false) + // Empty list state is not checked because repo is watched by many users } func TestDisabledStars(t *testing.T) { diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 19e8553bb2..b66726a3e6 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -1453,51 +1453,34 @@ func TestInitInstructions(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user.Name) - sha256Repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ - Name: optional.Some("sha256-instruction"), - AutoInit: optional.Some(false), - EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeCode}), - ObjectFormat: optional.Some("sha256"), - }) - defer f() - - sha1Repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ - Name: optional.Some("sha1-instruction"), - AutoInit: optional.Some(false), - EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeCode}), - ObjectFormat: optional.Some("sha1"), - }) - defer f() - - portMatcher := regexp.MustCompile(`localhost:\d+`) - - t.Run("sha256", func(t *testing.T) { + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { defer tests.PrintCurrentTest(t)() + name := objectFormat.Name() + var init string + if name == "sha1" { + init = "git init" + } else { + init = fmt.Sprintf("git init --object-format=%s", name) + } - resp := session.MakeRequest(t, NewRequest(t, "GET", "/"+sha256Repo.FullName()), http.StatusOK) + repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ + Name: optional.Some(name + "-instruction"), + AutoInit: optional.Some(false), + EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeCode}), + ObjectFormat: optional.Some(name), + }) + defer f() + + portMatcher := regexp.MustCompile(`localhost:\d+`) + resp := session.MakeRequest(t, NewRequest(t, "GET", "/"+repo.FullName()), http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - assert.Equal(t, `touch README.md -git init --object-format=sha256 + assert.Equal(t, fmt.Sprintf(`touch README.md +%s git switch -c main git add README.md git commit -m "first commit" -git remote add origin http://localhost/user2/sha256-instruction.git -git push -u origin main`, portMatcher.ReplaceAllString(htmlDoc.Find(".empty-repo-guide code").First().Text(), "localhost")) - }) - - t.Run("sha1", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - resp := session.MakeRequest(t, NewRequest(t, "GET", "/"+sha1Repo.FullName()), http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - assert.Equal(t, `touch README.md -git init -git switch -c main -git add README.md -git commit -m "first commit" -git remote add origin http://localhost/user2/sha1-instruction.git -git push -u origin main`, portMatcher.ReplaceAllString(htmlDoc.Find(".empty-repo-guide code").First().Text(), "localhost")) +git remote add origin http://localhost/user2/%s-instruction.git +git push -u origin main`, init, name), portMatcher.ReplaceAllString(htmlDoc.Find(".empty-repo-guide code").First().Text(), "localhost")) }) } diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index be1b9cbcec..ffddd47faa 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -337,6 +337,8 @@ func testWebhookFormsShared(t *testing.T, endpoint, name string, session *TestSe resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK) htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`) + testWebhookFormsSharedChooseEvents(t, htmlForm) + // fill the form payload := map[string]string{ "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), @@ -472,3 +474,37 @@ func assertHasFlashMessages(t *testing.T, resp *httptest.ResponseRecorder, expec t.Errorf("unexpected flash message %q: %q", k, v) } } + +func testWebhookFormsSharedChooseEvents(t *testing.T, htmlForm *goquery.Selection) { + webhookTypes := []string{ + "create", + "delete", + "fork", + "push", + "repository", + "release", + "package", + "wiki", + "issues", + "issue_assign", + "issue_label", + "issue_milestone", + "issue_comment", + "pull_request", + "pull_request_assign", + "pull_request_label", + "pull_request_milestone", + "pull_request_comment", + "pull_request_review", + "pull_request_sync", + "pull_request_review_request", + "action_failure", + "action_recover", + "action_success", + } + + // check all types of webhooks are present in the form + for _, webhookType := range webhookTypes { + assertInput(t, htmlForm, webhookType) + } +} diff --git a/tests/integration/repo_wiki_test.go b/tests/integration/repo_wiki_test.go index 96d0b3dda8..7ade01bf10 100644 --- a/tests/integration/repo_wiki_test.go +++ b/tests/integration/repo_wiki_test.go @@ -6,6 +6,8 @@ package integration import ( "fmt" "net/http" + "net/http/httptest" + "strings" "testing" auth_model "forgejo.org/models/auth" @@ -113,3 +115,104 @@ func TestWikiTOC(t *testing.T) { assert.Equal(t, "Helpdesk", htmlDoc.Find(".wiki-content-toc a").Text()) }) } + +func canEditWiki(t *testing.T, username, url string, canEdit bool) { + t.Helper() + // t.Parallel() + + req := NewRequest(t, "GET", url) + + var resp *httptest.ResponseRecorder + if username != "" { + session := loginUser(t, username) + resp = session.MakeRequest(t, req, http.StatusOK) + } else { + resp = MakeRequest(t, req, http.StatusOK) + } + doc := NewHTMLParser(t, resp.Body) + res := doc.Find(`a[href^="` + url + `"]`).Map(func(_ int, el *goquery.Selection) string { + return el.AttrOr("href", "") + }) + found := false + for _, href := range res { + if strings.HasSuffix(href, "?action=_new") { + if !canEdit { + t.Errorf("unexpected edit link: %s", href) + } + found = true + break + } + } + if canEdit { + assert.True(t, found, "could not find ?action=_new link among %v", res) + } +} + +func TestWikiPermissions(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("default settings", func(t *testing.T) { + t.Run("anonymous", func(t *testing.T) { + canEditWiki(t, "", "/user5/repo4/wiki", false) + }) + t.Run("owner", func(t *testing.T) { + canEditWiki(t, "user5", "/user5/repo4/wiki", true) + }) + t.Run("collaborator", func(t *testing.T) { + canEditWiki(t, "user4", "/user5/repo4/wiki", true) + canEditWiki(t, "user29", "/user5/repo4/wiki", true) + }) + t.Run("other user", func(t *testing.T) { + canEditWiki(t, "user2", "/user5/repo4/wiki", false) + }) + }) + + t.Run("saved unchanged settings", func(t *testing.T) { + session := loginUser(t, "user5") + csrf := GetCSRF(t, session, "/user5/repo4/settings/units") + req := NewRequestWithValues(t, "POST", "/user5/repo4/settings/units", map[string]string{ + "_csrf": csrf, + "enable_wiki": "on", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + t.Run("anonymous", func(t *testing.T) { + canEditWiki(t, "", "/user5/repo4/wiki", false) + }) + t.Run("owner", func(t *testing.T) { + canEditWiki(t, "user5", "/user5/repo4/wiki", true) + }) + t.Run("collaborator", func(t *testing.T) { + canEditWiki(t, "user4", "/user5/repo4/wiki", true) + canEditWiki(t, "user29", "/user5/repo4/wiki", true) + }) + t.Run("other user", func(t *testing.T) { + canEditWiki(t, "user2", "/user5/repo4/wiki", false) + }) + }) + + t.Run("globally writable", func(t *testing.T) { + session := loginUser(t, "user5") + csrf := GetCSRF(t, session, "/user5/repo4/settings/units") + req := NewRequestWithValues(t, "POST", "/user5/repo4/settings/units", map[string]string{ + "_csrf": csrf, + "enable_wiki": "on", + "globally_writeable_wiki": "on", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + t.Run("anonymous", func(t *testing.T) { + canEditWiki(t, "", "/user5/repo4/wiki", false) + }) + t.Run("owner", func(t *testing.T) { + canEditWiki(t, "user5", "/user5/repo4/wiki", true) + }) + t.Run("collaborator", func(t *testing.T) { + canEditWiki(t, "user4", "/user5/repo4/wiki", true) + canEditWiki(t, "user29", "/user5/repo4/wiki", true) + }) + t.Run("other user", func(t *testing.T) { + canEditWiki(t, "user2", "/user5/repo4/wiki", true) + }) + }) +} diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index b993fdf936..42d47c4591 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -294,6 +294,30 @@ func TestChangeRepoFiles(t *testing.T) { assert.Equal(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name) }) + t.Run("Update with commit ID (without blob sha)", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + opts := getUpdateRepoFilesOptions(repo) + + commit, err := gitRepo.GetBranchCommit(opts.NewBranch) + require.NoError(t, err) + + opts.Files[0].SHA = "" + opts.LastCommitID = commit.ID.String() + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) + require.NoError(t, err) + + commit, err = gitRepo.GetBranchCommit(opts.NewBranch) + require.NoError(t, err) + lastCommit, err := commit.GetCommitByPath(opts.Files[0].TreePath) + require.NoError(t, err) + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String(), lastCommit.Committer.When) + assert.Equal(t, expectedFileResponse.Content, filesResponse.Files[0]) + assert.Equal(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA) + assert.Equal(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) + assert.Equal(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email) + assert.Equal(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name) + }) + t.Run("Update and move", func(t *testing.T) { defer tests.PrintCurrentTest(t)() opts := getUpdateRepoFilesOptions(repo) @@ -415,6 +439,26 @@ func TestChangeRepoFilesErrors(t *testing.T) { assert.EqualError(t, err, expectedError) }) + t.Run("missing SHA", func(t *testing.T) { + opts := getUpdateRepoFilesOptions(repo) + opts.Files[0].SHA = "" + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) + assert.Nil(t, filesResponse) + require.Error(t, err) + expectedError := "a SHA or commit ID must be provided when updating a file" + assert.EqualError(t, err, expectedError) + }) + + t.Run("bad last commit ID", func(t *testing.T) { + opts := getUpdateRepoFilesOptions(repo) + opts.LastCommitID = "bad" + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) + assert.Nil(t, filesResponse) + require.Error(t, err) + expectedError := "ConvertToSHA1: Invalid last commit ID: object does not exist [id: bad, rel_path: ]" + assert.EqualError(t, err, expectedError) + }) + t.Run("new branch already exists", func(t *testing.T) { opts := getUpdateRepoFilesOptions(repo) opts.NewBranch = "develop" diff --git a/tests/integration/setting_test.go b/tests/integration/setting_test.go index 66790e5835..0456d5cfff 100644 --- a/tests/integration/setting_test.go +++ b/tests/integration/setting_test.go @@ -13,6 +13,7 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -98,8 +99,7 @@ func TestSettingShowUserEmailProfile(t *testing.T) { func TestSettingLandingPage(t *testing.T) { defer tests.PrepareTestEnv(t)() - - landingPage := setting.LandingPageURL + defer test.MockProtect(&setting.LandingPageURL)() setting.LandingPageURL = setting.LandingPageHome req := NewRequest(t, "GET", "/") @@ -119,8 +119,6 @@ func TestSettingLandingPage(t *testing.T) { req = NewRequest(t, "GET", "/") resp = MakeRequest(t, req, http.StatusSeeOther) assert.Equal(t, "/user/login", resp.Header().Get("Location")) - - setting.LandingPageURL = landingPage } func TestSettingSecurityAuthSource(t *testing.T) { @@ -157,3 +155,51 @@ func TestSettingSecurityAuthSource(t *testing.T) { assert.Contains(t, resp.Body.String(), `gitlab-active`) assert.Contains(t, resp.Body.String(), `gitlab-inactive`) } + +func TestUserAvatarSizeNotice(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + req := NewRequest(t, "GET", "/user/settings") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, + htmlDoc.doc.Find("form div:has(input#new-avatar) .help").Text(), + "Custom avatar may not exceed 1 MiB in size or be larger than 4096x4096 pixels") +} + +func TestRepoAvatarSizeNotice(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user2/repo1/settings") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, + htmlDoc.doc.Find("form div:has(input[name=\"avatar\"]) .help").Text(), + "Custom avatar may not exceed 1 MiB in size or be larger than 4096x4096 pixels") +} + +func TestOrgAvatarSizeNotice(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/org/org3/settings") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, + htmlDoc.doc.Find("form div:has(input[name=\"avatar\"]) .help").Text(), + "Custom avatar may not exceed 1 MiB in size or be larger than 4096x4096 pixels") +} + +func TestAdminAvatarSizeNotice(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + req := NewRequest(t, "GET", "/admin/users/2/edit") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Contains(t, + htmlDoc.doc.Find("form div:has(input[name=\"avatar\"]) .help").Text(), + "Custom avatar may not exceed 1 MiB in size or be larger than 4096x4096 pixels") +} diff --git a/tests/integration/signing_git_test.go b/tests/integration/signing_git_test.go index e82abea5e6..9d69306e0a 100644 --- a/tests/integration/signing_git_test.go +++ b/tests/integration/signing_git_test.go @@ -42,6 +42,10 @@ func TestInstanceSigning(t *testing.T) { defer test.MockProtect(&setting.Repository.Signing.CRUDActions)() t.Run("SSH", func(t *testing.T) { + if git.CheckGitVersionAtLeast("2.34") != nil { + t.Skip("Skipping, does not support git SSH signing") + return + } defer tests.PrintCurrentTest(t)() pubKeyContent, err := os.ReadFile("tests/integration/ssh-signing-key.pub") diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index 91752d415e..e66b193e15 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -23,8 +23,7 @@ import ( func TestSignup(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Service.EnableCaptcha = false + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ "user_name": "exampleUser", @@ -41,9 +40,8 @@ func TestSignup(t *testing.T) { func TestSignupAsRestricted(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Service.EnableCaptcha = false - setting.Service.DefaultUserIsRestricted = true + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() + defer test.MockVariableValue(&setting.Service.DefaultUserIsRestricted, true)() req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ "user_name": "restrictedUser", @@ -63,8 +61,7 @@ func TestSignupAsRestricted(t *testing.T) { func TestSignupEmail(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Service.EnableCaptcha = false + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() tests := []struct { email string diff --git a/tests/integration/timetracking_test.go b/tests/integration/timetracking_test.go index 49ae92033e..9aba9f43e6 100644 --- a/tests/integration/timetracking_test.go +++ b/tests/integration/timetracking_test.go @@ -7,7 +7,6 @@ import ( "net/http" "path" "testing" - "time" "forgejo.org/modules/test" "forgejo.org/tests" @@ -60,9 +59,6 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo htmlDoc.AssertElement(t, ".timetrack .issue-stop-time", true) htmlDoc.AssertElement(t, ".timetrack .issue-cancel-time", true) - // Sleep for 1 second to not get wrong order for stopping timer - time.Sleep(time.Second) - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{ "_csrf": htmlDoc.GetCSRF(), }) diff --git a/tests/integration/user_profile_activity_test.go b/tests/integration/user_profile_attributes_test.go similarity index 76% rename from tests/integration/user_profile_activity_test.go rename to tests/integration/user_profile_attributes_test.go index 47a8df94b2..15bf173922 100644 --- a/tests/integration/user_profile_activity_test.go +++ b/tests/integration/user_profile_attributes_test.go @@ -16,15 +16,15 @@ import ( "github.com/stretchr/testify/assert" ) -// TestUserProfileActivity ensures visibility and correctness of elements related to activity of a user: -// - RSS feed button (doesn't test `other.ENABLE_FEED:false`) +// TestUserProfileAttributes ensures visibility and correctness of elements related to activity of a user: +// - RSS/atom feed links (doesn't test `other.ENABLE_FEED:false`) and a few other links nearby // - Public activity tab // - Banner/hint in the tab // - "Configure" link in the hint // These elements might depend on the following: // - Profile visibility // - Public activity visibility -func TestUserProfileActivity(t *testing.T) { +func TestUserProfileAttributes(t *testing.T) { defer test.MockVariableValue(&setting.AppSubURL, "/sub")() defer tests.PrepareTestEnv(t)() // This test needs multiple users with different access statuses to check for all possible states @@ -38,10 +38,10 @@ func TestUserProfileActivity(t *testing.T) { // Set activity visibility of user2 to public. This is the default, but won't hurt to set it before testing. testChangeUserActivityVisibility(t, userRegular, "off") - // Verify availability of RSS button and activity tab - testUser2ActivityButtonsAvailability(t, userAdmin, true) - testUser2ActivityButtonsAvailability(t, userRegular, true) - testUser2ActivityButtonsAvailability(t, userGuest, true) + // Verify availability of activity tab and other links + testUser2ActivityLinksAvailability(t, userAdmin, true, true, false) + testUser2ActivityLinksAvailability(t, userRegular, true, false, true) + testUser2ActivityLinksAvailability(t, userGuest, true, false, false) // Verify the hint for all types of users: admin, self, guest testUser2ActivityVisibility(t, userAdmin, "This activity is visible to everyone, but as an administrator you can also see interactions in private spaces.", true) @@ -63,15 +63,15 @@ func TestUserProfileActivity(t *testing.T) { // Set profile visibility of user2 back to public testChangeUserProfileVisibility(t, userRegular, structs.VisibleTypePublic) - // = Private acitivty = + // = Private activity = // Set activity visibility of user2 to private testChangeUserActivityVisibility(t, userRegular, "on") - // Verify availability of RSS button and activity tab - testUser2ActivityButtonsAvailability(t, userAdmin, true) - testUser2ActivityButtonsAvailability(t, userRegular, true) - testUser2ActivityButtonsAvailability(t, userGuest, false) + // Verify availability of activity tab and other links + testUser2ActivityLinksAvailability(t, userAdmin, true, true, false) + testUser2ActivityLinksAvailability(t, userRegular, true, false, true) + testUser2ActivityLinksAvailability(t, userGuest, false, false, false) // Verify the hint for all types of users: admin, self, guest testUser2ActivityVisibility(t, userAdmin, "This activity is visible to you because you're an administrator, but the user wants it to remain private.", true) @@ -112,10 +112,7 @@ func testUser2ActivityVisibility(t *testing.T, session *TestSession, hint string hintLink, hintLinkExists := page.Find("#visibility-hint a").Attr("href") // Check that the hint aligns with the actual feed availability - assert.Equal(t, availability, page.Find("#activity-feed").Length() > 0) - - // Check availability of RSS feed button too - assert.Equal(t, availability, page.Find("#profile-avatar-card a[href='/sub/user2.rss']").Length() > 0) + page.AssertElement(t, "#activity-feed", availability) // Check that the current tab is displayed and is active regardless of it's actual availability // For example, on / it wouldn't be available to guest, but it should be still present on /?tab=activity @@ -126,10 +123,21 @@ func testUser2ActivityVisibility(t *testing.T, session *TestSession, hint string return "" } -// testUser2ActivityButtonsAvailability checks visibility of Public activity tab on main profile page -func testUser2ActivityButtonsAvailability(t *testing.T, session *TestSession, buttons bool) { +// testUser2ActivityLinksAvailability checks visibility of: +// * Public activity tab on main profile page +// * user details, profile edit, feed links +func testUser2ActivityLinksAvailability(t *testing.T, session *TestSession, activity, adminLink, editLink bool) { t.Helper() response := session.MakeRequest(t, NewRequest(t, "GET", "/user2"), http.StatusOK) page := NewHTMLParser(t, response.Body) - assert.Equal(t, buttons, page.Find("overflow-menu .item[href='/sub/user2?tab=activity']").Length() > 0) + page.AssertElement(t, "overflow-menu .item[href='/sub/user2?tab=activity']", activity) + + // User details - for admins only + page.AssertElement(t, "#profile-avatar-card a[href='/sub/admin/users/2']", adminLink) + // Edit profile - for self only + page.AssertElement(t, "#profile-avatar-card a[href='/sub/user/settings']", editLink) + + // Feed links + page.AssertElement(t, "#profile-avatar-card a[href='/sub/user2.rss']", activity) + page.AssertElement(t, "#profile-avatar-card a[href='/sub/user2.atom']", activity) } diff --git a/tests/integration/user_profile_follows_test.go b/tests/integration/user_profile_follows_test.go index 08bd58db16..b4216865b7 100644 --- a/tests/integration/user_profile_follows_test.go +++ b/tests/integration/user_profile_follows_test.go @@ -1,5 +1,5 @@ // Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later package integration @@ -32,6 +32,7 @@ func TestUserProfileFollows(t *testing.T) { followingLink := "#profile-avatar-card a[href='/user1?tab=following']" listHeader := ".user-cards h2" listItems := ".user-cards .list" + listMsg := ".user-cards > div" // = No follows = @@ -44,7 +45,8 @@ func TestUserProfileFollows(t *testing.T) { // Verify that user1 has no followers testSelectorEquals(t, page, followersLink, "0 followers") testSelectorEquals(t, page, listHeader, "Followers") - testListCount(t, page, listItems, followCount) + page.AssertElement(t, listItems, false) + page.AssertElement(t, listMsg, true) // Request the profile of user1, the Following tab response = user1.MakeRequest(t, NewRequest(t, "GET", "/user1?tab=following"), http.StatusOK) @@ -53,7 +55,8 @@ func TestUserProfileFollows(t *testing.T) { // Verify that user1 does not follow anyone testSelectorEquals(t, page, followingLink, "0 following") testSelectorEquals(t, page, listHeader, "Following") - testListCount(t, page, listItems, followCount) + page.AssertElement(t, listItems, false) + page.AssertElement(t, listMsg, true) // Make user1 and user2 follow each other testUserFollowUser(t, user1, "user2") @@ -71,6 +74,7 @@ func TestUserProfileFollows(t *testing.T) { testSelectorEquals(t, page, followersLink, "1 follower") testSelectorEquals(t, page, listHeader, "Follower") testListCount(t, page, listItems, followCount) + page.AssertElement(t, listMsg, false) // Request the profile of user1, the Following tab response = user1.MakeRequest(t, NewRequest(t, "GET", "/user1?tab=following"), http.StatusOK) @@ -80,6 +84,7 @@ func TestUserProfileFollows(t *testing.T) { testSelectorEquals(t, page, followingLink, "1 following") testSelectorEquals(t, page, listHeader, "Following") testListCount(t, page, listItems, followCount) + page.AssertElement(t, listMsg, false) // Make user1 and user3 follow each other testUserFollowUser(t, user1, "user5") @@ -97,6 +102,7 @@ func TestUserProfileFollows(t *testing.T) { testSelectorEquals(t, page, followersLink, "2 followers") testSelectorEquals(t, page, listHeader, "Followers") testListCount(t, page, listItems, followCount) + page.AssertElement(t, listMsg, false) // Request the profile of user1, the Following tab response = user1.MakeRequest(t, NewRequest(t, "GET", "/user1?tab=following"), http.StatusOK) @@ -106,6 +112,7 @@ func TestUserProfileFollows(t *testing.T) { testSelectorEquals(t, page, followingLink, "2 following") testSelectorEquals(t, page, listHeader, "Following") testListCount(t, page, listItems, followCount) + page.AssertElement(t, listMsg, false) } // testUserFollowUser simply follows a user `following` by session of user `follower` diff --git a/tests/integration/user_profile_test.go b/tests/integration/user_profile_test.go index 04d87c49ab..fb96efcd64 100644 --- a/tests/integration/user_profile_test.go +++ b/tests/integration/user_profile_test.go @@ -11,6 +11,8 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" @@ -63,5 +65,44 @@ func TestUserProfile(t *testing.T) { checkReadme(t, "readme.md", "readme.md", 1) checkReadme(t, "ReadMe.mD", "ReadMe.mD", 1) checkReadme(t, "readme.org does not render", "README.org", 0) + + t.Run("readme-size", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Prepare the test repository + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + _, _, f := tests.CreateDeclarativeRepo(t, user2, ".profile", nil, nil, []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "README.md", + ContentReader: strings.NewReader(`## Lorem ipsum +dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +## Ut enim ad minim veniam +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum`), + }, + }) + defer f() + + t.Run("full", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 500)() + + req := NewRequest(t, "GET", "/user2") + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Ut enim ad minim veniam") + assert.Contains(t, resp.Body.String(), "mollit anim id est laborum") + }) + + t.Run("truncated", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.UI.MaxDisplayFileSize, 146)() + + req := NewRequest(t, "GET", "/user2") + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Ut enim ad minim") + assert.NotContains(t, resp.Body.String(), "veniam") + }) + }) }) } diff --git a/tests/integration/user_redirect_test.go b/tests/integration/user_redirect_test.go index 8e59699d66..ad17b57713 100644 --- a/tests/integration/user_redirect_test.go +++ b/tests/integration/user_redirect_test.go @@ -143,7 +143,7 @@ func TestUserRedirect(t *testing.T) { defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)() defer tests.PrintCurrentTest(t)() - assert.Contains(t, getPrompt(t), "The old username will be available to everyone after a cooldown period of 8 days, you can still reclaim the old username during the cooldown period.") + assert.Contains(t, getPrompt(t), "The old username will be available to everyone after a cooldown period of 8 days. You can still reclaim the old username during the cooldown period.") }) }) @@ -167,7 +167,7 @@ func TestUserRedirect(t *testing.T) { defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)() defer tests.PrintCurrentTest(t)() - assert.Contains(t, getPrompt(t), "The old organization name will be available to everyone after a cooldown period of 8 days, you can still reclaim the old name during the cooldown period.") + assert.Contains(t, getPrompt(t), "The old organization name will be available to everyone after a cooldown period of 8 days. You can still reclaim the old name during the cooldown period.") }) }) } diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index 30103072a8..0f659e6b02 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -357,8 +357,8 @@ func TestListStopWatches(t *testing.T) { } func TestUserLocationMapLink(t *testing.T) { - setting.Service.UserLocationMapURL = "https://example/foo/" defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")() session := loginUser(t, "user2") req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ @@ -1062,7 +1062,7 @@ func TestUserPasswordReset(t *testing.T) { session.MakeRequest(t, req, http.StatusSeeOther) unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID}) - assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).ValidatePassword("new_password")) + assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).ValidatePassword(t.Context(), "new_password")) } func TestActivateEmailAddress(t *testing.T) { diff --git a/tests/integration/webfinger_test.go b/tests/integration/webfinger_test.go index 053528d919..078be6fa54 100644 --- a/tests/integration/webfinger_test.go +++ b/tests/integration/webfinger_test.go @@ -12,6 +12,7 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -19,11 +20,7 @@ import ( func TestWebfinger(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Federation.Enabled = true - defer func() { - setting.Federation.Enabled = false - }() + defer test.MockVariableValue(&setting.Federation.Enabled, true)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index e15e79952b..f44aff7594 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -92,7 +92,6 @@ DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ -DISABLE_QUERY_AUTH_TOKEN = true [lfs] PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs @@ -116,3 +115,12 @@ ENABLED = true [ui.notification] EVENT_SOURCE_UPDATE_TIME = 1s + +[cron.stop_zombie_tasks] +ENABLED = false + +[cron.stop_endless_tasks] +ENABLED = false + +[cron.check_repo_stats] +ENABLED = false diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index 340531fb38..829fdc5b75 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -97,7 +97,6 @@ DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ -DISABLE_QUERY_AUTH_TOKEN = true [lfs] MINIO_BASE_PATH = lfs/ @@ -130,3 +129,12 @@ ENABLED = true [ui.notification] EVENT_SOURCE_UPDATE_TIME = 1s + +[cron.stop_zombie_tasks] +ENABLED = false + +[cron.stop_endless_tasks] +ENABLED = false + +[cron.check_repo_stats] +ENABLED = false diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 277916a539..d36388405b 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -94,7 +94,6 @@ DISABLE_GIT_HOOKS = false INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTI3OTU5ODN9.OQkH5UmzID2XBdwQ9TAI6Jj2t1X-wElVTjbE7aoN4I8 -DISABLE_QUERY_AUTH_TOKEN = true [oauth2] JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko @@ -117,3 +116,12 @@ ENABLED = true [ui.notification] EVENT_SOURCE_UPDATE_TIME = 1s + +[cron.stop_zombie_tasks] +ENABLED = false + +[cron.stop_endless_tasks] +ENABLED = false + +[cron.check_repo_stats] +ENABLED = false diff --git a/tests/test_utils.go b/tests/test_utils.go index 66da5e6bea..b53159ae2c 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -24,6 +24,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/base" "forgejo.org/modules/git" + "forgejo.org/modules/gitrepo" "forgejo.org/modules/graceful" "forgejo.org/modules/log" "forgejo.org/modules/optional" @@ -41,6 +42,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "xorm.io/xorm/convert" ) func exitf(format string, args ...any) { @@ -341,6 +343,7 @@ type DeclarativeRepoOptions struct { Name optional.Option[string] EnabledUnits optional.Option[[]unit_model.Type] DisabledUnits optional.Option[[]unit_model.Type] + UnitConfig optional.Option[map[unit_model.Type]convert.Conversion] Files optional.Option[[]*files_service.ChangeRepoFile] WikiBranch optional.Option[string] AutoInit optional.Option[bool] @@ -389,9 +392,14 @@ func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts enabledUnits = make([]repo_model.RepoUnit, len(units)) for i, unitType := range units { + var config convert.Conversion + if cfg, ok := opts.UnitConfig.Value()[unitType]; ok { + config = cfg + } enabledUnits[i] = repo_model.RepoUnit{ RepoID: repo.ID, Type: unitType, + Config: config, } } } @@ -408,6 +416,9 @@ func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts assert.True(t, autoInit, "Files cannot be specified if AutoInit is disabled") files := opts.Files.Value() + commitID, err := gitrepo.GetBranchCommitID(git.DefaultContext, repo, "main") + require.NoError(t, err) + resp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, &files_service.ChangeRepoFilesOptions{ Files: files, Message: "add files", @@ -425,6 +436,7 @@ func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts Author: time.Now(), Committer: time.Now(), }, + LastCommitID: commitID, }) require.NoError(t, err) assert.NotEmpty(t, resp) diff --git a/tests/testdata/data/viewer/README.md b/tests/testdata/data/viewer/README.md new file mode 100644 index 0000000000..e26d11f3c8 --- /dev/null +++ b/tests/testdata/data/viewer/README.md @@ -0,0 +1,14 @@ +# GLTF 3D Model Viewer + +⚠️ Currently supports `.glb` files only. + +3D models with the `.glb` format are rendered in repository file view. + +## 🔨 How to Test + +1) Create a new repository or use an existing one. +2) Upload a `.glb` file such as [Unicode❤♻Test.glb](./Unicode❤♻Test.glb) (CC0 1.0 Universal). +3) View the file in the repository. + - Similar to image files, the 3D model should be rendered in a viewer. + - Use mouse clicks to turn and zoom. + diff --git a/tests/testdata/data/viewer/Unicode❤♻Test.glb b/tests/testdata/data/viewer/Unicode❤♻Test.glb new file mode 100644 index 0000000000..32b0f4a0e8 Binary files /dev/null and b/tests/testdata/data/viewer/Unicode❤♻Test.glb differ diff --git a/tools/lint-go-gopls.sh b/tools/lint-go-gopls.sh deleted file mode 100755 index a222ea14d7..0000000000 --- a/tools/lint-go-gopls.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -uo pipefail - -cd "$(dirname -- "${BASH_SOURCE[0]}")" && cd .. - -IGNORE_PATTERNS=( - "is deprecated" # TODO: fix these -) - -# lint all go files with 'gopls check' and look for lines starting with the -# current absolute path, indicating a error was found. This is necessary -# because the tool does not set non-zero exit code when errors are found. -# ref: https://github.com/golang/go/issues/67078 -ERROR_LINES=$("$GO" run "$GOPLS_PACKAGE" check "$@" 2>/dev/null | grep -E "^$PWD" | grep -vFf <(printf '%s\n' "${IGNORE_PATTERNS[@]}")); -NUM_ERRORS=$(echo -n "$ERROR_LINES" | wc -l) - -if [ "$NUM_ERRORS" -eq "0" ]; then - exit 0; -else - echo "$ERROR_LINES" - echo "Found $NUM_ERRORS 'gopls check' errors" - exit 1; -fi diff --git a/web_src/css/base.css b/web_src/css/base.css index 64e42be553..cbb21951b4 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -97,6 +97,17 @@ samp, font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */ } +kbd { + padding: 0.15em 0.35em; + line-height: 10px; + color: var(--color-text-light); + vertical-align: middle; + background-color: var(--color-markup-code-inline); + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + box-shadow: inset 0 -1px 0 var(--color-secondary); +} + b, strong, h1, @@ -676,10 +687,6 @@ img.ui.avatar, color: var(--color-yellow) !important; } -.text.olive { - color: var(--color-olive) !important; -} - .text.green { color: var(--color-green) !important; } @@ -692,18 +699,10 @@ img.ui.avatar, color: var(--color-blue) !important; } -.text.violet { - color: var(--color-violet) !important; -} - .text.purple { color: var(--color-purple) !important; } -.text.pink { - color: var(--color-pink) !important; -} - .text.brown { color: var(--color-brown) !important; } @@ -821,19 +820,6 @@ img.ui.avatar, margin-left: 25px; } -.ui .sha.label { - font-family: var(--fonts-monospace); - font-size: 13px; - font-weight: var(--font-weight-normal); - margin: 0 6px; - padding: 5px 10px; - flex-shrink: 0; -} - -.ui .sha.label .shortsha { - display: inline-block; /* not sure whether it is still needed */ -} - .ui .button.truncate { display: inline-block; max-width: 100%; @@ -853,46 +839,6 @@ img.ui.avatar, font-weight: var(--font-weight-normal); } -.ui .background.red { - background-color: var(--color-red) !important; -} - -.ui .background.blue { - background-color: var(--color-blue) !important; -} - -.ui .background.black { - background-color: var(--color-black) !important; -} - -.ui .background.grey { - background-color: var(--color-grey) !important; -} - -.ui .background.light.grey { - background-color: var(--color-grey) !important; -} - -.ui .background.green { - background-color: var(--color-green) !important; -} - -.ui .background.purple { - background-color: var(--color-purple) !important; -} - -.ui .background.yellow { - background-color: var(--color-yellow) !important; -} - -.ui .background.orange { - background-color: var(--color-orange) !important; -} - -.ui .background.gold { - background-color: var(--color-gold) !important; -} - .ui .migrate { color: var(--color-text-light-2) !important; } @@ -1184,41 +1130,6 @@ svg.text.purple, padding-left: 1ch; } -.lines-escape { - width: 0; -} - -.lines-code { - padding-left: 5px; -} - -.file-view tr.active { - color: inherit !important; - background: inherit !important; -} - -.file-view tr.active .lines-num, -.file-view tr.active .lines-code { - background: var(--color-highlight-bg) !important; -} - -.file-view tr.active:last-of-type .lines-code { - border-bottom-right-radius: var(--border-radius); -} - -.file-view tr.active .lines-num { - position: relative; -} - -.file-view tr.active .lines-num::before { - content: ""; - position: absolute; - left: 0; - width: 2px; - height: 100%; - background: var(--color-highlight-fg); -} - .code-inner { font: 12px var(--fonts-monospace); white-space: pre-wrap; diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css index 726ac7e9e2..9f44e884b7 100644 --- a/web_src/css/features/gitgraph.css +++ b/web_src/css/features/gitgraph.css @@ -125,14 +125,7 @@ } #git-graph-container #rev-list .sha.label { - padding-top: 5px; - padding-bottom: 3px; -} - -#git-graph-container #rev-list .sha.label .ui.detail.icon.button { - padding-top: 3px; - margin-top: -5px; - padding-bottom: 1px; + height: 23px; } #git-graph-container #rev-list .author img.ui.avatar { diff --git a/web_src/css/home.css b/web_src/css/home.css index 22f1a2dc98..84dd793612 100644 --- a/web_src/css/home.css +++ b/web_src/css/home.css @@ -70,7 +70,7 @@ .page-footer .right-links > a { border-left: 1px solid var(--color-secondary-dark-1); padding-left: 8px; - margin-left: 5px; + margin-left: 8px; } .page-footer .ui.dropdown.language .menu { diff --git a/web_src/css/index.css b/web_src/css/index.css index 0a5a0180aa..e7e5dda2d5 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -19,6 +19,7 @@ @import "./modules/dimmer.css"; @import "./modules/switch.css"; +@import "./modules/dropdown.css"; @import "./modules/select.css"; @import "./modules/tippy.css"; @import "./modules/breadcrumb.css"; @@ -27,6 +28,7 @@ @import "./modules/toast.css"; @import "./modules/svg.css"; @import "./modules/flexcontainer.css"; +@import "./modules/user-cards.css"; @import "./shared/flex-list.css"; @import "./shared/milestone.css"; @@ -56,6 +58,7 @@ @import "./form.css"; @import "./repo.css"; +@import "./repo/file-view.css"; @import "./repo/release-tag.css"; @import "./repo/issue-card.css"; @import "./repo/issue-label.css"; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 6574d413fc..8a872f2623 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -2,7 +2,7 @@ overflow: hidden; font-size: 16px; line-height: 1.5 !important; - overflow-wrap: anywhere; + overflow-wrap: break-word; } .markup > *:first-child { @@ -500,16 +500,7 @@ } .markup kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: var(--color-text-light); - vertical-align: middle; - background-color: var(--color-markup-code-inline); - border: 1px solid var(--color-secondary); - border-radius: var(--border-radius); - box-shadow: inset 0 -1px 0 var(--color-secondary); + padding: 0 0.35em; } .markup .ui.list .list, @@ -561,6 +552,10 @@ border-top-right-radius: 0 !important; } +.file-view.markup { + padding: 2em; +} + .file-view.markup.orgmode li.unchecked::before { content: "[ ] "; } diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css index 0799ab80ec..469a309a11 100644 --- a/web_src/css/modules/button.css +++ b/web_src/css/modules/button.css @@ -349,47 +349,6 @@ It needs some tricks to tweak the left/right borders with active state */ border-color: var(--color-yellow-dark-2); } -/* olive */ - -.ui.olive.labels .label, -.ui.ui.ui.olive.label, -.ui.olive.button, -.ui.olive.buttons .button, -.ui.olive.button:focus, -.ui.olive.buttons .button:focus { - background: var(--color-olive); -} - -.ui.olive.button:hover, -.ui.olive.buttons .button:hover { - background: var(--color-olive-dark-1); -} - -.ui.olive.button:active, -.ui.olive.buttons .button:active { - background: var(--color-olive-dark-2); -} - -.ui.basic.olive.buttons .button, -.ui.basic.olive.button, -.ui.basic.olive.buttons .button:focus, -.ui.basic.olive.button:focus { - color: var(--color-olive); - border-color: var(--color-olive); -} - -.ui.basic.olive.buttons .button:hover, -.ui.basic.olive.button:hover { - color: var(--color-olive-dark-1); - border-color: var(--color-olive-dark-1); -} - -.ui.basic.olive.buttons .button:active, -.ui.basic.olive.button:active { - color: var(--color-olive-dark-2); - border-color: var(--color-olive-dark-2); -} - /* green */ .ui.green.labels .label, @@ -472,88 +431,6 @@ It needs some tricks to tweak the left/right borders with active state */ border-color: var(--color-teal-dark-2); } -/* blue */ - -.ui.blue.labels .label, -.ui.ui.ui.blue.label, -.ui.blue.button, -.ui.blue.buttons .button, -.ui.blue.button:focus, -.ui.blue.buttons .button:focus { - background: var(--color-blue); -} - -.ui.blue.button:hover, -.ui.blue.buttons .button:hover { - background: var(--color-blue-dark-1); -} - -.ui.blue.button:active, -.ui.blue.buttons .button:active { - background: var(--color-blue-dark-2); -} - -.ui.basic.blue.buttons .button, -.ui.basic.blue.button, -.ui.basic.blue.buttons .button:focus, -.ui.basic.blue.button:focus { - color: var(--color-blue); - border-color: var(--color-blue); -} - -.ui.basic.blue.buttons .button:hover, -.ui.basic.blue.button:hover { - color: var(--color-blue-dark-1); - border-color: var(--color-blue-dark-1); -} - -.ui.basic.blue.buttons .button:active, -.ui.basic.blue.button:active { - color: var(--color-blue-dark-2); - border-color: var(--color-blue-dark-2); -} - -/* violet */ - -.ui.violet.labels .label, -.ui.ui.ui.violet.label, -.ui.violet.button, -.ui.violet.buttons .button, -.ui.violet.button:focus, -.ui.violet.buttons .button:focus { - background: var(--color-violet); -} - -.ui.violet.button:hover, -.ui.violet.buttons .button:hover { - background: var(--color-violet-dark-1); -} - -.ui.violet.button:active, -.ui.violet.buttons .button:active { - background: var(--color-violet-dark-2); -} - -.ui.basic.violet.buttons .button, -.ui.basic.violet.button, -.ui.basic.violet.buttons .button:focus, -.ui.basic.violet.button:focus { - color: var(--color-violet); - border-color: var(--color-violet); -} - -.ui.basic.violet.buttons .button:hover, -.ui.basic.violet.button:hover { - color: var(--color-violet-dark-1); - border-color: var(--color-violet-dark-1); -} - -.ui.basic.violet.buttons .button:active, -.ui.basic.violet.button:active { - color: var(--color-violet-dark-2); - border-color: var(--color-violet-dark-2); -} - /* purple */ .ui.purple.labels .label, @@ -595,47 +472,6 @@ It needs some tricks to tweak the left/right borders with active state */ border-color: var(--color-purple-dark-2); } -/* pink */ - -.ui.pink.labels .label, -.ui.ui.ui.pink.label, -.ui.pink.button, -.ui.pink.buttons .button, -.ui.pink.button:focus, -.ui.pink.buttons .button:focus { - background: var(--color-pink); -} - -.ui.pink.button:hover, -.ui.pink.buttons .button:hover { - background: var(--color-pink-dark-1); -} - -.ui.pink.button:active, -.ui.pink.buttons .button:active { - background: var(--color-pink-dark-2); -} - -.ui.basic.pink.buttons .button, -.ui.basic.pink.button, -.ui.basic.pink.buttons .button:focus, -.ui.basic.pink.button:focus { - color: var(--color-pink); - border-color: var(--color-pink); -} - -.ui.basic.pink.buttons .button:hover, -.ui.basic.pink.button:hover { - color: var(--color-pink-dark-1); - border-color: var(--color-pink-dark-1); -} - -.ui.basic.pink.buttons .button:active, -.ui.basic.pink.button:active { - color: var(--color-pink-dark-2); - border-color: var(--color-pink-dark-2); -} - /* brown */ .ui.brown.labels .label, diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css new file mode 100644 index 0000000000..66762ac45c --- /dev/null +++ b/web_src/css/modules/dropdown.css @@ -0,0 +1,118 @@ +/* This is an implementation of a dropdown menu based on details HTML tag. + * It is inspired by https://picocss.com/docs/dropdown. + * + * NoJS mode could be improved by forcing the same [name] onto all dropdowns, so + * that the browser will automatically close all but the one that was just opened + * using keyboard. But the code doing that will not be as clean. +*/ + +:root details.dropdown { + --dropdown-box-shadow: 0 6px 18px var(--color-shadow); + --dropdown-item-padding: 0.5rem 0.75rem; +} + +@media (pointer: coarse) { + :root details.dropdown { + --dropdown-item-padding: 0.75rem 1rem; + } +} + +details.dropdown { + position: relative; +} + +details.dropdown > summary { + /* Optional flex+gap in case summary contains multiple elements */ + display: flex; + gap: 0.75rem; + align-items: center; + /* Cancel some of default styling */ + user-select: none; + list-style-type: none; + /* Main visual properties */ + border-radius: var(--border-radius); + padding: 0.5rem; +} + +details.dropdown > summary:hover, +details.dropdown > summary + ul > li:hover { + background: var(--color-hover); +} + +details.dropdown[open] > summary, +details.dropdown > summary + ul > li:focus-within { + background: var(--color-active); +} + +/* NoJS mode. Creates a virtual fullscreen area. Clicking it closes the dropdown. */ +.no-js details.dropdown[open] > summary::before { + z-index: 1; + position: fixed; + width: 100vw; + height: 100vh; + inset: 0; + background: 0 0; + content: ""; + cursor: default; +} + +details.dropdown > summary + ul { + z-index: 99; + position: absolute; + min-width: max-content; + margin: 0; + margin-top: 0.5rem; + padding: 0; + display: flex; + flex-direction: column; + list-style-type: none; + border-radius: var(--border-radius); + background: var(--color-body); + box-shadow: var(--dropdown-box-shadow); + border: 1px solid var(--color-secondary); +} + +details.dropdown > summary + ul > li { + width: 100%; + background: none; +} + +details.dropdown > summary + ul > li:first-child { + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +details.dropdown > summary + ul > li:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +/* dir-auto option - switch the direction at a width point where most of layout changes occur. */ +/* There's no way to check with CSS if LTR dropdown will fit on screen without JS. */ +@media (max-width: 767.98px) { + details.dropdown.dir-auto > summary + ul { + inset-inline: 0 auto; + direction: rtl; + } + details.dropdown.dir-auto > summary + ul > li { + direction: ltr; + } +} +/* Note: https://css-tricks.com/css-anchor-positioning-guide/ +* looks like a great thing but FF still doesn't support it. */ + +/* Note: dropdown.dir-rtl can be implemented when needed, e.g. for navbar profile dropdown on desktop layout. */ + +details.dropdown > summary + ul > li > .item { + padding: var(--dropdown-item-padding); + width: 100%; + display: flex; + gap: 0.75rem; + align-items: center; + color: var(--color-text); + /* Suppress underline - hover is indicated by background color */ + text-decoration: none; +} + +/* Cancel default styling of button elements */ +details.dropdown > summary + ul > li button { + background: none; +} diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css index bc30baafac..4dc469631d 100644 --- a/web_src/css/modules/label.css +++ b/web_src/css/modules/label.css @@ -203,11 +203,6 @@ a.ui.ui.ui.basic.yellow.label:hover { border-color: var(--color-yellow-dark-1); color: var(--color-yellow-dark-1); } -.ui.ui.ui.olive.label { - background: var(--color-olive); - border-color: var(--color-olive); - color: var(--color-white); -} .ui.ui.ui.green.label { background: var(--color-green); diff --git a/web_src/css/modules/user-cards.css b/web_src/css/modules/user-cards.css new file mode 100644 index 0000000000..d89ea4588c --- /dev/null +++ b/web_src/css/modules/user-cards.css @@ -0,0 +1,55 @@ +.user-cards .list { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + margin: 0 0 10px; + padding: 0; +} + +@media (max-width: 767.98px) { + .user-cards .list { + grid-template-columns: repeat(1, 1fr); + } +} + +@media (max-width: 900px) { + .user.profile .user-cards .list { + grid-template-columns: repeat(1, 1fr); + } +} + +.user-cards .card { + display: flex; + flex-direction: row; + width: 100%; + margin: 0; + padding: 14px; + border-radius: 0.28571429rem; + border: 1px solid var(--color-secondary); + background: var(--color-box-body); +} + +.user-cards .card, +.user-cards .card .content, +.user-cards .card .name, +.user-cards .card .meta { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.user-cards .card .avatar { + width: 48px; + height: 48px; + margin-right: 14px; +} + +.user-cards .card .name { + margin-top: 0; + margin-bottom: 0; + font-weight: var(--font-weight-normal); +} + +.user-cards .card .meta { + margin-top: 5px; +} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index e2627fda8f..078ff7b4c4 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -415,6 +415,11 @@ td .commit-summary { max-width: 600px !important; } +model-viewer { + width: 100%; + height: 100vh; +} + .pdf-content { width: 100%; height: 100vh; @@ -865,43 +870,6 @@ td .commit-summary { .singular-commit .shabox .sha.label { margin: 0; - border: 1px solid var(--color-light-border); -} - -.singular-commit .shabox .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerified:hover { - background: var(--color-green-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { - background: var(--color-yellow-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { - background: var(--color-orange-badge-hover-bg) !important; } @media (min-width: 768px) { @@ -1272,189 +1240,100 @@ td .commit-summary { background-color: var(--color-light) !important; } -#activity-feed .sha.label, -.repository #commits-table td.sha .sha.label, -.repository #repo-files-table .sha.label, -.repository #repo-file-commit-box .sha.label, -.repository #rev-list .sha.label, -.repository .timeline-item.commits-list .singular-commit .sha.label { +.ui .sha.label { + font-family: var(--fonts-monospace); + font-size: 13px; + font-weight: var(--font-weight-normal); + margin: 0 6px; + padding: 0; + gap: 0; + flex-shrink: 0; +} + +.ui.ui .sha.label { border: 1px solid var(--color-light-border); } -#activity-feed .sha.label .ui.signature.avatar { - height: 13px; - margin-bottom: 0; - width: 13px; +.ui.primary.sha.label { + border: none !important; + background: var(--color-primary) !important; } -.repository #commits-table td.sha .sha.label .ui.signature.avatar, -.repository #repo-files-table .sha.label .ui.signature.avatar, -.repository #repo-file-commit-box .sha.label .ui.signature.avatar, -.repository #rev-list .sha.label .ui.signature.avatar, -.repository .timeline-item.commits-list .singular-commit .sha.label .ui.signature.avatar { +.sha.label .shortsha { + padding: 0.33rem 0.5rem; +} + +.sha.label .signature { + color: var(--color-text); + background: var(--color-light); + padding: 0.25rem 0.33rem; + border-left: 1px solid var(--color-light-border); +} + +.sha.label .signature-author { + display: flex; + gap: 0.25rem; +} + +.sha.label .signature-author .avatar { height: 16px; margin-bottom: 0; width: 16px; } -#activity-feed .sha.label .detail.icon, -.repository #commits-table td.sha .sha.label .detail.icon, -.repository #repo-files-table .sha.label .detail.icon, -.repository #repo-file-commit-box .sha.label .detail.icon, -.repository #rev-list .sha.label .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon { - background: var(--color-light); - margin: -6px -10px -4px 0; - padding: 5px 4px 5px 6px; - border-left: 1px solid var(--color-light-border); - border-top: 0; - border-right: 0; - border-bottom: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -#activity-feed .sha.label .detail.icon img, -.repository #commits-table td.sha .sha.label .detail.icon img, -.repository #repo-files-table .sha.label .detail.icon img, -.repository #repo-file-commit-box .sha.label .detail.icon img, -.repository #rev-list .sha.label .detail.icon img, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon img { - margin-right: 0; -} - -#activity-feed .sha.label .detail.icon .svg, -.repository #commits-table td.sha .sha.label .detail.icon .svg, -.repository #repo-files-table .sha.label .detail.icon .svg, -.repository #repo-file-commit-box .sha.label .detail.icon .svg, -.repository #rev-list .sha.label .detail.icon .svg, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg { - margin: 0 0.25em 0 0; -} - -#activity-feed .sha.label .detail.icon > div, -.repository #commits-table td.sha .sha.label .detail.icon > div, -.repository #repo-files-table .sha.label .detail.icon > div, -.repository #repo-file-commit-box .sha.label .detail.icon > div, -.repository #rev-list .sha.label .detail.icon > div, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div { - display: flex; - align-items: center; -} - -#activity-feed .sha.label.isSigned.isWarning, -.repository #commits-table td.sha .sha.label.isSigned.isWarning, -.repository #repo-files-table .sha.label.isSigned.isWarning, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning, -.repository #rev-list .sha.label.isSigned.isWarning, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning { +.sha.label.isSigned.isWarning { border: 1px solid var(--color-red-badge); background: var(--color-red-badge-bg); } -#activity-feed .sha.label.isSigned.isWarning .detail.icon, -.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon, -.repository #rev-list .sha.label.isSigned.isWarning .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon { +.sha.label.isSigned.isWarning .signature { border-left: 1px solid var(--color-red-badge); color: var(--color-red-badge); } -#activity-feed .sha.label.isSigned.isWarning:hover, -.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover, -.repository #repo-files-table .sha.label.isSigned.isWarning:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover, -.repository #rev-list .sha.label.isSigned.isWarning:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover { +.sha.label.isSigned.isWarning:hover { background: var(--color-red-badge-hover-bg) !important; } -#activity-feed .sha.label.isSigned.isVerified, -.repository #commits-table td.sha .sha.label.isSigned.isVerified, -.repository #repo-files-table .sha.label.isSigned.isVerified, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified, -.repository #rev-list .sha.label.isSigned.isVerified, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified { +.sha.label.isSigned.isVerified { border: 1px solid var(--color-green-badge); background: var(--color-green-badge-bg); } -#activity-feed .sha.label.isSigned.isVerified .detail.icon, -.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerified .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon { +.sha.label.isSigned.isVerified .signature { border-left: 1px solid var(--color-green-badge); color: var(--color-green-badge); } -#activity-feed .sha.label.isSigned.isVerified:hover, -.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover, -.repository #repo-files-table .sha.label.isSigned.isVerified:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover, -.repository #rev-list .sha.label.isSigned.isVerified:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover { +.sha.label.isSigned.isVerified:hover { background: var(--color-green-badge-hover-bg) !important; } -#activity-feed .sha.label.isSigned.isVerifiedUntrusted, -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted { +.sha.label.isSigned.isVerifiedUntrusted { border: 1px solid var(--color-yellow-badge); background: var(--color-yellow-badge-bg); } -#activity-feed .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon { +.sha.label.isSigned.isVerifiedUntrusted .signature { border-left: 1px solid var(--color-yellow-badge); color: var(--color-yellow-badge); } -#activity-feed .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover { +.sha.label.isSigned.isVerifiedUntrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -#activity-feed .sha.label.isSigned.isVerifiedUnmatched, -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched { +.sha.label.isSigned.isVerifiedUnmatched { border: 1px solid var(--color-orange-badge); background: var(--color-orange-badge-bg); } -#activity-feed .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon { +.sha.label.isSigned.isVerifiedUnmatched .signature { border-left: 1px solid var(--color-orange-badge); color: var(--color-orange-badge); } -#activity-feed .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover { +.sha.label.isSigned.isVerifiedUnmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } @@ -1810,9 +1689,6 @@ details.repo-search-result summary::marker { white-space: nowrap; } -.file-view.markup { - padding: 2em; -} .repository .activity-header { display: flex; justify-content: space-between; @@ -2136,53 +2012,6 @@ details.repo-search-result summary::marker { padding: 0 10px; } -.user-cards .list { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 15px; - margin: 0 0 10px; - padding: 0; -} - -@media (max-width: 767.98px) { - .user-cards .list { - grid-template-columns: repeat(1, 1fr); - } -} - -@media (max-width: 900px) { - .user.profile .user-cards .list { - grid-template-columns: repeat(1, 1fr); - } -} - -.user-cards .list .card { - display: flex; - flex-direction: row; - width: 100%; - margin: 0; - padding: 14px; - border-radius: 0.28571429rem; - border: 1px solid var(--color-secondary); - background: var(--color-box-body); -} - -.user-cards .list .card .avatar { - width: 48px; - height: 48px; - margin-right: 14px; -} - -.user-cards .list .card .name { - margin-top: 0; - margin-bottom: 0; - font-weight: var(--font-weight-normal); -} - -.user-cards .list .card .meta { - margin-top: 5px; -} - #search-user-box .results .result .image { order: 0; margin-right: 12px; @@ -2644,8 +2473,21 @@ tbody.commit-list { display: flex; } -#diff-file-boxes { +#diff-content-container { flex: 1; +} + +#diff-commit-header { + /* Counteract the `+2px` for width in `.segment` */ + padding: 0 2px; +} + +#diff-commit-header + h4, +#diff-commit-header + #diff-file-boxes { + margin-top: 8px; +} + +#diff-file-boxes { max-width: 100%; display: flex; flex-direction: column; diff --git a/web_src/css/repo/file-view.css b/web_src/css/repo/file-view.css new file mode 100644 index 0000000000..2ce7f3ec0f --- /dev/null +++ b/web_src/css/repo/file-view.css @@ -0,0 +1,39 @@ +.file-view tr.active { + color: inherit !important; + background: inherit !important; +} + +.lines-escape { + width: 0; +} + +.lines-code { + padding-left: 5px; +} + +.file-view tr.active .lines-num, +.file-view tr.active .lines-escape, +.file-view tr.active .lines-code { + background: var(--color-highlight-bg) !important; +} + +.file-view tr.active:last-of-type .lines-code { + border-bottom-right-radius: var(--border-radius); +} + +.file-view tr.active .lines-num { + position: relative; +} + +.file-view tr.active .interact-bg:hover { + background: var(--color-primary-alpha-50) !important; +} + +.file-view tr.active .lines-num::before { + content: ""; + position: absolute; + left: 0; + width: 2px; + height: 100%; + background: var(--color-highlight-fg); +} diff --git a/web_src/css/user.css b/web_src/css/user.css index b554f4e0b1..7fa81670fb 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -21,25 +21,26 @@ } .user.profile .ui.card .extra.content > ul > li { - padding: 10px; display: flex; + padding: 0.75rem; + gap: 0.5rem; list-style: none; align-items: center; - gap: 0.25em; } .user.profile .ui.card .extra.content > ul > li:not(:last-child) { border-bottom: 1px solid var(--color-secondary); } -.user.profile .ui.card .extra.content > ul > li .svg { - margin-left: 1px; - margin-right: 5px; +.user.profile .ui.card .actions { + padding: 0.75rem; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 0.75rem; } -.user.profile .ui.card .extra.content > ul > li.follow .ui.button, -.user.profile .ui.card .extra.content > ul > li.block .ui.button, -.user.profile .ui.card .extra.content > ul > li.report .ui.button { +.user.profile .ui.card .primary-action .ui.button { align-items: center; display: flex; justify-content: center; diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json index 0e6a1109fd..cea707672e 100644 --- a/web_src/fomantic/package-lock.json +++ b/web_src/fomantic/package-lock.json @@ -132,38 +132,38 @@ } }, "node_modules/@octokit/core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", - "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", + "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.2.2", - "@octokit/request": "^9.2.3", - "@octokit/request-error": "^6.1.8", + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", - "before-after-hook": "^3.0.2", + "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", "license": "MIT", "peer": true, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", "license": "MIT", "peer": true, "dependencies": { @@ -171,60 +171,60 @@ "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT", "peer": true }, "node_modules/@octokit/core/node_modules/@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", + "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", "license": "MIT", "peer": true, "dependencies": { "@octokit/types": "^14.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@octokit/openapi-types": "^25.1.0" } }, "node_modules/@octokit/core/node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0", "peer": true }, @@ -253,24 +253,24 @@ "license": "ISC" }, "node_modules/@octokit/graphql": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/request": "^9.2.3", + "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", "license": "MIT", "peer": true, "dependencies": { @@ -278,54 +278,54 @@ "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT", "peer": true }, "node_modules/@octokit/graphql/node_modules/@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", + "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", "license": "MIT", "peer": true, "dependencies": { "@octokit/types": "^14.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@octokit/openapi-types": "^25.1.0" } }, "node_modules/@octokit/graphql/node_modules/universal-user-agent": { @@ -494,12 +494,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", - "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/vinyl": { @@ -1086,9 +1086,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1117,9 +1117,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "funding": [ { "type": "opencollective", @@ -1136,8 +1136,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -1249,9 +1249,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", "funding": [ { "type": "opencollective", @@ -1969,9 +1969,9 @@ } }, "node_modules/editorconfig/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2005,9 +2005,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.153", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.153.tgz", - "integrity": "sha512-4bwluTFwjXZ0/ei1qDpHDGzVveuBfx4wiZ9VQ8j/30+T2JxSF2TfZ00d1X+wNMeDyUdZXgLkJFbarJdAMtd+/w==", + "version": "1.5.171", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", + "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2017,9 +2017,9 @@ "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -2333,9 +2333,9 @@ } }, "node_modules/fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", "funding": [ { "type": "github", @@ -2787,9 +2787,9 @@ } }, "node_modules/get-stream/node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -5019,9 +5019,9 @@ } }, "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8226,9 +8226,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/union-value": { diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 4b48376088..58c5461baa 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -411,7 +411,7 @@ export default sfc; // activate the IDE's Vue plugin
    - + diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 07ad336cf7..5e03019ef1 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -391,7 +391,7 @@ export default {
    #{{ index + 1 }} - +

    {{ contributor.name }}

    diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index 53c6b85728..3c94274175 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -2,13 +2,13 @@ import '@github/markdown-toolbar-element'; import '@github/text-expander-element'; import $ from 'jquery'; import {attachTribute} from '../tribute.js'; -import {hideElem, showElem, autosize, isElemVisible, replaceTextareaSelection} from '../../utils/dom.js'; +import {autosize, hideElem, isElemVisible, replaceTextareaSelection, showElem} from '../../utils/dom.js'; import {initEasyMDEPaste, initTextareaPaste} from './Paste.js'; import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; import {renderPreviewPanelContent} from '../repo-editor.js'; import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js'; import {initTextExpander} from './TextExpander.js'; -import {showErrorToast} from '../../modules/toast.js'; +import {showErrorToast, showHintToast} from '../../modules/toast.js'; import {POST} from '../../modules/fetch.js'; let elementIdCounter = 0; @@ -35,6 +35,9 @@ export function validateTextareaNonEmpty(textarea) { return true; } +// Matches the beginning of a line containing leading whitespace and possibly valid list or block quote prefix +const listPrefixRegex = /^\s*((\d+)[.)]\s|[-*+]\s{1,4}\[[ x]\]\s?|[-*+]\s|(>\s?)+)?/; + class ComboMarkdownEditor { constructor(container, options = {}) { container._giteaComboMarkdownEditor = this; @@ -88,24 +91,62 @@ class ComboMarkdownEditor { if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button'); } this.textareaMarkdownToolbar.querySelector('button[data-md-action="indent"]')?.addEventListener('click', () => { - this.indentSelection(false); + this.indentSelection(false, false); }); this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => { - this.indentSelection(true); + this.indentSelection(true, false); }); this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${elementIdCounter}"]`); this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${elementIdCounter}"]`); + // Track whether any actual input or pointer action was made after focusing, and only intercept Tab presses after that. + this.tabEnabled = false; + // This tracks whether last Tab action was ignored, and if it immediately happens *again*, lose focus. + this.ignoredTabAction = false; + this.ignoredTabToast = null; + + this.textarea.addEventListener('focus', () => { + this.tabEnabled = false; + this.ignoredTabAction = false; + }); + this.textarea.addEventListener('pointerup', () => { + // Assume if a pointer is used then Tab handling is a bit less of an issue. + this.tabEnabled = true; + }); this.textarea.addEventListener('keydown', (e) => { if (e.shiftKey) { e.target._shiftDown = true; } - if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.altKey) { - // Prevent special line break handling if currently a text expander popup is open - if (this.textarea.hasAttribute('aria-expanded')) return; + + // Prevent special keyboard handling if currently a text expander popup is open + if (this.textarea.hasAttribute('aria-expanded')) return; + + const noModifiers = !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey; + if (e.key === 'Escape') { + // Explicitly lose focus and reenable tab navigation. + e.target.blur(); + this.tabEnabled = false; + } else if (e.key === 'Tab' && this.tabEnabled && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (this.indentSelection(e.shiftKey, true)) { + this.options?.onContentChanged?.(this, e); + e.preventDefault(); + this.activateTabHandling(); + } else if (!this.ignoredTabAction) { + e.preventDefault(); + this.ignoredTabAction = true; + this.ignoredTabToast?.hideToast(); + this.ignoredTabToast = showHintToast( + this.container.dataset[e.shiftKey ? 'shiftTabHint' : 'tabHint'], + {gravity: 'bottom', useHtmlBody: true}, + ); + this.ignoredTabToast.toastElement.role = 'alert'; + } + } else if (e.key === 'Enter' && noModifiers) { if (!this.breakLine()) return; // Nothing changed, let the default handler work. this.options?.onContentChanged?.(this, e); e.preventDefault(); + } else if (noModifiers) { + this.activateTabHandling(); } }); this.textarea.addEventListener('keyup', (e) => { @@ -142,6 +183,15 @@ class ComboMarkdownEditor { } } + activateTabHandling() { + this.tabEnabled = true; + this.ignoredTabAction = false; + if (this.ignoredTabToast) { + this.ignoredTabToast.hideToast(); + this.ignoredTabToast = null; + } + } + setupDropzone() { const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); if (dropzoneParentContainer) { @@ -403,13 +453,15 @@ class ComboMarkdownEditor { } } - indentSelection(unindent) { + // Indent all lines that are included in the selection, partially or whole, while preserving the original selection at the end. + indentSelection(unindent, validOnly) { // Indent with 4 spaces, unindent 4 spaces or fewer or a lost tab. const indentPrefix = ' '; - const unindentRegex = /^( {1,4}|\t)/; + const unindentRegex = /^( {1,4}|\t|> {0,4})/; + const indentLevel = / {4}|\t|> /g; - // Indent all lines that are included in the selection, partially or whole, while preserving the original selection at the end. - const lines = this.textarea.value.split('\n'); + const value = this.textarea.value; + const lines = value.split('\n'); const changedLines = []; // The current selection or cursor position. const [start, end] = [this.textarea.selectionStart, this.textarea.selectionEnd]; @@ -419,31 +471,66 @@ class ComboMarkdownEditor { let [newStart, newEnd] = [start, end]; // The start and end position of the current line (where end points to the newline or EOF) let [lineStart, lineEnd] = [0, 0]; + // Index of the first line included in the selection (or containing the cursor) + let firstLineIdx = 0; - for (const line of lines) { + // Find all the lines in selection beforehand so we know the full set before we start changing. + const linePositions = []; + for (const [i, line] of lines.entries()) { lineEnd = lineStart + line.length + 1; if (lineEnd <= start) { lineStart = lineEnd; continue; } + linePositions.push([lineStart, line]); + if (start >= lineStart && start < lineEnd) { + firstLineIdx = i; + editStart = lineStart; + } + editEnd = lineEnd - 1; + if (lineEnd >= end) break; + lineStart = lineEnd; + } - const updated = unindent ? line.replace(unindentRegex, '') : indentPrefix + line; + // Block quotes need to be nested/unnested instead of whitespace added/removed. However, only do this if the *whole* selection is in a quote. + const isQuote = linePositions.every(([_, line]) => line[0] === '>'); + + const line = lines[firstLineIdx]; + // If there's no indent to remove, do nothing + if (unindent && start === end && !unindentRegex.test(line)) { + return false; + } + + // If there is no selection and this is an ambiguous command (Tab handling), only (un)indent if already in a code/list. + if (!unindent && validOnly && start === end) { + // Check there's any indentation or prefix at all. + const match = line.match(listPrefixRegex); + if (!match || !match[0].length) return false; + // Check that the line isn't already indented in relation to parent. + const levels = line.match(indentLevel)?.length ?? 0; + const parentLevels = !firstLineIdx ? 0 : lines[firstLineIdx - 1].match(indentLevel)?.length ?? 0; + // Quotes can *begin* multiple levels in, so just allow whatever for now. + if (levels - parentLevels > 0 && !isQuote) return false; + } + + // Apply indentation changes to lines. + for (const [i, [lineStart, line]] of linePositions.entries()) { + const updated = isQuote ? + (unindent ? line.replace(/^>\s{0,4}>/, '>') : `> ${line}`) : + (unindent ? line.replace(unindentRegex, '') : indentPrefix + line); changedLines.push(updated); const move = updated.length - line.length; - - if (start >= lineStart && start < lineEnd) { - editStart = lineStart; - newStart = Math.max(start + move, lineStart); - } - + if (i === 0) newStart = Math.max(start + move, lineStart); newEnd += move; - editEnd = lineEnd - 1; - lineStart = lineEnd; - if (lineStart > end) break; } // Update changed lines whole. const text = changedLines.join('\n'); + if (text === value.slice(editStart, editEnd)) { + // Nothing changed, likely due to Shift+Tab when no indents are left. + return false; + } + this.textarea.focus(); this.textarea.setSelectionRange(editStart, editEnd); if (!document.execCommand('insertText', false, text)) { @@ -454,6 +541,8 @@ class ComboMarkdownEditor { // Set selection to (effectively) be the same as before. this.textarea.setSelectionRange(newStart, Math.max(newStart, newEnd)); + + return true; } breakLine() { @@ -470,7 +559,7 @@ class ComboMarkdownEditor { const lineEnd = nextLF === -1 ? value.length : nextLF; const line = value.slice(lineStart, lineEnd); // Match any whitespace at the start + any repeatable prefix + exactly one space after. - const prefix = line.match(/^\s*((\d+)[.)]\s|[-*+]\s{1,4}\[[ x]\]\s?|[-*+]\s|(>\s?)+)?/); + const prefix = line.match(listPrefixRegex); // Defer to browser if we can't do anything more useful, or if the cursor is inside the prefix. if (!prefix) return false; @@ -489,14 +578,18 @@ class ComboMarkdownEditor { } // Insert newline + prefix. - let text = `\n${prefix[0]}`; + let text = `${prefix[0]}`; // Increment a number if present. (perhaps detecting repeating 1. and not doing that then would be a good idea) const num = text.match(/\d+/); if (num) text = text.replace(num[0], Number(num[0]) + 1); text = text.replace('[x]', '[ ]'); - if (!document.execCommand('insertText', false, text)) { - this.textarea.setRangeText(text); + // Split the newline and prefix addition in two, so that it's two separate undo entries in Firefox + // Chrome seems to bundle everything together more aggressively, even with prior text input. + if (document.execCommand('insertText', false, '\n')) { + document.execCommand('insertText', false, text); + } else { + this.textarea.setRangeText(`\n${text}`); } return true; diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index 889687da3e..bf76453428 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -125,16 +125,21 @@ function excludeLabel(item) { export function initRepoIssueSidebarList() { const repolink = $('#repolink').val(); const repoId = $('#repoId').val(); - const crossRepoSearch = $('#crossRepoSearch').val(); + const crossRepoSearch = $('#crossRepoSearch').val() === 'true'; const tp = $('#type').val(); - let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`; - if (crossRepoSearch === 'true') { - issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`; - } $('#new-dependency-drop-list') .dropdown({ apiSettings: { - url: issueSearchUrl, + beforeSend(settings) { + if (!settings.urlData.query.trim()) { + settings.url = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}&sort=updated`; + } else if (crossRepoSearch) { + settings.url = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}&sort=relevance`; + } else { + settings.url = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}&sort=relevance`; + } + return settings; + }, onResponse(response) { const filteredResponse = {success: true, results: []}; const currIssueId = $('#new-dependency-drop-list').data('issue-id'); @@ -142,7 +147,7 @@ export function initRepoIssueSidebarList() { for (const [_, issue] of Object.entries(response)) { // Don't list current issue in the dependency list. if (issue.id === currIssueId) { - return; + continue; } filteredResponse.results.push({ name: `#${issue.number} ${issueTitleHTML(htmlEscape(issue.title)) @@ -167,6 +172,7 @@ export function initRepoIssueSidebarList() { }); }); + // FIXME: this is broken, see discussion https://codeberg.org/forgejo/forgejo/pulls/8199 $('.menu .ui.dropdown.label-filter').on('keydown', (e) => { if (e.altKey && e.keyCode === 13) { const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected'); diff --git a/web_src/js/index.js b/web_src/js/index.js index 7d44b9ff56..1dab9ae292 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -23,6 +23,7 @@ import {initStopwatch} from './features/stopwatch.js'; import {initFindFileInRepo} from './features/repo-findfile.js'; import {initCommentContent, initMarkupContent} from './markup/content.js'; import {initPdfViewer} from './render/pdf.js'; +import {initGltfViewer} from './render/gltf.js'; import {initUserAuthOauth2, initUserAuth} from './features/user-auth.js'; import { @@ -74,6 +75,7 @@ import {initCopyContent} from './features/copycontent.js'; import {initCaptcha} from './features/captcha.js'; import {initRepositoryActionView} from './components/RepoActionView.vue'; import {initGlobalTooltips} from './modules/tippy.js'; +import {initDropdowns} from './modules/dropdown.ts'; import {initGiteaFomantic} from './modules/fomantic.js'; import {onDomReady} from './utils/dom.js'; import {initRepoIssueList} from './features/repo-issue-list.js'; @@ -102,6 +104,7 @@ onDomReady(() => { initGlobalEnterQuickSubmit(); initGlobalFormDirtyLeaveConfirm(); initGlobalLinkActions(); + initDropdowns(); initCommonOrganization(); initCommonIssueListQuickGoto(); @@ -187,6 +190,10 @@ onDomReady(() => { initUserAuth(); initRepoDiffView(); initPdfViewer(); + initGltfViewer(); initScopedAccessTokenCategories(); initColorPickers(); + + // Deactivate CSS-only noJS usability supplements + document.body.classList.remove('no-js'); }); diff --git a/web_src/js/modules/dropdown.ts b/web_src/js/modules/dropdown.ts new file mode 100644 index 0000000000..0731eeb86f --- /dev/null +++ b/web_src/js/modules/dropdown.ts @@ -0,0 +1,35 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Details can be opened by clicking summary or by pressing Space or Enter while +// being focused on summary. But without JS options for closing it are limited. +// Event listeners in this file provide more convenient options for that: +// click iteration with anything on the page and pressing Escape. + +export function initDropdowns() { + document.addEventListener('click', (event) => { + const dropdown = document.querySelector('details.dropdown[open]'); + // No open dropdowns on page, nothing to do. + if (dropdown === null) return; + + const target = event.target as HTMLElement; + // User clicked something in the open dropdown, don't interfere. + if (dropdown.contains(target)) return; + + // User clicked something that isn't the open dropdown, so close it. + dropdown.removeAttribute('open'); + }); + + // Close open dropdowns on Escape press + document.addEventListener('keydown', (event) => { + // This press wasn't escape, nothing to do. + if (event.key !== 'Escape') return; + + const dropdown = document.querySelector('details.dropdown[open]'); + // No open dropdowns on page, nothing to do. + if (dropdown === null) return; + + // User pressed Escape while having an open dropdown, probably wants it be closed. + dropdown.removeAttribute('open'); + }); +} diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js index d12d203718..177800d4fb 100644 --- a/web_src/js/modules/toast.js +++ b/web_src/js/modules/toast.js @@ -3,6 +3,11 @@ import {svg} from '../svg.js'; import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown const levels = { + hint: { + icon: 'octicon-light-bulb', + background: 'var(--color-black-light)', + duration: 2500, + }, info: { icon: 'octicon-check', background: 'var(--color-green)', @@ -42,6 +47,10 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, .. return toast; } +export function showHintToast(message, opts) { + return showToast(message, 'hint', opts); +} + export function showInfoToast(message, opts) { return showToast(message, 'info', opts); } diff --git a/web_src/js/render/gltf.js b/web_src/js/render/gltf.js new file mode 100644 index 0000000000..2d48e9f8e6 --- /dev/null +++ b/web_src/js/render/gltf.js @@ -0,0 +1,6 @@ +export async function initGltfViewer() { + const els = document.querySelectorAll('model-viewer'); + if (!els.length) return; + + await import(/* webpackChunkName: "@google/model-viewer" */'@google/model-viewer'); +} diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 3134279c84..13f26348c2 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -42,6 +42,7 @@ import octiconIssueClosed from '../../public/assets/img/svg/octicon-issue-closed import octiconIssueOpened from '../../public/assets/img/svg/octicon-issue-opened.svg'; import octiconItalic from '../../public/assets/img/svg/octicon-italic.svg'; import octiconKebabHorizontal from '../../public/assets/img/svg/octicon-kebab-horizontal.svg'; +import octiconLightBulb from '../../public/assets/img/svg/octicon-light-bulb.svg'; import octiconLink from '../../public/assets/img/svg/octicon-link.svg'; import octiconListOrdered from '../../public/assets/img/svg/octicon-list-ordered.svg'; import octiconListUnordered from '../../public/assets/img/svg/octicon-list-unordered.svg'; @@ -117,6 +118,7 @@ const svgs = { 'octicon-issue-opened': octiconIssueOpened, 'octicon-italic': octiconItalic, 'octicon-kebab-horizontal': octiconKebabHorizontal, + 'octicon-light-bulb': octiconLightBulb, 'octicon-link': octiconLink, 'octicon-list-ordered': octiconListOrdered, 'octicon-list-unordered': octiconListUnordered, diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js index 535aae874a..365eb63e30 100644 --- a/web_src/js/utils.test.js +++ b/web_src/js/utils.test.js @@ -1,3 +1,4 @@ +import {expect, test} from 'vitest'; import { basename, extname, isObject, stripTags, parseIssueHref, parseUrl, translateMonth, translateDay, blobToDataURI, @@ -182,5 +183,5 @@ async function testSleep(ms) { await sleep(ms); const endTime = Date.now(); // Record the end time const actualSleepTime = endTime - startTime; - expect(actualSleepTime >= ms).toBeTruthy(); + expect(actualSleepTime).toBeGreaterThanOrEqual(ms); } diff --git a/web_src/js/webcomponents/relative-time.js b/web_src/js/webcomponents/relative-time.js index a97814d2fd..d247ced3ca 100644 --- a/web_src/js/webcomponents/relative-time.js +++ b/web_src/js/webcomponents/relative-time.js @@ -130,15 +130,42 @@ export function DoUpdateRelativeTime(object, now) { return HALF_MINUTE; } -/** Update the displayed text of one relative-time DOM element with its human-readable, localized relative time string. */ -function UpdateRelativeTime(object) { - const next = DoUpdateRelativeTime(object); - if (next !== null) setTimeout(() => { UpdateRelativeTime(object) }, next); -} +window.customElements.define('relative-time', class extends HTMLElement { + static observedAttributes = ['datetime']; -/** Update the displayed text of all relative-time DOM elements with their respective human-readable, localized relative time string. */ -function UpdateAllRelativeTimes() { - for (const object of document.querySelectorAll('relative-time')) UpdateRelativeTime(object); -} + alive = false; + contentSpan = null; -document.addEventListener('DOMContentLoaded', UpdateAllRelativeTimes); + update = (recurring) => { + if (!this.alive) return; + + if (!this.shadowRoot) { + this.attachShadow({mode: 'open'}); + this.contentSpan = document.createElement('span'); + this.shadowRoot.append(this.contentSpan); + } + + const next = DoUpdateRelativeTime(this); + if (recurring && next !== null) setTimeout(() => { this.update(true) }, next); + }; + + connectedCallback() { + this.alive = true; + this.update(true); + } + + disconnectedCallback() { + this.alive = false; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'datetime' && oldValue !== newValue) this.update(false); + } + + set textContent(value) { + if (this.contentSpan) this.contentSpan.textContent = value; + } + get textContent() { + return this.contentSpan?.textContent; + } +}); diff --git a/web_src/js/webcomponents/relative-time.test.js b/web_src/js/webcomponents/relative-time.test.js index 8ffefd4139..5a8e2950e0 100644 --- a/web_src/js/webcomponents/relative-time.test.js +++ b/web_src/js/webcomponents/relative-time.test.js @@ -23,6 +23,7 @@ test('CalculateRelativeTimes', () => { 'relativetime.years': ['%d year ago', '%d years ago'], }; const mock = document.createElement('relative-time'); + document.body.append(mock); const now = Date.parse('2024-10-27T04:05:30+01:00'); // One hour after DST switchover, CET.