diff --git a/.deadcode-out b/.deadcode-out index 39b03c9704..39b2a5d2d6 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -14,6 +14,7 @@ package "code.gitea.io/gitea/models" func (ErrUpdateTaskNotExist).Error func (ErrUpdateTaskNotExist).Unwrap func IsErrSHANotFound + func IsErrMergeDivergingFastForwardOnly func GetYamlFixturesAccess package "code.gitea.io/gitea/models/actions" @@ -289,12 +290,14 @@ package "code.gitea.io/gitea/modules/timeutil" package "code.gitea.io/gitea/modules/translation" func (MockLocale).Language + func (MockLocale).TrString func (MockLocale).Tr func (MockLocale).TrN func (MockLocale).PrettyNumber package "code.gitea.io/gitea/modules/util" func UnsafeStringToBytes + func OptionalBoolFromGeneric package "code.gitea.io/gitea/modules/util/filebuffer" func CreateFromReader diff --git a/.dockerignore b/.dockerignore index 80cbeb040c..d1a08977a5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -71,7 +71,6 @@ cpu.out /tests/e2e/test-artifacts /tests/e2e/test-snapshots /tests/*.ini -/node_modules /yarn.lock /yarn-error.log /npm-debug.log* diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 78042a7598..e9991c02ba 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -12,6 +12,7 @@ plugins: - "@eslint-community/eslint-plugin-eslint-comments" - "@stylistic/eslint-plugin-js" - eslint-plugin-array-func + - eslint-plugin-github - eslint-plugin-i - eslint-plugin-jquery - eslint-plugin-no-jquery @@ -209,6 +210,29 @@ rules: func-names: [0] func-style: [0] getter-return: [2] + github/a11y-aria-label-is-well-formatted: [0] + github/a11y-no-title-attribute: [0] + github/a11y-no-visually-hidden-interactive-element: [0] + github/a11y-role-supports-aria-props: [0] + github/a11y-svg-has-accessible-name: [0] + github/array-foreach: [0] + github/async-currenttarget: [2] + github/async-preventdefault: [2] + github/authenticity-token: [0] + github/get-attribute: [0] + github/js-class-name: [0] + github/no-blur: [0] + github/no-d-none: [0] + github/no-dataset: [2] + github/no-dynamic-script-tag: [2] + github/no-implicit-buggy-globals: [2] + github/no-inner-html: [0] + github/no-innerText: [2] + github/no-then: [2] + github/no-useless-passive: [2] + github/prefer-observers: [2] + github/require-passive-events: [2] + github/unescaped-html-literal: [0] grouped-accessor-pairs: [2] guard-for-in: [0] id-blacklist: [0] @@ -272,7 +296,7 @@ rules: jquery/no-delegate: [2] jquery/no-each: [0] jquery/no-extend: [2] - jquery/no-fade: [0] + jquery/no-fade: [2] jquery/no-filter: [0] jquery/no-find: [0] jquery/no-global-eval: [2] @@ -285,7 +309,7 @@ rules: jquery/no-is-function: [2] jquery/no-is: [0] jquery/no-load: [2] - jquery/no-map: [0] + jquery/no-map: [2] jquery/no-merge: [2] jquery/no-param: [2] jquery/no-parent: [0] @@ -427,7 +451,7 @@ rules: no-jquery/no-load: [2] no-jquery/no-map-collection: [0] no-jquery/no-map-util: [2] - no-jquery/no-map: [0] + no-jquery/no-map: [2] no-jquery/no-merge: [2] no-jquery/no-node-name: [2] no-jquery/no-noop: [2] @@ -558,7 +582,6 @@ rules: prefer-rest-params: [2] prefer-spread: [2] prefer-template: [2] - quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}] radix: [2, as-needed] regexp/confusing-quantifier: [2] regexp/control-character-escape: [2] @@ -811,7 +834,7 @@ rules: wc/no-constructor-params: [2] wc/no-constructor: [2] wc/no-customized-built-in-elements: [2] - wc/no-exports-with-element: [2] + wc/no-exports-with-element: [0] wc/no-invalid-element-name: [2] wc/no-invalid-extends: [2] wc/no-method-prefixed-with-on: [2] diff --git a/.forgejo/testdata/build-release/Dockerfile b/.forgejo/testdata/build-release/Dockerfile index 4b6933845c..8dccad281c 100644 --- a/.forgejo/testdata/build-release/Dockerfile +++ b/.forgejo/testdata/build-release/Dockerfile @@ -1,3 +1,4 @@ -FROM public.ecr.aws/docker/library/alpine:3.18 +FROM code.forgejo.org/oci/alpine:3.19 +ARG RELEASE_VERSION=unkown RUN mkdir -p /app/gitea -RUN ( echo '#!/bin/sh' ; echo "echo forgejo v1.2.3" ) > /app/gitea/gitea ; chmod +x /app/gitea/gitea +RUN ( echo '#!/bin/sh' ; echo "echo forgejo v$RELEASE_VERSION" ) > /app/gitea/gitea ; chmod +x /app/gitea/gitea diff --git a/.forgejo/workflows/build-release-integration.yml b/.forgejo/workflows/build-release-integration.yml index 32e67964ba..e066f0af5e 100644 --- a/.forgejo/workflows/build-release-integration.yml +++ b/.forgejo/workflows/build-release-integration.yml @@ -34,10 +34,10 @@ jobs: lxc-ip-prefix: 10.0.9 - name: publish the forgejo release + shell: bash run: | set -x - version=1.2.3 cat > /etc/docker/daemon.json < $binary$suffix + if test "$suffix" = .xz ; then + unxz --keep $binary$suffix + fi + chmod +x $binary + ./$binary --version | grep $version + curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix.sha256 > $binary$suffix.sha256 + shasum -a 256 --check $binary$suffix.sha256 + rm $binary$suffix + done + done + + local sources=forgejo-src-$version.tar.gz + curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources > $sources + curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources.sha256 > $sources.sha256 + shasum -a 256 --check $sources.sha256 + + docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version + docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version-rootless + } + # # Create a new project with a fake forgejo and the release workflow only # @@ -62,46 +93,41 @@ jobs: cp $dir/Dockerfile $dir/Dockerfile.rootless forgejo-test-helper.sh push $dir $url root forgejo - sha=$(forgejo-test-helper.sh branch_tip $url root/forgejo main) + + forgejo-curl.sh api_json -X PUT --data-raw '{"data":"${{ steps.forgejo.outputs.token }}"}' $url/api/v1/repos/root/forgejo/actions/secrets/TOKEN + forgejo-curl.sh api_json -X PUT --data-raw '{"data":"root"}' $url/api/v1/repos/root/forgejo/actions/secrets/DOER + forgejo-curl.sh api_json -X PUT --data-raw '{"data":"true"}' $url/api/v1/repos/root/forgejo/actions/secrets/VERBOSE # # Push a tag to trigger the release workflow and wait for it to complete # + version=1.2.3 + sha=$(forgejo-test-helper.sh branch_tip $url root/forgejo main) forgejo-curl.sh api_json --data-raw '{"tag_name": "v'$version'", "target": "'$sha'"}' $url/api/v1/repos/root/forgejo/tags - forgejo-curl.sh api_json -X PUT --data-raw '{"data":"${{ steps.forgejo.outputs.token }}"}' $url/api/v1/repos/root/forgejo/actions/secrets/TOKEN - forgejo-curl.sh api_json -X PUT --data-raw '{"data":"root"}' $url/api/v1/repos/root/forgejo/actions/secrets/DOER LOOPS=180 forgejo-test-helper.sh wait_success "$url" root/forgejo $sha + sanity_check $url $version # - # uncomment to see the logs even when everything is reported to be working ok + # Push a commit to a branch that triggers the build of a test release # - #cat $FORGEJO_RUNNER_LOGS + version=1.2-test + ( + git clone $url/root/forgejo /tmp/forgejo + cd /tmp/forgejo + date > DATE + git config user.email root@example.com + git config user.name username + git add . + git commit -m 'update' + git push $url/root/forgejo main:forgejo + ) + sha=$(forgejo-test-helper.sh branch_tip $url root/forgejo forgejo) + LOOPS=180 forgejo-test-helper.sh wait_success "$url" root/forgejo $sha + sanity_check $url $version - # - # Minimal sanity checks. e2e test is for the setup-forgejo - # action and the infrastructure playbook. Since the binary - # is a script shell it does not test the sanity of the cross - # build, only the sanity of the naming of the binaries. - # - for arch in amd64 arm64 arm-6 ; do - binary=forgejo-$version-linux-$arch - for suffix in '' '.xz' ; do - curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix > $binary$suffix - if test "$suffix" = .xz ; then - unxz --keep $binary$suffix - fi - chmod +x $binary - ./$binary --version | grep $version - curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$binary$suffix.sha256 > $binary$suffix.sha256 - shasum -a 256 --check $binary$suffix.sha256 - rm $binary$suffix - done - done - - sources=forgejo-src-$version.tar.gz - curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources > $sources - curl --fail -L -sS $url/root/forgejo/releases/download/v$version/$sources.sha256 > $sources.sha256 - shasum -a 256 --check $sources.sha256 - - docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version - docker pull ${{ steps.forgejo.outputs.host-port }}/root/forgejo:$version-rootless + - name: full logs + if: always() + run: | + sed -e 's/^/[RUNNER LOGS] /' ${{ steps.forgejo.outputs.runner-logs }} + docker logs forgejo | sed -e 's/^/[FORGEJO LOGS]/' + sleep 5 # hack to avoid mixing outputs in Forgejo v1.21 diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml index ebd99ea62f..c012991b3a 100644 --- a/.forgejo/workflows/build-release.yml +++ b/.forgejo/workflows/build-release.yml @@ -14,11 +14,12 @@ # secrets.CASCADE_DESTINATION_TOKEN: scope read:user, write:repository, write:issue # vars.CASCADE_DESTINATION_DOER: forgejo-ci # -name: Build release - on: push: - tags: 'v*' + tags: 'v[0-9]+.[0-9]+.*' + branches: + - 'forgejo' + - 'v*/forgejo' jobs: release: @@ -27,6 +28,8 @@ jobs: if: vars.ROLE == 'forgejo-integration' || github.repository_owner == 'root' steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Sanitize the name of the repository id: repository @@ -43,17 +46,39 @@ jobs: go-version: "1.21" check-latest: true - - name: version from ref_name - id: tag-version + - name: version from ref + id: release-info + shell: bash run: | - version="${{ github.ref_name }}" - version=${version##*v} - echo "value=$version" >> "$GITHUB_OUTPUT" + set -x + ref="${{ github.ref }}" + if [[ $ref =~ ^refs/heads/ ]] ; then + if test "$ref" = "refs/heads/forgejo" ; then + version=$(git tag -l --sort=version:refname --merged | grep -v -e '-test$' | tail -1 | sed -E -e 's/^(v[0-9]+\.[0-9]+).*/\1/')-test + else + version=${ref#refs/heads/} + version=${version%/forgejo}-test + fi + override=true + fi + if [[ $ref =~ ^refs/tags/ ]] ; then + version=${ref#refs/tags/} + override=false + fi + if test -z "$version" ; then + echo failed to figure out the release version from the reference=$ref + exit 1 + fi + version=${version#v} + git describe --exclude '*-test' --tags --always + echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "override=$override" >> "$GITHUB_OUTPUT" - name: release notes id: release-notes run: | - anchor=${{ steps.tag-version.outputs.value }} + anchor=${{ steps.release-info.outputs.version }} anchor=${anchor//./-} cat >> "$GITHUB_OUTPUT" < /dev/null + curl -sS -X DELETE $url/api/v1/repos/forgejo-experimental/forgejo/tags/$tag > /dev/null + fi + # actions/checkout@v3 sets http.https://codeberg.org/.extraheader with the automatic token. + # Get rid of it so it does not prevent using the token that has write permissions + git config --local --unset http.https://codeberg.org/.extraheader + if test -f .git/shallow ; then + echo "unexptected .git/shallow file is present" + echo "it suggests a checkout --depth X was used which may prevent pushing the commit" + echo "it happens when actions/checkout is called without depth: 0" + fi + git push $url/forgejo-experimental/forgejo ${{ steps.release-info.outputs.sha }}:refs/tags/$tag diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml index 3a4099db5d..599c8c01ff 100644 --- a/.forgejo/workflows/mirror.yml +++ b/.forgejo/workflows/mirror.yml @@ -1,10 +1,8 @@ name: mirror on: - push: - branches: - - 'forgejo' - - 'v*/forgejo' + schedule: + - cron: '@daily' jobs: mirror: diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml index 1afa941952..eaa14c3693 100644 --- a/.forgejo/workflows/publish-release.yml +++ b/.forgejo/workflows/publish-release.yml @@ -42,16 +42,19 @@ jobs: - uses: actions/checkout@v3 - name: copy & sign - uses: https://code.forgejo.org/forgejo/forgejo-build-publish/publish@v1 + uses: https://code.forgejo.org/forgejo/forgejo-build-publish/publish@v4 with: - forgejo: ${{ vars.FORGEJO }} + from-forgejo: ${{ vars.FORGEJO }} + to-forgejo: ${{ vars.FORGEJO }} from-owner: ${{ vars.FROM_OWNER }} to-owner: ${{ vars.TO_OWNER }} repo: ${{ vars.REPO }} - ref-name: ${{ github.ref_name }} release-notes: "See https://codeberg.org/forgejo/forgejo/src/branch/forgejo/RELEASE-NOTES.md#{ANCHOR}" - doer: ${{ vars.DOER }} - token: ${{ secrets.TOKEN }} + ref-name: ${{ github.ref_name }} + sha: ${{ github.sha }} + from-token: ${{ secrets.TOKEN }} + to-doer: ${{ vars.DOER }} + to-token: ${{ secrets.TOKEN }} gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} verbose: ${{ vars.VERBOSE }} diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml index a4b54c4da5..80fd87152e 100644 --- a/.forgejo/workflows/testing.yml +++ b/.forgejo/workflows/testing.yml @@ -23,10 +23,22 @@ jobs: - run: make --always-make -j$(nproc) lint-backend checks-backend # ensure the "go-licenses" make target runs env: TAGS: bindata sqlite sqlite_unlock_notify + frontend-checks: + if: ${{ !startsWith(vars.ROLE, 'forgejo-') }} + runs-on: docker + container: + image: 'docker.io/node:20-bookworm' + steps: + - uses: https://code.forgejo.org/actions/checkout@v3 + - run: make deps-frontend + - run: make lint-frontend + - run: make checks-frontend + - run: make test-frontend + - run: make frontend test-unit: if: ${{ !startsWith(vars.ROLE, 'forgejo-') }} runs-on: docker - needs: [backend-checks] + needs: [backend-checks, frontend-checks] container: image: 'docker.io/node:20-bookworm' services: @@ -67,7 +79,7 @@ jobs: test-mysql: if: ${{ !startsWith(vars.ROLE, 'forgejo-') }} runs-on: docker - needs: [backend-checks] + needs: [backend-checks, frontend-checks] container: image: 'docker.io/node:20-bookworm' services: @@ -113,7 +125,7 @@ jobs: test-pgsql: if: ${{ !startsWith(vars.ROLE, 'forgejo-') }} runs-on: docker - needs: [backend-checks] + needs: [backend-checks, frontend-checks] container: image: 'docker.io/node:20-bookworm' services: @@ -161,7 +173,7 @@ jobs: test-sqlite: if: ${{ !startsWith(vars.ROLE, 'forgejo-') }} runs-on: docker - needs: [backend-checks] + needs: [backend-checks, frontend-checks] container: image: 'docker.io/node:20-bookworm' steps: diff --git a/.gitpod.yml b/.gitpod.yml index 35b22c45ae..ed2f57f4bf 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -10,10 +10,19 @@ tasks: - name: Run backend command: | gp sync-await setup - if [ ! -f custom/conf/app.ini ] - then + + # Get the URL and extract the domain + url=$(gp url 3000) + domain=$(echo $url | awk -F[/:] '{print $4}') + + if [ -f custom/conf/app.ini ]; then + sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini + sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini + sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini + sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini + else mkdir -p custom/conf/ - echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini + echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini fi export TAGS="sqlite sqlite_unlock_notify" diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml index a44294ee76..7dd0a566f2 100644 --- a/.stylelintrc.yaml +++ b/.stylelintrc.yaml @@ -98,7 +98,7 @@ rules: at-rule-allowed-list: null at-rule-disallowed-list: null at-rule-empty-line-before: null - at-rule-no-unknown: true + at-rule-no-unknown: [true, {ignoreAtRules: [tailwind]}] at-rule-no-vendor-prefix: true at-rule-property-required-list: null block-no-empty: true diff --git a/Dockerfile b/Dockerfile index 98d1d707b5..c7c15c0b52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21-alpine3.19 as build ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} -ARG GITEA_VERSION +ARG RELEASE_VERSION ARG TAGS="sqlite sqlite_unlock_notify" ENV TAGS "bindata timetzdata $TAGS" ARG CGO_EXTRA_CFLAGS @@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm COPY . ${GOPATH}/src/code.gitea.io/gitea WORKDIR ${GOPATH}/src/code.gitea.io/gitea -RUN make clean-all +RUN make clean RUN make frontend RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini -RUN make go-check generate-backend static-executable && xx-verify gitea +RUN make RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea # Copy local files COPY docker/root /tmp/local diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 0c09df61a9..bf3c92bbc5 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -5,7 +5,7 @@ FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21-alpine3.19 as build ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} -ARG GITEA_VERSION +ARG RELEASE_VERSION ARG TAGS="sqlite sqlite_unlock_notify" ENV TAGS "bindata timetzdata $TAGS" ARG CGO_EXTRA_CFLAGS @@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm COPY . ${GOPATH}/src/code.gitea.io/gitea WORKDIR ${GOPATH}/src/code.gitea.io/gitea -RUN make clean-all +RUN make clean RUN make frontend RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini -RUN make go-check generate-backend static-executable && xx-verify gitea +RUN make RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea # Copy local files COPY docker/rootless /tmp/local diff --git a/Makefile b/Makefile index aea352e6d5..9d0a92bdfb 100644 --- a/Makefile +++ b/Makefile @@ -29,10 +29,10 @@ XGO_VERSION := go-1.21.x AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 -MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.6-0.20240201115257-bcc7c78b7786 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3 @@ -85,19 +85,18 @@ endif STORED_VERSION_FILE := VERSION HUGO_VERSION ?= 0.111.3 +GITEA_COMPATIBILITY ?= gitea-1.22.0 STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null) ifneq ($(STORED_VERSION),) - GITEA_VERSION ?= $(STORED_VERSION) + FORGEJO_VERSION ?= $(STORED_VERSION) else - GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//') + FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//')+${GITEA_COMPATIBILITY} endif -VERSION = ${GITEA_VERSION} +RELEASE_VERSION ?= ${FORGEJO_VERSION} +VERSION ?= ${RELEASE_VERSION} -# SemVer -FORGEJO_VERSION := 7.0.0+0-gitea-1.22.0 - -LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)" +LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(FORGEJO_VERSION)" -X "main.Tags=$(TAGS)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)" LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 @@ -107,7 +106,7 @@ GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/m FOMANTIC_WORK_DIR := web_src/fomantic WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) -WEBPACK_CONFIGS := webpack.config.js +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 public/assets/img/webpack @@ -134,6 +133,8 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN GO_DIRS := build cmd models modules routers services tests WEB_DIRS := web_src/js web_src/css +SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github + GO_SOURCES := $(wildcard *.go) GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go) GO_SOURCES += $(GENERATED_GO_DEST) @@ -154,8 +155,8 @@ endif FORGEJO_API_SPEC := public/assets/forgejo/api.v1.yml SWAGGER_SPEC := templates/swagger/v1_json.tmpl -SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g -SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g +SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g +SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_NEWLINE_COMMAND := -e '$$a\' SWAGGER_SPEC_BRANDING := s|Gitea API|Forgejo API|g @@ -212,6 +213,8 @@ help: @echo " - lint-swagger lint swagger files" @echo " - lint-templates lint template files" @echo " - lint-yaml lint yaml files" + @echo " - lint-spell lint spelling" + @echo " - lint-spell-fix lint spelling and fix issues" @echo " - checks run various consistency checks" @echo " - checks-frontend check frontend files" @echo " - checks-backend check backend files" @@ -303,10 +306,6 @@ fmt-check: fmt exit 1; \ fi -.PHONY: misspell-check -misspell-check: - go run $(MISSPELL_PACKAGE) -error $(GO_DIRS) $(WEB_DIRS) - .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -368,13 +367,13 @@ checks: checks-frontend checks-backend checks-frontend: lockfile-check svg-check .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check misspell-check forgejo-api-validate swagger-validate security-check +checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check .PHONY: lint -lint: lint-frontend lint-backend +lint: lint-frontend lint-backend lint-spell .PHONY: lint-fix -lint-fix: lint-frontend-fix lint-backend-fix +lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix .PHONY: lint-frontend lint-frontend: lint-js lint-css @@ -412,6 +411,14 @@ lint-swagger: node_modules lint-md: node_modules npx markdownlint docs *.md +.PHONY: lint-spell +lint-spell: + @go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES) + +.PHONY: lint-spell-fix +lint-spell-fix: + @go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES) + .PHONY: lint-go lint-go: $(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) @@ -617,8 +624,7 @@ test-mssql\#%: integrations.mssql.test generate-ini-mssql test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test .PHONY: playwright -playwright: $(PLAYWRIGHT_DIR) - npm install --no-save @playwright/test +playwright: deps-frontend npx playwright install $(PLAYWRIGHT_FLAGS) .PHONY: test-e2e% @@ -631,7 +637,7 @@ test-e2e: test-e2e-sqlite .PHONY: test-e2e-sqlite test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e .PHONY: test-e2e-sqlite\#% test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite @@ -639,7 +645,7 @@ test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite .PHONY: test-e2e-mysql test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e .PHONY: test-e2e-mysql\#% test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql @@ -647,7 +653,7 @@ test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql .PHONY: test-e2e-pgsql test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e .PHONY: test-e2e-pgsql\#% test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql @@ -655,12 +661,17 @@ test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql .PHONY: test-e2e-mssql test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e .PHONY: test-e2e-mssql\#% test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$* +.PHONY: test-e2e-debugserver +test-e2e-debugserver: e2e.sqlite.test generate-ini-sqlite + sed -i s/3003/3000/g tests/sqlite.ini + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestDebugserver -test.timeout 24h + .PHONY: bench-sqlite bench-sqlite: integrations.sqlite.test generate-ini-sqlite GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . @@ -997,7 +1008,7 @@ generate-gitignore: .PHONY: generate-images generate-images: | node_modules - npm install --no-save --no-package-lock fabric@5 imagemin-zopfli@7 + npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7 node build/generate-images.js $(TAGS) .PHONY: generate-manpage @@ -1015,3 +1026,8 @@ docker: # This endif closes the if at the top of the file endif + +# Disable parallel execution because it would break some targets that don't +# specify exact dependencies like 'backend' which does currently not depend +# on 'frontend' to enable Node.js-less builds from source tarballs. +.NOTPARALLEL: diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 65f79e3628..c72c898f67 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -4,6 +4,54 @@ A Forgejo release is published shortly after a Gitea release is published and th The Forgejo admin should carefully read the required manual actions before upgrading. A point release (e.g. v1.21.1-0 or v1.21.2-0) does not require manual actions but others might (e.g. v1.20, v1.21). +## 1.21.6-0 + +The [complete list of commits](https://codeberg.org/forgejo/forgejo/commits/branch/v1.21/forgejo) included in the `Forgejo v1.21.6-0` release can be reviewed from the command line with: + +```shell +$ git clone https://codeberg.org/forgejo/forgejo/ +$ git -C forgejo log --oneline --no-merges v1.21.5-0..v1.21.6-0 +``` + +This stable release contains bug fixes and a **security fix**, as explained in the [v1.21.6-0 companion blog post](https://forgejo.org/2024-02-release-v1-21-6-0/). + +* Recommended Action + + We **strongly recommend** that all Forgejo installations are [upgraded](https://forgejo.org/docs/v1.21/admin/upgrade/) to the latest version as soon as possible. + +* [Forgejo Semantic Version](https://forgejo.org/docs/v1.21/user/semver/) + + The semantic version was updated to `6.0.6+0-gitea-1.21.6` + +* Security fix + + * [Fix XSS vulnerabilities](https://codeberg.org/forgejo/forgejo/pulls/2434). It enabled attackers to inject client-side scripts into web pages displayed to Forgejo visitors. + +* Bug fixes + + The most prominent ones are described here, others can be found in the list of commits included in the release as described above. + + * [Always write proc-receive hook for all git versions](https://codeberg.org/forgejo/forgejo/commit/a1fb6a2346193439dafaee5acf071632246e6dd7). + * [Fix debian InRelease Acquire-By-Hash newline](https://codeberg.org/forgejo/forgejo/commit/8a2c4e9ff2743f47a8d1f081b9e35dcc16431115). + * [Fix missing link on outgoing new release notifications](https://codeberg.org/forgejo/forgejo/commit/3a061083d65bdfc9acf0cb5839b84f6a9c17a727). + * [Workaround to clean up old reviews on creating a new one](https://codeberg.org/forgejo/forgejo/commit/8377ecbfe1f2b72ec7d65c46cbc9022ad0ccd75f). + * [Fix push to create with capitalize repo name](https://codeberg.org/forgejo/forgejo/commit/8782275c9c66ad6fc7c44503d7df9dae7196aa65). + * In Markdown [don't try to make the link absolute if the link has a schema that's defined in `[markdown].CUSTOM_URL_SCHEMES`](https://codeberg.org/forgejo/forgejo/commit/6c100083c29fb0ccf0cc52e8767e540a260d9468), because they can't be made absolute. + * [Fix Ctrl+Enter on submitting review comment](https://codeberg.org/forgejo/forgejo/commit/1c3a31d85112d10fb948d6f0b763191ed6f68e90). + * In Git version v2.43.1, the behavior of `GIT_FLUSH` was accidentially flipped. This causes Forgejo to hang on the `check-attr` command, because no output was being flushed. [Workaround this by detecting if Git v2.43.1 is used and set `GIT_FLUSH=0` thus getting the correct behavior](https://codeberg.org/forgejo/forgejo/commit/ff468ab5e426582b068586ce13d5a5348365e783). + * [When setting `url.host` on a URL object with no port specified (like is the case of default port), the resulting URL's port will not change. Workaround this quirk in the URL standard by explicitly setting port for the http and https protocols](https://codeberg.org/forgejo/forgejo/commit/628e1036cfbcfae442cb6494249fe11410447056). + * [Fix elasticsearch Request Entity Too Large](https://codeberg.org/forgejo/forgejo/commit/e6f59f6e1489d63d53de0da1de406a7a71a82adb). + * [Do not send update/delete release notifications when it is in a draft state](https://codeberg.org/forgejo/forgejo/commit/3c54a1dbf62e56d948feb1008512900140033737). + * [Do not run Forgejo Actions workflows synchronized events on the same commit as the one used to create a pull request](https://codeberg.org/forgejo/forgejo/commit/ce96379aef6e92cff2e9982031d5248ef8b01947). + * [Fix a MySQL performance regression introduced in v1.21.4-0](https://codeberg.org/forgejo/forgejo/commit/af98a0a7c6f4cbb5340974958ebe4389e3bf4e9a). + * [Fix Internal Server Error when resolving comments](https://codeberg.org/forgejo/forgejo/commit/ad67d9ef1a219b21309f811c14e7353cbc4982e3). + * Packages + * Swift: [fix a failure to resolve from package registry](https://codeberg.org/forgejo/forgejo/commit/fab6780fda5d8ded020a98253a793e87ed94f634). + * Alpine: [if the APKINFO contains an install if condition, write it in the APKINDEX](https://codeberg.org/forgejo/forgejo/commit/7afbc62057b876fb6711ef58743f664a2509dde4). + * org-mode files + * [It is possible that the description of an `Regularlink` is `Text` and not another `Regularlink`](https://codeberg.org/forgejo/forgejo/commit/781d2a68ccb276bf13caf0b378b74d9efeab3d39). + * [Fix relative links on orgmode](https://codeberg.org/forgejo/forgejo/commit/fa700333ba2649d14f1670dd2745957704a33b40). + ## 1.21.5-0 The [complete list of commits](https://codeberg.org/forgejo/forgejo/commits/branch/v1.21/forgejo) included in the `Forgejo v1.21.5-0` release can be reviewed from the command line with: diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 48dfabaeae..2aab21595b 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -24,6 +24,11 @@ "path": "codeberg.org/gusted/mcaptcha/LICENSE", "licenseText": "Copyright © 2022 William Zijl\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.\n" }, + { + "name": "connectrpc.com/connect", + "path": "connectrpc.com/connect/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 2021-2024 The Connect Authors\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": "dario.cat/mergo", "path": "dario.cat/mergo/LICENSE", @@ -229,11 +234,6 @@ "path": "github.com/bradfitz/gomemcache/memcache/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/bufbuild/connect-go", - "path": "github.com/bufbuild/connect-go/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 2021-2022 Buf Technologies, Inc.\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/buildkite/terminal-to-html/v3", "path": "github.com/buildkite/terminal-to-html/v3/LICENSE", diff --git a/build/generate-images.js b/build/generate-images.js index a3a0f8d8f3..db31d19e2a 100755 --- a/build/generate-images.js +++ b/build/generate-images.js @@ -1,20 +1,13 @@ #!/usr/bin/env node import imageminZopfli from 'imagemin-zopfli'; import {optimize} from 'svgo'; -import {fabric} from 'fabric'; +import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; import {readFile, writeFile} from 'node:fs/promises'; +import {argv, exit} from 'node:process'; -function exit(err) { +function doExit(err) { if (err) console.error(err); - process.exit(err ? 1 : 0); -} - -function loadSvg(svg) { - return new Promise((resolve) => { - fabric.loadSVGFromString(svg, (objects, options) => { - resolve({objects, options}); - }); - }); + exit(err ? 1 : 0); } async function generate(svg, path, {size, bg}) { @@ -35,14 +28,14 @@ async function generate(svg, path, {size, bg}) { return; } - const {objects, options} = await loadSvg(svg); - const canvas = new fabric.Canvas(); + const {objects, options} = await loadSVGFromString(svg); + const canvas = new Canvas(); canvas.setDimensions({width: size, height: size}); const ctx = canvas.getContext('2d'); ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1); if (bg) { - canvas.add(new fabric.Rect({ + canvas.add(new Rect({ left: 0, top: 0, height: size * (1 / (size / options.height)), @@ -51,7 +44,7 @@ async function generate(svg, path, {size, bg}) { })); } - canvas.add(fabric.util.groupSVGElements(objects, options)); + canvas.add(util.groupSVGElements(objects, options)); canvas.renderAll(); let png = Buffer.from([]); @@ -64,7 +57,7 @@ async function generate(svg, path, {size, bg}) { } async function main() { - const gitea = process.argv.slice(2).includes('gitea'); + const gitea = argv.slice(2).includes('gitea'); const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8'); const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8'); @@ -79,4 +72,8 @@ async function main() { ]); } -main().then(exit).catch(exit); +try { + doExit(await main()); +} catch (err) { + doExit(err); +} diff --git a/build/generate-svg.js b/build/generate-svg.js index b845da9367..660ac9157e 100755 --- a/build/generate-svg.js +++ b/build/generate-svg.js @@ -4,15 +4,16 @@ import {optimize} from 'svgo'; import {parse} from 'node:path'; import {readFile, writeFile, mkdir} from 'node:fs/promises'; import {fileURLToPath} from 'node:url'; +import {exit} from 'node:process'; const glob = (pattern) => fastGlob.sync(pattern, { cwd: fileURLToPath(new URL('..', import.meta.url)), absolute: true, }); -function exit(err) { +function doExit(err) { if (err) console.error(err); - process.exit(err ? 1 : 0); + exit(err ? 1 : 0); } async function processFile(file, {prefix, fullName} = {}) { @@ -59,8 +60,11 @@ async function main() { await Promise.all([ ...processFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}), ...processFiles('web_src/svg/*.svg'), - ...processFiles('public/assets/img/gitea.svg', {fullName: 'gitea-gitea'}), ]); } -main().then(exit).catch(exit); +try { + doExit(await main()); +} catch (err) { + doExit(err); +} diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index fefe18d39c..a257ce21c8 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -10,8 +10,8 @@ import ( auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" pwd "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/urfave/cli/v2" ) @@ -123,10 +123,10 @@ func runCreateUser(c *cli.Context) error { changePassword = c.Bool("must-change-password") } - restricted := util.OptionalBoolNone + restricted := optional.None[bool]() if c.IsSet("restricted") { - restricted = util.OptionalBoolOf(c.Bool("restricted")) + restricted = optional.Some(c.Bool("restricted")) } // default user visibility in app.ini @@ -142,7 +142,7 @@ func runCreateUser(c *cli.Context) error { } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), IsRestricted: restricted, } diff --git a/cmd/forgejo/actions.go b/cmd/forgejo/actions.go index afda831227..fc6b5f70f7 100644 --- a/cmd/forgejo/actions.go +++ b/cmd/forgejo/actions.go @@ -35,7 +35,8 @@ func SubcmdActionsGenerateRunnerToken(ctx context.Context) *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: prepareWorkPathAndCustomConf(ctx, func(cliCtx *cli.Context) error { return RunGenerateActionsRunnerToken(ctx, cliCtx) }), + Before: prepareWorkPathAndCustomConf(ctx), + Action: func(cliCtx *cli.Context) error { return RunGenerateActionsRunnerToken(ctx, cliCtx) }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "scope", @@ -59,7 +60,8 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command { return &cli.Command{ Name: "register", Usage: "Idempotent registration of a runner using a shared secret", - Action: prepareWorkPathAndCustomConf(ctx, func(cliCtx *cli.Context) error { return RunRegister(ctx, cliCtx) }), + Before: prepareWorkPathAndCustomConf(ctx), + Action: func(cliCtx *cli.Context) error { return RunRegister(ctx, cliCtx) }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "secret", @@ -219,25 +221,3 @@ func RunGenerateActionsRunnerToken(ctx context.Context, cliCtx *cli.Context) err } return nil } - -func prepareWorkPathAndCustomConf(ctx context.Context, action cli.ActionFunc) func(cliCtx *cli.Context) error { - return func(cliCtx *cli.Context) error { - if !ContextGetNoInit(ctx) { - var args setting.ArgWorkPathAndCustomConf - // from children to parent, check the global flags - for _, curCtx := range cliCtx.Lineage() { - if curCtx.IsSet("work-path") && args.WorkPath == "" { - args.WorkPath = curCtx.String("work-path") - } - if curCtx.IsSet("custom-path") && args.CustomPath == "" { - args.CustomPath = curCtx.String("custom-path") - } - if curCtx.IsSet("config") && args.CustomConf == "" { - args.CustomConf = curCtx.String("config") - } - } - setting.InitWorkPathAndCommonConfig(os.Getenv, args) - } - return action(cliCtx) - } -} diff --git a/cmd/forgejo/forgejo.go b/cmd/forgejo/forgejo.go index affb39157a..710996e1c0 100644 --- a/cmd/forgejo/forgejo.go +++ b/cmd/forgejo/forgejo.go @@ -145,3 +145,25 @@ 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 { + if !ContextGetNoInit(ctx) { + var args setting.ArgWorkPathAndCustomConf + // from children to parent, check the global flags + for _, curCtx := range c.Lineage() { + if curCtx.IsSet("work-path") && args.WorkPath == "" { + args.WorkPath = curCtx.String("work-path") + } + if curCtx.IsSet("custom-path") && args.CustomPath == "" { + args.CustomPath = curCtx.String("custom-path") + } + if curCtx.IsSet("config") && args.CustomConf == "" { + args.CustomConf = curCtx.String("config") + } + } + setting.InitWorkPathAndCommonConfig(os.Getenv, args) + } + return nil + } +} diff --git a/cmd/keys.go b/cmd/keys.go index 28b20958b9..81425a5722 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -71,7 +71,7 @@ func runKeys(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup(ctx, false) + setup(ctx, c.Bool("debug")) authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) // do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys diff --git a/cmd/serv.go b/cmd/serv.go index e014877bc6..9d26515254 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -63,21 +63,10 @@ func setup(ctx context.Context, debug bool) { setupConsoleLogger(log.FATAL, false, os.Stderr) } setting.MustInstalled() - if debug { - setting.RunMode = "dev" - } - - // Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when - // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection. if _, err := os.Stat(setting.RepoRootPath); err != nil { - if os.IsNotExist(err) { - _ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) - } else { - _ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) - } + _ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err) return } - if err := git.InitSimple(context.Background()); err != nil { _ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err) } @@ -216,16 +205,18 @@ func runServ(c *cli.Context) error { } } - // LowerCase and trim the repoPath as that's how they are stored. - repoPath = strings.ToLower(strings.TrimSpace(repoPath)) - rr := strings.SplitN(repoPath, "/", 2) if len(rr) != 2 { return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } - username := strings.ToLower(rr[0]) - reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git")) + username := rr[0] + reponame := strings.TrimSuffix(rr[1], ".git") + + // LowerCase and trim the repoPath as that's how they are stored. + // This should be done after splitting the repoPath into username and reponame + // so that username and reponame are not affected. + repoPath = strings.ToLower(strings.TrimSpace(repoPath)) if alphaDashDotPattern.MatchString(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 6d7b3bedf6..04714e5502 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -991,6 +991,9 @@ LEVEL = Info ;; Disable stars feature. ;DISABLE_STARS = false ;; +;; Disable repository forking. +;DISABLE_FORKS = false +;; ;; The default branch name of new repositories ;DEFAULT_BRANCH = main ;; @@ -1061,7 +1064,7 @@ LEVEL = Info ;; List of keywords used in Pull Request comments to automatically reopen a related issue ;REOPEN_KEYWORDS = reopen,reopens,reopened ;; -;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash +;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only ;DEFAULT_MERGE_STYLE = merge ;; ;; In the default merge message for squash commits include at most this many commits @@ -1489,6 +1492,9 @@ LEVEL = Info ;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false ;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false +;; Disabled features for users, could be "deletion", more features can be disabled in future +;; - deletion: a user cannot delete their own account +;USER_DISABLED_FEATURES = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index b7c6ceb431..aa2cbcee5d 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -126,7 +126,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build keywords used in Pull Request comments to automatically close a related issue - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen a related issue -- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash` +- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only` - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`. - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 2cee70daab..01906930cb 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -29,7 +29,7 @@ menu: [ini](https://github.com/go-ini/ini/#recursive-values) 这里的说明。 标注了 :exclamation: 的配置项表明除非你真的理解这个配置项的意义,否则最好使用默认值。 -在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`enviroment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。 +在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`environment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。 包含`#`或者`;`的变量必须使用引号(`` ` ``或者`""""`)包裹,否则会被解析为注释。 @@ -125,7 +125,7 @@ menu: - `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: 在拉取请求评论中用于自动关闭相关问题的关键词列表。 - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: 在拉取请求评论中用于自动重新打开相关问题的 关键词列表。 -- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash` +- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only` - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: 在默认合并消息中,对于`squash`提交,最多包括此数量的提交。设置为 -1 以包括所有提交。 - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: 在默认的合并消息中,对于`squash`提交,限制提交消息的大小。设置为 `-1`以取消限制。仅在`POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`为`true`时使用。 - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: 在默认合并消息中,对于`squash`提交,遍历所有提交以包括所有作者的`Co-authored-by`,否则仅使用限定列表中的作者。 diff --git a/docs/content/contributing/guidelines-backend.en-us.md b/docs/content/contributing/guidelines-backend.en-us.md index 084b3886e8..3159a5ff7d 100644 --- a/docs/content/contributing/guidelines-backend.en-us.md +++ b/docs/content/contributing/guidelines-backend.en-us.md @@ -101,6 +101,10 @@ i.e. `services/user`, `models/repository`. Since there are some packages which use the same package name, it is possible that you find packages like `modules/user`, `models/user`, and `services/user`. When these packages are imported in one Go file, it's difficult to know which package we are using and if it's a variable name or an import name. So, we always recommend to use import aliases. To differ from package variables which are commonly in camelCase, just use **snake_case** for import aliases. i.e. `import user_service "code.gitea.io/gitea/services/user"` +### Implementing `io.Closer` + +If a type implements `io.Closer`, calling `Close` multiple times must not fail or `panic` but return an error or `nil`. + ### Important Gotchas - Never write `x.Update(exemplar)` without an explicit `WHERE` clause: diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md index 4b132c49d9..df8a9047d6 100644 --- a/docs/content/development/hacking-on-gitea.en-us.md +++ b/docs/content/development/hacking-on-gitea.en-us.md @@ -243,10 +243,10 @@ documentation using: make generate-swagger ``` -You should validate your generated Swagger file and spell-check it with: +You should validate your generated Swagger file: ```bash -make swagger-validate misspell-check +make swagger-validate ``` You should commit the changed swagger JSON file. The continuous integration diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md index 364bbf1ffe..2dba3c92b6 100644 --- a/docs/content/development/hacking-on-gitea.zh-cn.md +++ b/docs/content/development/hacking-on-gitea.zh-cn.md @@ -228,10 +228,10 @@ Gitea Logo的 PNG 和 SVG 版本是使用 `TAGS="gitea" make generate-images` make generate-swagger ``` -您应该验证生成的 Swagger 文件并使用以下命令对其进行拼写检查: +您应该验证生成的 Swagger 文件: ```bash -make swagger-validate misspell-check +make swagger-validate ``` 您应该提交更改后的 swagger JSON 文件。持续集成服务器将使用以下方法检查是否已完成: diff --git a/docs/content/installation/from-source.en-us.md b/docs/content/installation/from-source.en-us.md index 601e074745..cd9fd56511 100644 --- a/docs/content/installation/from-source.en-us.md +++ b/docs/content/installation/from-source.en-us.md @@ -27,13 +27,7 @@ Next, [install Node.js with npm](https://nodejs.org/en/download/) which is required to build the JavaScript and CSS files. The minimum supported Node.js version is @minNodeVersion@ and the latest LTS version is recommended. -**Note**: When executing make tasks that require external tools, like -`make misspell-check`, Gitea will automatically download and build these as -necessary. To be able to use these, you must have the `"$GOPATH/bin"` directory -on the executable path. If you don't add the go bin directory to the -executable path, you will have to manage this yourself. - -**Note 2**: Go version @minGoVersion@ or higher is required. However, it is recommended to +**Note**: Go version @minGoVersion@ or higher is required. However, it is recommended to obtain the same version as our continuous integration, see the advice given in [Hacking on Gitea](development/hacking-on-gitea.md) diff --git a/docs/content/installation/from-source.zh-cn.md b/docs/content/installation/from-source.zh-cn.md index c2bd5785b2..3ff7efb4ed 100644 --- a/docs/content/installation/from-source.zh-cn.md +++ b/docs/content/installation/from-source.zh-cn.md @@ -21,9 +21,7 @@ menu: 接下来,[安装 Node.js 和 npm](https://nodejs.org/zh-cn/download/), 这是构建 JavaScript 和 CSS 文件所需的。最低支持的 Node.js 版本是 @minNodeVersion@,建议使用最新的 LTS 版本。 -**注意**:当执行需要外部工具的 make 任务(如`make misspell-check`)时,Gitea 将根据需要自动下载和构建这些工具。为了能够实现这个目的,你必须将`"$GOPATH/bin"`目录添加到可执行路径中。如果没有将 Go 的二进制目录添加到可执行路径中,你需要自行解决产生的问题。 - -**注意2**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。 +**注意**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。 ## 下载 diff --git a/docs/content/usage/packages/overview.en-us.md b/docs/content/usage/packages/overview.en-us.md index 44d18ff482..89fc6f286e 100644 --- a/docs/content/usage/packages/overview.en-us.md +++ b/docs/content/usage/packages/overview.en-us.md @@ -42,7 +42,7 @@ The following package managers are currently supported: | [PyPI](usage/packages/pypi.md) | Python | `pip`, `twine` | | [RPM](usage/packages/rpm.md) | - | `yum`, `dnf`, `zypper` | | [RubyGems](usage/packages/rubygems.md) | Ruby | `gem`, `Bundler` | -| [Swift](usage/packages/rubygems.md) | Swift | `swift` | +| [Swift](usage/packages/swift.md) | Swift | `swift` | | [Vagrant](usage/packages/vagrant.md) | - | `vagrant` | **The following paragraphs only apply if Packages are not globally disabled!** diff --git a/docs/content/usage/packages/swift.en-us.md b/docs/content/usage/packages/swift.en-us.md index 606fa20b36..38eb155641 100644 --- a/docs/content/usage/packages/swift.en-us.md +++ b/docs/content/usage/packages/swift.en-us.md @@ -26,7 +26,8 @@ To work with the Swift package registry, you need to use [swift](https://www.swi To register the package registry and provide credentials, execute: ```shell -swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password} +swift package-registry set https://gitea.example.com/api/packages/{owner}/swift +swift package-registry login https://gitea.example.com/api/packages/{owner}/swift --username {username} --password {password} ``` | Placeholder | Description | diff --git a/go.mod b/go.mod index 03f3cedbd2..c993526b36 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module code.gitea.io/gitea go 1.21 require ( - code.gitea.io/actions-proto-go v0.3.1 + code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/gitea-vet v0.2.3 code.gitea.io/sdk/gitea v0.17.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 + connectrpc.com/connect v1.15.0 gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 gitea.com/go-chi/cache v0.2.0 gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 @@ -19,7 +20,6 @@ require ( github.com/alecthomas/chroma/v2 v2.12.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.3.10 - github.com/bufbuild/connect-go v1.10.0 github.com/buildkite/terminal-to-html/v3 v3.10.1 github.com/caddyserver/certmagic v0.20.0 github.com/chi-middleware/proxy v1.1.1 @@ -102,11 +102,11 @@ require ( github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.19.0 golang.org/x/image v0.15.0 - golang.org/x/net v0.20.0 + golang.org/x/net v0.21.0 golang.org/x/oauth2 v0.16.0 - golang.org/x/sys v0.16.0 + golang.org/x/sys v0.17.0 golang.org/x/text v0.14.0 golang.org/x/tools v0.17.0 google.golang.org/grpc v1.60.1 diff --git a/go.sum b/go.sum index c43b51cd38..f8bf0567de 100644 --- a/go.sum +++ b/go.sum @@ -35,14 +35,16 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -code.gitea.io/actions-proto-go v0.3.1 h1:PMyiQtBKb8dNnpEO2R5rcZdXSis+UQZVo/SciMtR1aU= -code.gitea.io/actions-proto-go v0.3.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= +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/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8= code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= 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.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo= +connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -173,8 +175,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg= -github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= github.com/buildkite/terminal-to-html/v3 v3.10.1 h1:znT9eD26LQ59dDJJEpMCwkP4wEptEAPi74hsTBuHdEo= github.com/buildkite/terminal-to-html/v3 v3.10.1/go.mod h1:qtuRyYs6/Sw3FS9jUyVEaANHgHGqZsGqMknPLyau5cQ= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -898,8 +898,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -981,8 +981,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1061,8 +1061,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1073,8 +1073,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 56f97caf27..b8cc5668e1 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,8 @@ var ( Version = "development" // program version for this build Tags = "" // the Golang build tags MakeVersion = "" // "make" program version if built with make + + ReleaseVersion = "" ) var ForgejoVersion = "1.0.0" @@ -54,11 +56,18 @@ func main() { log.GetManager().Close() os.Exit(code) } - app := cmd.NewMainApp(Version, formatBuiltWith()) + app := cmd.NewMainApp(Version, formatReleaseVersion()+formatBuiltWith()) _ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp log.GetManager().Close() } +func formatReleaseVersion() string { + if len(ReleaseVersion) > 0 { + return " (release name " + ReleaseVersion + ")" + } + return "" +} + func formatBuiltWith() string { version := runtime.Version() if len(MakeVersion) > 0 { diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 5390f6288f..3d0a288e62 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -26,6 +26,8 @@ const ( ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired + ArtifactStatusPendingDeletion // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion + ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted ) func init() { @@ -147,8 +149,28 @@ func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) { Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts) } +// ListPendingDeleteArtifacts returns all artifacts in pending-delete status. +// limit is the max number of artifacts to return. +func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, limit) + return arts, db.GetEngine(ctx). + Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts) +} + // SetArtifactExpired sets an artifact to expired func SetArtifactExpired(ctx context.Context, artifactID int64) error { _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) return err } + +// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it +func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error { + _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)}) + return err +} + +// SetArtifactDeleted sets an artifact to deleted +func SetArtifactDeleted(ctx context.Context, artifactID int64) error { + _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)}) + return err +} diff --git a/models/actions/runner.go b/models/actions/runner.go index 4103ba4477..b646146ee6 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -97,7 +97,7 @@ func (r *ActionRunner) StatusName() string { } func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { - return lang.Tr("actions.runners.status." + r.StatusName()) + return lang.TrString("actions.runners.status." + r.StatusName()) } func (r *ActionRunner) IsOnline() bool { diff --git a/models/actions/status.go b/models/actions/status.go index c97578f2ac..eda2234137 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -41,7 +41,7 @@ func (s Status) String() string { // LocaleString returns the locale string name of the Status func (s Status) LocaleString(lang translation.Locale) string { - return lang.Tr("actions.status." + s.String()) + return lang.TrString("actions.status." + s.String()) } // IsDone returns whether the Status is final diff --git a/models/db/engine.go b/models/db/engine.go index 41207674e4..660ea1f5e3 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -7,6 +7,7 @@ package db import ( "context" "database/sql" + "errors" "fmt" "io" "reflect" @@ -145,6 +146,7 @@ func InitEngine(ctx context.Context) error { xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns) xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns) xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) + xormEngine.SetConnMaxIdleTime(setting.Database.ConnMaxIdleTime) xormEngine.SetDefaultContext(ctx) if setting.Database.SlowQueryThreshold > 0 { @@ -342,7 +344,7 @@ func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, e } func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error { - if c.Err != nil { + if c.Err != nil && !errors.Is(c.Err, context.Canceled) { h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err) } return nil diff --git a/models/error.go b/models/error.go index 83dfe29805..75c53245de 100644 --- a/models/error.go +++ b/models/error.go @@ -493,6 +493,23 @@ func (err ErrMergeUnrelatedHistories) Error() string { return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) } +// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge +type ErrMergeDivergingFastForwardOnly struct { + StdOut string + StdErr string + Err error +} + +// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly. +func IsErrMergeDivergingFastForwardOnly(err error) bool { + _, ok := err.(ErrMergeDivergingFastForwardOnly) + return ok +} + +func (err ErrMergeDivergingFastForwardOnly) Error() string { + return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} + // ErrRebaseConflicts represents an error if rebase fails with a conflict type ErrRebaseConflicts struct { Style repo_model.MergeStyle diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 1bb6a9a8ac..641c453eb7 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -135,3 +135,27 @@ user_id: 31 repo_id: 28 mode: 4 + +- + id: 24 + user_id: 38 + repo_id: 60 + mode: 2 + +- + id: 25 + user_id: 38 + repo_id: 61 + mode: 1 + +- + id: 26 + user_id: 39 + repo_id: 61 + mode: 1 + +- + id: 27 + user_id: 40 + repo_id: 61 + mode: 4 diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml index ef77d22b24..7603bdad32 100644 --- a/models/fixtures/collaboration.yml +++ b/models/fixtures/collaboration.yml @@ -45,3 +45,9 @@ repo_id: 22 user_id: 18 mode: 2 # write + +- + id: 9 + repo_id: 60 + user_id: 38 + mode: 2 # write diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml index 67a99f43e2..b2a0432635 100644 --- a/models/fixtures/email_address.yml +++ b/models/fixtures/email_address.yml @@ -293,3 +293,27 @@ lower_email: user37@example.com is_activated: true is_primary: true + +- + id: 38 + uid: 38 + email: user38@example.com + lower_email: user38@example.com + is_activated: true + is_primary: true + +- + id: 39 + uid: 39 + email: user39@example.com + lower_email: user39@example.com + is_activated: true + is_primary: true + +- + id: 40 + uid: 40 + email: user40@example.com + lower_email: user40@example.com + is_activated: true + is_primary: true diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index 0c9b6ff406..ca5b1c6cd1 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -338,3 +338,37 @@ created_unix: 978307210 updated_unix: 978307210 is_locked: false + +- + id: 21 + repo_id: 60 + index: 1 + poster_id: 39 + original_author_id: 0 + name: repo60 pull1 + content: content for the 1st issue + milestone_id: 0 + priority: 0 + is_closed: false + is_pull: true + num_comments: 0 + created_unix: 1707270422 + updated_unix: 1707270422 + is_locked: false + +- + id: 22 + repo_id: 61 + index: 1 + poster_id: 40 + original_author_id: 0 + name: repo61 pull1 + content: content for the 1st issue + milestone_id: 0 + priority: 0 + is_closed: false + is_pull: true + num_comments: 0 + created_unix: 1707270422 + updated_unix: 1707270422 + is_locked: false diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml index 8d58169a32..a7fbcb2c5a 100644 --- a/models/fixtures/org_user.yml +++ b/models/fixtures/org_user.yml @@ -99,3 +99,21 @@ uid: 5 org_id: 36 is_public: true + +- + id: 18 + uid: 38 + org_id: 41 + is_public: true + +- + id: 19 + uid: 39 + org_id: 41 + is_public: true + +- + id: 20 + uid: 40 + org_id: 41 + is_public: true diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index 560674c370..3fc8ce630d 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -9,6 +9,7 @@ head_branch: branch1 base_branch: master merge_base: 4a357436d925b5c974181ff12a994538ddc5a269 + merged_commit_id: 1a8823cd1a9549fde083f992f6b9b87a7ab74fb3 has_merged: true merger_id: 2 @@ -98,3 +99,21 @@ index: 1 head_repo_id: 23 base_repo_id: 23 + +- + id: 9 + type: 0 # gitea pull request + status: 2 # mergable + issue_id: 21 + index: 1 + head_repo_id: 60 + base_repo_id: 60 + +- + id: 10 + type: 0 # gitea pull request + status: 2 # mergable + issue_id: 22 + index: 1 + head_repo_id: 61 + base_repo_id: 61 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index e3590b06f0..e4fc5d9d00 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -708,3 +708,45 @@ type: 1 config: "{}" created_unix: 946684810 + +- + id: 102 + repo_id: 60 + type: 1 + config: "{}" + created_unix: 946684810 + +- + id: 103 + repo_id: 60 + type: 2 + config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}" + created_unix: 946684810 + +- + id: 104 + repo_id: 60 + type: 3 + config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" + created_unix: 946684810 + +- + id: 105 + repo_id: 61 + type: 1 + config: "{}" + created_unix: 946684810 + +- + id: 106 + repo_id: 61 + type: 2 + config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}" + created_unix: 946684810 + +- + id: 107 + repo_id: 61 + type: 3 + config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 8ef58b64ac..9d08c7bb0a 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1720,3 +1720,65 @@ is_private: true status: 0 num_issues: 0 + +- + id: 60 + owner_id: 40 + owner_name: user40 + lower_name: repo60 + name: repo60 + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 1 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: false + 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 + +- + id: 61 + owner_id: 41 + owner_name: org41 + lower_name: repo61 + name: repo61 + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 1 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: false + 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 diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index 295e51e39c..149fe90888 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -217,3 +217,25 @@ num_members: 1 includes_all_repositories: false can_create_org_repo: true + +- + id: 21 + org_id: 41 + lower_name: owners + name: Owners + authorize: 4 # owner + num_repos: 1 + num_members: 1 + includes_all_repositories: true + can_create_org_repo: true + +- + id: 22 + org_id: 41 + lower_name: team1 + name: Team1 + authorize: 1 # read + num_repos: 1 + num_members: 2 + includes_all_repositories: false + can_create_org_repo: false diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml index 8497720892..a29078107e 100644 --- a/models/fixtures/team_repo.yml +++ b/models/fixtures/team_repo.yml @@ -63,3 +63,15 @@ org_id: 17 team_id: 9 repo_id: 24 + +- + id: 12 + org_id: 41 + team_id: 21 + repo_id: 61 + +- + id: 13 + org_id: 41 + team_id: 22 + repo_id: 61 diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml index c5531aa57a..de0e8d738b 100644 --- a/models/fixtures/team_unit.yml +++ b/models/fixtures/team_unit.yml @@ -286,3 +286,39 @@ team_id: 2 type: 8 access_mode: 2 + +- + id: 49 + team_id: 21 + type: 1 + access_mode: 4 + +- + id: 50 + team_id: 21 + type: 2 + access_mode: 4 + +- + id: 51 + team_id: 21 + type: 3 + access_mode: 4 + +- + id: 52 + team_id: 22 + type: 1 + access_mode: 1 + +- + id: 53 + team_id: 22 + type: 2 + access_mode: 1 + +- + id: 54 + team_id: 22 + type: 3 + access_mode: 1 diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 9142fe609a..02d57ae644 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -129,3 +129,21 @@ org_id: 17 team_id: 9 uid: 15 + +- + id: 23 + org_id: 41 + team_id: 21 + uid: 40 + +- + id: 24 + org_id: 41 + team_id: 22 + uid: 38 + +- + id: 25 + org_id: 41 + team_id: 22 + uid: 39 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 16aa2a3ff1..07df059dc5 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -1369,3 +1369,151 @@ repo_admin_change_team_access: false theme: "" keep_activity_private: false + +- + id: 38 + lower_name: user38 + name: user38 + full_name: User38 + email: user38@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user38 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar38 + avatar_email: user38@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + +- + id: 39 + lower_name: user39 + name: user39 + full_name: User39 + email: user39@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user39 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar39 + avatar_email: user39@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + +- + id: 40 + lower_name: user40 + name: user40 + full_name: User40 + email: user40@example.com + keep_email_private: false + email_notifications_preference: onmention + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user40 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar40 + avatar_email: user40@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 1 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + +- + id: 41 + lower_name: org41 + name: org41 + full_name: Org41 + email: org41@example.com + keep_email_private: false + email_notifications_preference: onmention + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: org41 + type: 1 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar41 + avatar_email: org41@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 1 + num_teams: 2 + num_members: 3 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false diff --git a/models/git/branch_list.go b/models/git/branch_list.go index 0e8d28038a..8319e5ecd0 100644 --- a/models/git/branch_list.go +++ b/models/git/branch_list.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" ) @@ -67,7 +67,7 @@ type FindBranchOptions struct { db.ListOptions RepoID int64 ExcludeBranchNames []string - IsDeletedBranch util.OptionalBool + IsDeletedBranch optional.Option[bool] OrderBy string Keyword string } @@ -81,8 +81,8 @@ func (opts FindBranchOptions) ToConds() builder.Cond { if len(opts.ExcludeBranchNames) > 0 { cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames)) } - if !opts.IsDeletedBranch.IsNone() { - cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()}) + if opts.IsDeletedBranch.Has() { + cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.Value()}) } if opts.Keyword != "" { cond = cond.And(builder.Like{"name", opts.Keyword}) @@ -92,7 +92,7 @@ func (opts FindBranchOptions) ToConds() builder.Cond { func (opts FindBranchOptions) ToOrders() string { orderBy := opts.OrderBy - if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end + if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end if orderBy != "" { orderBy += ", " } diff --git a/models/git/branch_test.go b/models/git/branch_test.go index d480e2ec30..b984244cd2 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -12,7 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -49,7 +49,7 @@ func TestGetDeletedBranches(t *testing.T) { branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{ ListOptions: db.ListOptionsAll, RepoID: repo.ID, - IsDeletedBranch: util.OptionalBoolTrue, + IsDeletedBranch: optional.Some(true), }) assert.NoError(t, err) assert.Len(t, branches, 2) diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 1118b6cc8c..2d1d1bcb06 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -194,7 +194,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string { // LocaleString returns the locale string name of the Status func (status *CommitStatus) LocaleString(lang translation.Locale) string { - return lang.Tr("repo.commitstatus." + status.State.String()) + return lang.TrString("repo.commitstatus." + status.State.String()) } // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index eeb307e245..613333a5a2 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -8,7 +8,7 @@ import ( "sort" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/gobwas/glob" ) @@ -56,7 +56,7 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) Page: page, }, RepoID: repoID, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }) if err != nil { return nil, err diff --git a/models/issues/comment.go b/models/issues/comment.go index d9acca7e48..49c3159f6f 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -210,12 +210,12 @@ const ( // LocaleString returns the locale string name of the role func (r RoleInRepo) LocaleString(lang translation.Locale) string { - return lang.Tr("repo.issues.role." + string(r)) + return lang.TrString("repo.issues.role." + string(r)) } // LocaleHelper returns the locale tooltip of the role func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { - return lang.Tr("repo.issues.role." + string(r) + "_helper") + return lang.TrString("repo.issues.role." + string(r) + "_helper") } // Comment represents a comment in commit and issue page. @@ -695,8 +695,15 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository } func (c *Comment) loadReview(ctx context.Context) (err error) { + if c.ReviewID == 0 { + return nil + } if c.Review == nil { if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil { + // review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them. + if c.Type == CommentTypeReviewRequest { + return nil + } return err } } @@ -856,6 +863,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment // Check comment type. switch opts.Type { case CommentTypeCode: + if err = updateAttachments(ctx, opts, comment); err != nil { + return err + } if comment.ReviewID != 0 { if comment.Review == nil { if err := comment.loadReview(ctx); err != nil { @@ -873,22 +883,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment } fallthrough case CommentTypeReview: - // Check attachments - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) + if err = updateAttachments(ctx, opts, comment); err != nil { + return err } - - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) - } - } - - comment.Attachments = attachments case CommentTypeReopen, CommentTypeClose: if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil { return err @@ -898,6 +895,23 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment return UpdateIssueCols(ctx, opts.Issue, "updated_unix") } +func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) + } + for i := range attachments { + attachments[i].IssueID = opts.Issue.ID + attachments[i].CommentID = comment.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) + } + } + comment.Attachments = attachments + return nil +} + func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { var content string var commentType CommentType diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 384a595dd9..cef2ea281c 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -14,15 +14,58 @@ import ( "xorm.io/builder" ) +// CodeConversation contains the comment of a given review +type CodeConversation []*Comment + +// CodeConversationsAtLine contains the conversations for a given line +type CodeConversationsAtLine map[int64][]CodeConversation + +// CodeConversationsAtLineAndTreePath contains the conversations for a given TreePath and line +type CodeConversationsAtLineAndTreePath map[string]CodeConversationsAtLine + +func newCodeConversationsAtLineAndTreePath(comments []*Comment) CodeConversationsAtLineAndTreePath { + tree := make(CodeConversationsAtLineAndTreePath) + for _, comment := range comments { + tree.insertComment(comment) + } + return tree +} + +func (tree CodeConversationsAtLineAndTreePath) insertComment(comment *Comment) { + // attempt to append comment to existing conversations (i.e. list of comments belonging to the same review) + for i, conversation := range tree[comment.TreePath][comment.Line] { + if conversation[0].ReviewID == comment.ReviewID { + tree[comment.TreePath][comment.Line][i] = append(conversation, comment) + return + } + } + + // no previous conversation was found at this line, create it + if tree[comment.TreePath] == nil { + tree[comment.TreePath] = make(map[int64][]CodeConversation) + } + + tree[comment.TreePath][comment.Line] = append(tree[comment.TreePath][comment.Line], CodeConversation{comment}) +} + +// FetchCodeConversations will return a 2d-map: ["Path"]["Line"] = List of CodeConversation (one per review) for this line +func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model.User, showOutdatedComments bool) (CodeConversationsAtLineAndTreePath, error) { + opts := FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: issue.ID, + } + comments, err := findCodeComments(ctx, opts, issue, doer, nil, showOutdatedComments) + if err != nil { + return nil, err + } + + return newCodeConversationsAtLineAndTreePath(comments), nil +} + // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS type CodeComments map[string]map[int64][]*Comment -// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line -func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool) (CodeComments, error) { - return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments) -} - -func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) { +func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) { pathToLineToComment := make(CodeComments) if review == nil { review = &Review{ID: 0} @@ -33,7 +76,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u ReviewID: review.ID, } - comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments) + comments, err := findCodeComments(ctx, opts, issue, doer, review, showOutdatedComments) if err != nil { return nil, err } @@ -47,7 +90,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u return pathToLineToComment, nil } -func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) { +func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) { var comments CommentList if review == nil { review = &Review{ID: 0} @@ -91,7 +134,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu if re, ok := reviews[comment.ReviewID]; ok && re != nil { // If the review is pending only the author can see the comments (except if the review is set) if review.ID == 0 && re.Type == ReviewTypePending && - (currentUser == nil || currentUser.ID != re.ReviewerID) { + (doer == nil || doer.ID != re.ReviewerID) { continue } comment.Review = re @@ -121,13 +164,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return comments[:n], nil } -// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number -func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) { +// FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number) +func FetchCodeConversation(ctx context.Context, comment *Comment, doer *user_model.User) ([]*Comment, error) { opts := FindCommentsOptions{ Type: CommentTypeCode, - IssueID: issue.ID, - TreePath: treePath, - Line: line, + IssueID: comment.IssueID, + ReviewID: comment.ReviewID, + TreePath: comment.TreePath, + Line: comment.Line, } - return findCodeComments(ctx, opts, issue, currentUser, nil, showOutdatedComments) + return findCodeComments(ctx, opts, comment.Issue, doer, nil, true) } diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 93af45870e..30a437ea50 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -225,6 +225,10 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { for _, comment := range comments { comment.Assignee = assignees[comment.AssigneeID] + if comment.Assignee == nil { + comment.AssigneeID = user_model.GhostUserID + comment.Assignee = user_model.NewGhostUser() + } } return nil } @@ -430,7 +434,8 @@ func (comments CommentList) loadReviews(ctx context.Context) error { for _, comment := range comments { comment.Review = reviews[comment.ReviewID] if comment.Review == nil { - if comment.ReviewID > 0 { + // review request which has been replaced by actual reviews doesn't exist in database anymore, so don't log errors for them. + if comment.ReviewID > 0 && comment.Type != CommentTypeReviewRequest { log.Error("comment with review id [%d] but has no review record", comment.ReviewID) } continue diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index e08bd7fbf5..e7ceee4298 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -46,20 +46,20 @@ func TestCreateComment(t *testing.T) { unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } -func TestFetchCodeComments(t *testing.T) { +func TestFetchCodeConversations(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false) + res, err := issues_model.FetchCodeConversations(db.DefaultContext, issue, user, false) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) assert.Len(t, res["README.md"][4], 1) - assert.Equal(t, int64(4), res["README.md"][4][0].ID) + assert.Equal(t, int64(4), res["README.md"][4][0][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false) + res, err = issues_model.FetchCodeConversations(db.DefaultContext, issue, user2, false) assert.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/content_history.go b/models/issues/content_history.go index 8c333bc6dd..cd3e217b21 100644 --- a/models/issues/content_history.go +++ b/models/issues/content_history.go @@ -161,22 +161,18 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6 } for _, item := range res { - item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0) + if item.UserID > 0 { + item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0) + } else { + item.UserAvatarLink = avatars.DefaultAvatarLink() + } } return res, nil } // HasIssueContentHistory check if a ContentHistory entry exists func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) { - exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{ - IssueID: issueID, - CommentID: commentID, - }) - if err != nil { - log.Error("can not fetch issue content history. err=%v", err) - return false, err - } - return exists, err + return db.GetEngine(dbCtx).Where("issue_id = ? AND comment_id = ?", issueID, commentID).Exist(new(ContentHistory)) } // SoftDeleteIssueContentHistory soft delete diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go index 0ea1d0f7b2..89d77a1df3 100644 --- a/models/issues/content_history_test.go +++ b/models/issues/content_history_test.go @@ -78,3 +78,16 @@ func TestContentHistory(t *testing.T) { assert.EqualValues(t, 7, list2[1].HistoryID) assert.EqualValues(t, 4, list2[2].HistoryID) } + +func TestHasIssueContentHistory(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Ensures that comment_id is into taken account even if it's zero. + _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 11, 100, timeutil.TimeStampNow(), "c-a", true) + _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 11, 100, timeutil.TimeStampNow().Add(5), "c-b", false) + + hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 11, 0) + assert.False(t, hasHistory1) + hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 11, 100) + assert.True(t, hasHistory2) +} diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 2b20ede173..044666a3f0 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -381,7 +381,7 @@ func TestCountIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{}) assert.NoError(t, err) - assert.EqualValues(t, 20, count) + assert.EqualValues(t, 22, count) } func TestIssueLoadAttributes(t *testing.T) { diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index 9b1a447471..9c9d5d66cd 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -46,10 +46,10 @@ func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error for i, c := range active { ids[i] = c.ID } - return neuterCrossReferencesIds(ctx, nil, ids) + return neuterCrossReferencesIDs(ctx, nil, ids) } -func neuterCrossReferencesIds(stdCtx context.Context, ctx *crossReferencesContext, ids []int64) error { +func neuterCrossReferencesIDs(stdCtx context.Context, ctx *crossReferencesContext, ids []int64) error { sess := db.GetEngine(stdCtx).In("id", ids).Cols("`ref_action`") if ctx != nil && ctx.OrigIssue.NoAutoTime { sess.SetExpr("updated_unix", ctx.OrigIssue.UpdatedUnix).NoAutoTime() @@ -104,7 +104,7 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe } } if len(ids) > 0 { - if err = neuterCrossReferencesIds(stdCtx, ctx, ids); err != nil { + if err = neuterCrossReferencesIDs(stdCtx, ctx, ids); err != nil { return err } } diff --git a/models/issues/pull.go b/models/issues/pull.go index 2cb1e1b971..98d1617380 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -652,6 +652,35 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, return pr, pr.LoadAttributes(ctx) } +// GetPullRequestsByBaseHeadInfo returns the pull request by given base and head +func GetPullRequestByBaseHeadInfo(ctx context.Context, baseID, headID int64, base, head string) (*PullRequest, error) { + pr := &PullRequest{} + sess := db.GetEngine(ctx). + Join("INNER", "issue", "issue.id = pull_request.issue_id"). + Where("base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?", baseID, base, headID, head) + has, err := sess.Get(pr) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPullRequestNotExist{ + HeadRepoID: headID, + BaseRepoID: baseID, + HeadBranch: head, + BaseBranch: base, + } + } + + if err = pr.LoadAttributes(ctx); err != nil { + return nil, err + } + if err = pr.LoadIssue(ctx); err != nil { + return nil, err + } + + return pr, nil +} + // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request // By poster id. func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) { @@ -893,7 +922,14 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullReque } rules, _ := GetCodeOwnersFromContent(ctx, data) - changedFiles, err := repo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) + + prInfo, err := repo.GetCompareInfo(repo.Path, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false) + if err != nil { + return err + } + // Use the merge base as the base instead of the main branch to avoid problems + // if the pull request is out of date with the base branch. + changedFiles, err := repo.GetFilesChangedBetween(prInfo.MergeBase, pr.HeadCommitID) if err != nil { return err } @@ -1093,3 +1129,23 @@ func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { } return committer.Commit() } + +// GetPullRequestByMergedCommit returns a merged pull request by the given commit +func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string) (*PullRequest, error) { + pr := new(PullRequest) + has, err := db.GetEngine(ctx).Where("base_repo_id = ? AND merged_commit_id = ?", repoID, sha).Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} + } + + if err = pr.LoadAttributes(ctx); err != nil { + return nil, err + } + if err = pr.LoadIssue(ctx); err != nil { + return nil, err + } + + return pr, nil +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 4702049af2..213838abec 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -425,6 +425,18 @@ func TestGetApprovers(t *testing.T) { assert.EqualValues(t, expected, approvers) } +func TestGetPullRequestByMergedCommit(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr, err := issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") + assert.NoError(t, err) + assert.EqualValues(t, 1, pr.ID) + + _, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 0, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") + assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) + _, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "") + assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) +} + func TestMigrate_InsertPullRequests(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) reponame := "repo1" diff --git a/models/issues/review.go b/models/issues/review.go index f2022ae0aa..fc110630e0 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -159,6 +159,14 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) { return err } r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err) + } + r.ReviewerID = user_model.GhostUserID + r.Reviewer = user_model.NewGhostUser() + return nil + } return err } @@ -284,8 +292,14 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio // CreateReview creates a new review based on opts func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + sess := db.GetEngine(ctx) + review := &Review{ - Type: opts.Type, Issue: opts.Issue, IssueID: opts.Issue.ID, Reviewer: opts.Reviewer, @@ -295,15 +309,39 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error CommitID: opts.CommitID, Stale: opts.Stale, } + if opts.Reviewer != nil { + review.Type = opts.Type review.ReviewerID = opts.Reviewer.ID - } else { - if review.Type != ReviewTypeRequest { - review.Type = ReviewTypeRequest + + reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID} + // make sure user review requests are cleared + if opts.Type != ReviewTypePending { + if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { + return nil, err + } } + // make sure if the created review gets dismissed no old review surface + // other types can be ignored, as they don't affect branch protection + if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject { + if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))). + Cols("dismissed").Update(&Review{Dismissed: true}); err != nil { + return nil, err + } + } + + } else if opts.ReviewerTeam != nil { + review.Type = ReviewTypeRequest review.ReviewerTeamID = opts.ReviewerTeam.ID + + } else { + return nil, fmt.Errorf("provide either reviewer or reviewer team") } - return review, db.Insert(ctx, review) + + if _, err := sess.Insert(review); err != nil { + return nil, err + } + return review, committer.Commit() } // GetCurrentReview returns the current pending review of reviewer for given issue @@ -621,6 +659,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo return nil, err } + // func caller use the created comment to retrieve created review too. + comment.Review = review + return comment, committer.Commit() } diff --git a/models/issues/review_list.go b/models/issues/review_list.go index ed3d0bd028..282f18b4f7 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -18,11 +18,11 @@ type ReviewList []*Review // LoadReviewers loads reviewers func (reviews ReviewList) LoadReviewers(ctx context.Context) error { - reviewerIds := make([]int64, len(reviews)) + reviewerIDs := make([]int64, len(reviews)) for i := 0; i < len(reviews); i++ { - reviewerIds[i] = reviews[i].ReviewerID + reviewerIDs[i] = reviews[i].ReviewerID } - reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIds) + reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs) if err != nil { return err } @@ -38,12 +38,12 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error { } func (reviews ReviewList) LoadIssues(ctx context.Context) error { - issueIds := container.Set[int64]{} + issueIDs := container.Set[int64]{} for i := 0; i < len(reviews); i++ { - issueIds.Add(reviews[i].IssueID) + issueIDs.Add(reviews[i].IssueID) } - issues, err := GetIssuesByIDs(ctx, issueIds.Values()) + issues, err := GetIssuesByIDs(ctx, issueIDs.Values()) if err != nil { return err } diff --git a/models/organization/org.go b/models/organization/org.go index 23a4e2f96a..b4919defb4 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -594,9 +594,7 @@ func GetOrgByID(ctx context.Context, id int64) (*Organization, error) { return nil, err } else if !has { return nil, user_model.ErrUserNotExist{ - UID: id, - Name: "", - KeyID: 0, + UID: id, } } return u, nil diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 0b66e62d7d..7e39627a75 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -356,7 +356,6 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model. // CanBeAssigned return true if user can be assigned to issue or pull requests in repo // Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface. -// FIXME: user could send PullRequest also could be assigned??? func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) { if user.IsOrganization() { return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) @@ -365,7 +364,8 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. if err != nil { return false, err } - return perm.CanAccessAny(perm_model.AccessModeWrite, unit.TypeCode, unit.TypeIssues, unit.TypePullRequests), nil + return perm.CanAccessAny(perm_model.AccessModeWrite, unit.AllRepoUnitTypes...) || + perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil } // HasAccess returns true if user has access to repo diff --git a/models/repo/git.go b/models/repo/git.go index 610c554296..388bf86522 100644 --- a/models/repo/git.go +++ b/models/repo/git.go @@ -21,6 +21,8 @@ const ( MergeStyleRebaseMerge MergeStyle = "rebase-merge" // MergeStyleSquash squash commits into single commit before merging MergeStyleSquash MergeStyle = "squash" + // MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail + MergeStyleFastForwardOnly MergeStyle = "fast-forward-only" // MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly MergeStyleManuallyMerged MergeStyle = "manually-merged" // MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase diff --git a/models/repo/repo.go b/models/repo/repo.go index 037a79d504..f77454a94e 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -449,6 +449,31 @@ func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, e return nil, ErrUnitTypeNotExist{tp} } +// AllUnitsEnabled returns true if all units are enabled for the repo. +func (repo *Repository) AllUnitsEnabled(ctx context.Context) bool { + hasAnyUnitEnabled := func(unitGroup []unit.Type) bool { + // Loop over the group of units + for _, unit := range unitGroup { + // If *any* of them is enabled, return true. + if repo.UnitEnabled(ctx, unit) { + return true + } + } + + // If none are enabled, return false. + return false + } + + for _, unitGroup := range unit.AllowedRepoUnitGroups { + // If any disabled unit is found, return false immediately. + if !hasAnyUnitEnabled(unitGroup) { + return false + } + } + + return true +} + // LoadOwner loads owner user func (repo *Repository) LoadOwner(ctx context.Context) (err error) { if repo.Owner != nil { diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index a8b958109c..8006289460 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -138,12 +138,12 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, - count: 32, + count: 34, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, - count: 37, + count: 39, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", @@ -158,7 +158,7 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfOrganization", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, - count: 32, + count: 34, }, { name: "AllTemplates", diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 3df5236ea7..08058b0d45 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -153,6 +153,7 @@ type PullRequestsConfig struct { AllowRebase bool AllowRebaseMerge bool AllowSquash bool + AllowFastForwardOnly bool AllowManualMerge bool AutodetectManualMerge bool AllowRebaseUpdate bool @@ -179,6 +180,7 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool { mergeStyle == MergeStyleRebase && cfg.AllowRebase || mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge || mergeStyle == MergeStyleSquash && cfg.AllowSquash || + mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly || mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge } diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 5d6e24e2a5..219f4ff25d 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" api "code.gitea.io/gitea/modules/structs" @@ -78,7 +79,8 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us if err = e.Table("team_user"). Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite). + Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", + repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). Distinct("`team_user`.uid"). Select("`team_user`.uid"). Find(&additionalUserIDs); err != nil { diff --git a/models/shared/types/ownertype.go b/models/shared/types/ownertype.go index e6fe4e4cfd..a1d46c986f 100644 --- a/models/shared/types/ownertype.go +++ b/models/shared/types/ownertype.go @@ -17,13 +17,13 @@ const ( func (o OwnerType) LocaleString(locale translation.Locale) string { switch o { case OwnerTypeSystemGlobal: - return locale.Tr("concept_system_global") + return locale.TrString("concept_system_global") case OwnerTypeIndividual: - return locale.Tr("concept_user_individual") + return locale.TrString("concept_user_individual") case OwnerTypeRepository: - return locale.Tr("concept_code_repository") + return locale.TrString("concept_code_repository") case OwnerTypeOrganization: - return locale.Tr("concept_user_organization") + return locale.TrString("concept_user_organization") } - return locale.Tr("unknown") + return locale.TrString("unknown") } diff --git a/models/unit/unit.go b/models/unit/unit.go index b216712d37..e37adf995e 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -108,6 +108,10 @@ var ( // DisabledRepoUnits contains the units that have been globally disabled DisabledRepoUnits = []Type{} + + // AllowedRepoUnitGroups contains the units that have been globally enabled, + // with mutually exclusive units grouped together. + AllowedRepoUnitGroups = [][]Type{} ) // Get valid set of default repository units from settings @@ -162,6 +166,45 @@ func LoadUnitConfig() error { if len(DefaultForkRepoUnits) == 0 { return errors.New("no default fork repository units found") } + + // Collect the allowed repo unit groups. Mutually exclusive units are + // grouped together. + AllowedRepoUnitGroups = [][]Type{} + for _, unit := range []Type{ + TypeCode, + TypePullRequests, + TypeProjects, + TypePackages, + TypeActions, + } { + // If unit is globally disabled, ignore it. + if unit.UnitGlobalDisabled() { + continue + } + + // If it is allowed, add it to the group list. + AllowedRepoUnitGroups = append(AllowedRepoUnitGroups, []Type{unit}) + } + + addMutuallyExclusiveGroup := func(unit1, unit2 Type) { + var list []Type + + if !unit1.UnitGlobalDisabled() { + list = append(list, unit1) + } + + if !unit2.UnitGlobalDisabled() { + list = append(list, unit2) + } + + if len(list) > 0 { + AllowedRepoUnitGroups = append(AllowedRepoUnitGroups, list) + } + } + + addMutuallyExclusiveGroup(TypeIssues, TypeExternalTracker) + addMutuallyExclusiveGroup(TypeWiki, TypeExternalWiki) + return nil } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 69f3505c28..af5c31f157 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -44,12 +44,12 @@ func fatalTestError(fmtStr string, args ...any) { } // InitSettings initializes config provider and load common settings for tests -func InitSettings(extraConfigs ...string) { +func InitSettings() { if setting.CustomConf == "" { setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") _ = os.Remove(setting.CustomConf) } - setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n")) + setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() if err := setting.PrepareAppDataPath(); err != nil { diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index d47bceea1e..75898436fc 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -131,8 +131,8 @@ func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { } // AssertCount assert the count of a bean -func AssertCount(t assert.TestingT, bean, expected any) { - assert.EqualValues(t, expected, GetCount(t, bean)) +func AssertCount(t assert.TestingT, bean, expected any) bool { + return assert.EqualValues(t, expected, GetCount(t, bean)) } // AssertInt64InRange assert value is in range [low, high] @@ -150,7 +150,7 @@ func GetCountByCond(t assert.TestingT, tableName string, cond builder.Cond) int6 } // AssertCountByCond test the count of database entries matching bean -func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) { - assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), +func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) bool { + return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), "Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond) } diff --git a/models/user/error.go b/models/user/error.go index ef572c178a..cbf19998d1 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -31,9 +31,8 @@ func (err ErrUserAlreadyExist) Unwrap() error { // ErrUserNotExist represents a "UserNotExist" kind of error. type ErrUserNotExist struct { - UID int64 - Name string - KeyID int64 + UID int64 + Name string } // IsErrUserNotExist checks if an error is a ErrUserNotExist. @@ -43,7 +42,7 @@ func IsErrUserNotExist(err error) bool { } func (err ErrUserNotExist) Error() string { - return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID) + return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name) } // Unwrap unwraps this error as a ErrNotExist error diff --git a/models/user/user.go b/models/user/user.go index 1c305fff8e..975adc6e28 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -442,14 +443,14 @@ func (u *User) GetDisplayName() string { } // GetCompleteName returns the the full name and username in the form of -// "Full Name (@username)" if full name is not empty, otherwise it returns -// "@username". +// "Full Name (username)" if full name is not empty, otherwise it returns +// "username". func (u *User) GetCompleteName() string { trimmedFullName := strings.TrimSpace(u.FullName) if len(trimmedFullName) > 0 { - return fmt.Sprintf("%s (@%s)", trimmedFullName, u.Name) + return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name) } - return fmt.Sprintf("@%s", u.Name) + return u.Name } func gitSafeName(name string) string { @@ -591,14 +592,14 @@ func IsUsableUsername(name string) error { // CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation type CreateUserOverwriteOptions struct { - KeepEmailPrivate util.OptionalBool + KeepEmailPrivate optional.Option[bool] Visibility *structs.VisibleType - AllowCreateOrganization util.OptionalBool + AllowCreateOrganization optional.Option[bool] EmailNotificationsPreference *string MaxRepoCreation *int Theme *string - IsRestricted util.OptionalBool - IsActive util.OptionalBool + IsRestricted optional.Option[bool] + IsActive optional.Option[bool] } // CreateUser creates record of a new user. @@ -625,14 +626,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve // overwrite defaults if set if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { overwrite := overwriteDefault[0] - if !overwrite.KeepEmailPrivate.IsNone() { - u.KeepEmailPrivate = overwrite.KeepEmailPrivate.IsTrue() + if overwrite.KeepEmailPrivate.Has() { + u.KeepEmailPrivate = overwrite.KeepEmailPrivate.Value() } if overwrite.Visibility != nil { u.Visibility = *overwrite.Visibility } - if !overwrite.AllowCreateOrganization.IsNone() { - u.AllowCreateOrganization = overwrite.AllowCreateOrganization.IsTrue() + if overwrite.AllowCreateOrganization.Has() { + u.AllowCreateOrganization = overwrite.AllowCreateOrganization.Value() } if overwrite.EmailNotificationsPreference != nil { u.EmailNotificationsPreference = *overwrite.EmailNotificationsPreference @@ -643,11 +644,11 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve if overwrite.Theme != nil { u.Theme = *overwrite.Theme } - if !overwrite.IsRestricted.IsNone() { - u.IsRestricted = overwrite.IsRestricted.IsTrue() + if overwrite.IsRestricted.Has() { + u.IsRestricted = overwrite.IsRestricted.Value() } - if !overwrite.IsActive.IsNone() { - u.IsActive = overwrite.IsActive.IsTrue() + if overwrite.IsActive.Has() { + u.IsActive = overwrite.IsActive.Value() } } @@ -864,7 +865,7 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) { if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist{id, "", 0} + return nil, ErrUserNotExist{UID: id} } return u, nil } @@ -914,14 +915,14 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { // GetUserByNameCtx returns user by given name. func GetUserByName(ctx context.Context, name string) (*User, error) { if len(name) == 0 { - return nil, ErrUserNotExist{0, name, 0} + return nil, ErrUserNotExist{Name: name} } u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual} has, err := db.GetEngine(ctx).Get(u) if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist{0, name, 0} + return nil, ErrUserNotExist{Name: name} } return u, nil } @@ -1062,7 +1063,7 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) [] // GetUserByEmail returns the user object by given e-mail if exists. func GetUserByEmail(ctx context.Context, email string) (*User, error) { if len(email) == 0 { - return nil, ErrUserNotExist{0, email, 0} + return nil, ErrUserNotExist{Name: email} } email = strings.ToLower(email) @@ -1089,7 +1090,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } } - return nil, ErrUserNotExist{0, email, 0} + return nil, ErrUserNotExist{Name: email} } // GetUser checks if a user already exists @@ -1100,7 +1101,7 @@ func GetUser(ctx context.Context, user *User) (bool, error) { // GetUserByOpenID returns the user object by given OpenID if exists. func GetUserByOpenID(ctx context.Context, uri string) (*User, error) { if len(uri) == 0 { - return nil, ErrUserNotExist{0, uri, 0} + return nil, ErrUserNotExist{Name: uri} } uri, err := openid.Normalize(uri) @@ -1120,7 +1121,7 @@ func GetUserByOpenID(ctx context.Context, uri string) (*User, error) { return GetUserByID(ctx, oid.UID) } - return nil, ErrUserNotExist{0, uri, 0} + return nil, ErrUserNotExist{Name: uri} } // GetAdminUser returns the first administrator diff --git a/models/user/user_test.go b/models/user/user_test.go index 5105845394..52002e998d 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -98,7 +98,7 @@ func TestSearchUsers(t *testing.T) { []int64{19, 25}) testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, - []int64{26}) + []int64{26, 41}) testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, []int64{}) @@ -110,13 +110,13 @@ func TestSearchUsers(t *testing.T) { } testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, - []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37}) + []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse}, []int64{9}) testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, - []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37}) + []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) diff --git a/modules/actions/github.go b/modules/actions/github.go index a988b2a124..cc04ca5c5c 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -25,6 +25,45 @@ const ( GithubEventSchedule = "schedule" ) +// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch +func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool { + switch triggedEvent { + case webhook_module.HookEventDelete: + // GitHub "delete" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete + return true + case webhook_module.HookEventFork: + // GitHub "fork" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork + return true + case webhook_module.HookEventIssueComment: + // GitHub "issue_comment" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment + return true + case webhook_module.HookEventPullRequestComment: + // GitHub "pull_request_comment" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment + return true + case webhook_module.HookEventWiki: + // GitHub "gollum" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum + return true + case webhook_module.HookEventSchedule: + // GitHub "schedule" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule + return true + case webhook_module.HookEventIssues, + webhook_module.HookEventIssueAssign, + webhook_module.HookEventIssueLabel, + webhook_module.HookEventIssueMilestone: + // Github "issues" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues + return true + } + + return false +} + // canGithubEventMatch check if the input Github event can match any Gitea event. func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool { switch eventName { @@ -55,7 +94,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, - webhook_module.HookEventPullRequestLabel: + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: return true default: @@ -73,6 +114,11 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } + case GithubEventIssueComment: + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment + return triggedEvent == webhook_module.HookEventIssueComment || + triggedEvent == webhook_module.HookEventPullRequestComment + default: return eventName == string(triggedEvent) } diff --git a/modules/actions/github_test.go b/modules/actions/github_test.go index 4bf55ae03f..6652ff6eac 100644 --- a/modules/actions/github_test.go +++ b/modules/actions/github_test.go @@ -103,6 +103,12 @@ func TestCanGithubEventMatch(t *testing.T) { webhook_module.HookEventCreate, true, }, + { + "create pull request comment", + GithubEventIssueComment, + webhook_module.HookEventPullRequestComment, + true, + }, } for _, tc := range testCases { diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 00d83e06d7..81ab26bc27 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -186,7 +186,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, - webhook_module.HookEventPullRequestLabel: + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt) case // pull_request_review @@ -362,13 +364,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa } else { // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request // Actions with the same name: - // opened, edited, closed, reopened, assigned, unassigned + // opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned // Actions need to be converted: // synchronized -> synchronize // label_updated -> labeled // label_cleared -> unlabeled // Unsupported activity types: - // converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled + // converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued action := prPayload.Action switch action { diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index 2c7205b708..27074358a9 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -8,6 +8,7 @@ import ( "context" "crypto/rand" "errors" + "html/template" "math/big" "strings" "sync" @@ -121,15 +122,15 @@ func Generate(n int) (string, error) { } // BuildComplexityError builds the error message when password complexity checks fail -func BuildComplexityError(locale translation.Locale) string { +func BuildComplexityError(locale translation.Locale) template.HTML { var buffer bytes.Buffer - buffer.WriteString(locale.Tr("form.password_complexity")) + buffer.WriteString(locale.TrString("form.password_complexity")) buffer.WriteString("
    ") for _, c := range requiredList { buffer.WriteString("
  • ") - buffer.WriteString(locale.Tr(c.TrNameOne)) + buffer.WriteString(locale.TrString(c.TrNameOne)) buffer.WriteString("
  • ") } buffer.WriteString("
") - return buffer.String() + return template.HTML(buffer.String()) } diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index e920177f89..5b5febb906 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -5,11 +5,13 @@ package base import ( "math/big" + "strings" "unicode/utf8" ) // NaturalSortLess compares two strings so that they could be sorted in natural order func NaturalSortLess(s1, s2 string) bool { + s1, s2 = strings.ToLower(s1), strings.ToLower(s2) var i1, i2 int for { rune1, j1, end1 := getNextRune(s1, i1) diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index 91e864ad2a..7378d9a643 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -20,4 +20,10 @@ func TestNaturalSortLess(t *testing.T) { test("a-1-a", "a-1-b", true) test("2", "12", true) test("a", "ab", true) + + // Test for case insensitive. + test("A", "ab", true) + test("B", "ab", false) + test("a", "AB", true) + test("b", "AB", false) } diff --git a/modules/base/tool.go b/modules/base/tool.go index b72f3a1930..231507546d 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -115,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string { // create sha1 encode string sh := sha1.New() - _, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes))) + _, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes))) encoded := hex.EncodeToString(sh.Sum(nil)) code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded) @@ -174,7 +174,7 @@ func Int64sToStrings(ints []int64) []string { func EntryIcon(entry *git.TreeEntry) string { switch { case entry.IsLink(): - te, err := entry.FollowLink() + te, _, err := entry.FollowLink() if err != nil { log.Debug(err.Error()) return "file-symlink-file" diff --git a/modules/charset/escape.go b/modules/charset/escape.go index 92e417d1f7..ba0eb73a3a 100644 --- a/modules/charset/escape.go +++ b/modules/charset/escape.go @@ -10,6 +10,7 @@ package charset import ( "html/template" "io" + "slices" "strings" "code.gitea.io/gitea/modules/log" @@ -20,16 +21,29 @@ import ( // RuneNBSP is the codepoint for NBSP const RuneNBSP = 0xa0 +type escapeContext string + +// Keep this consistent with the documentation of [ui].SKIP_ESCAPE_CONTEXTS +// Defines the different contexts that could be used to escape in. +const ( + // Wiki pages. + WikiContext escapeContext = "wiki" + // Rendered content (except markup), source code and blames. + FileviewContext escapeContext = "file-view" + // Commits or pull requet's diff. + DiffContext escapeContext = "diff" +) + // EscapeControlHTML escapes the unicode control sequences in a provided html document -func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) { +func EscapeControlHTML(html template.HTML, locale translation.Locale, context escapeContext, allowed ...rune) (escaped *EscapeStatus, output template.HTML) { sb := &strings.Builder{} - escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader + escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, context, allowed...) // err has been handled in EscapeControlReader return escaped, template.HTML(sb.String()) } // EscapeControlReader escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus -func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) { - if !setting.UI.AmbiguousUnicodeDetection { +func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, context escapeContext, allowed ...rune) (escaped *EscapeStatus, err error) { + if !setting.UI.AmbiguousUnicodeDetection || slices.Contains(setting.UI.SkipEscapeContexts, string(context)) { _, err = io.Copy(writer, reader) return &EscapeStatus{}, err } diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 3f08fd94a4..29943eb858 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error { Val: "ambiguous-code-point", }, html.Attribute{ Key: "data-tooltip-content", - Val: e.locale.Tr("repo.ambiguous_character", r, c), + Val: e.locale.TrString("repo.ambiguous_character", r, c), }); err != nil { return err } diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index a353ced631..7442a80d7f 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -4,6 +4,7 @@ package charset import ( + "html/template" "strings" "testing" @@ -14,6 +15,8 @@ import ( "github.com/stretchr/testify/assert" ) +var testContext = escapeContext("test") + type escapeControlTest struct { name string text string @@ -159,7 +162,7 @@ func TestEscapeControlReader(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := &strings.Builder{} - status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{}) + status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{}, testContext) assert.NoError(t, err) assert.Equal(t, tt.status, *status) assert.Equal(t, tt.result, output.String()) @@ -169,9 +172,22 @@ func TestEscapeControlReader(t *testing.T) { func TestSettingAmbiguousUnicodeDetection(t *testing.T) { defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)() - _, out := EscapeControlHTML("a test", &translation.MockLocale{}) + + _, out := EscapeControlHTML("a test", &translation.MockLocale{}, testContext) assert.EqualValues(t, `a test`, out) setting.UI.AmbiguousUnicodeDetection = false - _, out = EscapeControlHTML("a test", &translation.MockLocale{}) + _, out = EscapeControlHTML("a test", &translation.MockLocale{}, testContext) assert.EqualValues(t, `a test`, out) } + +func TestAmbiguousUnicodeDetectionContext(t *testing.T) { + defer test.MockVariableValue(&setting.UI.SkipEscapeContexts, []string{"test"})() + + input := template.HTML("a test") + + _, out := EscapeControlHTML(input, &translation.MockLocale{}, escapeContext("not-test")) + assert.EqualValues(t, `a test`, out) + + _, out = EscapeControlHTML(input, &translation.MockLocale{}, testContext) + assert.EqualValues(t, input, out) +} diff --git a/modules/context/api.go b/modules/context/api.go index 05b6a7a533..fafd49fd42 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -247,7 +247,7 @@ func APIContexter() func(http.Handler) http.Handler { // NotFound handles 404s for APIContext // String will replace message, errors will be added to a slice func (ctx *APIContext) NotFound(objs ...any) { - message := ctx.Tr("error.not_found") + message := ctx.Locale.TrString("error.not_found") var errors []string for _, obj := range objs { // Ignore nil @@ -309,12 +309,6 @@ func RepoRefForAPI(next http.Handler) http.Handler { return } - objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat() - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) - return - } - if ref := ctx.FormTrim("ref"); len(ref) > 0 { commit, err := ctx.Repo.GitRepo.GetCommit(ref) if err != nil { @@ -333,6 +327,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { } refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny) + var err error if ctx.Repo.GitRepo.IsBranchExist(refName) { ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) @@ -348,7 +343,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if len(refName) == objectFormat.FullLength() { + } else if len(refName) == ctx.Repo.GetObjectFormat().FullLength() { ctx.Repo.CommitID = refName ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) if err != nil { diff --git a/modules/context/base.go b/modules/context/base.go index 8df1dde866..fa05850a16 100644 --- a/modules/context/base.go +++ b/modules/context/base.go @@ -6,6 +6,7 @@ package context import ( "context" "fmt" + "html/template" "io" "net/http" "net/url" @@ -286,11 +287,11 @@ func (b *Base) cleanUp() { } } -func (b *Base) Tr(msg string, args ...any) string { +func (b *Base) Tr(msg string, args ...any) template.HTML { return b.Locale.Tr(msg, args...) } -func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string { +func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return b.Locale.TrN(cnt, key1, keyN, args...) } diff --git a/modules/context/context.go b/modules/context/context.go index d19c5d1198..a06ebfb0dc 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -6,7 +6,8 @@ package context import ( "context" - "html" + "encoding/hex" + "fmt" "html/template" "io" "net/http" @@ -71,16 +72,6 @@ func init() { }) } -// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString. -// This is useful if the locale message is intended to only produce HTML content. -func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { - trArgs := make([]any, len(args)) - for i, arg := range args { - trArgs[i] = html.EscapeString(arg) - } - return ctx.Locale.Tr(msg, trArgs...) -} - type webContextKeyType struct{} var WebContextKey = webContextKeyType{} @@ -134,7 +125,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { func Contexter() func(next http.Handler) http.Handler { rnd := templates.HTMLRenderer() csrfOpts := CsrfOptions{ - Secret: setting.SecretKey, + Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), Cookie: setting.CSRFCookieName, SetCookie: true, Secure: setting.SessionConfig.Secure, @@ -207,6 +198,7 @@ func Contexter() func(next http.Handler) http.Handler { // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["DisableStars"] = setting.Repository.DisableStars + ctx.Data["DisableForks"] = setting.Repository.DisableForks ctx.Data["EnableActions"] = setting.Actions.Enabled ctx.Data["ManifestData"] = setting.ManifestData @@ -253,6 +245,13 @@ func (ctx *Context) JSONOK() { ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it } -func (ctx *Context) JSONError(msg string) { - ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg}) +func (ctx *Context) JSONError(msg any) { + switch v := msg.(type) { + case string: + ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"}) + case template.HTML: + ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"}) + default: + panic(fmt.Sprintf("unsupported type: %T", msg)) + } } diff --git a/modules/context/context_response.go b/modules/context/context_response.go index 5729865561..829bca1f59 100644 --- a/modules/context/context_response.go +++ b/modules/context/context_response.go @@ -90,6 +90,20 @@ func (ctx *Context) HTML(status int, name base.TplName) { } } +// JSONTemplate renders the template as JSON response +// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape +func (ctx *Context) JSONTemplate(tmpl base.TplName) { + t, err := ctx.Render.TemplateLookup(string(tmpl), nil) + if err != nil { + ctx.ServerError("unable to find template", err) + return + } + ctx.Resp.Header().Set("Content-Type", "application/json") + if err = t.Execute(ctx.Resp, ctx.Data); err != nil { + ctx.ServerError("unable to execute template", err) + } +} + // RenderToString renders the template content to a string func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) { var buf strings.Builder @@ -98,12 +112,11 @@ func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (stri } // RenderWithErr used for page has form validation but need to prompt error to users. -func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) { +func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) { if form != nil { middleware.AssignForm(form, ctx.Data) } - ctx.Flash.ErrorMsg = msg - ctx.Data["Flash"] = ctx.Flash + ctx.Flash.Error(msg, true) ctx.HTML(http.StatusOK, tpl) } diff --git a/modules/context/context_template.go b/modules/context/context_template.go index ba90fc170a..7878d409ca 100644 --- a/modules/context/context_template.go +++ b/modules/context/context_template.go @@ -5,10 +5,7 @@ package context import ( "context" - "errors" "time" - - "code.gitea.io/gitea/modules/log" ) var _ context.Context = TemplateContext(nil) @@ -36,14 +33,3 @@ func (c TemplateContext) Err() error { func (c TemplateContext) Value(key any) any { return c.parentContext().Value(key) } - -// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context -// as the current template's rendering context (request context), to help to find data race issues as early as possible. -// When the code is proven to be correct and stable, this function should be removed. -func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) { - if c.parentContext() != dataCtx { - log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2)) - return "", errors.New("parent context mismatch") - } - return "", nil -} diff --git a/modules/context/org.go b/modules/context/org.go index d068646577..018b76de43 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -11,6 +11,8 @@ import ( "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" ) @@ -255,6 +257,19 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) + + ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + if len(ctx.ContextUser.Description) != 0 { + content, err := markdown.RenderString(&markup.RenderContext{ + Metas: map[string]string{"mode": "document"}, + Ctx: ctx, + }, ctx.ContextUser.Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + ctx.Data["RenderedDescription"] = content + } } // OrgAssignment returns a middleware to handle organization assignment diff --git a/modules/context/repo.go b/modules/context/repo.go index 850edf666d..5e4f2718c4 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -6,6 +6,7 @@ package context import ( "context" + "errors" "fmt" "html" "net/http" @@ -26,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -81,11 +83,15 @@ func (r *Repository) CanCreateBranch() bool { return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() } +func (r *Repository) GetObjectFormat() git.ObjectFormat { + return git.ObjectFormatFromName(r.Repository.ObjectFormatName) +} + // RepoMustNotBeArchived checks if a repo is archived func RepoMustNotBeArchived() func(ctx *Context) { return func(ctx *Context) { if ctx.Repo.Repository.IsArchived { - ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title"))) + ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title"))) } } } @@ -687,7 +693,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { branchOpts := git_model.FindBranchOptions{ RepoID: ctx.Repo.Repository.ID, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), ListOptions: db.ListOptionsAll, } branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts) @@ -845,9 +851,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { } // For legacy and API support only full commit sha parts := strings.Split(path, "/") - objectFormat, _ := repo.GitRepo.GetObjectFormat() - if len(parts) > 0 && len(parts[0]) == objectFormat.FullLength() { + if len(parts) > 0 && len(parts[0]) == git.ObjectFormatFromName(repo.Repository.ObjectFormatName).FullLength() { repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } @@ -891,9 +896,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist) case RepoRefCommit: parts := strings.Split(path, "/") - objectFormat, _ := repo.GitRepo.GetObjectFormat() - if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= objectFormat.FullLength() { + if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= repo.GetObjectFormat().FullLength() { repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } @@ -952,12 +956,6 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context } } - objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat() - if err != nil { - log.Error("Cannot determine objectFormat for repository: %w", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() - } - // Get default branch. if len(ctx.Params("*")) == 0 { refName = ctx.Repo.Repository.DefaultBranch @@ -1024,7 +1022,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if len(refName) >= 7 && len(refName) <= objectFormat.FullLength() { + } else if len(refName) >= 7 && len(refName) <= ctx.Repo.GetObjectFormat().FullLength() { ctx.Repo.IsViewCommit = true ctx.Repo.CommitID = refName @@ -1034,7 +1032,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return cancel } // If short commit ID add canonical link header - if len(refName) < objectFormat.FullLength() { + if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"", util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)))) } diff --git a/modules/csv/csv.go b/modules/csv/csv.go index c5497befe7..35c5d6ab67 100644 --- a/modules/csv/csv.go +++ b/modules/csv/csv.go @@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune { func FormatError(err error, locale translation.Locale) (string, error) { if perr, ok := err.(*stdcsv.ParseError); ok { if perr.Err == stdcsv.ErrFieldCount { - return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil + return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil } - return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil + return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil } return "", err diff --git a/modules/generate/generate.go b/modules/generate/generate.go index df3e2474f9..41a6aa2815 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -7,6 +7,7 @@ package generate import ( "crypto/rand" "encoding/base64" + "fmt" "io" "time" @@ -38,6 +39,20 @@ func NewInternalToken() (string, error) { return internalToken, nil } +const defaultJwtSecretLen = 32 + +// DecodeJwtSecret decodes a base64 encoded jwt secret into bytes, and check its length +func DecodeJwtSecret(src string) ([]byte, error) { + encoding := base64.RawURLEncoding + decoded := make([]byte, encoding.DecodedLen(len(src))+3) + if n, err := encoding.Decode(decoded, []byte(src)); err != nil { + return nil, err + } else if n != defaultJwtSecretLen { + return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen) + } + return decoded[:defaultJwtSecretLen], nil +} + // NewJwtSecret generates a new base64 encoded value intended to be used for JWT secrets. func NewJwtSecret() ([]byte, string, error) { bytes := make([]byte, 32) diff --git a/modules/generate/generate_test.go b/modules/generate/generate_test.go new file mode 100644 index 0000000000..7d023b23ad --- /dev/null +++ b/modules/generate/generate_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package generate + +import ( + "encoding/base64" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeJwtSecret(t *testing.T) { + _, err := DecodeJwtSecret("abcd") + assert.ErrorContains(t, err, "invalid base64 decoded length") + _, err = DecodeJwtSecret(strings.Repeat("a", 64)) + assert.ErrorContains(t, err, "invalid base64 decoded length") + + str32 := strings.Repeat("x", 32) + encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32)) + decoded32, err := DecodeJwtSecret(encoded32) + assert.NoError(t, err) + assert.Equal(t, str32, string(decoded32)) +} + +func TestNewJwtSecret(t *testing.T) { + secret, encoded, err := NewJwtSecret() + assert.NoError(t, err) + assert.Len(t, secret, 32) + decoded, err := DecodeJwtSecret(encoded) + assert.NoError(t, err) + assert.Equal(t, secret, decoded) +} diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 53a9393d5f..043dbb44bd 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -203,16 +203,7 @@ headerLoop: } // Discard the rest of the tag - discard := size - n + 1 - for discard > math.MaxInt32 { - _, err := rd.Discard(math.MaxInt32) - if err != nil { - return id, err - } - discard -= math.MaxInt32 - } - _, err := rd.Discard(int(discard)) - return id, err + return id, DiscardFull(rd, size-n+1) } // ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream. @@ -238,16 +229,7 @@ headerLoop: } // Discard the rest of the commit - discard := size - n + 1 - for discard > math.MaxInt32 { - _, err := rd.Discard(math.MaxInt32) - if err != nil { - return id, err - } - discard -= math.MaxInt32 - } - _, err := rd.Discard(int(discard)) - return id, err + return id, DiscardFull(rd, size-n+1) } // git tree files are a list: @@ -345,3 +327,21 @@ func init() { _, filename, _, _ := runtime.Caller(0) callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go") } + +func DiscardFull(rd *bufio.Reader, discard int64) error { + if discard > math.MaxInt32 { + n, err := rd.Discard(math.MaxInt32) + discard -= int64(n) + if err != nil { + return err + } + } + for discard > 0 { + n, err := rd.Discard(int(discard)) + discard -= int64(n) + if err != nil { + return err + } + } + return nil +} diff --git a/modules/git/blame.go b/modules/git/blame.go index 64095a218a..69e1b08f93 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -115,6 +115,10 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // Close BlameReader - don't run NextPart after invoking that func (r *BlameReader) Close() error { + if r.bufferedReader == nil { + return nil + } + err := <-r.done r.bufferedReader = nil _ = r.reader.Close() diff --git a/modules/git/blame_sha256_test.go b/modules/git/blame_sha256_test.go index 01de0454a3..92f8b5b02a 100644 --- a/modules/git/blame_sha256_test.go +++ b/modules/git/blame_sha256_test.go @@ -11,6 +11,8 @@ import ( ) func TestReadingBlameOutputSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go index 6e8a48b1db..945a6bc432 100644 --- a/modules/git/blob_nogogit.go +++ b/modules/git/blob_nogogit.go @@ -9,7 +9,6 @@ import ( "bufio" "bytes" "io" - "math" "code.gitea.io/gitea/modules/log" ) @@ -103,26 +102,17 @@ func (b *blobReader) Read(p []byte) (n int, err error) { // Close implements io.Closer func (b *blobReader) Close() error { - defer b.cancel() - if b.n > 0 { - for b.n > math.MaxInt32 { - n, err := b.rd.Discard(math.MaxInt32) - b.n -= int64(n) - if err != nil { - return err - } - b.n -= math.MaxInt32 - } - n, err := b.rd.Discard(int(b.n)) - b.n -= int64(n) - if err != nil { - return err - } + if b.rd == nil { + return nil } - if b.n == 0 { - _, err := b.rd.Discard(1) - b.n-- + + defer b.cancel() + + if err := DiscardFull(b.rd, b.n+1); err != nil { return err } + + b.rd = nil + return nil } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index e469d2cab6..a5d18694f7 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -151,6 +151,9 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, return nil, err } if typ != "commit" { + if err := DiscardFull(batchReader, size+1); err != nil { + return nil, err + } return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID) } c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size)) diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 82112cb409..916169f4e2 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -14,6 +14,8 @@ import ( ) func TestCommitsCountSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") commitsCount, err := CommitsCount(DefaultContext, @@ -27,6 +29,8 @@ func TestCommitsCountSha256(t *testing.T) { } func TestCommitsCountWithoutBaseSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") commitsCount, err := CommitsCount(DefaultContext, @@ -41,6 +45,8 @@ func TestCommitsCountWithoutBaseSha256(t *testing.T) { } func TestGetFullCommitIDSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "f004f4") @@ -49,6 +55,8 @@ func TestGetFullCommitIDSha256(t *testing.T) { } func TestGetFullCommitIDErrorSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") id, err := GetFullCommitID(DefaultContext, bareRepo1Path, "unknown") @@ -59,6 +67,8 @@ func TestGetFullCommitIDErrorSha256(t *testing.T) { } func TestCommitFromReaderSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114 tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 @@ -131,6 +141,8 @@ signed commit`, commitFromReader.Signature.Payload) } func TestHasPreviousCommitSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") repo, err := openRepositoryWithDefaultContext(bareRepo1Path) @@ -159,6 +171,8 @@ func TestHasPreviousCommitSha256(t *testing.T) { } func TestGetCommitFileStatusMergesSha256(t *testing.T) { + skipIfSHA256NotSupported(t) + bareRepo1Path := filepath.Join(testReposDir, "repo6_merge_sha256") commitFileStatus, err := GetCommitFileStatus(DefaultContext, bareRepo1Path, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") diff --git a/modules/git/error.go b/modules/git/error.go index dc10d451b3..91d25eca69 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error { return util.ErrNotExist } -// ErrPushOutOfDate represents an error if merging fails due to unrelated histories +// ErrPushOutOfDate represents an error if merging fails due to the base branch being updated type ErrPushOutOfDate struct { StdOut string StdErr string diff --git a/modules/git/git.go b/modules/git/git.go index 89c23ff230..13a3127498 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -33,8 +33,9 @@ var ( // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx DefaultContext context.Context - SupportProcReceive bool // >= 2.29 - SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ + SupportProcReceive bool // >= 2.29 + SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ + InvertedGitFlushEnv bool // 2.43.1 gitVersion *version.Version ) @@ -192,6 +193,8 @@ func InitFull(ctx context.Context) (err error) { log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported") } + InvertedGitFlushEnv = CheckGitVersionEqual("2.43.1") == nil + if setting.LFS.StartServer { if CheckGitVersionAtLeast("2.1.2") != nil { return errors.New("LFS server support requires Git >= 2.1.2") @@ -320,6 +323,21 @@ func CheckGitVersionAtLeast(atLeast string) error { return nil } +// CheckGitVersionEqual checks if the git version is equal to the constraint version. +func CheckGitVersionEqual(equal string) error { + if _, err := loadGitVersion(); err != nil { + return err + } + atLeastVersion, err := version.NewVersion(equal) + if err != nil { + return err + } + if !gitVersion.Equal(atLeastVersion) { + return fmt.Errorf("installed git binary version %s is not equal to %s", gitVersion.Original(), equal) + } + return nil +} + func configSet(key, value string) error { stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) if err != nil && !err.IsExitCode(1) { diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 26a0d28098..9e345f3ee0 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -143,19 +143,19 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int } // Our "line" must look like: SP ( SP) * NUL - commitIds := string(g.next) + commitIDs := string(g.next) if g.buffull { more, err := g.rd.ReadString('\x00') if err != nil { return nil, err } - commitIds += more + commitIDs += more } - commitIds = commitIds[:len(commitIds)-1] - splitIds := strings.Split(commitIds, " ") - ret.CommitID = splitIds[0] - if len(splitIds) > 1 { - ret.ParentIDs = splitIds[1:] + commitIDs = commitIDs[:len(commitIDs)-1] + splitIDs := strings.Split(commitIDs, " ") + ret.CommitID = splitIDs[0] + if len(splitIDs) > 1 { + ret.ParentIDs = splitIDs[1:] } // now read the next "line" diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index a725f4799d..4c65249089 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -169,6 +169,10 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err } else { break commitReadingLoop } + default: + if err := git.DiscardFull(batchReader, size+1); err != nil { + return nil, err + } } } } diff --git a/modules/git/repo.go b/modules/git/repo.go index db99d285a8..60078f3273 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -271,7 +271,7 @@ func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error return time.Time{}, err } commitTime := strings.TrimSpace(stdout) - return time.Parse(GitTimeLayout, commitTime) + return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) } // DivergeObject represents commit count diverging commits diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 3c5a1429a9..197d626a4d 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -11,6 +11,7 @@ import ( "os" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" ) // CheckAttributeOpts represents the possible options to CheckAttribute @@ -133,7 +134,13 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error { c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree) } - c.env = append(c.env, "GIT_FLUSH=1") + // Version 2.43.1 has a bug where the behavior of `GIT_FLUSH` is flipped. + // Ref: https://lore.kernel.org/git/CABn0oJvg3M_kBW-u=j3QhKnO=6QOzk-YFTgonYw_UvFS1NTX4g@mail.gmail.com + if InvertedGitFlushEnv { + c.env = append(c.env, "GIT_FLUSH=0") + } else { + c.env = append(c.env, "GIT_FLUSH=1") + } c.cmd.AddDynamicArguments(c.Attributes...) @@ -316,3 +323,16 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe return checker, deferable } + +// true if "set"/"true", false if "unset"/"false", none otherwise +func attributeToBool(attr map[string]string, name string) optional.Option[bool] { + if value, has := attr[name]; has && value != "unspecified" { + switch value { + case "set", "true": + return optional.Some(true) + case "unset", "false": + return optional.Some(false) + } + } + return optional.None[bool]() +} diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 9270bb70f0..3ca5eb36c6 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -88,16 +88,17 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { } // Close this repository, in particular close the underlying gogitStorage if this is not nil -func (repo *Repository) Close() (err error) { +func (repo *Repository) Close() error { if repo == nil || repo.gogitStorage == nil { - return + return nil } if err := repo.gogitStorage.Close(); err != nil { gitealog.Error("Error closing storage: %v", err) } + repo.gogitStorage = nil repo.LastCommitCache = nil repo.tagCache = nil - return + return nil } // GoGitRepo gets the go-git repo representation diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index d5a350a926..86b6a93567 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -27,10 +27,12 @@ type Repository struct { gpgSettings *GPGSettings + batchInUse bool batchCancel context.CancelFunc batchReader *bufio.Reader batchWriter WriteCloserError + checkInUse bool checkCancel context.CancelFunc checkReader *bufio.Reader checkWriter WriteCloserError @@ -79,24 +81,29 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { // CatFileBatch obtains a CatFileBatch for this repository func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) { - if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 { + if repo.batchCancel == nil || repo.batchInUse { log.Debug("Opening temporary cat file batch for: %s", repo.Path) return CatFileBatch(ctx, repo.Path) } - return repo.batchWriter, repo.batchReader, func() {} + repo.batchInUse = true + return repo.batchWriter, repo.batchReader, func() { + repo.batchInUse = false + } } // CatFileBatchCheck obtains a CatFileBatchCheck for this repository func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) { - if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 { - log.Debug("Opening temporary cat file batch-check: %s", repo.Path) + if repo.checkCancel == nil || repo.checkInUse { + log.Debug("Opening temporary cat file batch-check for: %s", repo.Path) return CatFileBatchCheck(ctx, repo.Path) } - return repo.checkWriter, repo.checkReader, func() {} + repo.checkInUse = true + return repo.checkWriter, repo.checkReader, func() { + repo.checkInUse = false + } } -// Close this repository, in particular close the underlying gogitStorage if this is not nil -func (repo *Repository) Close() (err error) { +func (repo *Repository) Close() error { if repo == nil { return nil } @@ -105,14 +112,16 @@ func (repo *Repository) Close() (err error) { repo.batchReader = nil repo.batchWriter = nil repo.batchCancel = nil + repo.batchInUse = false } if repo.checkCancel != nil { repo.checkCancel() repo.checkCancel = nil repo.checkReader = nil repo.checkWriter = nil + repo.checkInUse = false } repo.LastCommitCache = nil repo.tagCache = nil - return err + return nil } diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index f0214e1ff8..a7031184e2 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -121,8 +121,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) return commit, nil default: log.Debug("Unknown typ: %s", typ) - _, err = rd.Discard(int(size) + 1) - if err != nil { + if err := DiscardFull(rd, size+1); err != nil { return nil, err } return nil, ErrNotExist{ diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go index 7ed2dc1587..c40d6937b5 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/repo_language_stats.go @@ -13,18 +13,6 @@ const ( bigFileSize int64 = 1024 * 1024 // 1 MiB ) -type LinguistBoolAttrib struct { - Value string -} - -func (attrib *LinguistBoolAttrib) IsTrue() bool { - return attrib.Value == "set" || attrib.Value == "true" -} - -func (attrib *LinguistBoolAttrib) IsFalse() bool { - return attrib.Value == "unset" || attrib.Value == "false" -} - // mergeLanguageStats mergers language names with different cases. The name with most upper case letters is used. func mergeLanguageStats(stats map[string]int64) map[string]int64 { names := map[string]struct { diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go index 558a83af74..1276ce1a44 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/repo_language_stats_gogit.go @@ -12,6 +12,7 @@ import ( "strings" "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/optional" "github.com/go-enry/go-enry/v2" "github.com/go-git/go-git/v5" @@ -53,31 +54,30 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err firstExcludedLanguage := "" firstExcludedLanguageSize := int64(0) + isTrue := func(v optional.Option[bool]) bool { + return v.ValueOrDefault(false) + } + isFalse := func(v optional.Option[bool]) bool { + return !v.ValueOrDefault(true) + } + err = tree.Files().ForEach(func(f *object.File) error { if f.Size == 0 { return nil } - isVendored := LinguistBoolAttrib{} - isGenerated := LinguistBoolAttrib{} - isDocumentation := LinguistBoolAttrib{} - isDetectable := LinguistBoolAttrib{} + isVendored := optional.None[bool]() + isGenerated := optional.None[bool]() + isDocumentation := optional.None[bool]() + isDetectable := optional.None[bool]() if checker != nil { attrs, err := checker.CheckPath(f.Name) if err == nil { - if vendored, has := attrs["linguist-vendored"]; has { - isVendored = LinguistBoolAttrib{Value: vendored} - } - if generated, has := attrs["linguist-generated"]; has { - isGenerated = LinguistBoolAttrib{Value: generated} - } - if documentation, has := attrs["linguist-documentation"]; has { - isDocumentation = LinguistBoolAttrib{Value: documentation} - } - if detectable, has := attrs["linguist-detectable"]; has { - isDetectable = LinguistBoolAttrib{Value: detectable} - } + isVendored = attributeToBool(attrs, "linguist-vendored") + isGenerated = attributeToBool(attrs, "linguist-generated") + isDocumentation = attributeToBool(attrs, "linguist-documentation") + isDetectable = attributeToBool(attrs, "linguist-detectable") if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { // group languages, such as Pug -> HTML; SCSS -> CSS group := enry.GetLanguageGroup(language) @@ -108,11 +108,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() || - (!isVendored.IsFalse() && analyze.IsVendor(f.Name)) || + if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) || + (!isFalse(isVendored) && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || enry.IsConfiguration(f.Name) || - (!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name)) { + (!isFalse(isDocumentation) && enry.IsDocumentation(f.Name)) { return nil } @@ -121,7 +121,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if f.Size <= bigFileSize { content, _ = readFile(f, fileSizeLimit) } - if !isGenerated.IsTrue() && enry.IsGenerated(f.Name, content) { + if !isTrue(isGenerated) && enry.IsGenerated(f.Name, content) { return nil } @@ -138,21 +138,22 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } included, checked := includedLanguage[language] + langType := enry.GetLanguageType(language) if !checked { - langtype := enry.GetLanguageType(language) - included = langtype == enry.Programming || langtype == enry.Markup - if !included { - if isDetectable.IsTrue() { - included = true - } else { - return nil - } + included = langType == enry.Programming || langType == enry.Markup + if !included && (isTrue(isDetectable) || (langType == enry.Prose && isFalse(isDocumentation))) { + included = true } includedLanguage[language] = included } if included { sizes[language] += f.Size } else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) { + // Only consider Programming or Markup languages as fallback + if !(langType == enry.Programming || langType == enry.Markup) { + return nil + } + firstExcludedLanguage = language firstExcludedLanguageSize += f.Size } diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 13876094cc..1a3fdbb1f8 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -7,14 +7,13 @@ package git import ( - "bufio" "bytes" "io" - "math" "strings" "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "github.com/go-enry/go-enry/v2" ) @@ -77,6 +76,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err firstExcludedLanguage := "" firstExcludedLanguageSize := int64(0) + isTrue := func(v optional.Option[bool]) bool { + return v.ValueOrDefault(false) + } + isFalse := func(v optional.Option[bool]) bool { + return !v.ValueOrDefault(true) + } + for _, f := range entries { select { case <-repo.Ctx.Done(): @@ -91,26 +97,18 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err continue } - isVendored := LinguistBoolAttrib{} - isGenerated := LinguistBoolAttrib{} - isDocumentation := LinguistBoolAttrib{} - isDetectable := LinguistBoolAttrib{} + isVendored := optional.None[bool]() + isGenerated := optional.None[bool]() + isDocumentation := optional.None[bool]() + isDetectable := optional.None[bool]() if checker != nil { attrs, err := checker.CheckPath(f.Name()) if err == nil { - if vendored, has := attrs["linguist-vendored"]; has { - isVendored = LinguistBoolAttrib{Value: vendored} - } - if generated, has := attrs["linguist-generated"]; has { - isGenerated = LinguistBoolAttrib{Value: generated} - } - if documentation, has := attrs["linguist-documentation"]; has { - isDocumentation = LinguistBoolAttrib{Value: documentation} - } - if detectable, has := attrs["linguist-detectable"]; has { - isDetectable = LinguistBoolAttrib{Value: detectable} - } + isVendored = attributeToBool(attrs, "linguist-vendored") + isGenerated = attributeToBool(attrs, "linguist-generated") + isDocumentation = attributeToBool(attrs, "linguist-documentation") + isDetectable = attributeToBool(attrs, "linguist-detectable") if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { // group languages, such as Pug -> HTML; SCSS -> CSS group := enry.GetLanguageGroup(language) @@ -142,11 +140,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() || - (!isVendored.IsFalse() && analyze.IsVendor(f.Name())) || + if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) || + (!isFalse(isVendored) && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || enry.IsConfiguration(f.Name()) || - (!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name())) { + (!isFalse(isDocumentation) && enry.IsDocumentation(f.Name())) { continue } @@ -174,12 +172,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } content = contentBuf.Bytes() - err = discardFull(batchReader, discard) - if err != nil { + if err := DiscardFull(batchReader, discard); err != nil { return nil, err } } - if !isGenerated.IsTrue() && enry.IsGenerated(f.Name(), content) { + if !isTrue(isGenerated) && enry.IsGenerated(f.Name(), content) { continue } @@ -197,25 +194,24 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } included, checked := includedLanguage[language] + langType := enry.GetLanguageType(language) if !checked { - langType := enry.GetLanguageType(language) included = langType == enry.Programming || langType == enry.Markup - if !included { - if isDetectable.IsTrue() { - included = true - } else { - continue - } + if !included && (isTrue(isDetectable) || (langType == enry.Prose && isFalse(isDocumentation))) { + included = true } includedLanguage[language] = included } if included { sizes[language] += f.Size() } else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) { + // Only consider Programming or Markup languages as fallback + if !(langType == enry.Programming || langType == enry.Markup) { + continue + } firstExcludedLanguage = language firstExcludedLanguageSize += f.Size() } - continue } // If there are no included languages add the first excluded language @@ -225,21 +221,3 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return mergeLanguageStats(sizes), nil } - -func discardFull(rd *bufio.Reader, discard int64) error { - if discard > math.MaxInt32 { - n, err := rd.Discard(math.MaxInt32) - discard -= int64(n) - if err != nil { - return err - } - } - for discard > 0 { - n, err := rd.Discard(int(discard)) - discard -= int64(n) - if err != nil { - return err - } - } - return nil -} diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index af9a75b29c..ae5dbd171f 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -183,11 +183,7 @@ func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, er } } - tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"])) - if err != nil { - return nil, fmt.Errorf("parse tagger: %w", err) - } - + tag.Tagger = parseSignatureFromCommitLine(ref["creator"]) tag.Message = ref["contents"] // strip PGP signature if present in contents field pgpStart := strings.Index(tag.Message, beginpgp) diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 5d98fadd54..cbab39f8c5 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -103,6 +103,9 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { return nil, err } if typ != "tag" { + if err := DiscardFull(rd, size+1); err != nil { + return nil, err + } return nil, ErrNotExist{ID: tagID.String()} } diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index da7b1455a8..9816e311a8 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -227,7 +227,7 @@ func TestRepository_parseTagRef(t *testing.T) { ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), Type: "commit", - Tagger: parseAuthorLine(t, "Foo Bar 1565789218 +0300"), + Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", Signature: nil, }, @@ -256,7 +256,7 @@ func TestRepository_parseTagRef(t *testing.T) { ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), Type: "tag", - Tagger: parseAuthorLine(t, "Foo Bar 1565789218 +0300"), + Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", Signature: nil, }, @@ -314,7 +314,7 @@ qbHDASXl ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), Type: "tag", - Tagger: parseAuthorLine(t, "Foo Bar 1565789218 +0300"), + Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md", Signature: &CommitGPGSignature{ Signature: `-----BEGIN PGP SIGNATURE----- @@ -363,14 +363,3 @@ Add changelog of v1.9.1 (#7859) }) } } - -func parseAuthorLine(t *testing.T, committer string) *Signature { - t.Helper() - - sig, err := newSignatureFromCommitline([]byte(committer)) - if err != nil { - t.Fatalf("parse author line '%s': %v", committer, err) - } - - return sig -} diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go index 20c92a79ed..582247b4a4 100644 --- a/modules/git/repo_tree_nogogit.go +++ b/modules/git/repo_tree_nogogit.go @@ -58,6 +58,9 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { tree.entriesParsed = true return tree, nil default: + if err := DiscardFull(rd, size+1); err != nil { + return nil, err + } return nil, ErrNotExist{ ID: id.String(), } diff --git a/modules/git/signature.go b/modules/git/signature.go index b5b17f23b0..f50a097758 100644 --- a/modules/git/signature.go +++ b/modules/git/signature.go @@ -4,7 +4,46 @@ package git -const ( - // GitTimeLayout is the (default) time layout used by git. - GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700" +import ( + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" ) + +// Helper to get a signature from the commit line, which looks like: +// +// full name 1378823654 +0200 +// +// Haven't found the official reference for the standard format yet. +// This function never fails, if the "line" can't be parsed, it returns a default Signature with "zero" time. +func parseSignatureFromCommitLine(line string) *Signature { + sig := &Signature{} + s1, sx, ok1 := strings.Cut(line, " <") + s2, s3, ok2 := strings.Cut(sx, "> ") + if !ok1 || !ok2 { + sig.Name = line + return sig + } + sig.Name, sig.Email = s1, s2 + + if strings.Count(s3, " ") == 1 { + ts, tz, _ := strings.Cut(s3, " ") + seconds, _ := strconv.ParseInt(ts, 10, 64) + if tzTime, err := time.Parse("-0700", tz); err == nil { + sig.When = time.Unix(seconds, 0).In(tzTime.Location()) + } + } else { + // the old gitea code tried to parse the date in a few different formats, but it's not clear why. + // according to public document, only the standard format "timestamp timezone" could be found, so drop other formats. + log.Error("suspicious commit line format: %q", line) + for _, fmt := range []string{ /*"Mon Jan _2 15:04:05 2006 -0700"*/ } { + if t, err := time.Parse(fmt, s3); err == nil { + sig.When = t + break + } + } + } + return sig +} diff --git a/modules/git/signature_gogit.go b/modules/git/signature_gogit.go index c984ad6e20..1fc6aabceb 100644 --- a/modules/git/signature_gogit.go +++ b/modules/git/signature_gogit.go @@ -7,52 +7,8 @@ package git import ( - "bytes" - "strconv" - "strings" - "time" - "github.com/go-git/go-git/v5/plumbing/object" ) // Signature represents the Author or Committer information. type Signature = object.Signature - -// Helper to get a signature from the commit line, which looks like these: -// -// author Patrick Gundlach 1378823654 +0200 -// author Patrick Gundlach Thu, 07 Apr 2005 22:13:13 +0200 -// -// but without the "author " at the beginning (this method should) -// be used for author and committer. -// -// FIXME: include timezone for timestamp! -func newSignatureFromCommitline(line []byte) (_ *Signature, err error) { - sig := new(Signature) - emailStart := bytes.IndexByte(line, '<') - if emailStart > 0 { // Empty name has already occurred, even if it shouldn't - sig.Name = strings.TrimSpace(string(line[:emailStart-1])) - } - emailEnd := bytes.IndexByte(line, '>') - sig.Email = string(line[emailStart+1 : emailEnd]) - - // Check date format. - if len(line) > emailEnd+2 { - firstChar := line[emailEnd+2] - if firstChar >= 48 && firstChar <= 57 { - timestop := bytes.IndexByte(line[emailEnd+2:], ' ') - timestring := string(line[emailEnd+2 : emailEnd+2+timestop]) - seconds, _ := strconv.ParseInt(timestring, 10, 64) - sig.When = time.Unix(seconds, 0) - } else { - sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:])) - if err != nil { - return nil, err - } - } - } else { - // Fall back to unix 0 time - sig.When = time.Unix(0, 0) - } - return sig, nil -} diff --git a/modules/git/signature_nogogit.go b/modules/git/signature_nogogit.go index 25277f99d5..0d19c0abdc 100644 --- a/modules/git/signature_nogogit.go +++ b/modules/git/signature_nogogit.go @@ -7,21 +7,17 @@ package git import ( - "bytes" "fmt" - "strconv" - "strings" "time" + + "code.gitea.io/gitea/modules/util" ) -// Signature represents the Author or Committer information. +// Signature represents the Author, Committer or Tagger information. type Signature struct { - // Name represents a person name. It is an arbitrary string. - Name string - // Email is an email, but it cannot be assumed to be well-formed. - Email string - // When is the timestamp of the signature. - When time.Time + Name string // the committer name, it can be anything + Email string // the committer email, it can be anything + When time.Time // the timestamp of the signature } func (s *Signature) String() string { @@ -30,71 +26,5 @@ func (s *Signature) String() string { // Decode decodes a byte array representing a signature to signature func (s *Signature) Decode(b []byte) { - sig, _ := newSignatureFromCommitline(b) - s.Email = sig.Email - s.Name = sig.Name - s.When = sig.When -} - -// Helper to get a signature from the commit line, which looks like these: -// -// author Patrick Gundlach 1378823654 +0200 -// author Patrick Gundlach Thu, 07 Apr 2005 22:13:13 +0200 -// -// but without the "author " at the beginning (this method should) -// be used for author and committer. -// FIXME: there are a lot of "return sig, err" (but the err is also nil), that's the old behavior, to avoid breaking -func newSignatureFromCommitline(line []byte) (sig *Signature, err error) { - sig = new(Signature) - emailStart := bytes.LastIndexByte(line, '<') - emailEnd := bytes.LastIndexByte(line, '>') - if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart { - return sig, err - } - - if emailStart > 0 { // Empty name has already occurred, even if it shouldn't - sig.Name = strings.TrimSpace(string(line[:emailStart-1])) - } - sig.Email = string(line[emailStart+1 : emailEnd]) - - hasTime := emailEnd+2 < len(line) - if !hasTime { - return sig, err - } - - // Check date format. - firstChar := line[emailEnd+2] - if firstChar >= 48 && firstChar <= 57 { - idx := bytes.IndexByte(line[emailEnd+2:], ' ') - if idx < 0 { - return sig, err - } - - timestring := string(line[emailEnd+2 : emailEnd+2+idx]) - seconds, _ := strconv.ParseInt(timestring, 10, 64) - sig.When = time.Unix(seconds, 0) - - idx += emailEnd + 3 - if idx >= len(line) || idx+5 > len(line) { - return sig, err - } - - timezone := string(line[idx : idx+5]) - tzhours, err1 := strconv.ParseInt(timezone[0:3], 10, 64) - tzmins, err2 := strconv.ParseInt(timezone[3:], 10, 64) - if err1 != nil || err2 != nil { - return sig, err - } - if tzhours < 0 { - tzmins *= -1 - } - tz := time.FixedZone("", int(tzhours*60*60+tzmins*60)) - sig.When = sig.When.In(tz) - } else { - sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:])) - if err != nil { - return sig, err - } - } - return sig, err + *s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b)) } diff --git a/modules/git/signature_test.go b/modules/git/signature_test.go new file mode 100644 index 0000000000..92681feea9 --- /dev/null +++ b/modules/git/signature_test.go @@ -0,0 +1,47 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseSignatureFromCommitLine(t *testing.T) { + tests := []struct { + line string + want *Signature + }{ + { + line: "a b 12345 +0100", + want: &Signature{ + Name: "a b", + Email: "c@d.com", + When: time.Unix(12345, 0).In(time.FixedZone("", 3600)), + }, + }, + { + line: "bad line", + want: &Signature{Name: "bad line"}, + }, + { + line: "bad < line", + want: &Signature{Name: "bad < line"}, + }, + { + line: "bad > line", + want: &Signature{Name: "bad > line"}, + }, + { + line: "bad-line ", + want: &Signature{Name: "bad-line "}, + }, + } + for _, test := range tests { + got := parseSignatureFromCommitLine(test.line) + assert.EqualValues(t, test.want, got) + } +} diff --git a/modules/git/tag.go b/modules/git/tag.go index 01a8d6f6a5..94e5cd7c63 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -7,6 +7,8 @@ import ( "bytes" "sort" "strings" + + "code.gitea.io/gitea/modules/util" ) const ( @@ -59,11 +61,7 @@ l: // A commit can have one or more parents tag.Type = string(line[spacepos+1:]) case "tagger": - sig, err := newSignatureFromCommitline(line[spacepos+1:]) - if err != nil { - return nil, err - } - tag.Tagger = sig + tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:])) } nextline += eol + 1 case eol == 0: diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 9513121487..2c47c8858c 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -23,15 +23,15 @@ func (te *TreeEntry) Type() string { } // FollowLink returns the entry pointed to by a symlink -func (te *TreeEntry) FollowLink() (*TreeEntry, error) { +func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) { if !te.IsLink() { - return nil, ErrBadLink{te.Name(), "not a symlink"} + return nil, "", ErrBadLink{te.Name(), "not a symlink"} } // read the link r, err := te.Blob().DataAsync() if err != nil { - return nil, err + return nil, "", err } closed := false defer func() { @@ -42,7 +42,7 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) { buf := make([]byte, te.Size()) _, err = io.ReadFull(r, buf) if err != nil { - return nil, err + return nil, "", err } _ = r.Close() closed = true @@ -56,33 +56,35 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) { } if t == nil { - return nil, ErrBadLink{te.Name(), "points outside of repo"} + return nil, "", ErrBadLink{te.Name(), "points outside of repo"} } target, err := t.GetTreeEntryByPath(lnk) if err != nil { if IsErrNotExist(err) { - return nil, ErrBadLink{te.Name(), "broken link"} + return nil, "", ErrBadLink{te.Name(), "broken link"} } - return nil, err + return nil, "", err } - return target, nil + return target, lnk, nil } // FollowLinks returns the entry ultimately pointed to by a symlink -func (te *TreeEntry) FollowLinks() (*TreeEntry, error) { +func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) { if !te.IsLink() { - return nil, ErrBadLink{te.Name(), "not a symlink"} + return nil, "", ErrBadLink{te.Name(), "not a symlink"} } entry := te + entryLink := "" for i := 0; i < 999; i++ { if entry.IsLink() { - next, err := entry.FollowLink() + next, link, err := entry.FollowLink() + entryLink = link if err != nil { - return nil, err + return nil, "", err } if next.ID == entry.ID { - return nil, ErrBadLink{ + return nil, "", ErrBadLink{ entry.Name(), "recursive link", } @@ -93,12 +95,12 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, error) { } } if entry.IsLink() { - return nil, ErrBadLink{ + return nil, "", ErrBadLink{ te.Name(), "too many levels of symbolic links", } } - return entry, nil + return entry, entryLink, nil } // returns the Tree pointed to by this TreeEntry, or nil if this is not a tree diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 89d3aebbc0..28d02c7e81 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -7,7 +7,6 @@ package git import ( "io" - "math" "strings" ) @@ -63,19 +62,8 @@ func (t *Tree) ListEntries() (Entries, error) { } // Not a tree just use ls-tree instead - for sz > math.MaxInt32 { - discarded, err := rd.Discard(math.MaxInt32) - sz -= int64(discarded) - if err != nil { - return nil, err - } - } - for sz > 0 { - discarded, err := rd.Discard(int(sz)) - sz -= int64(discarded) - if err != nil { - return nil, err - } + if err := DiscardFull(rd, sz+1); err != nil { + return nil, err } } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go new file mode 100644 index 0000000000..6d2b5c84d5 --- /dev/null +++ b/modules/git/tree_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubTree_Issue29101(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) + assert.NoError(t, err) + defer repo.Close() + + commit, err := repo.GetCommit("ce064814f4a0d337b333e646ece456cd39fab612") + assert.NoError(t, err) + + // old code could produce a different error if called multiple times + for i := 0; i < 10; i++ { + _, err = commit.SubTree("file1.txt") + assert.Error(t, err) + assert.True(t, IsErrNotExist(err)) + } +} diff --git a/modules/git/utils_test.go b/modules/git/utils_test.go new file mode 100644 index 0000000000..876a22924c --- /dev/null +++ b/modules/git/utils_test.go @@ -0,0 +1,15 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import "testing" + +// This file contains utility functions that are used across multiple tests, +// but not in production code. + +func skipIfSHA256NotSupported(t *testing.T) { + if isGogit || CheckGitVersionAtLeast("2.42") != nil { + t.Skip("skipping because installed Git version doesn't support SHA256") + } +} diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 2fadbfeb06..0f70f13485 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -180,11 +180,17 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st } if len(reqs) > 0 { - _, err := b.inner.Client.Bulk(). - Index(b.inner.VersionedIndexName()). - Add(reqs...). - Do(ctx) - return err + esBatchSize := 50 + + for i := 0; i < len(reqs); i += esBatchSize { + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). + Add(reqs[i:min(i+esBatchSize, len(reqs))]...). + Do(ctx) + if err != nil { + return err + } + } } return nil } diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go index 531866ad0f..a168e0cb9b 100644 --- a/modules/indexer/internal/bleve/indexer.go +++ b/modules/indexer/internal/bleve/indexer.go @@ -91,7 +91,7 @@ func (i *Indexer) Ping(_ context.Context) error { } func (i *Indexer) Close() { - if i == nil { + if i == nil || i.Indexer == nil { return } diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go index b037249d43..f4004849c1 100644 --- a/modules/indexer/internal/meilisearch/indexer.go +++ b/modules/indexer/internal/meilisearch/indexer.go @@ -87,8 +87,5 @@ func (i *Indexer) Close() { if i == nil { return } - if i.Client == nil { - return - } i.Client = nil } diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index da4fc9b878..3b96686d98 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -218,7 +218,7 @@ func searchIssueIsPull(t *testing.T) { SearchOptions{ IsPull: util.OptionalBoolTrue, }, - []int64{12, 11, 20, 19, 9, 8, 3, 2}, + []int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2}, }, } for _, test := range tests { @@ -239,7 +239,7 @@ func searchIssueIsClosed(t *testing.T) { SearchOptions{ IsClosed: util.OptionalBoolFalse, }, - []int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1}, + []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1}, }, { SearchOptions{ @@ -305,7 +305,7 @@ func searchIssueByLabelID(t *testing.T) { SearchOptions{ ExcludedLabelIDs: []int64{1}, }, - []int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3}, + []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3}, }, } for _, test := range tests { @@ -329,7 +329,7 @@ func searchIssueByTime(t *testing.T) { SearchOptions{ UpdatedAfterUnix: int64Pointer(0), }, - []int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, + []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, }, } for _, test := range tests { @@ -350,7 +350,7 @@ func searchIssueWithOrder(t *testing.T) { SearchOptions{ SortBy: internal.SortByCreatedAsc, }, - []int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17}, + []int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17, 21, 22}, }, } for _, test := range tests { @@ -410,8 +410,8 @@ func searchIssueWithPaginator(t *testing.T) { PageSize: 5, }, }, - []int64{17, 16, 15, 14, 13}, - 20, + []int64{22, 21, 17, 16, 15}, + 22, }, } for _, test := range tests { diff --git a/modules/markup/html.go b/modules/markup/html.go index 33dc1e9086..b7291823b5 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -804,7 +804,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { // indicate that in the text by appending (comment) if m[4] != -1 && m[5] != -1 { if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok { - text += " " + locale.Tr("repo.from_comment") + text += " " + locale.TrString("repo.from_comment") } else { text += " (comment)" } diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index 3e6e291ab2..72d32600f5 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -181,37 +181,3 @@ func IsColorPreview(node ast.Node) bool { _, ok := node.(*ColorPreview) return ok } - -const ( - AttentionNote string = "Note" - AttentionWarning string = "Warning" -) - -// Attention is an inline for a color preview -type Attention struct { - ast.BaseInline - AttentionType string -} - -// Dump implements Node.Dump. -func (n *Attention) Dump(source []byte, level int) { - m := map[string]string{} - m["AttentionType"] = n.AttentionType - ast.DumpHelper(n, source, level, m, nil) -} - -// KindAttention is the NodeKind for Attention -var KindAttention = ast.NewNodeKind("Attention") - -// Kind implements Node.Kind. -func (n *Attention) Kind() ast.NodeKind { - return KindAttention -} - -// NewAttention returns a new Attention node. -func NewAttention(attentionType string) *Attention { - return &Attention{ - BaseInline: ast.BaseInline{}, - AttentionType: attentionType, - } -} diff --git a/modules/markup/markdown/callout/ast.go b/modules/markup/markdown/callout/ast.go new file mode 100644 index 0000000000..a5b1bbc2a0 --- /dev/null +++ b/modules/markup/markdown/callout/ast.go @@ -0,0 +1,37 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package callout + +import ( + "github.com/yuin/goldmark/ast" +) + +// Attention is an inline for an attention +type Attention struct { + ast.BaseInline + AttentionType string +} + +// Dump implements Node.Dump. +func (n *Attention) Dump(source []byte, level int) { + m := map[string]string{} + m["AttentionType"] = n.AttentionType + ast.DumpHelper(n, source, level, m, nil) +} + +// KindAttention is the NodeKind for Attention +var KindAttention = ast.NewNodeKind("Attention") + +// Kind implements Node.Kind. +func (n *Attention) Kind() ast.NodeKind { + return KindAttention +} + +// NewAttention returns a new Attention node. +func NewAttention(attentionType string) *Attention { + return &Attention{ + BaseInline: ast.BaseInline{}, + AttentionType: attentionType, + } +} diff --git a/modules/markup/markdown/callout/github.go b/modules/markup/markdown/callout/github.go new file mode 100644 index 0000000000..58f1fc960f --- /dev/null +++ b/modules/markup/markdown/callout/github.go @@ -0,0 +1,142 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package callout + +import ( + "strings" + + "code.gitea.io/gitea/modules/svg" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +type GitHubCalloutTransformer struct{} + +// Transform transforms the given AST tree. +func (g *GitHubCalloutTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { + supportedAttentionTypes := map[string]bool{ + "note": true, + "tip": true, + "important": true, + "warning": true, + "caution": true, + } + + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + switch v := n.(type) { + case *ast.Blockquote: + // We only want attention blockquotes when the AST looks like: + // Text: "[" + // Text: "!TYPE" + // Text(SoftLineBreak): "]" + + // grab these nodes and make sure we adhere to the attention blockquote structure + firstParagraph := v.FirstChild() + if firstParagraph.ChildCount() < 3 { + return ast.WalkContinue, nil + } + firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text) + if !ok || string(firstTextNode.Text(reader.Source())) != "[" { + return ast.WalkContinue, nil + } + secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text) + if !ok { + return ast.WalkContinue, nil + } + // If the second node's text isn't one of the supported attention + // types, continue walking. + secondTextNodeText := secondTextNode.Text(reader.Source()) + attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNodeText), "!")) + if _, has := supportedAttentionTypes[attentionType]; !has { + return ast.WalkContinue, nil + } + + thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text) + if !ok || string(thirdTextNode.Text(reader.Source())) != "]" { + return ast.WalkContinue, nil + } + + // color the blockquote + v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType)) + + // create an emphasis to make it bold + emphasis := ast.NewEmphasis(2) + emphasis.SetAttributeString("class", []byte("attention-"+attentionType)) + firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis) + + // capitalize first letter + attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:])) + + // replace the ![TYPE] with icon+Type + emphasis.AppendChild(emphasis, attentionText) + for i := 0; i < 2; i++ { + lineBreak := ast.NewText() + lineBreak.SetSoftLineBreak(true) + firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak) + } + firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType)) + firstParagraph.RemoveChild(firstParagraph, firstTextNode) + firstParagraph.RemoveChild(firstParagraph, secondTextNode) + firstParagraph.RemoveChild(firstParagraph, thirdTextNode) + } + return ast.WalkContinue, nil + }) +} + +type GitHubCalloutHTMLRenderer struct { + html.Config +} + +// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. +func (r *GitHubCalloutHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindAttention, r.renderAttention) +} + +// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg +func (r *GitHubCalloutHTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + _, _ = w.WriteString(``) + + var octiconType string + switch n.AttentionType { + case "note": + octiconType = "info" + case "tip": + octiconType = "light-bulb" + case "important": + octiconType = "report" + case "warning": + octiconType = "alert" + case "caution": + octiconType = "stop" + } + _, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType))) + } else { + _, _ = w.WriteString("\n") + } + return ast.WalkContinue, nil +} + +func NewGitHubCalloutHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &GitHubCalloutHTMLRenderer{ + Config: html.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} diff --git a/modules/markup/markdown/callout/github_legacy.go b/modules/markup/markdown/callout/github_legacy.go new file mode 100644 index 0000000000..add6b0a847 --- /dev/null +++ b/modules/markup/markdown/callout/github_legacy.go @@ -0,0 +1,60 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package callout + +import ( + "strings" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +// Transformer for GitHub's legacy callout markup. +type GitHubLegacyCalloutTransformer struct{} + +func (g *GitHubLegacyCalloutTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { + supportedCalloutTypes := map[string]bool{"Note": true, "Warning": true} + + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + switch v := n.(type) { + case *ast.Blockquote: + // The first paragraph contains the callout type. + firstParagraph := v.FirstChild() + if firstParagraph.ChildCount() < 1 { + return ast.WalkContinue, nil + } + + // In the legacy GitHub callout markup, the first node of the first + // paragraph should be an emphasis. + calloutNode, ok := firstParagraph.FirstChild().(*ast.Emphasis) + if !ok { + return ast.WalkContinue, nil + } + calloutText := string(calloutNode.Text(reader.Source())) + calloutType := strings.ToLower(calloutText) + // We only support "Note" and "Warning" callouts in legacy mode, + // match only those. + if _, has := supportedCalloutTypes[calloutText]; !has { + return ast.WalkContinue, nil + } + + // Set the attention attribute on the emphasis + calloutNode.SetAttributeString("class", []byte("attention-"+calloutType)) + + // color the blockquote + v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+calloutType)) + + // Prepend callout icon before the callout node itself + firstParagraph.InsertBefore(firstParagraph, calloutNode, NewAttention(calloutType)) + } + + return ast.WalkContinue, nil + }) +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 178e3d2fdd..7ada8b5548 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -7,13 +7,13 @@ import ( "bytes" "fmt" "regexp" + "slices" "strings" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/svg" giteautil "code.gitea.io/gitea/modules/util" "github.com/microcosm-cc/bluemonday/css" @@ -53,7 +53,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } } - attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote]) _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil @@ -131,11 +130,17 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa case *ast.Link: // Links need their href to munged to be a real value link := v.Destination - if len(link) > 0 && !markup.IsLink(link) && - link[0] != '#' && !bytes.HasPrefix(link, byteMailto) { - // special case: this is not a link, a hash link or a mailto:, so it's a - // relative URL + // Do not process the link if it's not a link, starts with an hashtag + // (indicating it's an anchor link), starts with `mailto:` or any of the + // custom markdown URLs. + processLink := len(link) > 0 && !markup.IsLink(link) && + link[0] != '#' && !bytes.HasPrefix(link, byteMailto) && + !slices.ContainsFunc(setting.Markdown.CustomURLSchemes, func(s string) bool { + return bytes.HasPrefix(link, []byte(s+":")) + }) + + if processLink { var base string if ctx.IsWiki { base = ctx.Links.WikiLink() @@ -197,18 +202,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa if css.ColorHandler(strings.ToLower(string(colorContent))) { v.AppendChild(v, NewColorPreview(colorContent)) } - case *ast.Emphasis: - // check if inside blockquote for attention, expected hierarchy is - // Emphasis < Paragraph < Blockquote - blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote) - if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) { - fullText := string(n.Text(reader.Source())) - if fullText == AttentionNote || fullText == AttentionWarning { - v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText))) - v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText)) - attentionMarkedBlockquotes.Add(blockquote) - } - } } return ast.WalkContinue, nil }) @@ -299,7 +292,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindSummary, r.renderSummary) reg.Register(KindIcon, r.renderIcon) reg.Register(ast.KindCodeSpan, r.renderCodeSpan) - reg.Register(KindAttention, r.renderAttention) reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) } @@ -336,28 +328,6 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod return ast.WalkContinue, nil } -// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg -func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - _, _ = w.WriteString(``) - - var octiconType string - switch n.AttentionType { - case AttentionNote: - octiconType = "info" - case AttentionWarning: - octiconType = "alert" - } - _, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType))) - } else { - _, _ = w.WriteString("\n") - } - return ast.WalkContinue, nil -} - func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Document) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 771162b9a3..00d01a2f55 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/markup/markdown/callout" "code.gitea.io/gitea/modules/markup/markdown/math" "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" @@ -124,6 +125,8 @@ func SpecializedMarkdown() goldmark.Markdown { parser.WithAttribute(), parser.WithAutoHeadingID(), parser.WithASTTransformers( + util.Prioritized(&callout.GitHubLegacyCalloutTransformer{}, 8000), + util.Prioritized(&callout.GitHubCalloutTransformer{}, 9000), util.Prioritized(&ASTTransformer{}, 10000), ), ), @@ -135,6 +138,7 @@ func SpecializedMarkdown() goldmark.Markdown { // Override the original Tasklist renderer! specMarkdown.Renderer().AddOptions( renderer.WithNodeRenderers( + util.Prioritized(callout.NewGitHubCalloutHTMLRenderer(), 10), util.Prioritized(NewHTMLRenderer(), 10), ), ) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 61167dd0b7..1be8c6a273 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -1170,3 +1171,29 @@ space

assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i) } } + +func TestCustomMarkdownURL(t *testing.T) { + defer test.MockVariableValue(&setting.Markdown.CustomURLSchemes, []string{"abp"})() + + setting.AppURL = AppURL + setting.AppSubURL = AppSubURL + + test := func(input, expected string) { + buffer, err := markdown.RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + Links: markup.Links{ + Base: setting.AppSubURL, + BranchPath: "branch/main", + }, + }, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + test("[test](abp:subscribe?location=https://codeberg.org/filters.txt&title=joy)", + `

test

`) + + // Ensure that the schema itself without `:` is still made absolute. + test("[test](abp)", + `

test

`) +} diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go index 9602040931..38f744a25f 100644 --- a/modules/markup/markdown/toc.go +++ b/modules/markup/markdown/toc.go @@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str details.SetAttributeString(k, []byte(v)) } - summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc")))) + summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc")))) details.AppendChild(details, summary) ul := ast.NewList('-') details.AppendChild(details, ul) diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index e0c9b13a9e..8efe4e395d 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -147,11 +147,21 @@ func (r *Writer) resolveLink(node org.Node) string { } if len(link) > 0 && !markup.IsLinkStr(link) && link[0] != '#' && !strings.HasPrefix(link, mailto) { - base := r.Ctx.Links.Base + + var base string + if r.Ctx.IsWiki { + base = r.Ctx.Links.WikiLink() + } else if r.Ctx.Links.HasBranchInfo() { + base = r.Ctx.Links.SrcLink() + } else { + base = r.Ctx.Links.Base + } + switch l.Kind() { case "image", "video": base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki) } + link = util.URLJoin(base, link) } return link diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index e963718883..5ced819984 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -36,6 +36,10 @@ func TestRender_StandardLinks(t *testing.T) { assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } + // No BranchPath or TreePath set. + test("[[file:comfy][comfy]]", + `

comfy

`) + test("[[https://google.com/]]", `

https://google.com/

`) @@ -44,6 +48,46 @@ func TestRender_StandardLinks(t *testing.T) { `

WikiPage

`) } +func TestRender_BaseLinks(t *testing.T) { + setting.AppURL = AppURL + setting.AppSubURL = AppSubURL + + testBranch := func(input, expected string) { + buffer, err := RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + Links: markup.Links{ + Base: setting.AppSubURL, + BranchPath: "branch/main", + }, + }, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + testBranchTree := func(input, expected string) { + buffer, err := RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + Links: markup.Links{ + Base: setting.AppSubURL, + BranchPath: "branch/main", + TreePath: "deep/nested/folder", + }, + }, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + testBranch("[[file:comfy][comfy]]", + `

comfy

`) + testBranchTree("[[file:comfy][comfy]]", + `

comfy

`) + + testBranch("[[file:./src][./src/]]", + `

./src/

`) + testBranchTree("[[file:./src][./src/]]", + `

./src/

`) +} + func TestRender_Media(t *testing.T) { setting.AppURL = AppURL setting.AppSubURL = AppSubURL diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 992e85b989..ffc33c3b8e 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -64,9 +64,10 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span") // For attention + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong") - policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+$`)).OnElements("span", "strong") - policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-mr-2 gt-vm attention-\w+$`)).OnElements("span", "strong") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-(\w|-)+$`)).OnElements("svg") policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg") policy.AllowAttrs("fill-rule", "d").OnElements("path") diff --git a/modules/migration/messenger.go b/modules/migration/messenger.go index 924aac9769..6f9cad3f10 100644 --- a/modules/migration/messenger.go +++ b/modules/migration/messenger.go @@ -3,7 +3,7 @@ package migration -// Messenger is a formatting function similar to i18n.Tr +// Messenger is a formatting function similar to i18n.TrString type Messenger func(key string, args ...any) // NilMessenger represents an empty formatting function diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go index 7ec345b6ba..410fd73577 100644 --- a/modules/optional/option_test.go +++ b/modules/optional/option_test.go @@ -1,48 +1,49 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package optional +package optional_test import ( "testing" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) func TestOption(t *testing.T) { - var uninitialized Option[int] + var uninitialized optional.Option[int] assert.False(t, uninitialized.Has()) assert.Equal(t, int(0), uninitialized.Value()) assert.Equal(t, int(1), uninitialized.ValueOrDefault(1)) - none := None[int]() + none := optional.None[int]() assert.False(t, none.Has()) assert.Equal(t, int(0), none.Value()) assert.Equal(t, int(1), none.ValueOrDefault(1)) - some := Some[int](1) + some := optional.Some[int](1) assert.True(t, some.Has()) assert.Equal(t, int(1), some.Value()) assert.Equal(t, int(1), some.ValueOrDefault(2)) var ptr *int - assert.False(t, FromPtr(ptr).Has()) + assert.False(t, optional.FromPtr(ptr).Has()) - opt1 := FromPtr(util.ToPointer(1)) + int1 := 1 + opt1 := optional.FromPtr(&int1) assert.True(t, opt1.Has()) assert.Equal(t, int(1), opt1.Value()) - assert.False(t, FromNonDefault("").Has()) + assert.False(t, optional.FromNonDefault("").Has()) - opt2 := FromNonDefault("test") + opt2 := optional.FromNonDefault("test") assert.True(t, opt2.Has()) assert.Equal(t, "test", opt2.Value()) - assert.False(t, FromNonDefault(0).Has()) + assert.False(t, optional.FromNonDefault(0).Has()) - opt3 := FromNonDefault(1) + opt3 := optional.FromNonDefault(1) assert.True(t, opt3.Has()) assert.Equal(t, int(1), opt3.Value()) } diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go new file mode 100644 index 0000000000..6688e78cd1 --- /dev/null +++ b/modules/optional/serialization.go @@ -0,0 +1,46 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package optional + +import ( + "code.gitea.io/gitea/modules/json" + + "gopkg.in/yaml.v3" +) + +func (o *Option[T]) UnmarshalJSON(data []byte) error { + var v *T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *o = FromPtr(v) + return nil +} + +func (o Option[T]) MarshalJSON() ([]byte, error) { + if !o.Has() { + return []byte("null"), nil + } + + return json.Marshal(o.Value()) +} + +func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { + var v *T + if err := value.Decode(&v); err != nil { + return err + } + *o = FromPtr(v) + return nil +} + +func (o Option[T]) MarshalYAML() (interface{}, error) { + if !o.Has() { + return nil, nil + } + + value := new(yaml.Node) + err := value.Encode(o.Value()) + return value, err +} diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go new file mode 100644 index 0000000000..09a4bddea0 --- /dev/null +++ b/modules/optional/serialization_test.go @@ -0,0 +1,190 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package optional_test + +import ( + std_json "encoding/json" //nolint:depguard + "testing" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/optional" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +type testSerializationStruct struct { + NormalString string `json:"normal_string" yaml:"normal_string"` + NormalBool bool `json:"normal_bool" yaml:"normal_bool"` + OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` + OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"` + OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"` + OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"` +} + +func TestOptionalToJson(t *testing.T) { + tests := []struct { + name string + obj *testSerializationStruct + want string + }{ + { + name: "empty", + obj: new(testSerializationStruct), + want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`, + }, + { + name: "some", + obj: &testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + OptTwoBool: optional.None[bool](), + OptTwoString: optional.None[string](), + }, + want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + b, err := json.Marshal(tc.obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, string(b), "gitea json module returned unexpected") + + b, err = std_json.Marshal(tc.obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, string(b), "std json module returned unexpected") + }) + } +} + +func TestOptionalFromJson(t *testing.T) { + tests := []struct { + name string + data string + want testSerializationStruct + }{ + { + name: "empty", + data: `{}`, + want: testSerializationStruct{ + NormalString: "", + }, + }, + { + name: "some", + data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + want: testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var obj1 testSerializationStruct + err := json.Unmarshal([]byte(tc.data), &obj1) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, obj1, "gitea json module returned unexpected") + + var obj2 testSerializationStruct + err = std_json.Unmarshal([]byte(tc.data), &obj2) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, obj2, "std json module returned unexpected") + }) + } +} + +func TestOptionalToYaml(t *testing.T) { + tests := []struct { + name string + obj *testSerializationStruct + want string + }{ + { + name: "empty", + obj: new(testSerializationStruct), + want: `normal_string: "" +normal_bool: false +optional_two_bool: null +optional_two_string: null +`, + }, + { + name: "some", + obj: &testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + want: `normal_string: a string +normal_bool: true +optional_bool: false +optional_string: "" +optional_two_bool: null +optional_two_string: null +`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + b, err := yaml.Marshal(tc.obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, string(b), "yaml module returned unexpected") + }) + } +} + +func TestOptionalFromYaml(t *testing.T) { + tests := []struct { + name string + data string + want testSerializationStruct + }{ + { + name: "empty", + data: ``, + want: testSerializationStruct{}, + }, + { + name: "empty but init", + data: `normal_string: "" +normal_bool: false +optional_bool: +optional_two_bool: +optional_two_string: +`, + want: testSerializationStruct{}, + }, + { + name: "some", + data: ` +normal_string: a string +normal_bool: true +optional_bool: false +optional_string: "" +optional_two_bool: null +optional_twostring: null +`, + want: testSerializationStruct{ + NormalString: "a string", + NormalBool: true, + OptBool: optional.Some(false), + OptString: optional.Some(""), + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var obj testSerializationStruct + err := yaml.Unmarshal([]byte(tc.data), &obj) + assert.NoError(t, err) + assert.EqualValues(t, tc.want, obj, "yaml module returned unexpected") + }) + } +} diff --git a/modules/references/references.go b/modules/references/references.go index 7758312564..fce893cf5b 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -29,7 +29,7 @@ var ( // TODO: fix invalid linking issue // mentionPattern matches all mentions in the form of "@user" or "@org/team" - mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) + mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:'|\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\')([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 diff --git a/modules/references/references_test.go b/modules/references/references_test.go index ba7dda80cc..3f1f52401a 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -392,6 +392,9 @@ func TestRegExp_mentionPattern(t *testing.T) { {"@gitea,", "@gitea"}, {"@gitea;", "@gitea"}, {"@gitea/team1;", "@gitea/team1"}, + {"@jess'", "@jess"}, + {"@forgejo's", "@forgejo"}, + {"Оно сломалось из-за коммитов от @jopik'а", "@jopik"}, } falseTestCases := []string{ "@ 0", diff --git a/modules/repository/create.go b/modules/repository/create.go index 7c954a1412..ca2150b972 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -87,7 +87,11 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: tp, - Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true}, + Config: &repo_model.PullRequestsConfig{ + AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, + DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), + AllowRebaseUpdate: true, + }, }) } else { units = append(units, repo_model.RepoUnit{ diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 013dd8f76f..f622383bb5 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -94,7 +94,7 @@ type GiteaTemplate struct { } // Globs parses the .gitea/template globs or returns them if they were already parsed -func (gt GiteaTemplate) Globs() []glob.Glob { +func (gt *GiteaTemplate) Globs() []glob.Glob { if gt.globs != nil { return gt.globs } diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go index daab7c3091..95849789ab 100644 --- a/modules/repository/hooks.go +++ b/modules/repository/hooks.go @@ -9,7 +9,6 @@ import ( "path/filepath" "runtime" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -94,15 +93,14 @@ done `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), } - if git.SupportProcReceive { - hookNames = append(hookNames, "proc-receive") - hookTpls = append(hookTpls, - fmt.Sprintf(`#!/usr/bin/env %s + // although only new git (>=2.29) supports proc-receive, it's still good to create its hook, in case the user upgrades git + hookNames = append(hookNames, "proc-receive") + hookTpls = append(hookTpls, + fmt.Sprintf(`#!/usr/bin/env %s # AUTO GENERATED BY GITEA, DO NOT MODIFY %s hook --config=%s proc-receive `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf))) - giteaHookTpls = append(giteaHookTpls, "") - } + giteaHookTpls = append(giteaHookTpls, "") return hookNames, hookTpls, giteaHookTpls } diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 65b50b2e45..a863bec996 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -6,16 +6,13 @@ package repository import ( "context" - "errors" "fmt" "io" - "net/http" "strings" "time" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -23,10 +20,8 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) /* @@ -48,267 +43,6 @@ func WikiRemoteURL(ctx context.Context, remote string) string { return "" } -// MigrateRepositoryGitData starts migrating git related data after created migrating repository -func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, - repo *repo_model.Repository, opts migration.MigrateOptions, - httpTransport *http.Transport, -) (*repo_model.Repository, error) { - repoPath := repo_model.RepoPath(u.Name, opts.RepoName) - - if u.IsOrganization() { - t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) - if err != nil { - return nil, err - } - repo.NumWatches = t.NumMembers - } else { - repo.NumWatches = 1 - } - - migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second - - var err error - if err = util.RemoveAll(repoPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err) - } - - if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{ - Mirror: true, - Quiet: true, - Timeout: migrateTimeout, - SkipTLSVerify: setting.Migrations.SkipTLSVerify, - }); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err) - } - return repo, fmt.Errorf("Clone: %w", err) - } - - if err := git.WriteCommitGraph(ctx, repoPath); err != nil { - return repo, err - } - - if opts.Wiki { - wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) - wikiRemotePath := WikiRemoteURL(ctx, opts.CloneAddr) - if len(wikiRemotePath) > 0 { - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - - if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ - Mirror: true, - Quiet: true, - Timeout: migrateTimeout, - SkipTLSVerify: setting.Migrations.SkipTLSVerify, - }); err != nil { - log.Warn("Clone wiki: %v", err) - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - } else { - // Figure out the branch of the wiki we just cloned. We assume - // that the default branch is to be used, and we'll use the same - // name as the source. - gitRepo, err := git.OpenRepository(ctx, wikiPath) - if err != nil { - log.Warn("Failed to open wiki repository during migration: %v", err) - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - return repo, err - } - defer gitRepo.Close() - - branch, err := gitRepo.GetDefaultBranch() - if err != nil { - log.Warn("Failed to get the default branch of a migrated wiki repo: %v", err) - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - - return repo, err - } - repo.WikiBranch = branch - - if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { - return repo, err - } - } - } - } - - if repo.OwnerID == u.ID { - repo.Owner = u - } - - if err = CheckDaemonExportOK(ctx, repo); err != nil { - return repo, fmt.Errorf("checkDaemonExportOK: %w", err) - } - - if stdout, _, err := git.NewCommand(ctx, "update-server-info"). - SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) - return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err) - } - - gitRepo, err := git.OpenRepository(ctx, repoPath) - if err != nil { - return repo, fmt.Errorf("OpenRepository: %w", err) - } - defer gitRepo.Close() - - repo.IsEmpty, err = gitRepo.IsEmpty() - if err != nil { - return repo, fmt.Errorf("git.IsEmpty: %w", err) - } - - if !repo.IsEmpty { - if len(repo.DefaultBranch) == 0 { - // Try to get HEAD branch and set it as default branch. - headBranch, err := gitRepo.GetHEADBranch() - if err != nil { - return repo, fmt.Errorf("GetHEADBranch: %w", err) - } - if headBranch != nil { - repo.DefaultBranch = headBranch.Name - } - } - - if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { - return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) - } - - if !opts.Releases { - // note: this will greatly improve release (tag) sync - // for pull-mirrors with many tags - repo.IsMirror = opts.Mirror - if err = SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { - log.Error("Failed to synchronize tags to releases for repository: %v", err) - } - } - - if opts.LFS { - endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, httpTransport) - if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { - log.Error("Failed to store missing LFS objects for repository: %v", err) - } - } - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - if opts.Mirror { - remoteAddress, err := util.SanitizeURL(opts.CloneAddr) - if err != nil { - return repo, err - } - mirrorModel := repo_model.Mirror{ - RepoID: repo.ID, - Interval: setting.Mirror.DefaultInterval, - EnablePrune: true, - NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval), - LFS: opts.LFS, - RemoteAddress: remoteAddress, - } - if opts.LFS { - mirrorModel.LFSEndpoint = opts.LFSEndpoint - } - - if opts.MirrorInterval != "" { - parsedInterval, err := time.ParseDuration(opts.MirrorInterval) - if err != nil { - log.Error("Failed to set Interval: %v", err) - return repo, err - } - if parsedInterval == 0 { - mirrorModel.Interval = 0 - mirrorModel.NextUpdateUnix = 0 - } else if parsedInterval < setting.Mirror.MinInterval { - err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval) - log.Error("Interval: %s is too frequent", opts.MirrorInterval) - return repo, err - } else { - mirrorModel.Interval = parsedInterval - mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval) - } - } - - if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil { - return repo, fmt.Errorf("InsertOne: %w", err) - } - - repo.IsMirror = true - if err = UpdateRepository(ctx, repo, false); err != nil { - return nil, err - } - - // this is necessary for sync local tags from remote - configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName()) - if stdout, _, err := git.NewCommand(ctx, "config"). - AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err) - return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*): %w", err) - } - } else { - if err = UpdateRepoSize(ctx, repo); err != nil { - log.Error("Failed to update size for repository: %v", err) - } - if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil { - return nil, err - } - } - - return repo, committer.Commit() -} - -// cleanUpMigrateGitConfig removes mirror info which prevents "push --all". -// This also removes possible user credentials. -func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { - cmd := git.NewCommand(ctx, "remote", "rm", "origin") - // if the origin does not exist - _, stderr, err := cmd.RunStdString(&git.RunOpts{ - Dir: repoPath, - }) - if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { - return err - } - return nil -} - -// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. -func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { - repoPath := repo.RepoPath() - if err := CreateDelegateHooks(repoPath); err != nil { - return repo, fmt.Errorf("createDelegateHooks: %w", err) - } - if repo.HasWiki() { - if err := CreateDelegateHooks(repo.WikiPath()); err != nil { - return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) - } - } - - _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { - return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) - } - - if repo.HasWiki() { - if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil { - return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err) - } - } - - return repo, UpdateRepository(ctx, repo, false) -} - // SyncRepoTags synchronizes releases table with repository tags func SyncRepoTags(ctx context.Context, repoID int64) error { repo, err := repo_model.GetRepositoryByID(ctx, repoID) @@ -376,7 +110,9 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR } if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil { - return fmt.Errorf("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %w", tagName, repo.ID, repo.OwnerName, repo.Name, err) + // sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11 + // this is a tree object, not a tag object which created before git + log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err) } return nil diff --git a/modules/setting/admin.go b/modules/setting/admin.go index d7f0ee827d..502efd0eb9 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -3,15 +3,23 @@ package setting +import "code.gitea.io/gitea/modules/container" + // Admin settings var Admin struct { DisableRegularOrgCreation bool DefaultEmailNotification string SendNotificationEmailOnNewUser bool + UserDisabledFeatures container.Set[string] } func loadAdminFrom(rootCfg ConfigProvider) { - mustMapSetting(rootCfg, "admin", &Admin) sec := rootCfg.Section("admin") + Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false) Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") + Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...) } + +const ( + UserFeatureDeletion = "deletion" +) diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index e1c25ed9c7..12cf36aa59 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -197,7 +197,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { // NewConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. -func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { +func NewConfigProviderFromFile(file string) (ConfigProvider, error) { cfg := ini.Empty(configProviderLoadOptions()) loadedFromEmpty := true @@ -214,12 +214,6 @@ func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvi } } - for _, s := range extraConfigs { - if err := cfg.Append([]byte(s)); err != nil { - return nil, fmt.Errorf("unable to append more config: %v", err) - } - } - cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ file: file, diff --git a/modules/setting/database.go b/modules/setting/database.go index c7bc92e673..47d79d0de9 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -42,6 +42,7 @@ var ( DBConnectBackoff time.Duration MaxIdleConns int MaxOpenConns int + ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration IterateBufferSize int AutoMigration bool @@ -81,6 +82,7 @@ func loadDBSetting(rootCfg ConfigProvider) { } else { Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(0) } + Database.ConnMaxIdleTime = sec.Key("CONN_MAX_IDLETIME").MustDuration(0) Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(0) Database.IterateBufferSize = sec.Key("ITERATE_BUFFER_SIZE").MustInt(50) diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 7ab90669e7..750101747f 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -4,22 +4,19 @@ package setting import ( - "encoding/base64" "fmt" "time" "code.gitea.io/gitea/modules/generate" - "code.gitea.io/gitea/modules/util" ) // LFS represents the configuration for Git LFS var LFS = struct { - StartServer bool `ini:"LFS_START_SERVER"` - JWTSecretBase64 string `ini:"LFS_JWT_SECRET"` - JWTSecretBytes []byte `ini:"-"` - HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"` - MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` - LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` + StartServer bool `ini:"LFS_START_SERVER"` + JWTSecretBytes []byte `ini:"-"` + HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"` + MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` + LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` Storage *Storage }{} @@ -61,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error { return nil } - LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") - LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32) + jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") + LFS.JWTSecretBytes, err = generate.DecodeJwtSecret(jwtSecretBase64) if err != nil { - LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecret() + LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecret() if err != nil { return fmt.Errorf("error generating JWT Secret for custom config: %v", err) } @@ -74,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error { if err != nil { return fmt.Errorf("error saving JWT Secret for custom config: %v", err) } - rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) - saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64) + saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64) if err := saveCfg.Save(); err != nil { return fmt.Errorf("error saving JWT Secret for custom config: %v", err) } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index e93ce188df..d3c4d5c387 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -4,13 +4,12 @@ package setting import ( - "encoding/base64" "math" "path/filepath" + "sync/atomic" "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" ) // OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data @@ -98,7 +97,6 @@ var OAuth2 = struct { RefreshTokenExpirationTime int64 InvalidateRefreshTokens bool JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` - JWTSecretBase64 string `ini:"JWT_SECRET"` JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` MaxTokenLength int DefaultApplications []string @@ -130,28 +128,50 @@ func loadOAuth2From(rootCfg ConfigProvider) { return } - OAuth2.JWTSecretBase64 = loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET") + jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET") if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) } if InstallLock { - if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil { - _, OAuth2.JWTSecretBase64, err = generate.NewJwtSecret() + jwtSecretBytes, err := generate.DecodeJwtSecret(jwtSecretBase64) + if err != nil { + jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecret() if err != nil { log.Fatal("error generating JWT secret: %v", err) } - saveCfg, err := rootCfg.PrepareSaving() if err != nil { log.Fatal("save oauth2.JWT_SECRET failed: %v", err) } - rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64) - saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64) + rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) + saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) if err := saveCfg.Save(); err != nil { log.Fatal("save oauth2.JWT_SECRET failed: %v", err) } } + generalSigningSecret.Store(&jwtSecretBytes) } } + +// generalSigningSecret is used as container for a []byte value +// instead of an additional mutex, we use CompareAndSwap func to change the value thread save +var generalSigningSecret atomic.Pointer[[]byte] + +func GetGeneralTokenSigningSecret() []byte { + old := generalSigningSecret.Load() + if old == nil || len(*old) == 0 { + jwtSecret, _, err := generate.NewJwtSecret() + if err != nil { + log.Fatal("Unable to generate general JWT secret: %s", err.Error()) + } + if generalSigningSecret.CompareAndSwap(old, &jwtSecret) { + // FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...) + log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes") + return jwtSecret + } + return *generalSigningSecret.Load() + } + return *old +} diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go new file mode 100644 index 0000000000..da36d100aa --- /dev/null +++ b/modules/setting/oauth2_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "code.gitea.io/gitea/modules/generate" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestGetGeneralSigningSecret(t *testing.T) { + // when there is no general signing secret, it should be generated, and keep the same value + assert.Nil(t, generalSigningSecret.Load()) + s1 := GetGeneralTokenSigningSecret() + assert.NotNil(t, s1) + s2 := GetGeneralTokenSigningSecret() + assert.Equal(t, s1, s2) + + // the config value should always override any pre-generated value + cfg, _ := NewConfigProviderFromData(` +[oauth2] +JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +`) + defer test.MockVariableValue(&InstallLock, true)() + loadOAuth2From(cfg) + actual := GetGeneralTokenSigningSecret() + expected, _ := generate.DecodeJwtSecret("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + assert.Len(t, actual, 32) + assert.EqualValues(t, expected, actual) +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 4ab566b7ff..7a07fec85c 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -49,7 +49,8 @@ var ( DownloadOrCloneMethods []string PrefixArchiveFiles bool DisableMigrations bool - DisableStars bool `ini:"DISABLE_STARS"` + DisableStars bool + DisableForks bool DefaultBranch string AllowAdoptionOfUnadoptedRepositories bool AllowDeleteOfUnadoptedRepositories bool @@ -172,6 +173,7 @@ var ( PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, + DisableForks: false, DefaultBranch: "main", AllowForkWithoutMaximumLimit: true, diff --git a/modules/setting/setting.go b/modules/setting/setting.go index c0d8d0ee23..87e7b08c6e 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -92,9 +92,9 @@ func PrepareAppDataPath() error { return nil } -func InitCfgProvider(file string, extraConfigs ...string) { +func InitCfgProvider(file string) { var err error - if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil { + if CfgProvider, err = NewConfigProviderFromFile(file); err != nil { log.Fatal("Unable to init config provider from %q: %v", file, err) } CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 02a213d478..47e1393ef3 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -38,6 +38,7 @@ var UI = struct { PreferredTimestampTense string AmbiguousUnicodeDetection bool + SkipEscapeContexts []string Notification struct { MinTimeout time.Duration @@ -89,6 +90,7 @@ var UI = struct { PreferredTimestampTense: "mixed", AmbiguousUnicodeDetection: true, + SkipEscapeContexts: []string{}, Notification: struct { MinTimeout time.Duration diff --git a/modules/structs/repo.go b/modules/structs/repo.go index e20b6bc26e..a50cddaf7e 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -99,6 +99,7 @@ type Repository struct { AllowRebase bool `json:"allow_rebase"` AllowRebaseMerge bool `json:"allow_rebase_explicit"` AllowSquash bool `json:"allow_squash_merge"` + AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"` AllowRebaseUpdate bool `json:"allow_rebase_update"` DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"` DefaultMergeStyle string `json:"default_merge_style"` @@ -198,6 +199,8 @@ type EditRepoOption struct { AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"` // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. AllowSquash *bool `json:"allow_squash_merge,omitempty"` + // either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging. + AllowFastForwardOnly *bool `json:"allow_fast_forward_only_merge,omitempty"` // either `true` to allow mark pr as merged manually, or `false` to prevent it. AllowManualMerge *bool `json:"allow_manual_merge,omitempty"` // either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur. @@ -206,7 +209,7 @@ type EditRepoOption struct { AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"` // set to `true` to delete pr branch after merge by default DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"` - // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash". + // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only". DefaultMergeStyle *string `json:"default_merge_style,omitempty"` // set to `true` to allow edits from maintainers by default DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"` diff --git a/modules/structs/settings.go b/modules/structs/settings.go index e48b1a493d..b127b58462 100644 --- a/modules/structs/settings.go +++ b/modules/structs/settings.go @@ -9,6 +9,7 @@ type GeneralRepoSettings struct { HTTPGitDisabled bool `json:"http_git_disabled"` MigrationsDisabled bool `json:"migrations_disabled"` StarsDisabled bool `json:"stars_disabled"` + ForksDisabled bool `json:"forks_disabled"` TimeTrackingDisabled bool `json:"time_tracking_disabled"` LFSDisabled bool `json:"lfs_disabled"` } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 9b2668d318..7f87c032d5 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -9,12 +9,12 @@ import ( "html" "html/template" "net/url" + "slices" "strings" "time" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" @@ -35,10 +35,11 @@ func NewFuncMap() template.FuncMap { // html/template related functions "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. "Eval": Eval, - "Safe": Safe, - "Escape": html.EscapeString, + "SafeHTML": SafeHTML, + "HTMLFormat": HTMLFormat, + "HTMLEscape": HTMLEscape, "QueryEscape": url.QueryEscape, - "JSEscape": template.JSEscapeString, + "JSEscape": JSEscapeSafe, "Str2html": Str2html, // TODO: rename it to SanitizeHTML "URLJoin": util.URLJoin, "DotEscape": DotEscape, @@ -165,7 +166,6 @@ func NewFuncMap() template.FuncMap { "RenderCodeBlock": RenderCodeBlock, "RenderIssueTitle": RenderIssueTitle, "RenderEmoji": RenderEmoji, - "RenderEmojiPlain": emoji.ReplaceAliases, "ReactionToEmoji": ReactionToEmoji, "RenderMarkdownToHtml": RenderMarkdownToHtml, @@ -185,14 +185,57 @@ func NewFuncMap() template.FuncMap { } } -// Safe render raw as HTML -func Safe(raw string) template.HTML { - return template.HTML(raw) +func HTMLFormat(s string, rawArgs ...any) template.HTML { + args := slices.Clone(rawArgs) + for i, v := range args { + switch v := v.(type) { + case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: + // for most basic types (including template.HTML which is safe), just do nothing and use it + case string: + args[i] = template.HTMLEscapeString(v) + case fmt.Stringer: + args[i] = template.HTMLEscapeString(v.String()) + default: + args[i] = template.HTMLEscapeString(fmt.Sprint(v)) + } + } + return template.HTML(fmt.Sprintf(s, args...)) } -// Str2html render Markdown text to HTML -func Str2html(raw string) template.HTML { - return template.HTML(markup.Sanitize(raw)) +// SafeHTML render raw as HTML +func SafeHTML(s any) template.HTML { + switch v := s.(type) { + case string: + return template.HTML(v) + case template.HTML: + return v + } + panic(fmt.Sprintf("unexpected type %T", s)) +} + +// Str2html sanitizes the input by pre-defined markdown rules +func Str2html(s any) template.HTML { + switch v := s.(type) { + case string: + return template.HTML(markup.Sanitize(v)) + case template.HTML: + return template.HTML(markup.Sanitize(string(v))) + } + panic(fmt.Sprintf("unexpected type %T", s)) +} + +func HTMLEscape(s any) template.HTML { + switch v := s.(type) { + case string: + return template.HTML(html.EscapeString(v)) + case template.HTML: + return v + } + panic(fmt.Sprintf("unexpected type %T", s)) +} + +func JSEscapeSafe(s string) template.HTML { + return template.HTML(template.JSEscapeString(s)) } // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index ec83e9ac33..8f5d633d4f 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -4,6 +4,7 @@ package templates import ( + "html/template" "testing" "github.com/stretchr/testify/assert" @@ -52,3 +53,11 @@ func TestSubjectBodySeparator(t *testing.T) { "", "Insuficient\n--\nSeparators") } + +func TestJSEscapeSafe(t *testing.T) { + assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`)) +} + +func TestHTMLFormat(t *testing.T) { + assert.Equal(t, template.HTML("< < 1"), HTMLFormat("%s %s %d", "<", template.HTML("<"), 1)) +} diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 8648967d38..465640e08d 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -59,6 +59,11 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func TestApostrophesInMentions(t *testing.T) { + rendered := RenderMarkdownToHtml(context.Background(), "@mention-user's comment") + assert.EqualValues(t, "

@mention-user's comment

\n", rendered) +} + func TestRenderCommitBody(t *testing.T) { type args struct { ctx context.Context diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 2771b1e223..3f51c122b1 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -4,6 +4,7 @@ package templates import ( + "html/template" "strings" "code.gitea.io/gitea/modules/base" @@ -17,8 +18,14 @@ func NewStringUtils() *StringUtils { return &stringUtils } -func (su *StringUtils) HasPrefix(s, prefix string) bool { - return strings.HasPrefix(s, prefix) +func (su *StringUtils) HasPrefix(s any, prefix string) bool { + switch v := s.(type) { + case string: + return strings.HasPrefix(v, prefix) + case template.HTML: + return strings.HasPrefix(string(v), prefix) + } + return false } func (su *StringUtils) Contains(s, substr string) bool { diff --git a/modules/templates/util_string_test.go b/modules/templates/util_string_test.go new file mode 100644 index 0000000000..5844cf201d --- /dev/null +++ b/modules/templates/util_string_test.go @@ -0,0 +1,20 @@ +// Copyright Earl Warren +// SPDX-License-Identifier: MIT + +package templates + +import ( + "html/template" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_StringUtils_HasPrefix(t *testing.T) { + su := &StringUtils{} + assert.True(t, su.HasPrefix("ABC", "A")) + assert.False(t, su.HasPrefix("ABC", "B")) + assert.True(t, su.HasPrefix(template.HTML("ABC"), "A")) + assert.False(t, su.HasPrefix(template.HTML("ABC"), "B")) + assert.False(t, su.HasPrefix(123, "B")) +} diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go index 1cb3c4f288..dfaa0e3e3a 100644 --- a/modules/timeutil/since.go +++ b/modules/timeutil/since.go @@ -28,54 +28,54 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) { switch { case diff <= 0: diff = 0 - diffStr = lang.Tr("tool.now") + diffStr = lang.TrString("tool.now") case diff < 2: diff = 0 - diffStr = lang.Tr("tool.1s") + diffStr = lang.TrString("tool.1s") case diff < 1*Minute: - diffStr = lang.Tr("tool.seconds", diff) + diffStr = lang.TrString("tool.seconds", diff) diff = 0 case diff < 2*Minute: diff -= 1 * Minute - diffStr = lang.Tr("tool.1m") + diffStr = lang.TrString("tool.1m") case diff < 1*Hour: - diffStr = lang.Tr("tool.minutes", diff/Minute) + diffStr = lang.TrString("tool.minutes", diff/Minute) diff -= diff / Minute * Minute case diff < 2*Hour: diff -= 1 * Hour - diffStr = lang.Tr("tool.1h") + diffStr = lang.TrString("tool.1h") case diff < 1*Day: - diffStr = lang.Tr("tool.hours", diff/Hour) + diffStr = lang.TrString("tool.hours", diff/Hour) diff -= diff / Hour * Hour case diff < 2*Day: diff -= 1 * Day - diffStr = lang.Tr("tool.1d") + diffStr = lang.TrString("tool.1d") case diff < 1*Week: - diffStr = lang.Tr("tool.days", diff/Day) + diffStr = lang.TrString("tool.days", diff/Day) diff -= diff / Day * Day case diff < 2*Week: diff -= 1 * Week - diffStr = lang.Tr("tool.1w") + diffStr = lang.TrString("tool.1w") case diff < 1*Month: - diffStr = lang.Tr("tool.weeks", diff/Week) + diffStr = lang.TrString("tool.weeks", diff/Week) diff -= diff / Week * Week case diff < 2*Month: diff -= 1 * Month - diffStr = lang.Tr("tool.1mon") + diffStr = lang.TrString("tool.1mon") case diff < 1*Year: - diffStr = lang.Tr("tool.months", diff/Month) + diffStr = lang.TrString("tool.months", diff/Month) diff -= diff / Month * Month case diff < 2*Year: diff -= 1 * Year - diffStr = lang.Tr("tool.1y") + diffStr = lang.TrString("tool.1y") default: - diffStr = lang.Tr("tool.years", diff/Year) + diffStr = lang.TrString("tool.years", diff/Year) diff -= (diff / Year) * Year } return diff, diffStr @@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string { diff := now.Unix() - then.Unix() if then.After(now) { - return lang.Tr("tool.future") + return lang.TrString("tool.future") } if diff == 0 { - return lang.Tr("tool.now") + return lang.TrString("tool.now") } var timeStr, diffStr string @@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string { return strings.TrimPrefix(timeStr, ", ") } -func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML { +func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML { friendlyText := then.Format("2006-01-02 15:04:05 -07:00") // document: https://github.com/github/relative-time-element diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 42475545b3..1555cd961e 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -4,26 +4,25 @@ package i18n import ( + "html/template" "io" ) var DefaultLocales = NewLocaleStore() type Locale interface { - // Tr translates a given key and arguments for a language - Tr(trKey string, trArgs ...any) string - // Has reports if a locale has a translation for a given key - Has(trKey string) bool + // TrString translates a given key and arguments for a language + TrString(trKey string, trArgs ...any) string + // TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML + TrHTML(trKey string, trArgs ...any) template.HTML + // HasKey reports if a locale has a translation for a given key + HasKey(trKey string) bool } // LocaleStore provides the functions common to all locale stores type LocaleStore interface { io.Closer - // Tr translates a given key and arguments for a language - Tr(lang, trKey string, trArgs ...any) string - // Has reports if a locale has a translation for a given key - Has(lang, trKey string) bool // SetDefaultLang sets the default language to fall back to SetDefaultLang(lang string) // ListLangNameDesc provides paired slices of language names to descriptors @@ -45,7 +44,7 @@ func ResetDefaultLocales() { DefaultLocales = NewLocaleStore() } -// GetLocales returns the locale from the default locales +// GetLocale returns the locale from the default locales func GetLocale(lang string) (Locale, bool) { return DefaultLocales.Locale(lang) } diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index 1d1be43318..b364992dfe 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -4,6 +4,7 @@ package i18n import ( + "html/template" "strings" "testing" @@ -17,7 +18,7 @@ fmt = %[1]s %[2]s [section] sub = Sub String -mixed = test value; more text +mixed = test value; %s `) testData2 := []byte(` @@ -32,29 +33,33 @@ sub = Changed Sub String assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil)) ls.SetDefaultLang("lang1") - result := ls.Tr("lang1", "fmt", "a", "b") + lang1, _ := ls.Locale("lang1") + lang2, _ := ls.Locale("lang2") + + result := lang1.TrString("fmt", "a", "b") assert.Equal(t, "a b", result) - result = ls.Tr("lang2", "fmt", "a", "b") + result = lang2.TrString("fmt", "a", "b") assert.Equal(t, "b a", result) - result = ls.Tr("lang1", "section.sub") + result = lang1.TrString("section.sub") assert.Equal(t, "Sub String", result) - result = ls.Tr("lang2", "section.sub") + result = lang2.TrString("section.sub") assert.Equal(t, "Changed Sub String", result) - result = ls.Tr("", ".dot.name") + langNone, _ := ls.Locale("none") + result = langNone.TrString(".dot.name") assert.Equal(t, "Dot Name", result) - result = ls.Tr("lang2", "section.mixed") - assert.Equal(t, `test value; more text`, result) + result2 := lang2.TrHTML("section.mixed", "a&b") + assert.EqualValues(t, `test value; a&b`, result2) langs, descs := ls.ListLangNameDesc() assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) - found := ls.Has("lang1", "no-such") + found := lang1.HasKey("no-such") assert.False(t, found) assert.NoError(t, ls.Close()) } @@ -72,9 +77,75 @@ c=22 ls := NewLocaleStore() assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2)) - assert.Equal(t, "11", ls.Tr("lang1", "a")) - assert.Equal(t, "21", ls.Tr("lang1", "b")) - assert.Equal(t, "22", ls.Tr("lang1", "c")) + lang1, _ := ls.Locale("lang1") + assert.Equal(t, "11", lang1.TrString("a")) + assert.Equal(t, "21", lang1.TrString("b")) + assert.Equal(t, "22", lang1.TrString("c")) +} + +type stringerPointerReceiver struct { + s string +} + +func (s *stringerPointerReceiver) String() string { + return s.s +} + +type stringerStructReceiver struct { + s string +} + +func (s stringerStructReceiver) String() string { + return s.s +} + +type errorStructReceiver struct { + s string +} + +func (e errorStructReceiver) Error() string { + return e.s +} + +type errorPointerReceiver struct { + s string +} + +func (e *errorPointerReceiver) Error() string { + return e.s +} + +func TestLocaleWithTemplate(t *testing.T) { + ls := NewLocaleStore() + assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=%s`), nil)) + lang1, _ := ls.Locale("lang1") + + tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) + tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`)) + + cases := []struct { + in any + want string + }{ + {"", "<str>"}, + {[]byte(""), "[60 98 121 116 101 115 62]"}, + {template.HTML(""), ""}, + {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"}, + {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"}, + {stringerStructReceiver{""}, "<stringerStructReceiver>"}, + {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"}, + {errorStructReceiver{""}, "<errorStructReceiver>"}, + {&errorStructReceiver{""}, "<errorStructReceiver ptr>"}, + {errorPointerReceiver{""}, "{<errorPointerReceiver>}"}, + {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"}, + } + + buf := &strings.Builder{} + for _, c := range cases { + buf.Reset() + assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in})) + assert.Equal(t, c.want, buf.String()) + } } func TestLocaleStoreQuirks(t *testing.T) { @@ -110,8 +181,9 @@ func TestLocaleStoreQuirks(t *testing.T) { for _, testData := range testDataList { ls := NewLocaleStore() err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil) + lang1, _ := ls.Locale("lang1") assert.NoError(t, err, testData.hint) - assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint) + assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint) assert.NoError(t, ls.Close()) } diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index 42b95dda54..44c3fb0c88 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -5,6 +5,8 @@ package i18n import ( "fmt" + "html/template" + "slices" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -18,6 +20,8 @@ type locale struct { idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap } +var _ Locale = (*locale)(nil) + type localeStore struct { // After initializing has finished, these fields are read-only. langNames []string @@ -88,20 +92,6 @@ func (store *localeStore) SetDefaultLang(lang string) { store.defaultLang = lang } -// Tr translates content to target language. fall back to default language. -func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string { - l, _ := store.Locale(lang) - - return l.Tr(trKey, trArgs...) -} - -// Has returns whether the given language has a translation for the provided key -func (store *localeStore) Has(lang, trKey string) bool { - l, _ := store.Locale(lang) - - return l.Has(trKey) -} - // Locale returns the locale for the lang or the default language func (store *localeStore) Locale(lang string) (Locale, bool) { l, found := store.localeMap[lang] @@ -116,13 +106,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) { return l, found } -// Close implements io.Closer func (store *localeStore) Close() error { return nil } -// Tr translates content to locale language. fall back to default language. -func (l *locale) Tr(trKey string, trArgs ...any) string { +func (l *locale) TrString(trKey string, trArgs ...any) string { format := trKey idx, ok := l.store.trKeyToIdxMap[trKey] @@ -144,8 +132,25 @@ func (l *locale) Tr(trKey string, trArgs ...any) string { return msg } -// Has returns whether a key is present in this locale or not -func (l *locale) Has(trKey string) bool { +func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { + args := slices.Clone(trArgs) + for i, v := range args { + switch v := v.(type) { + case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: + // for most basic types (including template.HTML which is safe), just do nothing and use it + case string: + args[i] = template.HTMLEscapeString(v) + case fmt.Stringer: + args[i] = template.HTMLEscapeString(v.String()) + default: + args[i] = template.HTMLEscapeString(fmt.Sprint(v)) + } + } + return template.HTML(l.TrString(trKey, args...)) +} + +// HasKey returns whether a key is present in this locale or not +func (l *locale) HasKey(trKey string) bool { idx, ok := l.store.trKeyToIdxMap[trKey] if !ok { return false diff --git a/modules/translation/mock.go b/modules/translation/mock.go index 2d0cb17324..1f0559f38d 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -3,7 +3,10 @@ package translation -import "fmt" +import ( + "fmt" + "html/template" +) // MockLocale provides a mocked locale without any translations type MockLocale struct{} @@ -14,12 +17,16 @@ func (l MockLocale) Language() string { return "en" } -func (l MockLocale) Tr(s string, _ ...any) string { +func (l MockLocale) TrString(s string, _ ...any) string { return s } -func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string { - return key1 +func (l MockLocale) Tr(s string, a ...any) template.HTML { + return template.HTML(s) +} + +func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { + return template.HTML(key1) } func (l MockLocale) PrettyNumber(v any) string { diff --git a/modules/translation/translation.go b/modules/translation/translation.go index dba4de6607..b7c18f610a 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -5,6 +5,7 @@ package translation import ( "context" + "html/template" "sort" "strings" "sync" @@ -27,8 +28,11 @@ var ContextKey any = &contextKey{} // Locale represents an interface to translation type Locale interface { Language() string - Tr(string, ...any) string - TrN(cnt any, key1, keyN string, args ...any) string + TrString(string, ...any) string + + Tr(key string, args ...any) template.HTML + TrN(cnt any, key1, keyN string, args ...any) template.HTML + PrettyNumber(v any) string } @@ -144,6 +148,8 @@ type locale struct { msgPrinter *message.Printer } +var _ Locale = (*locale)(nil) + // NewLocale return a locale func NewLocale(lang string) Locale { if lock != nil { @@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{ }, } +func (l *locale) Tr(s string, args ...any) template.HTML { + return l.TrHTML(s, args...) +} + // TrN returns translated message for plural text translation -func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string { +func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { var c int64 if t, ok := cnt.(int); ok { c = int64(t) diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go index 6b07bd0413..739543e297 100644 --- a/modules/util/filebuffer/file_backed_buffer.go +++ b/modules/util/filebuffer/file_backed_buffer.go @@ -149,6 +149,7 @@ func (b *FileBackedBuffer) Close() error { if b.file != nil { err := b.file.Close() os.Remove(b.file.Name()) + b.file = nil return err } return nil diff --git a/modules/util/util.go b/modules/util/util.go index c47931f6c9..28b549f405 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -6,12 +6,13 @@ package util import ( "bytes" "crypto/rand" - "encoding/base64" "fmt" "math/big" "strconv" "strings" + "code.gitea.io/gitea/modules/optional" + "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -43,6 +44,22 @@ func (o OptionalBool) IsNone() bool { return o == OptionalBoolNone } +// ToGeneric converts OptionalBool to optional.Option[bool] +func (o OptionalBool) ToGeneric() optional.Option[bool] { + if o.IsNone() { + return optional.None[bool]() + } + return optional.Some[bool](o.IsTrue()) +} + +// OptionalBoolFromGeneric converts optional.Option[bool] to OptionalBool +func OptionalBoolFromGeneric(o optional.Option[bool]) OptionalBool { + if o.Has() { + return OptionalBoolOf(o.Value()) + } + return OptionalBoolNone +} + // OptionalBoolOf get the corresponding OptionalBool of a bool func OptionalBoolOf(b bool) OptionalBool { if b { @@ -246,13 +263,3 @@ func ToFloat64(number any) (float64, error) { func ToPointer[T any](val T) *T { return &val } - -func Base64FixedDecode(encoding *base64.Encoding, src []byte, length int) ([]byte, error) { - decoded := make([]byte, encoding.DecodedLen(len(src))+3) - if n, err := encoding.Decode(decoded, src); err != nil { - return nil, err - } else if n != length { - return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length) - } - return decoded[:length], nil -} diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 8509d8aced..c5830ce01c 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -4,7 +4,6 @@ package util import ( - "encoding/base64" "regexp" "strings" "testing" @@ -234,16 +233,3 @@ func TestToPointer(t *testing.T) { val123 := 123 assert.False(t, &val123 == ToPointer(val123)) } - -func TestBase64FixedDecode(t *testing.T) { - _, err := Base64FixedDecode(base64.RawURLEncoding, []byte("abcd"), 32) - assert.ErrorContains(t, err, "invalid base64 decoded length") - _, err = Base64FixedDecode(base64.RawURLEncoding, []byte(strings.Repeat("a", 64)), 32) - assert.ErrorContains(t, err, "invalid base64 decoded length") - - str32 := strings.Repeat("x", 32) - encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32)) - decoded32, err := Base64FixedDecode(base64.RawURLEncoding, []byte(encoded32), 32) - assert.NoError(t, err) - assert.Equal(t, str32, string(decoded32)) -} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 4e7fca80e2..4891e43f27 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -105,44 +105,44 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo trName := field.Tag.Get("locale") if len(trName) == 0 { - trName = l.Tr("form." + field.Name) + trName = l.TrString("form." + field.Name) } else { - trName = l.Tr(trName) + trName = l.TrString(trName) } switch errs[0].Classification { case binding.ERR_REQUIRED: - data["ErrorMsg"] = trName + l.Tr("form.require_error") + data["ErrorMsg"] = trName + l.TrString("form.require_error") case binding.ERR_ALPHA_DASH: - data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error") + data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error") case binding.ERR_ALPHA_DASH_DOT: - data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error") + data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error") case validation.ErrGitRefName: - data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error") + data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error") case binding.ERR_SIZE: - data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field)) + data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field)) case binding.ERR_MIN_SIZE: - data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field)) + data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field)) case binding.ERR_MAX_SIZE: - data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field)) + data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field)) case binding.ERR_EMAIL: - data["ErrorMsg"] = trName + l.Tr("form.email_error") + data["ErrorMsg"] = trName + l.TrString("form.email_error") case binding.ERR_URL: - data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message) + data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message) case binding.ERR_INCLUDE: - data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) + data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field)) case validation.ErrGlobPattern: - data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message) + data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message) case validation.ErrRegexPattern: - data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) + data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message) case validation.ErrUsername: if setting.Service.AllowDotsInUsernames { - data["ErrorMsg"] = trName + l.Tr("form.username_error") + data["ErrorMsg"] = trName + l.TrString("form.username_error") } else { - data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots") + data["ErrorMsg"] = trName + l.TrString("form.username_error_no_dots") } case validation.ErrInvalidGroupTeamMap: - data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message) + data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message) default: msg := errs[0].Classification if msg != "" && errs[0].Message != "" { @@ -151,7 +151,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo msg += errs[0].Message if msg == "" { - msg = l.Tr("form.unknown_error") + msg = l.TrString("form.unknown_error") } data["ErrorMsg"] = trName + ": " + msg } diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index 41f3aac27c..88da2049a4 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -3,7 +3,11 @@ package middleware -import "net/url" +import ( + "fmt" + "html/template" + "net/url" +) // Flash represents a one time data transfer between two requests. type Flash struct { @@ -26,26 +30,36 @@ func (f *Flash) set(name, msg string, current ...bool) { } } +func flashMsgStringOrHTML(msg any) string { + switch v := msg.(type) { + case string: + return v + case template.HTML: + return string(v) + } + panic(fmt.Sprintf("unknown type: %T", msg)) +} + // Error sets error message -func (f *Flash) Error(msg string, current ...bool) { - f.ErrorMsg = msg - f.set("error", msg, current...) +func (f *Flash) Error(msg any, current ...bool) { + f.ErrorMsg = flashMsgStringOrHTML(msg) + f.set("error", f.ErrorMsg, current...) } // Warning sets warning message -func (f *Flash) Warning(msg string, current ...bool) { - f.WarningMsg = msg - f.set("warning", msg, current...) +func (f *Flash) Warning(msg any, current ...bool) { + f.WarningMsg = flashMsgStringOrHTML(msg) + f.set("warning", f.WarningMsg, current...) } // Info sets info message -func (f *Flash) Info(msg string, current ...bool) { - f.InfoMsg = msg - f.set("info", msg, current...) +func (f *Flash) Info(msg any, current ...bool) { + f.InfoMsg = flashMsgStringOrHTML(msg) + f.set("info", f.InfoMsg, current...) } // Success sets success message -func (f *Flash) Success(msg string, current ...bool) { - f.SuccessMsg = msg - f.set("success", msg, current...) +func (f *Flash) Success(msg any, current ...bool) { + f.SuccessMsg = flashMsgStringOrHTML(msg) + f.set("success", f.SuccessMsg, current...) } diff --git a/options/gitignore/Zig b/options/gitignore/Zig new file mode 100644 index 0000000000..236ae6be8c --- /dev/null +++ b/options/gitignore/Zig @@ -0,0 +1,5 @@ +zig-cache/ +zig-out/ +build/ +build-*/ +docgen_tmp/ \ No newline at end of file diff --git a/options/license/Brian-Gladman-2-Clause b/options/license/Brian-Gladman-2-Clause new file mode 100644 index 0000000000..7276f63e9e --- /dev/null +++ b/options/license/Brian-Gladman-2-Clause @@ -0,0 +1,17 @@ +Copyright (C) 1998-2013, Brian Gladman, Worcester, UK. All + rights reserved. + +The redistribution and use of this software (with or without +changes) is allowed without the payment of fees or royalties +provided that: + + source code distributions include the above copyright notice, + this list of conditions and the following disclaimer; + + binary distributions include the above copyright notice, this + list of conditions and the following disclaimer in their + documentation. + +This software is provided 'as is' with no explicit or implied +warranties in respect of its operation, including, but not limited +to, correctness and fitness for purpose. diff --git a/options/license/CMU-Mach-nodoc b/options/license/CMU-Mach-nodoc new file mode 100644 index 0000000000..c81d74fee7 --- /dev/null +++ b/options/license/CMU-Mach-nodoc @@ -0,0 +1,11 @@ +Copyright (C) 2002 Naval Research Laboratory (NRL/CCS) + +Permission to use, copy, modify and distribute this software and +its documentation is hereby granted, provided that both the +copyright notice and this permission notice appear in all copies of +the software, derivative works or modified versions, and any +portions thereof. + +NRL ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION AND +DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER +RESULTING FROM THE USE OF THIS SOFTWARE. diff --git a/options/license/GNOME-examples-exception b/options/license/GNOME-examples-exception new file mode 100644 index 0000000000..0f0cd53b50 --- /dev/null +++ b/options/license/GNOME-examples-exception @@ -0,0 +1 @@ +As a special exception, the copyright holders give you permission to copy, modify, and distribute the example code contained in this document under the terms of your choosing, without restriction. diff --git a/options/license/Gmsh-exception b/options/license/Gmsh-exception new file mode 100644 index 0000000000..6d28f704e4 --- /dev/null +++ b/options/license/Gmsh-exception @@ -0,0 +1,16 @@ +The copyright holders of Gmsh give you permission to combine Gmsh + with code included in the standard release of Netgen (from Joachim + Sch"oberl), METIS (from George Karypis at the University of + Minnesota), OpenCASCADE (from Open CASCADE S.A.S) and ParaView + (from Kitware, Inc.) under their respective licenses. You may copy + and distribute such a system following the terms of the GNU GPL for + Gmsh and the licenses of the other code concerned, provided that + you include the source code of that other code when and as the GNU + GPL requires distribution of source code. + + Note that people who make modified versions of Gmsh are not + obligated to grant this special exception for their modified + versions; it is their choice whether to do so. The GNU General + Public License gives permission to release a modified version + without this exception; this exception also makes it possible to + release a modified version which carries forward this exception. diff --git a/options/license/HPND-Fenneberg-Livingston b/options/license/HPND-Fenneberg-Livingston new file mode 100644 index 0000000000..aaf524f3aa --- /dev/null +++ b/options/license/HPND-Fenneberg-Livingston @@ -0,0 +1,13 @@ +Copyright (C) 1995,1996,1997,1998 Lars Fenneberg + +Permission to use, copy, modify, and distribute this software for any +purpose and without fee is hereby granted, provided that this copyright and +permission notice appear on all copies and supporting documentation, the +name of Lars Fenneberg not be used in advertising or publicity pertaining to +distribution of the program without specific prior permission, and notice be +given in supporting documentation that copying and distribution is by +permission of Lars Fenneberg. + +Lars Fenneberg makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or implied +warranty. diff --git a/options/license/HPND-INRIA-IMAG b/options/license/HPND-INRIA-IMAG new file mode 100644 index 0000000000..87d09d92cb --- /dev/null +++ b/options/license/HPND-INRIA-IMAG @@ -0,0 +1,9 @@ +This software is available with usual "research" terms with +the aim of retain credits of the software. Permission to use, +copy, modify and distribute this software for any purpose and +without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies, and +the name of INRIA, IMAG, or any contributor not be used in +advertising or publicity pertaining to this material without +the prior explicit permission. The software is provided "as +is" without any warranties, support or liabilities of any kind. diff --git a/options/license/Mackerras-3-Clause b/options/license/Mackerras-3-Clause new file mode 100644 index 0000000000..6467f0c98e --- /dev/null +++ b/options/license/Mackerras-3-Clause @@ -0,0 +1,25 @@ +Copyright (c) 1995 Eric Rosenquist. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + 3. The name(s) of the authors of this software must not be used to + endorse or promote products derived from this software without + prior written permission. + + THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/options/license/Mackerras-3-Clause-acknowledgment b/options/license/Mackerras-3-Clause-acknowledgment new file mode 100644 index 0000000000..5f0187add7 --- /dev/null +++ b/options/license/Mackerras-3-Clause-acknowledgment @@ -0,0 +1,25 @@ +Copyright (c) 1993-2002 Paul Mackerras. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The name(s) of the authors of this software must not be used to + endorse or promote products derived from this software without + prior written permission. + +3. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes software developed by Paul Mackerras + ". + +THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/options/license/OpenVision b/options/license/OpenVision new file mode 100644 index 0000000000..983505389e --- /dev/null +++ b/options/license/OpenVision @@ -0,0 +1,33 @@ +Copyright, OpenVision Technologies, Inc., 1993-1996, All Rights +Reserved + +WARNING: Retrieving the OpenVision Kerberos Administration system +source code, as described below, indicates your acceptance of the +following terms. If you do not agree to the following terms, do +not retrieve the OpenVision Kerberos administration system. + +You may freely use and distribute the Source Code and Object Code +compiled from it, with or without modification, but this Source +Code is provided to you "AS IS" EXCLUSIVE OF ANY WARRANTY, +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY OR +FITNESS FOR A PARTICULAR PURPOSE, OR ANY OTHER WARRANTY, WHETHER +EXPRESS OR IMPLIED. IN NO EVENT WILL OPENVISION HAVE ANY LIABILITY +FOR ANY LOST PROFITS, LOSS OF DATA OR COSTS OF PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, INDIRECT, OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, INCLUDING, +WITHOUT LIMITATION, THOSE RESULTING FROM THE USE OF THE SOURCE +CODE, OR THE FAILURE OF THE SOURCE CODE TO PERFORM, OR FOR ANY +OTHER REASON. + +OpenVision retains all copyrights in the donated Source Code. +OpenVision also retains copyright to derivative works of the Source +Code, whether created by OpenVision or by a third party. The +OpenVision copyright notice must be preserved if derivative works +are made based on the donated Source Code. + +OpenVision Technologies, Inc. has donated this Kerberos +Administration system to MIT for inclusion in the standard Kerberos +5 distribution. This donation underscores our commitment to +continuing Kerberos technology development and our gratitude for +the valuable work which has been performed by MIT and the Kerberos +community. diff --git a/options/license/Sun-PPP b/options/license/Sun-PPP new file mode 100644 index 0000000000..5f94a13437 --- /dev/null +++ b/options/license/Sun-PPP @@ -0,0 +1,13 @@ +Copyright (c) 2001 by Sun Microsystems, Inc. +All rights reserved. + +Non-exclusive rights to redistribute, modify, translate, and use +this software in source and binary forms, in whole or in part, is +hereby granted, provided that the above copyright notice is +duplicated in any source form, and that neither the name of the +copyright holder nor the author is used to endorse or promote +products derived from this software. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. diff --git a/options/license/UMich-Merit b/options/license/UMich-Merit new file mode 100644 index 0000000000..93e304b90e --- /dev/null +++ b/options/license/UMich-Merit @@ -0,0 +1,19 @@ +[C] The Regents of the University of Michigan and Merit Network, Inc. 1992, +1993, 1994, 1995 All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided +that the above copyright notice and this permission notice appear in all +copies of the software and derivative works or modified versions thereof, +and that both the copyright notice and this permission and disclaimer +notice appear in supporting documentation. + +THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE REGENTS OF THE +UNIVERSITY OF MICHIGAN AND MERIT NETWORK, INC. DO NOT WARRANT THAT THE +FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET LICENSEE'S REQUIREMENTS OR +THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. The Regents of the +University of Michigan and Merit Network, Inc. shall not be liable for any +special, indirect, incidental or consequential damages with respect to any +claim by Licensee or any third party arising from use of the software. diff --git a/options/license/bcrypt-Solar-Designer b/options/license/bcrypt-Solar-Designer new file mode 100644 index 0000000000..8cb05017fc --- /dev/null +++ b/options/license/bcrypt-Solar-Designer @@ -0,0 +1,11 @@ +Written by Solar Designer in 1998-2014. +No copyright is claimed, and the software is hereby placed in the public +domain. In case this attempt to disclaim copyright and place the software +in the public domain is deemed null and void, then the software is +Copyright (c) 1998-2014 Solar Designer and it is hereby released to the +general public under the following terms: + +Redistribution and use in source and binary forms, with or without +modification, are permitted. + +There's ABSOLUTELY NO WARRANTY, express or implied. diff --git a/options/license/gtkbook b/options/license/gtkbook new file mode 100644 index 0000000000..91215e80d6 --- /dev/null +++ b/options/license/gtkbook @@ -0,0 +1,6 @@ +Copyright 2005 Syd Logan, All Rights Reserved + +This code is distributed without warranty. You are free to use +this code for any purpose, however, if this code is republished or +redistributed in its original form, as hardcopy or electronically, +then you must include this copyright notice along with the code. diff --git a/options/license/softSurfer b/options/license/softSurfer new file mode 100644 index 0000000000..1bbc88c34c --- /dev/null +++ b/options/license/softSurfer @@ -0,0 +1,6 @@ +Copyright 2001, softSurfer (www.softsurfer.com) +This code may be freely used and modified for any purpose +providing that this copyright notice is included with it. +SoftSurfer makes no warranty for this code, and cannot be held +liable for any real or imagined damage resulting from its use. +Users of this code must verify correctness for their application. diff --git a/options/locale/locale_ar.ini b/options/locale/locale_ar.ini index 9f0dde5c94..c60ed5c42c 100644 --- a/options/locale/locale_ar.ini +++ b/options/locale/locale_ar.ini @@ -64,7 +64,7 @@ copy_url = انسخ الرابط admin_panel = إدارة الموقع copy_error = فشل النسخ new_mirror = مرآة جديدة -re_type = أكّد كلمة المرور الجديدة +re_type = تأكيد كلمة المرور webauthn_unsupported_browser = متصفحك لا يدعم ويب آوثن حالياً. copy = انسخ enabled = مُفَعَّل @@ -122,6 +122,10 @@ powered_by = مدعوم بواسطة %s retry = أعد المحاولة tracked_time_summary = ملخص للتتبع الزمني وفقًا لنتائج تصفية قائمة المسائل copy_hash = انسخ البصمة +remove = أزل +remove_all = أزل الكل +remove_label_str = أزل العنصر "%s" +confirm_delete_artifact = هل أنت متأكد أنك تريد حذف العنصر '%s'؟ [install] db_name = اسم قاعدة البيانات @@ -206,34 +210,34 @@ admin_email = عنوان البريد الإلكتروني install_btn_confirm = تثبت فورجيو secret_key_failed = لم يتم توليد مفتاح سري: %v save_config_failed = فشل في حفظ الإعداد: %s -sqlite3_not_available = هذا الأصدار من فورجيو لا يدعم SQLite3. من فضلك قم بتحميل الاصدار الملفي الرسمي من %s (ليس اصدار 'gobuild'). -test_git_failed = لم يتمكن من أختبار أمر جِت: %v +sqlite3_not_available = هذا الإصدار من فورجيو لا يدعم SQLite3. من فضلك قم بتنزيل الإصدار الرسمي من %s (ليس إصدار 'gobuild'). +test_git_failed = يتعذر اختبار أمر جِت: %v confirm_password = أكّد كلمة المرور invalid_admin_setting = إعداد حساب المدير غير صالح: %v invalid_log_root_path = مسار السجل غير صالح: %v -default_enable_timetracking = فعل تتبع الوقت افتراضياً +default_enable_timetracking = فعّل تتبع الوقت مبدئيا env_config_keys_prompt = ستطبق المتغيرات البيئية التالية أيضاً على ملف الإعدادات: admin_title = إعدادات حساب المدير no_reply_address_helper = النطاق للمستخدمين بعنوان بريد إلكتروني مخفي. مثلاً، اسم المستخدم "sarah" سوف يسجل في جِت كـ"sarah@noreply.example.org" لو كان نطاق البريد الإلكتروني الخفي مدخل كـ"noreply.example.org". enable_update_checker = فعل فحص التحديثات -default_enable_timetracking_popup = فعل تتبع الوقت للمستودعات الجديدة افتراضياً. +default_enable_timetracking_popup = فعل تتبع الوقت للمستودعات الجديدة مبدئيا. run_user_not_match = مستخدم التشغيل غير مطابق لأسم المستخدم الحالي: %s -> %s invalid_db_setting = إعدادات قاعدة البيانات غير صالحة: %v invalid_db_table = جدول قاعدة البيانات "%s" غير صالح: %v -default_keep_email_private_popup = اخفي عناوين البريد الإلكتروني للحسابات الجديدة افتراضياً. +default_keep_email_private_popup = أخفِ عناوين البريد الإلكتروني للحسابات الجديدة مبدئيا. env_config_keys = إعدادات بيئية -default_allow_create_organization = اسمح بإنشاء المنظمات افتراضياً +default_allow_create_organization = اسمح بإنشاء المنظمات مبدئيا invalid_app_data_path = مسار بيانات التطبيق غير صالح: %v enable_update_checker_helper = يفحص لإيجاد اصدارات جديدة عن طريق الإتصال بسيرفرات فورجيو. invalid_repo_path = المسار الجزري للمستودع غير صالح: %v internal_token_failed = فشل توليد الرمز الداخلي: %v no_reply_address = نطاقات البريد الإلكتروني المخفية -default_keep_email_private = اخفي عناوين البريد الإلكتروني افتراضياً +default_keep_email_private = أخفِ عناوين البريد الإلكتروني مبدئيا admin_name = اسم مستخدم المدير -default_allow_create_organization_popup = اسمح بحسابات المستخدمين الجديدة بإنشاء المنظمات افتراضياً. +default_allow_create_organization_popup = اسمح بحسابات المستخدمين الجديدة بإنشاء المنظمات مبدئيا. password_algorithm = خوارزمية تجزئة كلمة المرور -invalid_password_algorithm = خوارزمية تجزئة كلمة المرور غير صالحة -password_algorithm_helper = اختر خوارزمية تجزئة كلمة المرور. الخوارزميات لديهم متطلبات وقوى مختلفة. خوارزمية argon2 هي آمنة ولكن تتطلب الكثير من الذاكرة ولذلك قد تكون غير ملائمة للأنظمة الصغيرة. +invalid_password_algorithm = خوارزمية بصمة كلمة المرور غير صالحة +password_algorithm_helper = اختر خوارزمية بصمة كلمة المرور. تختلف الخوارزميات في متطلباتها وقوتها. خوارزمية argon2 آمنة لكن تتطلب الكثير من الذاكرة ولذلك قد تكون غير ملائمة للأنظمة الصغيرة. [editor] buttons.list.ordered.tooltip = أضف قائمة مرقمة @@ -321,7 +325,7 @@ twofa_disable = تعطيل الاستيثاق الثنائي retype_new_password = تأكيد كلمة المرور الجديدة manage_emails = أدر عناوين البريد الإلكتروني then_enter_passcode = وأدخل رمز الدخول الظاهر في التطبيق: -change_password = حدّث كلمة المرور +update_password = حدّث كلمة المرور continue = استمر emails = عناوين البريد الإلكتروني confirm_delete_account = أكُد الحذف @@ -381,7 +385,7 @@ key_state_desc = هذا المفتاح أستُعمل خلال آخر 7 أيام webauthn_delete_key = أزِل مفتاح الأمان valid_forever = صالح للأبد can_read_info = قراءة -create_oauth2_application_button = أنشئ تطبيق +create_oauth2_application_button = أنشئ تطبيقا save_application = احفظ permissions_access_all = الكل (عام، خاص، ومحدود) valid_until_date = صالح حتى %s @@ -405,7 +409,7 @@ comment_type_group_deadline = الموعد النهائي add_key = أضف مفتاح gpg_no_key_email_found = هذا المفتاح الـGPG لا يُطابق أي بريد إلكتروني مُفعل ومربوط بحسابك. لا زال يمكن إضافته إن وقعت الرمز المقدم. keep_activity_private = اخف النشاط من صفحة الملف الشخصي -profile_desc = تحكم في كيفية ظهور ملفك الشخصي للمستخدمين الآخرين. سيتم استخدام عنوان بريدك الإلكتروني الأساسي للإشعارات واستعادة كلمة المرور وعمليات Git المستندة إلى الويب. +profile_desc = تحكم في كيفية ظهور ملفك الشخصي للمستخدمين الآخرين. سيتم استخدام عنوان بريدك الإلكتروني الأساسي للإشعارات واستعادة كلمة المرور وعمليات Git المعتمدة على الويب. can_not_add_email_activations_pending = هناك تفعيل قيد الانتظار، حاول مجدداً خلال بضع دقائق إن أردت أضافه بريد إلكتروني جديد. gpg_key_id_used = هناك مفتاح GPG عام بنفس المعرف موجود بالفعل. add_new_gpg_key = أضف مفتاح GPG @@ -420,7 +424,7 @@ add_new_key = أضف مفتاح SSH hidden_comment_types_description = أنواع التعليق المُختارة هنا لن تظهر داخل صفحات المسائل. أختيار "تصنيف" مثلاً يمسح كل تعليقات "<مستخدم> أضاف/مسح <تصنيف>". key_content_gpg_placeholder = يبدأ بـ '-----BEGIN PGP PUBLIC KEY BLOCK-----' add_email_confirmation_sent = بريد تفعيل جديد تم إرساله إلى "%s". يُرجى التحقق من البريد الوارد خلال %s لتأكيد عنوان البريد الإلكتروني. -ssh_desc = ترتبط هذه المفاتيح الـSSH العامة بحسابك. تسمح المفاتيح الخاصة المطابقة بالوصول الكامل إلى مستودعاتك. +ssh_desc = مفاتيح SSH العمومية هذه مرتبطة بحسابك. وتسمح المفاتيح الخصوصية المرافقة بالوصول الكامل إلى مستودعاتك. ويمكن استعمال مفاتيح SSH الموثَّقة لتوثيق إيداعات جت الموقَّعة بمفاتيح SSH. ssh_gpg_keys = مفاتيح SSH / GPG authorized_oauth2_applications_description = لقد منحتَ إمكانية الوصول إلى حسابك الشخصي على فورجيو لهذه التطبيقات من تطبيقات خارجية. الرجاء إلغاء وصول التطبيقات التي لم تعد بحاجة إليها. ssh_key_been_used = هذا المفتاح الـSSH تم إضافته بالفعل إلى هذا الخادم. @@ -449,7 +453,7 @@ gpg_key_verified_long = تم التحقق من المفتاح بواسطة رم change_username_redirect_prompt = اسم المستخدم القديم سوف يعاد توجيهه حتى يطالب به شخص آخر. add_key_success = تم إضافة مفتاح SSH "%s". key_name = اسم المفتاح -comment_type_group_time_tracking = تعقب الوقت +comment_type_group_time_tracking = تتبع الوقت gpg_invalid_token_signature = مفتاح GPG المزود، والتوقيع والرمز لا يتطابقوا أو الرمز قديم. ssh_key_verified = مفتاح مُتحقق منه ssh_key_deletion_success = تم إزالة مفتاح SSH. @@ -470,6 +474,16 @@ gpg_token = رمز gpg_key_matched_identities_long = الهويات المدمجة في هذا المفتاح تطابق عناوين البريد الإلكتروني المفعلة التالية لهذا المستخدم. ويمكن التحقق من صحة الإيداعات المطابقة لهذه العناوين البريدية مع هذا المفتاح. key_content = المحتوى key_signature_gpg_placeholder = يبدأ بـ'-----BEGIN PGP SIGNATURE-----' +manage_oauth2_applications = إدارة تطبيقات OAuth2 +edit_oauth2_application = تعديل تطبيق OAuth2 +remove_oauth2_application = إزالة تطبيق OAuth2 +create_oauth2_application = أنشئ تطبيق OAuth2 جديدا +create_oauth2_application_success = لقد أنشأت بنجاح تطبيق OAuth2 جديدا. +remove_oauth2_application_success = أُزيل التطبيق. +update_oauth2_application_success = لقد حدّثت بنجاح تطبيق OAuth2. +oauth2_redirect_uris = روابط إعادة التوجيه. نرجو وضع كل رابط في سطر وحده. +remove_account_link = أزل الحساب المربوط +remove_account_link_success = أُزيل الحساب المربوط. [org] follow_blocked_user = لا يمكنك إتباع هذه المنظمة لأن هذه المنظمة حظرتك. @@ -560,7 +574,7 @@ settings.add_collaborator_blocked_our = لا يمكن إضافة المشترك commits.browse_further = تصفح أكثر settings.ignore_stale_approvals = تجاهل الطلبات الراكدة rss.must_be_on_branch = يجب أن تكون على فرع لتحصل على موجز RSS. -clone_in_vscodium = إستنسخ في VS Codium +clone_in_vscodium = إستنسخ في VSCodium admin.enabled_flags = العلامات المفعلة لهذا المستودع: settings.new_owner_blocked_doer = المالك الجديد حظرك. issues.comment.blocked_by_user = لا يمكنك أن ترسل تعليقاً على هذه المسألة لأنك محظور من قبل مالك المستودع أو مرسل المسألة. @@ -710,9 +724,9 @@ issues.role.first_time_contributor = مساهم لأول مرة issues.label_title = الاسم issues.filter_projects = تصفية المشروعات issues.role.collaborator = مشترك -issues.action_open = حالية +issues.action_open = افتح issues.filter_sort.latest = الأحدث -issues.action_close = تامة +issues.action_close = أغلق issues.is_stale = تغيّر طلب السحب هذا بعد المراجعة issues.dependency.remove = أزل issues.label_templates.info = لا توجد تصنيفات بعد. أنشئ تصنيفا بزر «تصنيف جديد» أو استخدم باقة تصنيفات معرّفة سابقا: @@ -797,7 +811,7 @@ branch.rename_branch_to = غيّر اسم "%s" إلى: issues.reopen_comment_issue = علّق وأعد فتحها issues.dependency.add = أضف اعتمادية… issues.label_deletion_desc = حذف تصنيف يحذفه من كل المسائل، أتريد الاستمرار؟ -labels = تصنيفات +labels = التصنيفات delete_preexisting_content = احذف الملفات في %s milestones.deletion = احذف الهدف issues.comment_pull_merged_at = دمج الإيداع %[1]s إلى %[2]s %[3]s @@ -1003,7 +1017,7 @@ settings.web_hook_name_discord = دسكورد settings.web_hook_name_telegram = تيليجرام editor.commit_signed_changes = أودع التعديلات الموقّعة editor.filename_is_invalid = اسم الملف غير صالح: "%s". -pulls.no_merge_access = أنت غير مأذون بدمج طلب السحب. +pulls.no_merge_access = ليس مسموحا لك دمج هذا الطلب. visibility_helper = اجعل المستودع خاصًا visibility_helper_forced = يفرض مدير موقعك أن تكون المستودعات الجديدة خاصة. wiki.page_content = محتوى الصفحة @@ -1107,9 +1121,197 @@ settings.collaborator_deletion = أزل مشتركا settings.event_wiki = الموسوعة settings.event_wiki_desc = إنشاء صفحة موسوعة أو تغيير اسمها أو تعديلها أو حذفها. settings.transfer_desc = انقل ملكية هذا المستودع إلى مستخدم أو إلى منظمة تديرها. +settings.transfer.rejected = رُفض نقل ملكية المستودع. +pulls.cannot_auto_merge_desc = لا يمكن دمج هذا الطلب آليا بسبب النزاعات. +pulls.auto_merge_newly_scheduled = هذا الطلب مجدول للدمج عند نجاح جميع الفحوص. +pulls.approve_count_n = "%d موافقة" +pulls.cannot_auto_merge_helper = ادمجه آليا لحل النزاعات. +pulls.num_conflicting_files_n = "%d ملفا متنازع عليها" +pulls.approve_count_1 = "%d موافقة" +pulls.reject_count_1 = "%d طلب تغيير" +pulls.reject_count_n = "%d طلب تغيير" +pulls.waiting_count_1 = "%d انتظار مراجعة" +pulls.waiting_count_n = "%d انتظار مراجعة" +pulls.no_merge_desc = لا يمكن دمج هذا الطلب لأن كل خيارات الدمج في هذا المستودع معطّلة. +pulls.no_merge_helper = فعّل خيارات الدمج في إعدادات المستودع أو ادمج الطلب يدويا. +pulls.no_merge_not_ready = هذا الطلب ليس جاهزا للدمج؛ انظر حالة المراجعة وفحوص الحالة. +pulls.merge_pull_request = أنشئ إيداع دمج +pulls.rebase_merge_pull_request = أعد التأسيس ثم قم بالتسريع +pulls.rebase_merge_commit_pull_request = أعد التأسيس ثم أنشئ إيداع دمج +pulls.merge_manually = دُمِج يدويا +pulls.merge_commit_id = معرّف إيداع الدمج +pulls.invalid_merge_option = لا يمكنك استخدام خيار الدمج هذا مع هذا الطلب. +pulls.merge_conflict_summary = رسالة خطأ +pulls.rebase_conflict_summary = رسالة خطأ +pulls.push_rejected_summary = رسالة الرفض الكاملة +pulls.status_checking = في انتظار بعض الفحوص +pulls.status_checks_failure = بعض الفحوص فشلت +pulls.status_checks_success = جميع الفحوص ناجحة +pulls.status_checks_warning = بعض الفحوص تعطي تحذيرات +pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع %[2]s` +pulls.cmd_instruction_hint = `أظهر شرح استخدام سطر الأوامر.` +pulls.cmd_instruction_checkout_title = اسحب +pulls.cmd_instruction_checkout_desc = من مستودع مشروعك، اسحب (check out) فرعا جديدا واختبر التغييرات. +pulls.cmd_instruction_merge_title = ادمج +pulls.cmd_instruction_merge_desc = ادمج التغييرات وحدّث المستودع في فورجيو. +pulls.clear_merge_message = نظّف رسالة الدمج +pulls.clear_merge_message_hint = تنظيف رسالة الدمج إنما يزيل محتوى رسالة الإيداع ويُبقى التذييلات التي يضيفها جت، مثل "Co-Authored-By …". +pulls.auto_merge_when_succeed = ادمج آليا عند نجاح جميع الفحوص +pulls.auto_merge_has_pending_schedule = %[1]s أمر بجدولة هذا الطلب للدمج عند نجاح جميع الفحوص %[2]s. +pulls.auto_merge_cancel_schedule = ألغِ الدمج الآلي +pulls.auto_merge_not_scheduled = لم يعد هذا الطلب مجدولا للدمج الآلي. +pulls.auto_merge_canceled_schedule = لقد أُلغي الدمج الآلي لهذا الطلب. +pulls.delete.title = حذف هذا الطلب؟ +pulls.delete.text = هل تريد حقا حذف هذا الطلب؟ (هذا سيحذف كل محتواه إلى الأبد. فكّر في إغلاقه بدلا من ذلك، إذا أردت الاحتفاظ به مؤرشفا) +pulls.recently_pushed_new_branches = لقد دفعت إلى الفرع %[1]s %[2]s +milestones.new = هدف جديد +milestones.closed = أُغلق %s +milestones.update_ago = حُدِّث %s +milestones.open = حالي +milestones.close = تام +milestones.new_subheader = تساعدك الأهداف في تنظيم المسائل وتتبع سيرها. +milestones.completeness = %d%% مكتمل +milestones.create = أنشئ هدفا +search.match.tooltip = لا تأت إلا بالنتائج التي تطابق كلمة البحث تماما +search.results = نتائج البحث عن "%s" في %s +settings.collaboration = المشتركون +settings.collaboration.admin = مدير +settings.collaboration.write = تحرير +settings.collaboration.read = اطلاع +settings.basic_settings = إعدادات أساسية +settings.sync_mirror = زامن الآن +settings.pull_mirror_sync_in_progress = يجذب التغييرات من المستودع البعيد %s الآن. +settings.push_mirror_sync_in_progress = يدفع التغييرات إلى المستودع البعيد %s الآن. +settings.site = موقع الويب +settings.update_settings = حدّث الإعدادات +settings.branches.update_default_branch = حدّث الفرع المبدئي +settings.branches.add_new_rule = أضف قاعدة جديدة +settings.advanced_settings = إعدادات متقدمة +settings.wiki_desc = فعّل موسوعة المستودع +settings.use_internal_wiki = استعمل الموسوعة المدمجة +settings.use_external_wiki = استعمل موسوعة خارجية +settings.external_wiki_url = رابط الموسوعة الخارجية +settings.external_wiki_url_error = رابط الموسوعة الخارجية ليس رابطا صالحا. +settings.external_wiki_url_desc = سيُوّجه الزائرين إلى الموسوعة الخارجية عندما يضغطون على زر الموسوعة. +settings.issues_desc = فعّل متتبع المسائل في المستودع +settings.use_internal_issue_tracker = استعمل متتبع المسائل المدمج +settings.use_external_issue_tracker = استعمل متتبع مسائل خارجي +settings.external_tracker_url = رابط متتبع المسائل الخارجي +settings.external_tracker_url_error = رابط متتبع المسائل الخارجي ليس رابطا صالحا. +settings.external_tracker_url_desc = سيُوّجه الزائرين إلى متتبع المسائل الخارجي عندما يضغطون على زر المسائل. +settings.tracker_url_format = صيغة رابط متتبع المسائل الخارجي +settings.tracker_url_format_error = صيغة رابط متتبع المسائل الخارجي ليست رابطا صالحا. +settings.tracker_issue_style = صيغة رقم متتبع المسائل الخارجي +settings.tracker_issue_style.numeric = أرقام +settings.tracker_issue_style.alphanumeric = أرقام وحروف رومية +settings.tracker_issue_style.regexp = تعبير نمطي +settings.tracker_issue_style.regexp_pattern = نمط التعبير +settings.enable_timetracker = فعّل تتبع الوقت +settings.allow_only_contributors_to_track_time = لا تجعل إلا المشتركين في المستودع يتتبعون الوقت +settings.pulls_desc = فعّل طلب الدمج في المستودع +settings.pulls.ignore_whitespace = تجاهل المسافات في النزاعات +settings.danger_zone = منطقة الخطر +settings.new_owner_has_same_repo = المالك الجديد لديه مستودع بالاسم نفسه؛ برجاء اختيار اسم آخر. +settings.transfer = نقل الملكية +settings.transfer_abort = ألغِ نقل الملكية +settings.transfer_abort_invalid = لا يمكنك إلغاء عملية غير موجودة لنقل ملكية مستودع. +settings.transfer.success = نجح نقل ملكية المستودع. +settings.transfer_abort_success = أُلغي بنجاح نقل ملكية المستودع إلى %s. +settings.transfer_owner = المالك الجديد +settings.transfer_perform = أتم نقل الملكية +settings.transfer_succeed = تم نقل ملكية المستودع. +pulls.auto_merge_newly_scheduled_comment = `أمر بجدولة هذا الطلب للدمج آليا عند نجاح جميع الفحوص %[1]s +pulls.auto_merge_canceled_schedule_comment = `ألغى الدمج الآلي لهذا الطلب عند نجاح جميع الفحوص %[1]s +ext_issues.desc = رابط متتبع المسائل الخارجي. +projects.edit_subheader = تساعد المشروعات في تنظيم المسائل وتتبع سيرها. +issues.tracker = متتبع الوقت +issues.start_tracking_short = شغّل المؤقت +issues.start_tracking = ابدأ تتبع الوقت +issues.start_tracking_history = `بدأ العمل %s` +issues.tracker_auto_close = سيتوقف المؤقت تلقائيا عندما تُغلق هذه المسألة +issues.stop_tracking = أوقف المؤقت +issues.stop_tracking_history = `توقف عن العمل %s` +pulls.can_auto_merge_desc = يمكن دمج هذا الطلب آليا. +pulls.num_conflicting_files_1 = "%d ملف متنازع عليه" +pulls.status_checks_error = بعض الفحوص تعطي أخطاء +pulls.status_checks_requested = مطلوب +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` +milestones.title = العنوان +milestones.desc = الوصف +milestones.edit = عدّل الهدف +milestones.edit_subheader = تساعد الأهداف في تنظيم المسائل وتتبع سيرها. +settings.options = المستودع +settings.branches.switch_default_branch = بدّل الفرع المبدئي +settings.admin_settings = إعدادات المدير +settings.trust_model.default = نموذج الثقة المبدئي +settings.trust_model.collaborator = مشترك +settings.trust_model.collaborator.long = مشترك: ثق بتوقيعات المشترِكين +wiki.file_revision = مراجعة الصفحة +wiki.wiki_page_revisions = مراجعات صفحة الموسوعة +wiki.back_to_wiki = عد إلى صفحة الموسوعة +search.type.tooltip = نوع البحث +search.fuzzy = تقريبي +search.fuzzy.tooltip = ائت بالنتائج القريبة من كلمة البحث +search.match = مطابق +settings = الإعدادات +settings.mirror_settings.push_mirror.remote_url = رابط مستودع جت البعيد +settings.releases_desc = فعّل الإصدارات في المستودع +settings.projects_desc = فعّل المشروعات في المستودع +settings.signing_settings = إعدادات التحقق من التوقيعات +settings.trust_model = نموذج الثقة في التوقيعات +settings.trust_model.committer = مودِع +settings.trust_model.collaboratorcommitter = مشترك+مودع +settings.trust_model.collaboratorcommitter.long = مشترك+مودع: ثق بتوقيعات المشتركين التي تطابق المودع +tagged_this = وسم هذا +branches = الفروع +tags = الوسوم +issues = المسائل +pulls = طلبات الدمج +project_board = المشروعات +packages = الحزم +actions = الإجراءات +released_this = أصدر هذا +commit.load_referencing_branches_and_tags = حمّل الفروع والوسوم التي تشير إلى هذا الإيداع +issues.opened_by = أنشأها %[1]s %[3]s +issues.closed_by = من %[3]s أُغلقت %[1]s +issues.opened_by_fake = أنشأها %[1]s %[2]s +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.ref_closed_from = `أغلق هذه المسألة %[4]s %[2]s` +issues.ref_reopened_from = `أعاد فتح هذه المسألة %[4]s %[2]s` +issues.reference_issue.body = المحتوى +issues.reference_link = للإشارة: %s +settings.actions_desc = فعّل الإجراءات في المستودع +pulls.push_rejected = تعذر الدمج: تم رفض الدفع. راجع خطاطيف جت لهذا المستودع. +contributors.contribution_type.additions = الإضافات +search = بحث +pulls.require_signed_wont_sign = يطلب الفرع إيداعات موقّعة، لكن لن يكون هذا الدمج موقّعًا +pulls.update_branch = تحديث الفرع بالدمج +pulls.update_branch_rebase = تحديث الفرع بإعادة التأسيس +pulls.update_branch_success = نجح تحديث الفرع +pulls.update_not_allowed = ليس مسموحا لك تحديث الفرع +contributors.contribution_type.commits = الإيداعات +contributors.contribution_type.deletions = الإزالات +unit_disabled = لقد عطّل مدير الموقع قسم المستودع هذا. +pulls.fast_forward_only_merge_pull_request = تسريع وحسب +pulls.merge_conflict = تعذر الدمج: حدث نزاع خلال الدمج. مساعدة: جرب طريقة أخرى +pulls.rebase_conflict = تعذر الدمج: حدث نزاع خلال إعادة تأسيس الإيداع: %[1]s. مساعدة: جرب طريقة أخرى +pulls.has_merged = فشل: لقد تم دمج هذا الطلب، فلا يمكنك دمجه مجددا أو تغيير الفرع الهدف. [mail] -admin.new_user.text = من فضلك اضغط هنا لإدارة المستخدم من لوحة الإدارة. +admin.new_user.text = من فضلك اضغط هنا لإدارة هذا المستخدم من لوحة الإدارة. admin.new_user.subject = مستخدم جديد: %s سجل حالاً admin.new_user.user_info = معلومات المستخدم activate_account.text_1 = أهلا يا %[1]s، شكرا لك للتسجيل في %[2]s! @@ -1145,23 +1347,23 @@ activate_email.title = %s، يرجى تأكيد عنوان بريدك الإلك release.note = ملاحظة: issue.action.close = @%[1]s أغلق #%[2]d. issue.action.merge = @%[1]s دمج #%[2]d مع %[3]s. -issue.action.force_push = %[1]s فرض الدفع بـ%[2]s من %[3]s إلى %[4]s. +issue.action.force_push = %[1]s فرض دفع %[2]s من %[3]s إلى %[4]s. repo.transfer.body = للقبول أو الرفض، زر %s أو تجاهل. team_invite.text_3 = ملاحظة: هذه الدعوة معنيه إلى %[1]s. إن لم تكن مترقب هذه الدعوة، يمكنك تجاهل هذه الرسالة. issue.action.reopen = @%[1]s أعاد فتح #%[2]d. issue.action.approve = @%[1]s وافق على هذا الطلب للسحب. -issue.action.push_1 = @%[1]s دفع بـ %[3]d إيداع إلى %[2]s -release.new.text = @%[1]s اصدر %[2]s في %[3]s +issue.action.push_1 = @%[1]s دفع %[3]d إيداع إلى %[2]s +release.new.text = @%[1]s أصدر %[2]s في %[3]s issue.action.reject = @%[1]s طلب تغييرات في هذا الطلب للسحب. release.download.zip = البرمجية (ZIP) release.download.targz = البرمجية (TAR.GZ) issue.action.review = @%[1]s علّق على هذا الطلب للسحب. issue.action.new = @%[1]s انشأ #%[2]d. issue_assigned.issue = @%[1]s عيّنك إلى مسألة %[2]s في مستودع %[3]s. -issue.action.push_n = @%[1]s دفع بـ %[3]d إيداعات إلى %[2]s -release.new.subject = صُدر %s في %s -repo.transfer.subject_to_you = %s يود نقل "%s" إليك -repo.transfer.subject_to = %s يود نقل "%s" إلى %s +issue.action.push_n = @%[1]s دفع %[3]d إيداعات إلى %[2]s +release.new.subject = أُصدر %s في %s +repo.transfer.subject_to_you = %s يود نقل ملكية "%s" إليك +repo.transfer.subject_to = %s يود نقل ملكية "%s" إلى %s issue.action.ready_for_review = @%[1]s علّم هذا الطلب للسحب كجاهز للمراجعة. issue_assigned.pull = @%[1]s عيّنك إلى طلب سحب %[2]s في مستودع %[3]s. issue.action.review_dismissed = @%[1]s أستبعد آخر مراجعة من %[2]s لهذا الطلب للسحب. @@ -1173,6 +1375,7 @@ network_error = خطأ في الشبكة invalid_csrf = طلب سيئ: رمز CSRF غير صالح occurred = حدث خطأ missing_csrf = طلب سيئ: لا يوجد رمز CSRF +server_internal = خطأ داخلي في الخادم [startpage] install = سهلة التثبيت @@ -1288,6 +1491,9 @@ sspi_auth_failed = فشلت عملية استيثاق SSPI openid_connect_desc = مسار الـOpenID المختار مجهول. اربطه مع حساب جديد هنا. openid_signin_desc = أدخل مسار الـOpenID الخاص بك. مثلاً: alice.openid.example.org أو https://openid.example.org/alice. openid_register_desc = مسار الـOpenID المختار مجهول. اربطه مع حساب جديد هنا. +remember_me = تذكر هذا الجهاز +remember_me.compromised = رمز الاحتفاظ بتسجيل الدخول لم يعد صالحا، مما قد يعني اختراق الحساب. نرجو مراجعة حسابك لرؤية أي نشاط غير مألوف. +authorization_failed_desc = فشل التفويض لأننا اكتشفنا طلبًا غير صالح. يرجى الاتصال بمشرف التطبيق الذي حاولت ترخيصه. [packages] rpm.repository.multiple_groups = هذه الحزمة متوفرة في مجموعات متعددة. @@ -1445,9 +1651,14 @@ repos.issues = المسائل users.created = أُنشئ auths.oauth2_authURL = رابط الأذن dashboard.resync_all_sshkeys = حدّث ملف '.ssh/authorized_keys' بمفاتيح SSH لفورجيو. +config.default_keep_email_private = أخفِ عناوين البريد الإلكتروني مبدئيا +config.default_allow_create_organization = اسمح بإنشاء المنظمات مبدئيا +config.enable_timetracking = فعّل تتبع الوقت +config.default_enable_timetracking = فعّل تتبع الوقت مبدئيا +config.default_allow_only_contributors_to_track_time = اسمح للمشتركين في المستودع موحدهم بتتبع الوقت [form] -username_error_no_dots = ` يُمكن أن يحتوي فقط على أرقام "0-9 "، أبجدية "A-Z" ،"a-z"، شرطة "-"، وخط أسفل "_" ولا يمكن أن تبدأ أو تنتهي بغير الأبجدية الرقمية، كما يحظر تتالي رموز غير أبجدية رقمية.` +username_error_no_dots = ` يُمكنه أن يحتوي على حروف إنجليزية وأرقام وشرطة ("-") وشرطة سفلية ("_") فقط. ويمكنه ان يبدأ وينتهي بحرف او برقم.` Password = كلمة المرور admin_cannot_delete_self = لا يمكنك أن تحذف نفسك عندما تكون مدير من فضلك ازيل امتيازاتك الإدارية اولا. enterred_invalid_repo_name = اسم المستودع الذي أدخلته خطأ. @@ -1475,8 +1686,8 @@ username_password_incorrect = اسم المستخدم أو كلمة المرور org_still_own_repo = "لدى هذه المنظمة مستودع واحد أو أكثر؛ احذفهم أو انقل ملكيتهم أولا." enterred_invalid_org_name = اسم المنظمة التي أدخلته خطأ. lang_select_error = اختر لغة من القائمة. -alpha_dash_error = ` لا يجب أن يحتوي إلا على الحروف الإنجليزية والأرقام والشرطة ('-') والشرطة السفلية ('_').` -alpha_dash_dot_error = ` لا يجب أن يحتوي إلا على الحروف الإنجليزية والأرقام والشرطة ('-') والشرطة السفلية ('_') والنقطة ('.').` +alpha_dash_error = ` لا يجب أن يحتوي إلا على الحروف الإنجليزية والأرقام والشرطة ("-") والشرطة السفلية ("_").` +alpha_dash_dot_error = ` لا يجب أن يحتوي إلا على الحروف الإنجليزية والأرقام والشرطة ("-") والشرطة السفلية ("_") والنقطة (".").` repo_name_been_taken = اسم المستودع مستعمل بالفعل. Email = البريد الإلكتروني auth_failed = فشل الاستيثاق: %v @@ -1503,7 +1714,7 @@ username_has_not_been_changed = لم يتم تغيير اسم المستخدم username_change_not_local_user = المستخدمين غير المحليين غير مسموح لهم بتغيير أسماؤهم. captcha_incorrect = الكابتشا خاطئة. AdminEmail = عنوان البريد الإلكتروني للمدير -team_no_units_error = اسمح الوصول بعلى الأقل قسم مستودع واحد. +team_no_units_error = اسمح بالوصول إلى قسم واحد على الأقل في المستودعات. must_use_public_key = المفتاح الذي قدمته هو مفتاح خاص. من فضلك لا ترفع مفتاحك الخاص في أي مكان. استخدم مفتاحك العام بدلاً من ذلك. unable_verify_ssh_key = "تعذر التحقق من مفتاح الـSSH، تأكد منه مجدداً." invalid_gpg_key = فشل تحقق مفتاح الـGPG: %s @@ -1520,6 +1731,13 @@ invalid_ssh_key = فشل تحقق مفتاح الـSSH: %s AuthName = اسم الأذن SSPISeparatorReplacement = الفاصلة openid_been_used = عنوان الـOpenID "%s" مُستخدم بالفعل. +git_ref_name_error = `يجب أن يكون اسمًا مرجعيًا جيدًا لـ Git.` +include_error = ` يجب أن يحتوي على سلسلة فرعية "%s".` +size_error = `يجب أن يكون بالحجم %s.' +glob_pattern_error = `النمط الشامل غير صالح: %s.` +CommitChoice = إختيار الإداع +regex_pattern_error = ` نمط التعبير النمطي غير صالح: %s.` +username_error = ` يُمكنه أن يحتوي على حروف إنجليزية وأرقام وشرطة ("-") وشرطة سفلية ("_") و نقطة (".") فقط. ويمكنه ان يبدأ وينتهي بحرف او برقم.` [home] filter = تصفيات أخرى @@ -1589,29 +1807,50 @@ runners.status.active = نشيط runners.status = الحالة runners.description = الوصف runners.update_runner = حدّث التغييرات -runners.name = الأسم -runners.version = الأصدار +runners.name = الاسم +runners.version = النسخة runs.status = الحالة status.unknown = "مجهول" runners.owner_type = النوع status.waiting = "ينتظر" -runners.labels = التسميات +runners.labels = التصنيفات runners.status.unspecified = مجهول runs.commit = إيداع status.success = "نجح" -runs.no_workflows.documentation = +runs.no_workflows.documentation =لمعرفة المزيد عن إجراءات فورجيو، برجاء رؤية التوثيق. runs.empty_commit_message = (رسالة إيداع فارغة) status.cancelled = "ملغي" runs.status_no_select = كل الحالات runs.scheduled = مُجدوَل -variables.edit = تعديل المتغير -variables.update.success = تم تحرير المتغير. +variables.edit = عدّل المتغير +variables.update.success = عُدِّل المتغير. variables.update.failed = فشل تعديل المتغير. variables.deletion.failed = فشل حذف المتغير. variables.creation.failed = فشل إضافة المتغير. variables.creation.success = تم إضافة المتغير "%s". variables.deletion.success = تم حذف المتغير. -variables.id_not_exist = المتغير ذو المعرّف %d ليس موجود. +variables.id_not_exist = المتغير ذو المعرّف %d ليس موجودا. +actions = الإجراءات +unit.desc = أدر الإجراءات +status.skipped = متخطى +runners = المشغلون +runners.runner_manage_panel = إدارة المشغلين +runners.new = أنشئ مشغلا جديدا +runners.new_notice = كيف تبدأ مشغلا (بالإنجليزية) +runners.id = المعرّف +runners.last_online = آخر مرة كان متصلا +runners.none = لا مشغّل متاح +runners.status.offline = غير متصل +runs.pushed_by = دفعه +runs.no_matching_online_runner_helper = لا يوجد +runners.edit_runner = عدّل المشغّل +runners.update_runner_success = نجح تحديث المشغّل +runners.update_runner_failed = تعذر تحديث المشغّل +runners.delete_runner = احذف هذا المشغّل +runners.delete_runner_success = نجح حذف المشغّل +runners.delete_runner_failed = تعذر حذف المشغّل +runners.delete_runner_header = تأكيد حذف هذا المشغّل +variables.description = تمرر المتغيرات إلى إجراءات معينة ولا يمكن قراءتها بطريقة أخرى. [modal] no = لا @@ -1636,6 +1875,7 @@ symbolic_link = رابط رمزي invalid_input_type = لا يمكنك رفع ملفات من هذا النوع. default_message = اسحب الملفات أو اضغط هنا لرفعها. file_too_big = حجم الملف ({{filesize}} مب) يتعدى الحد الأقصى ({{maxFilesize}} مب). +remove_file = أزل الملف [notification] notifications = الإشعارات @@ -1673,6 +1913,42 @@ compare_commits_general = قارن الإيداعات review_dismissed_reason = السبب: compare_branch = قارن compare_commits = قارن %d إيداع +create_repo = أنشأ المستودع %s +rename_repo = غيّر اسم المستودع %[1]s إلى %[3]s +commit_repo = دفع إلى %[3]s في %[4]s +create_issue = `أنشأ المسألة %[3]s#%[2]s` +close_issue = `أغلق المسألة %[3]s#%[2]s` +reopen_issue = `أعاد فتح المسألة %[3]s#%[2]s` +create_pull_request = `أنشأ طلب الدمج %[3]s#%[2]s` +close_pull_request = `أغلق طلب الدمج %[3]s#%[2]s` +reopen_pull_request = `أعاد فتح طلب الدمج %[3]s#%[2]s` +comment_issue = `علّق على المسألة %[3]s#%[2]s` +comment_pull = `علّق على طلب الدمج %[3]s#%[2]s` +merge_pull_request = `دمج الطلب %[3]s#%[2]s` +create_branch = أنشأ الفرع %[3]s في %[4]s +transfer_repo = نقل ملكية المستودع %s إلى %s +push_tag = دفع الوسم %[3]s إلى %[4]s +delete_tag = حذف الوسم %[2]s من %[3]s +delete_branch = حذف الفرع %[2]s من %[3]s +approve_pull_request = `قبِل %[3]s#%[2]s` +reject_pull_request = `اقترح تغييرات في %[3]s#%[2]s` +publish_release = `أصدر "%[4]s" في %[3]s` [units] -unit = وحدة \ No newline at end of file +unit = وحدة +error.no_unit_allowed_repo = ليس مسموحا لك الوصول إلى أي قسم في هذا المستودع. +error.unit_not_allowed = ليس مسموحا لك الوصول إلى هذا القسم في المستودع. + +[gpg] +default_key = موقّع بالمفتاح المبدئي +error.extract_sign = تعذّر استخراج التوقيع +error.generate_hash = تعذّر إنشاء بصمة الإيداع +error.no_committer_account = لا حساب مرتبط ببريد المودِع +error.not_signed_commit = "ليس إيداعًا موقّعًا" +error.failed_retrieval_gpg_keys = "تعذّر جلب مفتاح مرتبط بحساب المودِع" + +[graphs] +component_loading = يحمّل %s... +component_loading_failed = تعذر تحميل %s +component_loading_info = قد يحتاج هذا وقتا… +component_failed_to_load = حدث خطأ غير متوقع. diff --git a/options/locale/locale_be.ini b/options/locale/locale_be.ini new file mode 100644 index 0000000000..f9d8e738c3 --- /dev/null +++ b/options/locale/locale_be.ini @@ -0,0 +1,19 @@ + + + +[common] +dashboard = Панэль кіравання +explore = Агляд +help = Дапамога +logo = Лагатып +sign_in = Увайсці +sign_in_or = або +sign_out = Выйсці +sign_up = Зарэгістравацца +link_account = Звязаць Уліковы запіс +register = Рэгістрацыя +version = Версія +powered_by = Працуе на ℅s +page = Старонка +home = Галоўная Старонка +sign_in_with_provider = Увайсці з %s \ No newline at end of file diff --git a/options/locale/locale_bg.ini b/options/locale/locale_bg.ini new file mode 100644 index 0000000000..3eca60f551 --- /dev/null +++ b/options/locale/locale_bg.ini @@ -0,0 +1,1385 @@ + + + +[settings] +ui = Тема +delete_key = Премахване +applications = Приложения +visibility = Видимост на потребителя +location = Местоположение +password = Парола +appearance = Облик +new_password = Нова парола +oauth2_application_edit = Редактиране +repos = Хранилища +can_write_info = Писане +delete = Изтриване на акаунта +social = Социални акаунти +twofa = Двуфакторно удостоверяване (TOTP) +update_theme = Обновяване на темата +can_read_info = Четене +access_token_deletion_confirm_action = Изтриване +website = Уебсайт +cancel = Отказ +delete_token = Изтриване +uid = UID +language = Език +save_application = Запазване +privacy = Поверителност +avatar = Профилна снимка +add_key = Добавяне на ключ +account_link = Свързани акаунти +delete_email = Премахване +update_language = Обновяване на езика +organization = Организации +link_account = Свързване на акаунт +add_new_gpg_key = Добавяне на GPG ключ +manage_gpg_keys = Управление на GPG ключовете +manage_ssh_keys = Управление на SSH ключовете +old_password = Текуща парола +public_profile = Публичен профил +full_name = Пълно име +security = Сигурност +add_new_key = Добавяне на SSH ключ +account = Акаунт +update_avatar = Обновяване на профилната снимка +ssh_gpg_keys = SSH / GPG Ключове +comment_type_group_milestone = Етап +manage_emails = Управление на адресите на ел. поща +permission_read = Четене +update_password = Обновяване на паролата +biography_placeholder = Разкажете ни малко за себе си! (Можете да използвате Markdown) +orgs = Управление на организациите +continue = Продължаване +blocked_users = Блокирани потребители +emails = Адреси на ел. поща +update_profile = Обновяване на профила +profile = Профил +change_password = Промяна на паролата +retype_new_password = Потвърдете новата парола +choose_new_avatar = Изберете нова профилна снимка +delete_current_avatar = Изтриване на текущата профилна снимка +gpg_key_deletion_success = GPG ключът е премахнат. +permission_no_access = Без достъп +ssh_key_deletion_success = SSH ключът е премахнат. +comment_type_group_project = Проект +update_language_success = Езикът е обновен. +add_key_success = SSH ключът "%s" е добавен. +add_gpg_key_success = GPG ключът "%s" е добавен. +user_unblock_success = Потребителят е отблокиран успешно. +user_block_success = Потребителят е блокиран успешно. +update_profile_success = Профилът ви е обновен. +update_user_avatar_success = Профилната снимка на потребителя е обновена. +remove_oauth2_application_success = Приложението е изтрито. +email_deletion_success = Адресът на ел. поща е премахнат. +update_avatar_success = Профилната ви снимка е обновена. +change_username = Потребителското ви име е променено. +comment_type_group_assignee = Изпълнител +enable_custom_avatar = Използване на персонализирана профилна снимка +requires_activation = Изисква активиране +activated = Активиран +primary = Основен +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 на вашия акаунт. +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 = Паролата ви е обновена. Влизайте с новата си парола от сега нататък. +manage_themes = Избор на тема по подразбиране +manage_openid = Управление на OpenID адресите +primary_email = Да е основен +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" +hide_openid = Скриване от профила +key_content = Съдържание +ssh_key_deletion = Премахване на SSH ключ +gpg_key_deletion = Премахване на GPG ключ +key_name = Име на ключа +key_id = ID на ключа +show_openid = Показване в профила +visibility.public = Публична +visibility.limited = Ограничена +visibility.private = Частна +location_placeholder = Споделете приблизителното си местоположение с другите +key_signature_gpg_placeholder = Започва с "-----BEGIN PGP SIGNATURE-----" +key_signature_ssh_placeholder = Започва с "-----BEGIN SSH SIGNATURE-----" +saved_successfully = Настройките бяха запазени успешно. +no_activity = Няма скорошна дейност +theme_desc = Това ще бъде вашата тема по подразбиране в целия сайт. +keep_activity_private = Скриване на дейността от профилната страница +lookup_avatar_by_mail = Търсене на профилна снимка по адреса на ел. поща +password_incorrect = Текущата парола е неправилна. +change_username_redirect_prompt = Старото потребителско име ще се пренасочва, докато някой не го вземе. +principal_content = Съдържание +manage_ssh_principals = Управление на SSH Certificate Principals +twofa_disabled = Двуфакторното удостоверяване е изключено. +orgs_none = Не сте участник в никакви организации. +repos_none = Не притежавате никакви хранилища. +blocked_users_none = Няма блокирани потребители. +profile_desc = Контролирайте как вашият профил се показва на другите потребители. Вашият основен адрес на ел. поща ще се използва за известия, възстановяване на паролата и уеб базирани Git операции. +permission_write = Четене и Писане +twofa_disable = Изключване на двуфакторното удостоверяване +twofa_enroll = Включване на двуфакторно удостоверяване +ssh_key_name_used = Вече съществува SSH ключ със същото име във вашия акаунт. + +[packages] +container.labels.value = Стойност +alpine.repository.repositories = Хранилища +dependency.version = Версия +title = Пакети +empty = Все още няма пакети. +empty.documentation = За повече информация относно регистъра на пакетите вижте документацията. +container.labels.key = Ключ +requirements = Изисквания +details = Подробности +details.license = Лиценз +container.labels = Етикети +versions = Версии +empty.repo = Качихте ли пакет, но той не се показва тук? Отидете в настройките за пакети и го свържете към това хранилище. +keywords = Ключови думи +details.author = Автор +about = Относно този пакет +settings.delete.success = Пакетът бе изтрит. +settings.delete = Изтриване на пакета +container.details.platform = Платформа +settings.delete.error = Неуспешно изтриване на пакет. + +[tool] +hours = %d часа +now = сега +raw_seconds = секунди +1m = 1 минута +1s = 1 секунда +months = %d месеца +weeks = %d седмици +1w = 1 седмица +years = %d години +seconds = %d секунди +days = %d дни +1d = 1 ден +minutes = %d минути +1mon = 1 месец +1h = 1 час +1y = 1 година +future = бъдеще +raw_minutes = минути + +[common] +language = Език +cancel = Отказ +captcha = CAPTCHA +create_new = Създаване… +preview = Преглеждане +disabled = Изключено +licenses = Лицензи +sign_in = Вход +copy_content = Копиране на съдържанието +user_profile_and_more = Профил и Настройки… +view = Преглед +your_settings = Настройки +mirrors = Огледала +explore = Разглеждане +write = Писане +twofa = Двуфакторно удостоверяване +version = Версия +copy_success = Копирано! +help = Помощ +loading = Зареждане… +name = Име +sign_in_or = или +edit = Редактиране +concept_code_repository = Хранилище +page = Страница +forks = Разклонения +concept_user_organization = Организация +link_account = Свързване на акаунт +your_profile = Профил +sign_out = Изход +settings = Настройки +locked = Заключено +error = Грешка +dashboard = Табло +logo = Лого +toc = Съдържание +copy_url = Копиране на URL +new_mirror = Ново огледало +re_type = Потвърдете паролата +copy = Копиране +enabled = Включено +new_org = Нова организация +milestones = Етапи +rss_feed = RSS Емисия +never = Никога +new_project = Нов проект +your_starred = Отбелязани +value = Стойност +sources = Източници +notifications = Известия +repository = Хранилище +add_all = Добавяне на всичко +new_project_column = Нова колона +add = Добавяне +organization = Организация +new_migrate = Нова миграция +save = Запазване +sign_in_with_provider = Влизане с %s +ok = Добре +manage_org = Управление на организациите +new_repo = Ново хранилище +register = Регистрация +mirror = Огледало +username = Потребителско име +password = Парола +template = Шаблон +signed_in_as = Влезли сте като +sign_up = Регистриране +enable_javascript = Този сайт изисква JavaScript. +home = Начало +email = Адрес на ел. поща +issues = Задачи +retry = Повторен опит +remove = Премахване +admin_panel = Управление на сайта +account_settings = Настройки на акаунта +powered_by = Осъществено от %s +pull_requests = Заявки за сливане +collaborative = Съвместни +all = Всички +activities = Дейности +new_fork = Ново разклонение на хранилище +unpin = Откачване +pin = Закачване + +[repo] +issues.context.edit = Редактиране +stargazers = Отбелязали със звезда +template = Шаблон +issues.delete = Изтриване +repo_lang = Език +unwatch = Прекратяване на наблюдаването +settings.slack_username = Потребителско име +settings.discord_username = Потребителско име +issues.filter_sort.mostforks = Най-много разклонения +activity = Дейност +issues = Задачи +settings.update_settings = Обновяване на настройките +visibility = Видимост +settings.site = Уебсайт +watchers = Наблюдаващи +projects.new = Нов проект +issues.dependency.remove = Премахване +issues.filter_sort.moststars = Най-много звезди +desc.template = Шаблон +activity.new_issues_count_n = Нови задачи +settings.options = Хранилище +activity.overview = Обзор +fork = Разклоняване +wiki.new_page = Страница +commitstatus.error = Грешка +projects.description_placeholder = Описание +issues.filter_sort.feweststars = Най-малко звезди +code = Код +repo_desc = Описание +no_desc = Няма описание +milestones = Етапи +issues.label_description = Описание +wiki.page = Страница +settings.delete_collaborator = Премахване +settings.units.overview = Обзор +watch = Наблюдаване +issues.filter_sort.fewestforks = Най-малко разклонения +star = Отбелязване +issues.edit = Редактиране +issues.create = Създаване на задача +activity.new_issues_count_1 = Нова задача +milestones.desc = Описание +issues.new.milestone = Етап +issues.new = Нова задача +release.source_code = Програмен код +settings = Настройки +forks = Разклонения +editor.or = или +search = Търсене +issues.new_label_desc_placeholder = Описание +watch_guest_user = Влезте, за да наблюдавате това хранилище. +migrate_items_milestones = Етапи +unstar = Премахване на звездата +owner = Притежател +issues.num_comments_1 = %d коментар +issues.context.delete = Изтриване +issues.label_title = Име +issues.save = Запазване +issues.label_edit = Редактиране +issues.label_delete = Изтриване +issues.previous = Предишна +create_repo = Създаване на хранилище +template_helper = Хранилището да е шаблон +repo_name = Име на хранилището +issues.label.filter_sort.alphabetically = По азбучен ред +settings.event_repository = Хранилище +issues.label.filter_sort.reverse_alphabetically = По низходящ азбучен ред +issues.filter_sort.oldest = Най-стари +issues.filter_sort = Подреждане +issues.filter_sort.latest = Най-нови +projects.column.new = Нова колона +issues.closed_title = Затворени +issues.context.quote_reply = Цитиране в отговор +issues.context.reference_issue = Препратка в нова задача +milestones.title = Заглавие +packages = Пакети +settings.title = Заглавие +pulls.status_checks_details = Подробности +issues.context.copy_link = Копиране на връзката +projects.column.new_submit = Създаване на колона +projects = Проекти +projects.create = Създаване на проект +projects.title = Заглавие +issues.filter_sort.recentupdate = Наскоро обновени +issues.filter_sort.leastupdate = Отдавна обновени +issues.next = Следваща +issues.open_title = Отворени +pulls.made_using_agit = AGit +issues.num_comments = %d коментара +issues.filter_sort.leastcomment = Най-малко коментирани +issues.filter_sort.mostcomment = Най-много коментирани +issues.keyword_search_unavailable = В момента търсенето по ключова дума не е налично. Моля, свържете се с вашия администратор на сайта. +repo_desc_helper = Въведете кратко описание (опционално) +mirror_address = Клониране от URL +owner_helper = Някои организации може да не се показват в падащото меню поради ограничение за максимален брой хранилища. +new_repo_helper = Хранилище съдържа всички файлове на проекта, включително хронологията на ревизиите. Вече хоствате хранилище другаде? Мигрирайте хранилище. +repo_name_helper = Добрите имена на хранилища използват кратки, запомнящи се и уникални ключови думи. +migrated_from = Мигрирано от %[2]s +visibility_description = Само притежателят или участниците в организацията, ако имат права, ще могат да го видят. +projects.description = Описание (опционално) +template_select = Изберете шаблон. +visibility_helper = Хранилището да е частно +license = Лиценз +license_helper = Изберете лицензионен файл. +readme = README +migrate.clone_address = Мигриране / Клониране от URL +migrated_from_fake = Мигрирано от %[1]s +migrate.migrate = Мигриране от %s +settings.search_user_placeholder = Потърсете потребител… +issues.new_label = Нов етикет +issues.new_label_placeholder = Име на етикета +issues.label_count = %d етикета +issues.new.labels = Етикети +issues.new.clear_labels = Изчистване на етикетите +issues.create_label = Създаване на етикет +issues.filter_label_no_select = Всички етикети +milestones.filter_sort.least_issues = Най-малко задачи +milestones.filter_sort.most_issues = Най-много задачи +settings.add_webhook = Добавяне на уеб-кука +template.webhooks = Уеб-куки +issues.label_templates.info = Все още няма етикети. Създайте етикет с "Нов етикет" или използвайте предварително зададен набор от етикети: +labels = Етикети +license_helper_desc = Лицензът определя какво могат и какво не могат да правят другите с вашия код. Не сте сигурни кой е подходящ за вашия проект? Вижте Избиране на лиценз. +issues.choose.blank = По подразбиране +settings.hooks = Уеб-куки +issue_labels = Етикети за задачите +issue_labels_helper = Изберете набор от етикети за задачите. +readme_helper_desc = Това е мястото, където можете да напишете пълно описание на вашия проект. +repo_gitignore_helper = Изберете .gitignore шаблони. +auto_init = Да се инициализира хранилище (Добавя .gitignore, Лиценз и README) +template.issue_labels = Етикети за задачите +migrate_items_labels = Етикети +issues.label_templates.title = Зареждане на предварително зададен набор от етикети +issues.label_templates.helper = Изберете набор от етикети +projects.template.desc = Шаблон +projects.card_type.text_only = Само текст +projects.card_type.images_and_text = Изображения и текст +wiki = Уики +wiki.welcome = Добре дошли в Уикито. +wiki.create_first_page = Създаване на първата страница +editor.upload_file = Качване на файл +projects.column.color = Цвят +editor.cancel_lower = Отказ +pulls = Заявки за сливане +editor.upload_files_to_dir = Качване на файлове в "%s" +settings.slack_color = Цвят +issues.label_color = Цвят +create_new_repo_command = Създаване на ново хранилище в командния ред +editor.new_file = Нов файл +wiki.welcome_desc = Уикито ви позволява да пишете и споделяте документация със сътрудници. +wiki.cancel = Отказ +projects.template.desc_helper = Изберете шаблон за проекта, за да започнете +issues.cancel = Отказ +settings.transfer_owner = Нов притежател +wiki.new_page_button = Нова страница +commit_graph.color = Цвят +projects.create_success = Проектът "%s" е създаден. +projects.type.none = Няма +projects.new_subheader = Координирайте, проследявайте и обновявайте работата си на едно място, така че проектите да останат прозрачни и по график. +projects.open = Отваряне +projects.close = Затваряне +milestones.new = Нов етап +milestones.cancel = Отказ +settings.http_method = HTTP Метод +clone_helper = Нуждаете се от помощ за клониране? Посетете Помощ. +migrate_items_pullrequests = Заявки за сливане +migrate_items_wiki = Уики +quick_guide = Бързо ръководство +clone_this_repo = Клонирайте това хранилище +push_exist_repo = Изтласкване на съществуващо хранилище от командния ред +editor.cancel = Отказ +projects.column.new_title = Име +projects.column.edit_title = Име +fork_from = Разклоняване от +diff.comment.placeholder = Оставете коментар +projects.edit = Редактиране на проекта +projects.modify = Редактиране на проекта +issues.new.no_label = Няма етикет +issues.new.title_empty = Заглавието не може да бъде празно +issues.new.projects = Проекти +issues.new.clear_projects = Изчистване на проектите +issues.new.no_projects = Няма проект +issues.new.open_projects = Отворени проекти +issues.new.closed_projects = Затворени проекти +milestones.update_ago = Обновен %s +issues.filter_type.assigned_to_you = Възложени на вас +issues.filter_type.created_by_you = Създадени от вас +issues.filter_type.mentioning_you = Споменаващи ви +issues.filter_sort.farduedate = Най-далечен краен срок +issues.filter_sort.nearduedate = Най-близък краен срок +wiki.edit_page_button = Редактиране +activity.period.monthly = 1 месец +activity.period.quarterly = 3 месеца +activity.period.semiyearly = 6 месеца +activity.title.user_1 = %d потребител +activity.title.user_n = %d потребители +activity.title.prs_n = %d заявки за сливане +activity.merged_prs_count_1 = Слята заявка за сливане +activity.merged_prs_count_n = Слети заявки за сливане +activity.active_issues_count_1 = %d активна задача +activity.active_issues_count_n = %d активни задачи +activity.closed_issues_count_1 = Затворена задача +activity.closed_issues_count_n = Затворени задачи +activity.title.issues_1 = %d задача +activity.title.issues_n = %d задачи +wiki.pages = Страници +activity.git_stats_author_1 = %d автор +activity.git_stats_and_deletions = и +project_board = Проекти +wiki.save_page = Запазване на страницата +activity.git_stats_author_n = %d автори +wiki.delete_page_button = Изтриване на страницата +activity.title.prs_1 = %d заявка за сливане +activity.active_prs_count_n = %d активни заявки за сливане +activity.period.filter_label = Период: +activity.period.daily = 1 ден +activity.period.halfweekly = 3 дни +activity.period.weekly = 1 седмица +activity.period.yearly = 1 година +activity.active_prs_count_1 = %d активна заявка за сливане +wiki.page_title = Заглавие на страницата +wiki.page_content = Съдържание на страницата +wiki.filter_page = Филтриране на страница +wiki.back_to_wiki = Обратно към уики страницата +wiki.wiki_page_revisions = Ревизии на уики страницата +wiki.file_revision = Ревизия на страницата +activity.title.issues_created_by = %s създадена от %s +wiki.delete_page_notice_1 = Изтриването на уики страницата "%s" не може да бъде отменено. Продължаване? +wiki.page_name_desc = Въведете име за тази уики страница. Някои специални имена са: "Home", "_Sidebar" и "_Footer". +wiki.page_already_exists = Вече съществува уики страница със същото име. +wiki.reserved_page = Името на уики страницата "%s" е резервирано. +wiki.last_updated = Последно обновяване %s +settings.event_release = Издание +wiki.desc = Пишете и споделяйте документация със сътрудници. +wiki.default_commit_message = Напишете бележка относно това обновяване на страницата (опционално). +release.releases = Издания +wiki.last_commit_info = %s редактира тази страница %s +migrate_items_releases = Издания +release = Издание +releases = Издания +settings.desc = Настройките са мястото, където можете да управлявате настройките за хранилището +settings.external_wiki_url_error = URL на външното уики не е валиден URL. +settings.external_wiki_url = URL на външно уики +settings.confirm_wiki_delete = Изтриване на данните на уикито +settings.delete = Изтриване на това хранилище +settings.deletion_success = Хранилището е изтрито. +settings.update_settings_success = Настройките на хранилището са обновени. +settings.confirm_delete = Изтриване на хранилището +settings.add_collaborator_success = Сътрудникът е добавен. +settings.event_wiki = Уики +settings.delete_desc = Изтриването на хранилище е перманентно и не може да бъде отменено. +settings.advanced_settings = Разширени настройки +settings.delete_notices_1 = - Тази операция НЕ МОЖЕ да бъде отменена. +settings.enter_repo_name = Въведете имената на притежателя и хранилището точно както е показано: +settings.use_external_wiki = Използване на външно уики +settings.wiki_delete_desc = Изтриването на данните на уикито на хранилището е перманентно и не може да бъде отменено. +settings.wiki_delete = Изтриване на данните на уикито +settings.collaboration.admin = Администратор +settings.collaboration = Сътрудници +settings.collaboration.write = Писане +settings.collaboration.read = Четене +settings.collaboration.owner = Притежател +settings.basic_settings = Основни настройки +settings.wiki_desc = Включване на уики на хранилището +settings.use_internal_wiki = Използване на вграденото уики +settings.wiki_globally_editable = Позволяване на всеки да редактира уикито +settings.add_collaborator = Добавяне на сътрудник +repo_size = Размер на хранилището +settings.danger_zone = Опасна зона +issues.closed_by = от %[3]s бе затворена %[1]s +issues.delete_comment_confirm = Сигурни ли сте, че искате да изтриете този коментар? +issues.author_helper = Този потребител е авторът. +issues.ref_closed_from = `затвори тази задача %[4]s %[2]s` +milestones.due_date = Краен срок (опционално) +settings.wiki_delete_notices_1 = - Това ще изтрие перманентно и ще деактивира уикито на хранилището %s. +settings.wiki_deletion_success = Данните на уикито на хранилището са изтрити. +settings.collaborator_deletion = Премахване на сътрудника +settings.remove_collaborator_success = Сътрудникът е премахнат. +settings.archive.header = Архивиране на това хранилище +issues.filter_poster = Автор +issues.commented_at = `коментира %s` +settings.transfer_desc = Прехвърлете това хранилище на потребител или на организация, за които имате администраторски права. +settings.archive.button = Архивиране на хранилището +issues.role.owner_helper = Този потребител е притежателят на това хранилище. +settings.delete_notices_2 = - Тази операция ще изтрие перманентно хранилището %s, включително кода, задачите, коментарите, данните на уикито и настройките за сътрудници. +settings.admin_settings = Администраторски настройки +issues.role.owner = Притежател +settings.transfer = Прехвърляне на притежанието +issues.author = Автор +issues.closed_at = `затвори тази задача %[2]s` +settings.collaborator_deletion_desc = Премахването на сътрудник ще отнеме достъпа му до това хранилище. Продължаване? +commits.message = Съобщение +issues.due_date_not_set = Няма зададен краен срок. +issues.subscribe = Записване +issues.no_content = Няма предоставено описание. +issues.unsubscribe = Отписване +issues.new.no_milestone = Няма етап +issues.action_close = Затваряне +issues.action_label = Етикет +issues.action_milestone = Етап +issues.opened_by_fake = отворена %[1]s от %[2]s +issues.closed_by_fake = от %[2]s бе затворена %[1]s +issues.due_date = Краен срок +milestones.no_due_date = Няма краен срок +commits.author = Автор +commits.date = Дата +issues.filter_project_all = Всички проекти +issues.filter_label_select_no_label = Без етикет +issues.filter_label = Етикет +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` +projects.column.edit = Редактиране на колоната +issues.close = Затваряне на задачата +issues.ref_reopened_from = `отвори наново тази задача %[4]s %[2]s` +projects.deletion = Изтриване на проекта +projects.edit_success = Проектът "%s" е обновен. +projects.deletion_success = Проектът е изтрит. +issues.create_comment = Коментиране +unescape_control_characters = Отекраниране +editor.file_delete_success = Файлът "%s" е изтрит. +projects.type.uncategorized = Некатегоризирано +projects.column.set_default = Задаване по подразбиране +projects.column.assigned_to = Възложено на +issues.reopen_comment_issue = Коментиране и Отваряне +issues.reopen_issue = Отваряне наново +issues.close_comment_issue = Коментиране и Затваряне +milestones.filter_sort.latest_due_date = Най-късен краен срок +diff.view_file = Преглед на файла +release.deletion_success = Изданието е изтрито. +projects.column.delete = Изтриване на колоната +migrate.migrating = Мигриране от %s ... +escape_control_characters = Екраниране +issues.label_deletion_success = Етикетът е изтрит. +pulls.is_closed = Заявката за сливане е затворена. +milestones.filter_sort.earliest_due_data = Най-ранен краен срок +issues.filter_type = Тип +issues.filter_milestones = Филтриране на етап +issues.filter_assignees = Филтриране на изпълнител +issues.filter_projects = Филтриране на проект +issues.filter_labels = Филтриране на етикет +issues.new.clear_milestone = Изчистване на етапа +issues.new.open_milestone = Отворени етапи +issues.new.closed_milestone = Затворени етапи +issues.new.assignees = Възложена на +issues.new.no_items = Няма елементи +issues.new.no_assignees = Няма изпълнители +issues.new.clear_assignees = Изчистване на изпълнителите +issues.filter_project_none = Без проект +issues.filter_milestone = Етап +issues.filter_milestone_all = Всички етапи +issues.filter_milestone_open = Отворени етапи +issues.filter_milestone_none = Без етапи +issues.filter_project = Проект +issues.num_participants = %d участващи +issues.filter_assignee = Изпълнител +issues.filter_milestone_closed = Затворени етапи +issues.filter_assginee_no_select = Всички изпълнители +issues.filter_assginee_no_assignee = Без изпълнител +activity.opened_prs_count_1 = Предложена заявка за сливане +activity.opened_prs_count_n = Предложени заявки за сливане +activity.title.prs_merged_by = %s слята от %s +activity.merged_prs_label = Слята +activity.opened_prs_label = Предложена +activity.title.issues_closed_from = %s затворена от %s +activity.closed_issue_label = Затворена +activity.new_issue_label = Отворена +activity.title.releases_1 = %d издание +activity.title.releases_n = %d издания +milestones.completeness = %d%% Завършен +activity.title.prs_opened_by = %s предложена от %s +issues.action_milestone_no_select = Без етап +issues.action_assignee_no_select = Без изпълнител +milestones.edit = Редактиране на етапа +milestones.create_success = Етапът "%s" е създаден. +milestones.create = Създаване на етап +milestones.clear = Изчистване +milestones.deletion = Изтриване на етапа +milestones.edit_success = Етапът "%s" е обновен. +milestones.modify = Обновяване на етапа +milestones.deletion_success = Етапът е изтрит. +milestones.filter_sort.most_complete = Най-много завършен +milestones.filter_sort.least_complete = Най-малко завършен +activity.git_stats_file_n = %d файла +activity.git_stats_file_1 = %d файл +issues.action_assignee = Изпълнител +milestones.closed = Затворен %s +milestones.open = Отваряне +milestones.close = Затваряне +issues.label_templates.use = Използване на набор от етикети +issues.add_milestone_at = `добави това към етапа %s %s` +issues.add_label = добави етикета %s %s +issues.add_labels = добави етикетите %s %s +issues.remove_label = премахна етикета %s %s +issues.remove_labels = премахна етикетите %s %s +issues.add_remove_labels = добави етикетите %s и премахна %s %s +issues.add_project_at = `добави това към проекта %s %s` +issues.remove_project_at = `премахна това от проекта %s %s` +issues.remove_milestone_at = `премахна това от етапа %s %s` +issues.change_title_at = `промени заглавието от %s на %s %s` +template.avatar = Профилна снимка +desc.sha256 = SHA256 +editor.filename_cannot_be_empty = Името на файла не може да бъде празно. +release.title_empty = Заглавието не може да бъде празно. +settings.webhook.payload = Съдържание +settings.deploy_key_content = Съдържание +clone_in_vsc = Клониране във VS Code +use_template = Използване на този шаблон +download_file = Изтегляне на файла +editor.add_file = Добавяне на файл +editor.edit_file = Редактиране на файла +editor.this_file_locked = Файлът е заключен +editor.delete_this_file = Изтриване на файла +clone_in_vscodium = Клониране във VSCodium +download_zip = Изтегляне на ZIP +download_tar = Изтегляне на TAR.GZ +desc.public = Публично +desc.archived = Архивирано +desc.internal = Вътрешно +migrate_items_merge_requests = Заявки за Merge +migrate_items_issues = Задачи +fork_guest_user = Влезте, за да разклоните това хранилище. +actions = Действия +more_operations = Още операции +download_archive = Изтегляне на хранилището +branch = Клон +tree = Дърво +branches = Клони +tags = Маркери +tag = Маркер +filter_branch_and_tag = Филтриране на клон или маркер +symbolic_link = Символна връзка +executable_file = Изпълним файл +blame = Авторство +editor.patch = Прилагане на кръпка +editor.new_patch = Нова кръпка +signing.wont_sign.not_signed_in = Не сте влезли. +settings.tags = Маркери +release.tags = Маркери +star_guest_user = Влезте, за отбелязване на това хранилище със звезда. +download_bundle = Изтегляне на BUNDLE +desc.private = Частно +settings.branches = Клонове +editor.name_your_file = Име на файла… +issues.label.filter_sort.by_size = Най-малък размер +issues.delete.title = Изтриване на тази задача? +pulls.new = Нова заявка за сливане +pulls.no_results = Няма намерени резултати. +release.title = Заглавие на изданието +issues.unpin_issue = Откачване на задачата +issues.pin_comment = закачи това %s +issues.lock = Заключване на обсъждането +issues.lock_confirm = Заключване +issues.unlock_confirm = Отключване +issues.due_date_form_edit = Редактиране +issues.due_date_form_remove = Премахване +issues.due_date_modified = промени крайния срок от %[2]s на %[1]s %[3]s +pulls.compare_changes = Нова заявка за сливане +activity.title.releases_published_by = %s публикувано от %s +topic.manage_topics = Управление на темите +topic.done = Готово +find_file.go_to_file = Отиване към файл +reactions_more = и още %d +issues.unpin_comment = откачи това %s +lines = реда +line = ред +editor.edit_this_file = Редактиране на файла +editor.preview_changes = Преглеждане на промените +default_branch = Стандартен клон +default_branch_label = стандартен +template.topics = Теми +editor.branch_does_not_exist = Клонът "%s" не съществува в това хранилище. +editor.no_changes_to_show = Няма промени за показване. +issues.choose.get_started = Първи стъпки +issues.change_milestone_at = `промени етапа от %s на %s %s` +issues.change_project_at = `промени проекта от %s на %s %s` +issues.self_assign_at = `си само-възложи това %s` +issues.remove_assignee_at = `е премахнат като изпълнител от %s %s` +issues.remove_self_assignment = `се само-премахна като изпълнител %s` +issues.add_assignee_at = `му бе възложено това от %s %s` +pulls.merged_by = от %[3]s бе слята %[1]s +pulls.merged_by_fake = от %[2]s бе слята %[1]s +issues.label_deletion = Изтриване на етикета +issues.label_modify = Редактиране на етикета +issues.due_date_added = добави крайния срок %s %s +issues.due_date_remove = премахна крайния срок %s %s +release.new_release = Ново издание +release.tag_helper_existing = Съществуващ маркер. +release.tag_name = Име на маркера +issues.no_ref = Няма указан клон/маркер +issues.lock.reason = Причина за заключването +pulls.create = Създаване на заявка за сливане +issues.label.filter_sort.reverse_by_size = Най-голям размер +issues.unlock = Отключване на обсъждането +issues.due_date_form_add = Добавяне на краен срок +release.save_draft = Запазване на чернова +release.add_tag = Създаване само на маркер +release.publish = Публикуване на издание +file_view_source = Преглед на изходния код +diff.parent = родител +issues.unlock_comment = отключи това обсъждане %s +release.edit_subheader = Изданията ви позволяват да управлявате версиите на проекта. +branch.already_exists = Вече съществува клон на име "%s". +contributors.contribution_type.deletions = Изтривания +contributors.contribution_type.additions = Добавяния +diff.browse_source = Разглеждане на изходния код +file_view_rendered = Преглед на визуализация +issues.lock_with_reason = заключи като %s и ограничи обсъждането до сътрудници %s +milestones.new_subheader = Етапите ви помагат да управлявате задачите и да проследявате напредъка им. +release.edit = редактиране +activity.published_release_label = Публикувано +activity.navbar.contributors = Допринесли +pulls.recently_pushed_new_branches = Изтласкахте в клона %[1]s %[2]s +branch.branch_name_conflict = Името на клон "%s" е в конфликт с вече съществуващия клон "%s". +all_branches = Всички клонове +file_raw = Директно +file_history = История +file_permalink = Постоянна връзка +projects.edit_subheader = Проектите ви позволяват да управлявате задачите и да проследявате напредъка. +release.compare = Сравняване +released_this = публикува това +file_too_large = Файлът е твърде голям, за да бъде показан. +commits = Подавания +commit = Подаване +editor.commit_changes = Подаване на промените +editor.add_tmpl = Добавяне на "<име на файла>" +editor.add = Добавяне на %s +editor.delete = Изтриване на %s +editor.update = Обновяване на %s +editor.commit_message_desc = Добавете опционално разширено описание… +commit_graph.monochrome = Моно +commit.contained_in = Това подаване се съдържа в: +editor.new_branch_name_desc = Име на новия клон… +editor.propose_file_change = Предлагане на промяна на файла +editor.create_new_branch = Създаване на нов клон за това подаване и започване на заявка за сливане. +editor.create_new_branch_np = Създаване на нов клон за това подаване. +editor.filename_is_invalid = Името на файла е невалидно: "%s". +editor.commit_directly_to_this_branch = Подаване директно към клона %s. +editor.branch_already_exists = Клонът "%s" вече съществува в това хранилище. +editor.file_already_exists = Файл с име "%s" вече съществува в това хранилище. +editor.commit_empty_file_header = Подаване на празен файл +editor.commit_empty_file_text = Файлът, който сте на път да подадете, е празен. Продължаване? +editor.fail_to_update_file_summary = Съобщение за грешка: +editor.fail_to_update_file = Неуспешно обновяване/създаване на файл "%s". +editor.add_subdir = Добавяне на директория… +commits.commits = Подавания +commits.find = Търсене +commits.search_all = Всички клони +commits.search = Потърсете подавания… +commit.operations = Операции +issues.deleted_milestone = `(изтрит)` +issues.deleted_project = `(изтрит)` +milestones.edit_subheader = Етапите ви позволяват да управлявате задачите и да проследявате напредъка. +activity.navbar.recent_commits = Скорошни подавания +activity.git_stats_deletion_n = %d изтривания +activity.git_stats_addition_n = %d добавяния +release.draft = Чернова +release.detail = Подробности за изданието +releases.desc = Проследявайте версиите на проекта и изтеглянията. +release.ahead.target = в %s след това издание +release.prerelease = Предварително издание +release.target = Цел +release.new_subheader = Изданията ви позволяват да управлявате версиите на проекта. +release.tag_helper = Изберете съществуващ маркер или създайте нов маркер. +release.tag_helper_new = Нов маркер. Този маркер ще бъде създаден от целта. +release.message = Опишете това издание +release.prerelease_desc = Отбелязване като предварително издание +release.delete_release = Изтриване на изданието +release.delete_tag = Изтриване на маркера +release.edit_release = Обновяване на изданието +diff.committed_by = подадено от +release.downloads = Изтегляния +issues.sign_in_require_desc = Влезте за да се присъедините към това обсъждане. +activity.git_stats_push_to_all_branches = към всички клони. +release.deletion_tag_success = Маркерът е изтрит. +release.cancel = Отказ +release.deletion = Изтриване на изданието +release.download_count = Изтегляния: %s +release.tag_name_invalid = Името на маркера не е валидно. +diff.stats_desc = %d променени файла с %d добавяния и %d изтривания +release.tag_name_already_exist = Вече съществува издание с това име на маркер. +branch.branch_already_exists = Клонът "%s" вече съществува в това хранилище. +diff.download_patch = Изтегляне на файл-кръпка +diff.show_diff_stats = Показване на статистика +diff.commit = подаване +diff.download_diff = Изтегляне на файл-разлики +diff.whitespace_show_everything = Показване на всички промени +diff.show_split_view = Разделен изглед +diff.show_unified_view = Обединен изглед +issues.review.self.approval = Не можете да одобрите собствената си заявка за сливане. +fork_repo = Разклоняване на хранилището +pulls.merged = Слети +issues.push_commits_n = добави %d подавания %s +pulls.num_conflicting_files_n = %d конфликтни файла +issues.push_commit_1 = добави %d подаване %s +fork_visibility_helper = Видимостта на разклонено хранилище не може да бъде променена. +language_other = Други +stars_remove_warning = Това ще премахне всички звезди от това хранилище. +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.accept = Приемане на прехвърлянето +transfer.reject = Отхвърляне на прехвърлянето +archive.issue.nocomment = Това хранилище е архивирано. Не можете да коментирате в задачите. +forked_from = разклонено от +issues.delete_branch_at = `изтри клон %s %s` +pulls.has_viewed_file = Прегледано +pulls.viewed_files_label = %[1]d / %[2]d прегледани файла +pulls.approve_count_n = %d одобрения +activity.git_stats_commit_1 = %d подаване +activity.git_stats_deletion_1 = %d изтриване +diff.review.approve = Одобряване +diff.review.comment = Коментиране +issues.stop_tracking = Спиране на таймера +issues.stop_tracking_history = `спря работа %s` +issues.cancel_tracking = Отхвърляне +issues.add_time = Ръчно добавяне на време +issues.start_tracking_history = `започна работа %s` +issues.start_tracking_short = Пускане на таймера +issues.review.approve = одобри тези промени %s +pulls.tab_conversation = Обсъждане +pulls.close = Затваряне на заявката за сливане +issues.add_time_short = Добавяне на време +issues.add_time_hours = Часове +issues.add_time_minutes = Минути +issues.add_time_cancel = Отказ +pulls.tab_commits = Подавания +pulls.tab_files = Променени файлове +pulls.approve_count_1 = %d одобрение +pulls.can_auto_merge_desc = Тази заявка за сливане може да бъде слята автоматично. +pulls.num_conflicting_files_1 = %d конфликтен файл +activity.git_stats_commit_n = %d подавания +settings.event_issues = Задачи +branch.delete_head = Изтриване +branch.delete = Изтриване на клона "%s" +branch.delete_html = Изтриване на клона +tag.create_success = Маркерът "%s" е създаден. +branch.new_branch_from = Създаване на нов клон от "%s" +branch.new_branch = Създаване на нов клон +branch.confirm_rename_branch = Преименуване на клона +branch.create_from = от "%s" +settings.add_team_duplicate = Екипът вече разполага с това хранилище +settings.slack_domain = Домейн +editor.directory_is_a_file = Името на директорията "%s" вече се използва като име на файл в това хранилище. +editor.filename_is_a_directory = Името на файла "%s" вече се използва като име на директория в това хранилище. +editor.file_editing_no_longer_exists = Файлът, който се редактира, "%s", вече не съществува в това хранилище. +editor.file_deleting_no_longer_exists = Файлът, който се изтрива, "%s", вече не съществува в това хранилище. +editor.unable_to_upload_files = Неуспешно качване на файлове в "%s" с грешка: %v +settings.web_hook_name_slack = Slack +settings.web_hook_name_discord = Discord +settings.web_hook_name_telegram = Telegram +settings.web_hook_name_matrix = Matrix +settings.web_hook_name_gogs = Gogs +settings.web_hook_name_feishu_or_larksuite = Feishu / Lark Suite +settings.web_hook_name_feishu = Feishu +settings.web_hook_name_larksuite = Lark Suite +settings.web_hook_name_wechatwork = WeCom (Wechat Work) +settings.web_hook_name_packagist = Packagist +diff.file_byte_size = Размер +branch.create_success = Клонът "%s" е създаден. +branch.deletion_success = Клонът "%s" е изтрит. +branch.deletion_failed = Неуспешно изтриване на клон "%s". +branch.rename_branch_to = Преименуване от "%s" на: +settings.web_hook_name_msteams = Microsoft Teams +settings.web_hook_name_dingtalk = DingTalk +issues.error_removing_due_date = Неуспешно премахване на крайния срок. +branch.renamed = Клонът %s е преименуван на %s. +settings.teams = Екипи +settings.add_team = Добавяне на екип +settings.web_hook_name_gitea = Gitea +settings.web_hook_name_forgejo = Forgejo +release.tag_already_exist = Вече съществува маркер с това име. +branch.name = Име на клона +settings.rename_branch = Преименуване на клона +branch.restore_failed = Неуспешно възстановяване на клон "%s". +branch.download = Изтегляне на клона "%s" +branch.rename = Преименуване на клона "%s" + +[modal] +confirm = Потвърждаване +no = Не +modify = Обновяване +cancel = Отказ +yes = Да + +[editor] +buttons.list.ordered.tooltip = Добавяне на номериран списък +buttons.bold.tooltip = Добавяне на удебелен текст +buttons.quote.tooltip = Цитиран текст +buttons.code.tooltip = Добавяне на код +buttons.list.unordered.tooltip = Добавяне на неномериран списък +buttons.heading.tooltip = Добавяне на заглавка +buttons.switch_to_legacy.tooltip = Използване на стария редактор +buttons.list.task.tooltip = Добавяне на списък със задачи +buttons.enable_monospace_font = Включване на равноширок шрифт +buttons.mention.tooltip = Споменаване на потребител или екип +buttons.italic.tooltip = Добавяне на курсив текст +buttons.link.tooltip = Добавяне на връзка +buttons.disable_monospace_font = Изключване на равноширокия шрифт +buttons.ref.tooltip = Препратка към задача или заявка за сливане + +[org] +teams.write_access = Писане +settings.location = Местоположение +team_desc = Описание +teams.join = Присъединяване +org_desc = Описание +settings.update_settings = Обновяване на настройките +teams.leave = Напускане +settings = Настройки +members.remove.detail = Премахване на %[1]s от %[2]s? +settings.visibility = Видимост +settings.options = Организация +teams.leave.detail = Напускане на %s? +teams.can_create_org_repo = Създаване на хранилища +teams.settings = Настройки +settings.website = Уебсайт +code = Код +members.remove = Премахване +teams.all_repositories = Всички хранилища +teams.update_settings = Обновяване на настройките +settings.full_name = Пълно име +members.leave = Напускане +members.leave.detail = Напускане на %s? +teams.read_access = Четене +org_name_holder = Име на организацията +create_org = Създаване на организация +settings.visibility.public = Публична +settings.visibility.limited_shortname = Ограничена +settings.visibility.private_shortname = Частна +settings.permission = Разрешения +settings.visibility.limited = Ограничена (Видима само за удостоверени потребители) +settings.visibility.private = Частна (Видима само за участниците в организацията) +org_name_helper = Имената на организациите е добре да са кратки и запомнящи се. +org_full_name_holder = Пълно име на организацията +teams = Екипи +lower_members = участници +lower_repositories = хранилища +settings.repoadminchangeteam = Админ. на хранилището да може да добавя и премахва достъп за екипи +settings.email = Ел. поща за връзка +settings.delete_account = Изтриване на тази организация +settings.delete_org_title = Изтриване на организацията +settings.confirm_delete_account = Потвърждаване на изтриването +create_new_team = Нов екип +create_team = Създаване на екип +team_name = Име на екипа +team_name_helper = Имената на екипите е добре да са кратки и запомнящи се. +members = Участници +team_desc_helper = Опишете предназначението или ролята на екипа. +settings.delete = Изтриване на организацията +follow_blocked_user = Не можете да следвате тази организация, защото тя ви е блокирала. +settings.delete_prompt = Организацията ще бъде премахната завинаги. Това НЕ МОЖЕ да бъде отменено! +settings.labels_desc = Добавете етикети, които могат да се използват за задачи за всички хранилища в тази организация. +teams.none_access = Без достъп +teams.members.none = Няма членове в този екип. +repo_updated = Обновено +teams.delete_team_success = Екипът е изтрит. +teams.search_repo_placeholder = Потърсете хранилище… +teams.delete_team_title = Изтриване на екипа +teams.add_team_member = Добавяне на член на екипа +teams.read_access_helper = Членовете могат да преглеждат и клонират хранилищата на екипа. +teams.invite.description = Моля, щракнете върху бутона по-долу, за да се присъедините към екипа. +teams.invite.title = Поканени сте да се присъедините към екип %s в организация %s. +team_permission_desc = Разрешение +members.public_helper = да е скрит +teams.members = Членове на екипа +teams.delete_team = Изтриване на екипа +members.owner = Притежател +members.member_role = Роля на участника: +members.member = Участник +members.private_helper = да е видим + +[install] +admin_password = Парола +user = Потребителско име +admin_email = Адрес на ел. поща +path = Път +password = Парола +host = Хост +ssl_mode = SSL +install = Инсталация +install_btn_confirm = Инсталиране на Forgejo +app_name = Заглавие на сайта +admin_name = Потреб. име за администратор +confirm_password = Потвърдете паролата +title = Първоначална конфигурация +domain = Домейн на сървъра +require_db_desc = Forgejo изисква MySQL, PostgreSQL, MSSQL, SQLite3 или TiDB (MySQL протокол). +general_title = Общи настройки +email_title = Настройки на ел. поща +db_schema = Схема +db_title = Настройки на базата данни +db_type = Тип база данни +db_name = Име на база данни +optional_title = Опционални настройки +mailer_user = SMTP Потребителско име +mailer_password = SMTP Парола +disable_gravatar = Изключване на Gravatar +smtp_addr = SMTP Хост +smtp_port = SMTP Порт +app_name_helper = Можете да въведете името на компанията си тук. +admin_title = Настройки на администраторския акаунт +err_empty_admin_password = Администраторската парола не може да бъде празна. +docker_helper = Ако стартирате Forgejo в Docker, моля, прочетете документацията преди да промените настройки. +sqlite_helper = Път на файла за SQLite3 базата данни.
Въведете абсолютен път, ако стартирате Forgejo като service. +err_empty_admin_email = Администраторският адрес на ел. поща не може да бъде празен. +password_algorithm = Алгоритъм за хеш. на паролите +default_keep_email_private = Скриване на адресите на ел. поща по подразбиране + +[filter] +string.asc = А - Я +string.desc = Я - А + +[mail] +release.title = Заглавие: %s +issue.action.close = @%[1]s затвори #%[2]d. +register_success = Успешна регистрация +release.downloads = Изтегляния: +issue.x_mentioned_you = @%s ви спомена: +release.download.zip = Програмен код (ZIP) +release.download.targz = Програмен код (TAR.GZ) +issue.in_tree_path = В %s: +release.note = Бележка: +hi_user_x = Здравейте %s, +admin.new_user.user_info = Информация за потребителя +register_notify = Добре дошли във Forgejo +issue.action.new = @%[1]s създаде #%[2]d. +issue.action.review = @%[1]s коментира в тази заявка за сливане. +issue.action.reopen = @%[1]s отвори наново #%[2]d. +issue.action.approve = @%[1]s одобри тази заявка за сливане. + +[user] +joined_on = Присъединени на %s +user_bio = Биография +repositories = Хранилища +activity = Публична дейност +projects = Проекти +code = Код +overview = Обзор +watched = Наблюдавани хранилища +unfollow = Прекратяване на следването +block = Блокиране +settings = Потребителски настройки +starred = Отбелязани хранилища +following = Следвани +unblock = Прекратяване на блокирането +follow = Последване +followers = Последователи +block_user = Блокиране на потребителя +change_avatar = Променете профилната си снимка… +email_visibility.limited = Вашият адрес на ел. поща е видим за всички удостоверени потребители +disabled_public_activity = Този потребител е изключил публичната видимост на дейността. +email_visibility.private = Вашият адрес на ел. поща е видим само за вас и администраторите + +[home] +filter = Други филтри +show_archived = Архивирани +search_repos = Намиране на хранилище… +my_orgs = Моите организации +uname_holder = Потреб. име или Адрес на ел. поща +my_repos = Хранилища +show_both_archived_unarchived = Показване на и архивирани и неархивирани +feed_of = Емисия на "%s" +issues.in_your_repos = Във вашите хранилища +show_both_private_public = Показване на и публични и частни +show_only_private = Показване само на частни +show_private = Частни +password_holder = Парола +show_more_repos = Показване на повече хранилища… +show_only_unarchived = Показване само на неархивирани +my_mirrors = Моите огледала +show_only_archived = Показване само на архивирани +view_home = Преглед на %s +collaborative_repos = Съвместни хранилища +switch_dashboard_context = Превключване на контекста на таблото +show_only_public = Показване само на публични + +[admin] +packages.version = Версия +packages.name = Име +users.full_name = Пълно име +dashboard = Табло +repositories = Хранилища +users.name = Потребителско име +organizations = Организации +repos.forks = Разклонения +repos.stars = Звезди +config.mailer_name = Име +repos.name = Име +orgs.name = Име +users.edit = Редактиране +config.db_user = Потребителско име +config.db_name = Име +first_page = Първа +config.app_name = Заглавие на сайта +packages.repository = Хранилище +notices.type_1 = Хранилище +config.domain = Домейн на сървъра +users.max_repo_creation = Максимален брой хранилища +defaulthooks = Уеб-куки по подразбиране +auths.sspi_default_language = Потребителски език по подразбиране +hooks = Уеб-куки +systemhooks = Системни уеб-куки +orgs.new_orga = Нова организация +config.https_only = Само HTTPS +users.update_profile_success = Потребителският акаунт бе обновен. +users.new_success = Потребителският акаунт "%s" бе създаден. +users.deletion_success = Потребителският акаунт бе изтрит. +last_page = Последна +config.test_email_placeholder = Ел. поща (напр. test@example.com) +users.cannot_delete_self = Не можете да изтриете себе си +repos.owner = Притежател +auths.domain = Домейн +auths.host = Хост +auths.port = Порт +auths.type = Тип +config.ssh_config = SSH Конфигурация +monitor.stats = Статистика +monitor.queue = Опашка: %s +config = Конфигурация +config.mailer_user = Потребител +config.enable_captcha = Включване на CAPTCHA +repos.size = Размер +auths.enabled = Включено +config.git_config = Git Конфигурация +config.mailer_protocol = Протокол +users.bot = Бот +config.db_path = Път +monitor.queues = Опашки +config.server_config = Сървърна конфигурация +packages.size = Размер +settings = Админ. настройки +users = Потребителски акаунти +emails.duplicate_active = Този адрес на ел. поща вече е активен за друг потребител. +config.app_ver = Версия на Forgejo +config.custom_conf = Път на конфигурационния файл +config.git_version = Версия на Git +config.lfs_config = LFS Конфигурация +config.db_ssl_mode = SSL +users.admin = Админ +auths.name = Име +repos.issues = Задачи +packages.owner = Притежател +packages.creator = Създател +packages.type = Тип +orgs.teams = Екипи +orgs.members = Участници + +[error] +not_found = Целта не може да бъде намерена. +report_message = Ако смятате, че това е грешка на Forgejo, моля, потърсете в задачите на Codeberg или отворете нова задача, ако е необходимо. +network_error = Мрежова грешка +occurred = Възникна грешка + +[form] +UserName = Потребителско име +Email = Адрес на ел. поща +Password = Парола +RepoName = Име на хранилището +username_been_taken = Потребителското име вече е заето. +SSPIDefaultLanguage = Език по подразбиране +password_not_match = Паролите не съвпадат. +captcha_incorrect = CAPTCHA кодът е неправилен. +username_change_not_local_user = Потребители, които не са локални не могат да променят потребителското си име. +username_password_incorrect = Неправилно потребителско име или парола. +user_not_exist = Потребителят не съществува. +lang_select_error = Изберете език от списъка. +HttpsUrl = HTTPS URL +require_error = ` не може да бъде празно.` +Retype = Потвърдете паролата +url_error = `"%s" не е валиден URL.` +Content = Съдържание +team_not_exist = Екипът не съществува. +TeamName = Име на екипа +email_error = ` не е валиден адрес на ел. поща.` +email_invalid = Адресът на ел. поща е невалиден. +SSHTitle = Име на SSH ключ +repo_name_been_taken = Името на хранилището вече е използвано. +team_name_been_taken = Името на екипа вече е заето. +org_name_been_taken = Името на организацията вече е заето. + +[action] +close_issue = `затвори задача %[3]s#%[2]s` +rename_repo = преименува хранилище от %[1]s на %[3]s +review_dismissed_reason = Причина: +comment_issue = `коментира в задача %[3]s#%[2]s` +starred_repo = отбеляза със звезда %[2]s +create_repo = създаде хранилище %s +create_issue = `отвори задача %[3]s#%[2]s` +reopen_pull_request = `отвори наново заявка за сливане %[3]s#%[2]s` +create_pull_request = `създаде заявка за сливане %[3]s#%[2]s` +reopen_issue = `отвори наново задача %[3]s#%[2]s` +commit_repo = изтласка към %[3]s на %[4]s +close_pull_request = `затвори заявка за сливане %[3]s#%[2]s` +comment_pull = `коментира в заявка за сливане %[3]s#%[2]s` +merge_pull_request = `сля заявка за сливане %[3]s#%[2]s` +auto_merge_pull_request = `сля автоматично заявка за сливане %[3]s#%[2]s` +watched_repo = започна да наблюдава %[2]s +delete_tag = изтри маркер %[2]s от %[3]s +delete_branch = изтри клон %[2]s от %[3]s +create_branch = създаде клон %[3]s на %[4]s +publish_release = `публикува издание "%[4]s" на %[3]s` +push_tag = изтласка маркер %[3]s към %[4]s +approve_pull_request = `одобри %[3]s#%[2]s` +reject_pull_request = `предложи промени за %[3]s#%[2]s` + +[auth] +login_openid = OpenID +openid_connect_submit = Свързване +sign_up_successful = Акаунтът е създаден успешно. Добре дошли! +login_userpass = Влизане +forgot_password = Забравена парола? +sign_up_now = Нуждаете се от акаунт? Регистрирайте се сега. +forgot_password_title = Забравена парола +openid_register_title = Създаване на нов акаунт +account_activated = Акаунтът е активиран +social_register_helper_msg = Вече имате акаунт? Свържете го сега! +verify = Потвърждаване +create_new_account = Регистриране на акаунт +active_your_account = Активирайте акаунта си +register_helper_msg = Вече имате акаунт? Влезте сега! +reset_password = Възстановяване на акаунта +disable_register_prompt = Регистрирането е изключено. Моля, свържете се с вашия администратор на сайта. +remember_me = Запомни ме +openid_signin_desc = Въведете своя OpenID URI. Например: alice.openid.example.org или https://openid.example.org/alice. +disable_register_mail = Потвърждението по ел. поща за регистрация е изключено. +manual_activation_only = Свържете се с вашия администратор на сайта, за да завършите активирането. +must_change_password = Обновете паролата си +password_too_short = Дължината на паролата не може да бъде по-малка от %d знака. + +[aria] +footer.software = Относно софтуера +footer.links = Връзки +footer = Долен колонтитул + +[startpage] +install = Лесен за инсталиране +lightweight = Лек +license = Отворен код +install_desc = Просто стартирайте двоичния файл за вашата платформа, използвайте Docker, или го получете пакетирано. +app_desc = Безпроблемна Git услуга със самостоятелен хостинг +platform = Междуплатформен +lightweight_desc = Forgejo има ниски минимални изисквания и може да работи на икономичен Raspberry Pi. Спестете енергията на вашата машина! +platform_desc = Forgejo работи навсякъде, където Go може да се компилира: Windows, macOS, Linux, ARM, и т.н. Изберете, което харесвате! +license_desc = Вземете Forgejo! Присъединете се към нас, допринасяйки, за да направите този проект още по-добър. Не се колебайте да сътрудничите! + +[notification] +subscriptions = Абонаменти +unread = Непрочетени +no_subscriptions = Няма абонаменти +mark_as_unread = Отбелязване като непрочетено +no_read = Няма прочетени известия. +mark_as_read = Отбелязване като прочетено +notifications = Известия +read = Прочетени +watching = Наблюдавани +no_unread = Няма непрочетени известия. +mark_all_as_read = Отбелязване на всички като прочетени +pin = Закачване на известието + +[explore] +code_search_results = Резултати от търсенето на "%s" +go_to = Отиване към +repos = Хранилища +users = Потребители +search = Търсене +code = Код +organizations = Организации +code_last_indexed_at = Последно индексиран %s +repo_no_results = Няма намерени съответстващи хранилища. +user_no_results = Няма намерени съответстващи потребители. +org_no_results = Няма намерени съответстващи организации. + +[actions] +runners.version = Версия +variables = Променливи +runners.labels = Етикети +actions = Действия +variables.none = Все още няма променливи. +variables.creation.failed = Неуспешно добавяне на променлива. +variables.update.failed = Неуспешно редактиране на променлива. +variables.creation.success = Променливата "%s" е добавена. +variables.deletion.success = Променливата е премахната. +variables.edit = Редактиране на променливата +variables.deletion = Премахване на променливата +variables.update.success = Променливата е редактирана. +variables.creation = Добавяне на променлива +variables.deletion.failed = Неуспешно премахване на променлива. +runners.task_list.repository = Хранилище +runners.description = Описание + +[heatmap] +less = По-малко +number_of_contributions_in_the_last_12_months = %s приноса през последните 12 месеца +no_contributions = Няма приноси +more = Повече + +[git.filemode] +directory = Директория +symbolic_link = Символна връзка +normal_file = Обикновен файл +executable_file = Изпълним файл +changed_filemode = %[1]s → %[2]s +submodule = Подмодул + + +[dropzone] +default_message = Пуснете файлове тук или щракнете, за качване. +remove_file = Премахване на файла +file_too_big = Размерът на файла ({{filesize}} MB) надвишава максималния размер от ({{maxFilesize}} MB). +invalid_input_type = Не можете да качвате файлове от този тип. + +[graphs] +component_loading_failed = Неуспешно зареждане на %s +contributors.what = приноси +recent_commits.what = скорошни подавания +component_loading = Зареждане на %s... + +[projects] +type-1.display_name = Индивидуален проект \ No newline at end of file diff --git a/options/locale/locale_bn.ini b/options/locale/locale_bn.ini new file mode 100644 index 0000000000..8741fee98c --- /dev/null +++ b/options/locale/locale_bn.ini @@ -0,0 +1,6 @@ + + + +[common] +help = সাহায্য +dashboard = ড্যাশবোর্ড \ No newline at end of file diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 5b0caf67c9..be798000f5 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -5,6 +5,7 @@ explore=Procházet help=Nápověda logo=Logo sign_in=Přihlásit se +sign_in_with_provider=Přihlásit se pomocí %s sign_in_or=nebo sign_out=Odhlásit se sign_up=Registrovat se @@ -17,6 +18,7 @@ template=Šablona language=Jazyk notifications=Oznámení active_stopwatch=Aktivní sledování času +tracked_time_summary=Shrnutí sledovaného času na základě filtrů v seznamu problémů create_new=Vytvořit… user_profile_and_more=Profily a nastavení… signed_in_as=Přihlášen jako @@ -29,7 +31,7 @@ username=Uživatelské jméno email=E-mailová adresa password=Heslo access_token=Přístupový token -re_type=Potvrdit heslo +re_type=Potvrzení hesla captcha=CAPTCHA twofa=Dvoufaktorové ověřování twofa_scratch=Dvoufaktorový pomocný kód @@ -80,6 +82,7 @@ milestones=Milníky ok=OK cancel=Zrušit +retry=Znovu rerun=Znovu spustit rerun_all=Znovu spustit všechny úlohy save=Uložit @@ -87,14 +90,17 @@ add=Přidat add_all=Přidat vše remove=Odstranit remove_all=Odstranit vše -remove_label_str=`Odstranit položku "%s"` +remove_label_str=Odstranit položku „%s“ edit=Upravit +view=Zobrazit enabled=Povolený disabled=Zakázané +locked=Uzamčeno copy=Kopírovat copy_url=Kopírovat URL +copy_hash=Kopírovat hash copy_content=Kopírovat obsah copy_branch=Kopírovat jméno větve copy_success=Zkopírováno! @@ -107,6 +113,7 @@ loading=Načítá se… error=Chyba error404=Stránka, kterou se snažíte zobrazit, buď neexistuje, nebo nemáte oprávnění ji zobrazit. +go_back=Zpět never=Nikdy unknown=Neznámý @@ -128,11 +135,15 @@ concept_user_organization=Organizace show_timestamps=Zobrazit časové značky show_log_seconds=Zobrazit sekundy show_full_screen=Zobrazit celou obrazovku +download_logs=Stáhnout logy +confirm_delete_selected=Potvrdit odstranění všech vybraných položek? name=Název value=Hodnota sign_in_with_provider = Přihlásit se přes %s +confirm_delete_artifact = Opravdu chcete odstranit artefakt „%s“? +toggle_menu = Přepnout nabídku [aria] navbar=Navigační lišta @@ -155,7 +166,7 @@ buttons.code.tooltip=Přidat kód buttons.link.tooltip=Přidat odkaz buttons.list.unordered.tooltip=Přidat seznam odrážek buttons.list.ordered.tooltip=Přidat číslovaný seznam -buttons.list.task.tooltip=Přidat seznam úkolů +buttons.list.task.tooltip=Přidat seznam úloh buttons.mention.tooltip=Uveďte uživatele nebo tým buttons.ref.tooltip=Odkaz na issue nebo pull request buttons.switch_to_legacy.tooltip=Místo toho použít starší editor @@ -168,16 +179,19 @@ string.desc=Z – A [error] occurred=Došlo k chybě +report_message=Pokud jste si jisti, že se jedná o chybu Gitea, prosím vyhledejte problém na GitHub a v případě potřeby založte nový problém. missing_csrf=Špatný požadavek: Neexistuje CSRF token invalid_csrf=Špatný požadavek: Neplatný CSRF token not_found=Cíl nebyl nalezen. network_error=Chyba sítě +server_internal = Interní chyba serveru [startpage] app_desc=Snadno přístupný vlastní Git install=Jednoduchá na instalaci +install_desc=Jednoduše spusťte jako binární program pro vaši platformu, nasaďte jej pomocí Docker, nebo jej stáhněte jako balíček. platform=Multiplatformní -platform_desc=Forgejo běží všude, kde Go může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete! +platform_desc=Forgejo běží na všech platformách, na které může kompilovat jazyk Go: Windows, macOS, Linux, ARM, atd. Výběr je opravdu velký! lightweight=Lehká lightweight_desc=Forgejo má minimální požadavky a může běžet na Raspberry Pi. Šetřete energii vašeho stroje! license=Open Source @@ -220,6 +234,7 @@ repo_path_helper=Všechny vzdálené repozitáře Gitu budou uloženy do tohoto lfs_path=Kořenový adresář Git LFS lfs_path_helper=V tomto adresáři budou uloženy soubory, které jsou sledovány Git LFS. Pokud ponecháte prázdné, LFS zakážete. run_user=Spustit jako uživatel +run_user_helper=Zadejte uživatelské jméno, pod kterým Gitea běží v operačním systému. Pozor: tento uživatel musí mít přístup ke kořenovému adresáři repozitářů. domain=Doména serveru domain_helper=Adresa domény, nebo hostitele serveru. ssh_port=Port SSH serveru @@ -266,13 +281,13 @@ admin_password=Heslo confirm_password=Potvrdit heslo admin_email=E-mailová adresa install_btn_confirm=Nainstalovat Forgejo -test_git_failed=Chyba při testu příkazu 'git': %v -sqlite3_not_available=Tato verze Forgejo nepodporuje SQLite3. Stáhněte si oficiální binární verzi od %s (nikoli verzi „gobuild“). +test_git_failed=Chyba při testu příkazu „git“: %v +sqlite3_not_available=Tato verze Forgejo nepodporuje SQLite3. Stáhněte si oficiální binární verzi z %s (nikoli verzi „gobuild“). invalid_db_setting=Nastavení databáze je neplatné: %v -invalid_db_table=Databázová tabulka "%s" je neplatná: %v +invalid_db_table=Databázová tabulka „%s“ je neplatná: %v invalid_repo_path=Kořenový adresář repozitářů není správný: %v invalid_app_data_path=Cesta k datům aplikace je neplatná: %v -run_user_not_match=`"Run as" uživatelské jméno není aktuální uživatelské jméno: %s -> %s` +run_user_not_match=Uživatelské jméno v poli „Spustit jako“ není aktuální uživatelské jméno: %s -> %s internal_token_failed=Nepodařilo se vytvořit interní token: %v secret_key_failed=Nepodařilo se vytvořit tajný klíč: %v save_config_failed=Uložení konfigurace se nezdařilo: %v @@ -285,12 +300,17 @@ default_allow_create_organization_popup=Povolit novým uživatelským účtům v default_enable_timetracking=Povolit sledování času ve výchozím nastavení default_enable_timetracking_popup=Povolí sledování času pro nové repozitáře. no_reply_address=Skrytá e-mailová doména -no_reply_address_helper=Název domény pro uživatele se skrytou e-mailovou adresou. Příklad: Pokud je název skryté e-mailové domény nastaven na „noreply.example.org“, uživatelské jméno „joe“ bude zaznamenáno v Gitu jako „joe@noreply.example.org“. +no_reply_address_helper=Název domény pro uživatele se skrytou e-mailovou adresou. Příklad: pokud je název skryté e-mailové domény nastaven na „noreply.example.org“, uživatelské jméno „joe“ bude zaznamenáno v Gitu jako „joe@noreply.example.org“. password_algorithm=Hash algoritmus hesla invalid_password_algorithm=Neplatný algoritmus hash hesla password_algorithm_helper=Nastavte algoritmus hashování hesla. Algoritmy mají odlišné požadavky a sílu. Algoritmus argon2 je poměrně bezpečný, ale používá spoustu paměti a může být nevhodný pro malé systémy. enable_update_checker=Povolit kontrolu aktualizací enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke gitea.io. +env_config_keys=Konfigurace prostředí +env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor: +enable_update_checker_helper_forgejo = Pravidelně kontroluje nové verze Forgejo kontrolou DNS TXT záznamu na adrese release.forgejo.org. +allow_dots_in_usernames = Povolit uživatelům používat tečky ve svých uživatelských jménech. Neovlivní stávající účty. +smtp_from_invalid = Adresa v poli „Poslat e-mail jako“ je neplatná [home] uname_holder=Uživatelské jméno nebo e-mailová adresa @@ -336,7 +356,7 @@ repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře. user_no_results=Nebyly nalezeni žádní odpovídající uživatelé. org_no_results=Nebyly nalezeny žádné odpovídající organizace. code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu. -code_search_results=`Výsledky hledání pro "%s"` +code_search_results=Výsledky hledání pro „%s“ code_last_indexed_at=Naposledy indexováno %s relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty. relevant_repositories=Zobrazují se pouze relevantní repositáře, zobrazit nefiltrované výsledky. @@ -349,9 +369,11 @@ disable_register_prompt=Registrace jsou vypnuty. Prosíme, kontaktujte správce disable_register_mail=E-mailové potvrzení o registraci je zakázané. manual_activation_only=Pro dokončení aktivace kontaktujte správce webu. remember_me=Pamatovat si toto zařízení +remember_me.compromised=Přihlašovací token již není platný, což může znamenat napadení účtu. Zkontrolujte prosím svůj účet pro neobvyklé aktivity. forgot_password_title=Zapomenuté heslo forgot_password=Zapomenuté heslo? sign_up_now=Potřebujete účet? Zaregistrujte se. +sign_up_successful=Účet byl úspěšně vytvořen. Vítejte! confirmation_mail_sent_prompt=Na adresu %s byl zaslán nový potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces registrace. must_change_password=Aktualizujte své heslo allow_password_change=Vyžádat od uživatele změnu hesla (doporučeno) @@ -359,6 +381,7 @@ reset_password_mail_sent_prompt=Na adresu %s byl zaslán potvrzovací e-m active_your_account=Aktivujte si váš účet account_activated=Účet byl aktivován prohibit_login=Přihlášení zakázáno +prohibit_login_desc=Vašemu účtu je zakázáno se přihlásit, kontaktujte prosím správce webu. resent_limit_prompt=Omlouváme se, ale před chvílí jste požádal o zaslání aktivačního e-mailu. Počkejte prosím 3 minuty a pak to zkuste znovu. has_unconfirmed_mail=Zdravím, %s, máte nepotvrzenou e-mailovou adresu (%s). Pokud jste nedostali e-mail pro potvrzení nebo potřebujete zaslat nový, klikněte prosím na tlačítku níže. resend_mail=Klikněte zde pro odeslání aktivačního e-mailu @@ -366,8 +389,10 @@ email_not_associate=Tato e-mailová adresa není spojena s žádným účtem. send_reset_mail=Zaslat e-mail pro obnovení účtu reset_password=Obnovení účtu invalid_code=Tento potvrzující kód je neplatný nebo mu vypršela platnost. +invalid_code_forgot_password=Váš potvrzovací kód je neplatný nebo mu vypršela platnost. Klikněte zde pro vytvoření nového kódu. invalid_password=Vaše heslo se neshoduje s heslem, které bylo použito k vytvoření účtu. reset_password_helper=Obnovit účet +reset_password_wrong_user=Jste přihlášen/a jako %s, ale odkaz pro obnovení účtu je pro %s password_too_short=Délka hesla musí být minimálně %d znaků. non_local_account=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Forgejo. verify=Ověřit @@ -392,6 +417,7 @@ openid_connect_title=Připojení k existujícímu účtu openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde. openid_register_title=Vytvořit nový účet openid_register_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde. +openid_signin_desc=Zadejte vaši OpenID URI. Například: alice.openid.example.org nebo https://openid.example.org/alice. disable_forgot_password_mail=Obnovení účtu je zakázáno, protože není nastaven žádný e-mail. Obraťte se na správce webu. disable_forgot_password_mail_admin=Obnovení účtu je dostupné pouze po nastavení e-mailu. Pro povolení obnovy účtu nastavte prosím e-mail. email_domain_blacklisted=Nemůžete se registrovat s vaší e-mailovou adresou. @@ -401,13 +427,19 @@ authorize_application_created_by=Tuto aplikaci vytvořil %s. authorize_application_description=Pokud povolíte přístup, bude moci přistupovat a zapisovat do všech vašich informací o účtu včetně soukromých repozitářů a organizací. authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu? authorization_failed=Autorizace selhala +authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný požadavek. Kontaktujte prosím správce aplikace, kterou jste se pokoušeli autorizovat. sspi_auth_failed=SSPI autentizace selhala +password_pwned=Heslo, které jste zvolili, je na seznamu odcizených hesel, která byla dříve odhalena při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem. password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned +change_unconfirmed_email = Pokud jste při registraci zadali nesprávnou e-mailovou adresu, můžete ji změnit níže. Potvrzovací e-mail bude místo toho odeslán na novou adresu. +change_unconfirmed_email_error = Nepodařilo se změnit e-mailovou adresu: %v +change_unconfirmed_email_summary = Změna e-mailové adresy, na kterou bude odeslán aktivační e-mail. +last_admin = Nemůžete odebrat posledního administrátora. Vždy musí existovat alespoň jeden administrátor. [mail] view_it_on=Zobrazit na %s reply=nebo přímo odpovědět na tento e-mail -link_not_working_do_paste=Nefunguje? Zkuste jej zkopírovat a vložit do svého prohlížeče. +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, activate_account=Prosíme, aktivujte si váš účet @@ -416,17 +448,18 @@ activate_account.text_1=Ahoj %[1]s, děkujeme za registraci na %[2]s! activate_account.text_2=Pro aktivaci vašeho účtu do %s klikněte na následující odkaz: activate_email=Ověřte vaši e-mailovou adresu +activate_email.title=%s, prosím ověřte vaši e-mailovou adresu activate_email.text=Pro aktivaci vašeho účtu do %s klikněte na následující odkaz: register_notify=Vítejte v Forgejo register_notify.title=%[1]s vítejte v %[2]s register_notify.text_1=toto je váš potvrzovací e-mail pro %s! -register_notify.text_2=Nyní se můžete přihlásit přes uživatelské jméno: %s. -register_notify.text_3=Pokud pro vás byl vytvořen tento účet, nejprve nastavte své heslo. +register_notify.text_2=Do svého účtu se můžete přihlásit svým uživatelským jménem: %s +register_notify.text_3=Pokud vám tento účet vytvořil někdo jiný, musíte si nejprve nastavit své heslo. reset_password=Obnovit váš účet -reset_password.title=%s, požádal jste o obnovení vašeho účtu -reset_password.text=Klikněte prosím na následující odkaz pro obnovení vašeho účtu v rámci %s: +reset_password.title=Uživateli %s, obdrželi jsme žádost o obnovu vašeho účtu +reset_password.text=Pokud jste to byli vy, klikněte na následující odkaz pro obnovení vašeho účtu do %s: register_success=Registrace byla úspěšná @@ -468,6 +501,9 @@ team_invite.subject=%[1]s vás pozval/a, abyste se připojili k organizaci %[2]s team_invite.text_1=%[1]s vás pozval/a do týmu %[2]s v organizaci %[3]s. team_invite.text_2=Pro připojení k týmu klikněte na následující odkaz: team_invite.text_3=Poznámka: Tato pozvánka byla určena pro %[1]s. Pokud jste neočekávali tuto pozvánku, můžete tento e-mail ignorovat. +admin.new_user.user_info = Informace o uživateli +admin.new_user.text = Klikněte sem pro správu tohoto uživatele z administrátorského panelu. +admin.new_user.subject = Právě se zaregistroval nový uživatel %s [modal] yes=Ano @@ -500,8 +536,8 @@ SSPISeparatorReplacement=Oddělovač SSPIDefaultLanguage=Výchozí jazyk require_error=` nemůže být prázdný.` -alpha_dash_error=` by měl obsahovat pouze alfanumerické znaky, pomlčku („-“) a podtržítka („_“). ` -alpha_dash_dot_error=` by měl obsahovat pouze alfanumerické znaky, pomlčku („-“), podtržítka („_“) nebo tečku („.“). ` +alpha_dash_error=` by měl obsahovat pouze alfanumerické znaky, pomlčky („-“) a podtržítka („_“). ` +alpha_dash_dot_error=` by měl obsahovat pouze alfanumerické znaky, pomlčky („-“), podtržítka („_“) nebo tečky („.“). ` git_ref_name_error=` musí být správný název odkazu Git.` size_error=` musí být minimálně velikosti %s.` min_size_error=` musí obsahovat nejméně %s znaků.` @@ -511,6 +547,7 @@ url_error=`„%s“ není platná adresa URL.` include_error=` musí obsahovat substring „%s“.` glob_pattern_error=`zástupný vzor je neplatný: %s.` regex_pattern_error=` regex vzor je neplatný: %s.` +username_error=` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčky („-“), podtržítka („_“) a tečky („.“). Nemůže začínat nebo končit nealfanumerickými znaky. Jsou také zakázány po sobě jdoucí nealfanumerické znaky.` invalid_group_team_map_error=` mapování je neplatné: %s` unknown_error=Neznámá chyba: captcha_incorrect=CAPTCHA kód není správný. @@ -546,7 +583,7 @@ enterred_invalid_owner_name=Nové jméno vlastníka není správné. enterred_invalid_password=Zadané heslo není správné. user_not_exist=Tento uživatel neexistuje. team_not_exist=Tento tým neexistuje. -last_org_owner=Nemůžete odstranit posledního uživatele z týmu „vlastníci“. Musí existovat alespoň jeden vlastník pro organizaci. +last_org_owner=Nemůžete odebrat posledního uživatele z týmu „vlastníci“. Organizace musí obsahovat alespoň jednoho vlastníka. cannot_add_org_to_team=Organizace nemůže být přidána jako člen týmu. duplicate_invite_to_team=Uživatel byl již pozván jako člen týmu. organization_leave_success=Úspěšně jste opustili organizaci %s. @@ -555,13 +592,23 @@ invalid_ssh_key=Nelze ověřit váš SSH klíč: %s invalid_gpg_key=Nelze ověřit váš GPG klíč: %s invalid_ssh_principal=Neplatný SSH Principal certifikát: %s must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč. +unable_verify_ssh_key=Nepodařilo se ověřit klíč SSH, zkontrolujte, zda neobsahuje chyby. auth_failed=Ověření selhalo: %v +still_own_repo=Váš účet vlastní jeden nebo více repozitářů. Nejprve je odstraňte nebo přesuňte. +still_has_org=Váš účet je členem jedné nebo více organizací. Nejdříve je musíte opustit. +still_own_packages=Váš účet vlastní jeden nebo více balíčků. Nejprve je musíte odstranit. +org_still_own_repo=Organizace stále vlastní jeden nebo více repozitářů. Nejdříve je odstraňte nebo přesuňte. +org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. Nejdříve je odstraňte. target_branch_not_exist=Cílová větev neexistuje. +admin_cannot_delete_self = Nemůžete odstranit sami sebe, když jste administrátorem. Nejprve prosím odeberte svá práva administrátora. +username_error_no_dots = ` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčky („-“) a podtržítka („_“). Nemůže začínat nebo končit nealfanumerickými znaky. Jsou také zakázány po sobě jdoucí nealfanumerické znaky.` + [user] change_avatar=Změnit váš avatar… +joined_on=Přidal/a se %s repositories=Repozitáře activity=Veřejná aktivita followers=Sledující @@ -577,10 +624,20 @@ user_bio=Životopis disabled_public_activity=Tento uživatel zakázal veřejnou viditelnost aktivity. email_visibility.limited=Vaše e-mailová adresa je viditelná pro všechny ověřené uživatele email_visibility.private=Vaše e-mailová adresa je viditelná pouze pro vás a administrátory +show_on_map=Zobrazit toto místo na mapě +settings=Uživatelská nastavení -form.name_reserved=Uživatelské jméno "%s" je rezervováno. -form.name_pattern_not_allowed=Vzor "%s" není povolen v uživatelském jméně. -form.name_chars_not_allowed=Uživatelské jméno "%s" obsahuje neplatné znaky. +form.name_reserved=Uživatelské jméno „%s“ je rezervováno. +form.name_pattern_not_allowed=Vzor „%s“ není povolen v uživatelském jméně. +form.name_chars_not_allowed=Uživatelské jméno „%s“ obsahuje neplatné znaky. +block_user = Zablokovat uživatele +block_user.detail = Pokud zablokujete tohoto uživatele, budou provedeny i další akce. Například: +block_user.detail_1 = Tento uživatel vás nebude moci sledovat. +block_user.detail_2 = Tento uživatel nebude moci interagovat s vašimi repozitáři, vytvářet problémy a komentáře. +block_user.detail_3 = Tento uživatel vás nebude moci přidat jako spolupracovníka a naopak. +follow_blocked_user = Tohoto uživatele nemůžete sledovat, protože jste si jej zablokovali nebo si on zablokoval vás. +block = Zablokovat +unblock = Odblokovat [settings] profile=Profil @@ -598,9 +655,13 @@ delete=Smazat účet twofa=Dvoufaktorové ověřování account_link=Propojené účty organization=Organizace +uid=UID webauthn=Bezpečnostní klíče public_profile=Veřejný profil +biography_placeholder=Řekněte nám něco o sobě! (Můžete použít Markdown) +location_placeholder=Sdílejte svou přibližnou polohu s ostatními +profile_desc=Nastavte, jak bude váš profil zobrazen ostatním uživatelům. Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla a operace Git. password_username_disabled=Externí uživatelé nemohou měnit svoje uživatelské jméno. Kontaktujte prosím svého administrátora pro více detailů. full_name=Celé jméno website=Web @@ -608,15 +669,20 @@ location=Místo update_theme=Aktualizovat motiv vzhledu update_profile=Aktualizovat profil update_language=Aktualizovat jazyk -update_language_not_found=Jazyk "%s" není k dispozici. +update_language_not_found=Jazyk „%s“ není k dispozici. update_language_success=Jazyk byl aktualizován. update_profile_success=Váš profil byl aktualizován. change_username=Vaše uživatelské jméno bylo změněno. +change_username_prompt=Poznámka: Změna uživatelského jména také změní URL vašeho účtu. +change_username_redirect_prompt=Staré uživatelské jméno bude přesměrováváno, dokud nebude znovu obsazeno. continue=Pokračovat cancel=Zrušit language=Jazyk ui=Motiv vzhledu hidden_comment_types=Skryté typy komentářů +hidden_comment_types_description=Zde zkontrolované typy komentářů nebudou zobrazeny na stránkách problémů. Zaškrtnutí „Štítek“ například odstraní všechny komentáře „ přidal/odstranil
%s 中的团队 %s。 teams.invite.by=邀请人 %s teams.invite.description=请点击下面的按钮加入团队。 +follow_blocked_user = 你无法关注此组织,因为此组织已屏蔽你。 [admin] dashboard=管理面板 +self_check=自我检查 identity_access=身份及认证 users=帐户管理 organizations=组织管理 @@ -2772,6 +2831,7 @@ dashboard.delete_missing_repos=删除所有丢失 Git 文件的仓库 dashboard.delete_missing_repos.started=删除所有丢失 Git 文件的仓库任务已启动。 dashboard.delete_generated_repository_avatars=删除生成的仓库头像 dashboard.sync_repo_branches=将缺少的分支从 git 数据同步到数据库 +dashboard.sync_repo_tags=从 git 数据同步标签到数据库 dashboard.update_mirrors=更新镜像仓库 dashboard.repo_health_check=健康检查所有仓库 dashboard.check_repo_stats=检查所有仓库统计 @@ -2826,6 +2886,7 @@ dashboard.stop_endless_tasks=停止永不停止的任务 dashboard.cancel_abandoned_jobs=取消丢弃的任务 dashboard.start_schedule_tasks=开始调度任务 dashboard.sync_branch.started=分支同步已开始 +dashboard.sync_tag.started=标签同步已开始 dashboard.rebuild_issue_indexer=重建工单索引 users.user_manage_panel=用户帐户管理 @@ -2868,7 +2929,7 @@ users.cannot_delete_self=你不能删除自己 users.still_own_repo=此用户仍然拥有一个或多个仓库。必须首先删除或转让这些仓库。 users.still_has_org=此用户是组织的成员。必须先从组织中删除用户。 users.purge=清理用户 -users.purge_help=强制删除用户和用户拥有的任何仓库、组织和软件包。所有评论也将被删除。 +users.purge_help=强制删除用户和用户拥有的任何仓库、组织和软件包。所有评论和工单也将被删除。 users.still_own_packages=此用户仍然拥有一个或多个软件包,请先删除这些软件包。 users.deletion_success=用户帐户已被删除。 users.reset_2fa=重置两步验证 @@ -3251,6 +3312,20 @@ notices.type_2=任务 notices.desc=提示描述 notices.op=操作 notices.delete_success=系统通知已被删除。 +dashboard.sync_repo_tags = 将 git 数据中的标签同步到数据库 +dashboard.sync_tag.started = 标签同步开始 +self_check = 自检 +self_check.no_problem_found = 未找到问题。 +self_check.database_collation_mismatch = 期望数据库使用排序规则:%s +self_check.database_collation_case_insensitive = 数据库正在使用 %s 排序规则,这是一种不敏感的排序规则。 尽管 Forgejo 可以使用它,但在极少数情况下可能无法按照预期工作。 +self_check.database_inconsistent_collation_columns = 数据库正在使用 %s 排序规则,但与这些列使用的排序规则不匹配。 这可能会导致一些意想不到的问题。 +self_check.database_fix_mysql = 对于 MySQL/MariaDB 用户,您可以使用 "gitea doctor convert" 命令来修复排序规则问题,也可以通过SQL命令 "ALTER ... COLLATE ..." 来手动修复问题。 +self_check.database_fix_mssql = 对于 MSSQL 用户,目前您只能通过SQL命令 "ALTER ... COLLATE ..." 来手动修复问题。 + +self_check.no_problem_found=尚未发现问题。 +self_check.database_collation_mismatch=期望数据库使用的校验方式:%s +self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。 +self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。 [action] create_repo=创建了仓库 %s @@ -3436,6 +3511,9 @@ rpm.registry=从命令行设置此注册中心: rpm.distros.redhat=在基于 RedHat 的发行版 rpm.distros.suse=在基于 SUSE 的发行版 rpm.install=要安装包,请运行以下命令: +rpm.repository=仓库信息 +rpm.repository.architectures=架构 +rpm.repository.multiple_groups=此软件包可在多个组中使用。 rubygems.install=要使用 gem 安装软件包,请运行以下命令: rubygems.install2=或将它添加到 Gemfile: rubygems.dependencies.runtime=运行时依赖 @@ -3489,6 +3567,9 @@ owner.settings.cleanuprules.success.delete=清理规则已删除。 owner.settings.chef.title=Chef 注册中心 owner.settings.chef.keypair=生成密钥对 owner.settings.chef.keypair.description=需要密钥对才能向 Chef 注册中心进行身份验证。如果您之前已经生成过密钥对,生成新的密钥对将丢弃旧的密钥对。 +rpm.repository = 仓库信息 +rpm.repository.architectures = 架构 +rpm.repository.multiple_groups = 该软件包可在多个组中使用。 [secrets] secrets=密钥 @@ -3568,8 +3649,8 @@ runs.actors_no_select=所有操作者 runs.status_no_select=所有状态 runs.no_results=没有匹配的结果。 runs.no_workflows=目前还没有工作流。 -runs.no_workflows.quick_start=不知道如何启动Gitea Action?请参阅 快速启动指南 -runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 文档。 +runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗?请查看 快速启动指南。 +runs.no_workflows.documentation=关于Gitea Actions的更多信息,请参阅 文档。 runs.no_runs=工作流尚未运行过。 runs.empty_commit_message=(空白的提交消息) @@ -3588,7 +3669,7 @@ variables.none=目前还没有变量。 variables.deletion=删除变量 variables.deletion.description=删除变量是永久性的,无法撤消。继续吗? variables.description=变量将被传给特定的 Actions,其它情况将不能读取 -variables.id_not_exist=ID %d 变量不存在。 +variables.id_not_exist=ID为 %d 的变量不存在。 variables.edit=编辑变量 variables.deletion.failed=删除变量失败。 variables.deletion.success=变量已被删除。 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index b00c6e6b3d..289a3e7b5d 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -196,6 +196,7 @@ auth_failed=授權驗證失敗:%v target_branch_not_exist=目標分支不存在 + [user] repositories=儲存庫列表 activity=公開活動 @@ -538,6 +539,8 @@ activity.merged_prs_label=已合併 activity.closed_issue_label=已關閉 activity.new_issues_count_1=建立問題 +contributors.contribution_type.commits=提交歷史 + search=搜尋 settings=儲存庫設定 @@ -640,6 +643,8 @@ release.downloads=下載附件 +[graphs] + [org] org_name_holder=組織名稱 org_full_name_holder=組織全名 @@ -916,6 +921,7 @@ notices.desc=描述 notices.op=操作 notices.delete_success=已刪除系統提示。 + [action] create_repo=建立了儲存庫 %s rename_repo=重新命名儲存庫 %[1]s%[3]s diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 604f727292..e49be30691 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -556,6 +556,7 @@ org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除 target_branch_not_exist=目標分支不存在 + [user] change_avatar=更改大頭貼... repositories=儲存庫 @@ -641,7 +642,7 @@ uploaded_avatar_not_a_image=上傳的檔案不是圖片 update_avatar_success=您的大頭貼已更新 update_user_avatar_success=已更新使用者的大頭貼。 -change_password=更新密碼 +update_password=更新密碼 old_password=目前的密碼 new_password=新的密碼 retype_new_password=確認新密碼 @@ -1777,6 +1778,8 @@ activity.git_stats_and_deletions=和 activity.git_stats_deletion_1=刪除 %d 行 activity.git_stats_deletion_n=刪除 %d 行 +contributors.contribution_type.commits=提交歷史 + search=搜尋 search.search_repo=搜尋儲存庫 search.type.tooltip=搜尋類型 @@ -2323,6 +2326,8 @@ error.csv.too_large=無法渲染此檔案,因為它太大了。 error.csv.unexpected=無法渲染此檔案,因為它包含了未預期的字元,於第 %d 行第 %d 列。 error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d 行的欄位數量有誤。 +[graphs] + [org] org_name_holder=組織名稱 org_full_name_holder=組織全名 @@ -2935,6 +2940,7 @@ notices.desc=描述 notices.op=操作 notices.delete_success=已刪除系統提示。 + [action] create_repo=建立了儲存庫 %s rename_repo=重新命名儲存庫 %[1]s%[3]s @@ -3111,6 +3117,8 @@ pypi.requires=需要 Python pypi.install=執行下列命令以使用 pip 安裝此套件: rpm.registry=透過下列命令設定此註冊中心: rpm.install=執行下列命令安裝此套件: +rpm.repository=儲存庫資訊 +rpm.repository.architectures=架構 rubygems.install=執行下列命令以使用 gem 安裝此套件: rubygems.install2=或將它加到 Gemfile: rubygems.dependencies.runtime=執行階段相依性 diff --git a/package-lock.json b/package-lock.json index 3572749b89..c833670f72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "gitea", + "name": "forgejo", "lockfileVersion": 3, "requires": true, "packages": { @@ -18,59 +18,69 @@ "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi_up": "6.0.2", - "asciinema-player": "3.6.3", + "asciinema-player": "3.6.4", + "chart.js": "4.4.1", + "chartjs-adapter-dayjs-4": "1.0.4", + "chartjs-plugin-zoom": "2.0.1", "clippie": "4.0.6", "css-loader": "6.10.0", + "css-variables-parser": "1.0.1", + "dayjs": "1.11.10", "dropzone": "6.0.0-beta.2", "easymde": "2.18.0", "esbuild-loader": "4.0.3", "escape-goat": "4.0.0", "fast-glob": "3.3.2", "htmx.org": "1.9.10", + "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.9", "license-checker-webpack-plugin": "0.2.1", - "lightningcss-loader": "2.1.0", - "mermaid": "10.7.0", + "mermaid": "10.8.0", "mini-css-extract-plugin": "2.8.0", "minimatch": "9.0.3", - "monaco-editor": "0.45.0", + "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", - "pdfobject": "2.2.12", + "pdfobject": "2.3.0", + "postcss": "8.4.35", + "postcss-loader": "8.1.0", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", - "swagger-ui-dist": "5.11.2", + "swagger-ui-dist": "5.11.6", + "tailwindcss": "3.4.1", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", - "vue": "3.4.15", + "vue": "3.4.19", "vue-bar-graph": "2.0.0", + "vue-chartjs": "5.3.0", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.90.1", + "webpack": "5.90.2", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.1.0", - "@playwright/test": "1.41.1", + "@playwright/test": "1.41.2", "@stoplight/spectral-cli": "6.11.0", - "@stylistic/eslint-plugin-js": "1.5.4", + "@stylistic/eslint-plugin-js": "1.6.2", "@stylistic/stylelint-plugin": "2.0.0", - "@vitejs/plugin-vue": "5.0.3", + "@vitejs/plugin-vue": "5.0.4", "eslint": "8.56.0", "eslint-plugin-array-func": "4.0.0", + "eslint-plugin-github": "4.10.1", "eslint-plugin-i": "2.29.1", "eslint-plugin-jquery": "1.5.1", "eslint-plugin-no-jquery": "2.7.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-regexp": "2.2.0", - "eslint-plugin-sonarjs": "0.23.0", - "eslint-plugin-unicorn": "50.0.1", - "eslint-plugin-vitest": "0.3.21", + "eslint-plugin-sonarjs": "0.24.0", + "eslint-plugin-unicorn": "51.0.1", + "eslint-plugin-vitest": "0.3.22", "eslint-plugin-vitest-globals": "1.4.0", "eslint-plugin-vue": "9.21.1", "eslint-plugin-vue-scoped-css": "2.7.2", @@ -82,8 +92,8 @@ "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.4", "svgo": "3.2.0", - "updates": "15.1.1", - "vite-string-plugin": "1.1.3", + "updates": "15.1.2", + "vite-string-plugin": "1.1.5", "vitest": "1.2.2" }, "engines": { @@ -99,6 +109,17 @@ "node": ">=0.10.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@asyncapi/specs": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.3.1.tgz", @@ -112,7 +133,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" @@ -125,7 +145,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -137,7 +156,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -151,7 +169,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -159,14 +176,12 @@ "node_modules/@babel/code-frame/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -175,7 +190,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -184,7 +198,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -196,7 +209,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -205,7 +217,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -219,7 +230,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -231,7 +241,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -245,7 +254,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -253,14 +261,12 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -269,7 +275,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -277,14 +282,12 @@ "node_modules/@babel/highlight/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -1017,6 +1020,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@github/browserslist-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@github/browserslist-config/-/browserslist-config-1.0.0.tgz", + "integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==", + "dev": true + }, "node_modules/@github/combobox-nav": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.1.tgz", @@ -1099,7 +1108,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1116,7 +1124,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -1128,7 +1135,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -1139,14 +1145,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1163,7 +1167,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1178,7 +1181,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1217,9 +1219,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } @@ -1279,6 +1281,11 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "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", @@ -1364,19 +1371,30 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@playwright/test": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", - "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", "dev": true, "dependencies": { - "playwright": "1.41.1" + "playwright": "1.41.2" }, "bin": { "playwright": "cli.js" @@ -1447,9 +1465,9 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.11.0.tgz", + "integrity": "sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==", "cpu": [ "arm" ], @@ -1460,9 +1478,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.11.0.tgz", + "integrity": "sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==", "cpu": [ "arm64" ], @@ -1473,9 +1491,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.11.0.tgz", + "integrity": "sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==", "cpu": [ "arm64" ], @@ -1486,9 +1504,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.11.0.tgz", + "integrity": "sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==", "cpu": [ "x64" ], @@ -1499,9 +1517,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.11.0.tgz", + "integrity": "sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==", "cpu": [ "arm" ], @@ -1512,9 +1530,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.11.0.tgz", + "integrity": "sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==", "cpu": [ "arm64" ], @@ -1525,9 +1543,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.11.0.tgz", + "integrity": "sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==", "cpu": [ "arm64" ], @@ -1538,9 +1556,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.11.0.tgz", + "integrity": "sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==", "cpu": [ "riscv64" ], @@ -1551,9 +1569,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.11.0.tgz", + "integrity": "sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==", "cpu": [ "x64" ], @@ -1564,9 +1582,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.11.0.tgz", + "integrity": "sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==", "cpu": [ "x64" ], @@ -1577,9 +1595,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.11.0.tgz", + "integrity": "sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==", "cpu": [ "arm64" ], @@ -1590,9 +1608,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.11.0.tgz", + "integrity": "sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==", "cpu": [ "ia32" ], @@ -1603,9 +1621,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.11.0.tgz", + "integrity": "sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==", "cpu": [ "x64" ], @@ -2073,11 +2091,12 @@ "dev": true }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.4.tgz", - "integrity": "sha512-3ctWb3NvJNV1MsrZN91cYp2EGInLPSoZKphXIbIRx/zjZxKwLDr9z4LMOWtqjq14li/OgqUUcMq5pj8fgbLoTw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.2.tgz", + "integrity": "sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==", "dev": true, "dependencies": { + "@types/eslint": "^8.56.2", "acorn": "^8.11.3", "escape-string-regexp": "^4.0.0", "eslint-visitor-keys": "^3.4.3", @@ -2197,6 +2216,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/marked": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", @@ -2216,9 +2241,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.11.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", - "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -2236,9 +2261,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, "node_modules/@types/tern": { @@ -2260,14 +2285,77 @@ "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", "dev": true }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", - "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2277,10 +2365,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", - "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2291,13 +2406,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", - "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2319,17 +2434,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.20.0.tgz", - "integrity": "sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -2344,12 +2459,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", - "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2367,9 +2482,9 @@ "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz", - "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", + "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -2449,9 +2564,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/magic-string": { - "version": "0.30.6", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz", - "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -2503,46 +2618,46 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz", - "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", + "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/shared": "3.4.15", + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.19", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz", - "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", + "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", "dependencies": { - "@vue/compiler-core": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-core": "3.4.19", + "@vue/shared": "3.4.19" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz", - "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz", + "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==", "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/compiler-core": "3.4.15", - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15", + "@babel/parser": "^7.23.9", + "@vue/compiler-core": "3.4.19", + "@vue/compiler-dom": "3.4.19", + "@vue/compiler-ssr": "3.4.19", + "@vue/shared": "3.4.19", "estree-walker": "^2.0.2", - "magic-string": "^0.30.5", + "magic-string": "^0.30.6", "postcss": "^8.4.33", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-sfc/node_modules/magic-string": { - "version": "0.30.6", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz", - "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -2551,57 +2666,57 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz", - "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz", + "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==", "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-dom": "3.4.19", + "@vue/shared": "3.4.19" } }, "node_modules/@vue/reactivity": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz", - "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz", + "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==", "dependencies": { - "@vue/shared": "3.4.15" + "@vue/shared": "3.4.19" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz", - "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz", + "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==", "dependencies": { - "@vue/reactivity": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/reactivity": "3.4.19", + "@vue/shared": "3.4.19" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz", - "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz", + "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==", "dependencies": { - "@vue/runtime-core": "3.4.15", - "@vue/shared": "3.4.15", + "@vue/runtime-core": "3.4.19", + "@vue/shared": "3.4.19", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz", - "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz", + "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==", "dependencies": { - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-ssr": "3.4.19", + "@vue/shared": "3.4.19" }, "peerDependencies": { - "vue": "3.4.15" + "vue": "3.4.19" } }, "node_modules/@vue/shared": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", - "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==" + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", + "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -2960,19 +3075,53 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2986,6 +3135,25 @@ "node": ">=0.10.0" } }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2995,18 +3163,93 @@ "node": ">=8" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "node_modules/array.prototype.filter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", + "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz", + "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -3026,9 +3269,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.3.tgz", - "integrity": "sha512-62aDgLpbuduhmpFfNgPOzf6fOluACLsftVnjpWJjUXX6dqhqTckFqWoJ+ayA0XjSlZ9l9wXTcJqRqvAAIpMblg==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.4.tgz", + "integrity": "sha512-yyMHTjoDuz82/BYPrc3J5GjOtlNI5t2VHTZWss8BmRcY/6nXv+Vilip+XzwIyRBa3/2SSn9FJIEg8bJXBc9o4w==", "dependencies": { "@babel/runtime": "^7.21.0", "solid-js": "^1.3.0" @@ -3055,6 +3298,12 @@ "node": ">=4" } }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3073,6 +3322,15 @@ "astring": "bin/astring" } }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3092,9 +3350,9 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", "dev": true, "engines": { "node": ">= 0.4" @@ -3103,6 +3361,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3135,6 +3411,14 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3161,9 +3445,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -3179,8 +3463,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001580", - "electron-to-chromium": "^1.4.648", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -3247,14 +3531,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3264,15 +3553,22 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001581", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", - "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "version": "1.0.30001587", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", + "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", "funding": [ { "type": "opencollective", @@ -3330,6 +3626,40 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, + "node_modules/chartjs-adapter-dayjs-4": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz", + "integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "chart.js": ">=4.0.1", + "dayjs": "^1.9.7" + } + }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz", + "integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==", + "dependencies": { + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -3342,6 +3672,40 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -3529,12 +3893,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/core-js-compat": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", - "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", + "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "dev": true, "dependencies": { - "browserslist": "^4.22.2" + "browserslist": "^4.22.3" }, "funding": { "type": "opencollective", @@ -3553,7 +3917,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -3671,6 +4034,35 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css-variables-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-variables-parser/-/css-variables-parser-1.0.1.tgz", + "integrity": "sha512-GWaqrwGtAWVr/yjjE17iyvbcy+W3voe0vko1/xLCwFeYd3kTLstzUdVH+g5TTXejrtlsb1FS4L9rP6PmeTa8wQ==", + "dependencies": { + "postcss": "^7.0.36" + } + }, + "node_modules/css-variables-parser/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/css-variables-parser/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -3767,30 +4159,6 @@ "cytoscape": "^3.2.0" } }, - "node_modules/cytoscape-fcose": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", - "dependencies": { - "cose-base": "^2.2.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/cose-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", - "dependencies": { - "layout-base": "^2.0.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/layout-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" - }, "node_modules/d3": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", @@ -4213,6 +4581,12 @@ "lodash-es": "^4.17.21" } }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, "node_modules/data-uri-to-buffer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", @@ -4308,17 +4682,20 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -4372,21 +4749,15 @@ "node": ">=6" } }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "engines": { "node": ">=0.3.1" } @@ -4412,6 +4783,11 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4496,8 +4872,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/easymde": { "version": "2.18.0", @@ -4512,9 +4887,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.653", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.653.tgz", - "integrity": "sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA==" + "version": "1.4.671", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.671.tgz", + "integrity": "sha512-UUlE+/rWbydmp+FW8xlnnTA5WNA0ZZd2XL8CuMS72rh+k4y1f8+z6yk3UQhEwqHQWj6IBdL78DwWOdGMvYfQyA==" }, "node_modules/elkjs": { "version": "0.9.1", @@ -4561,15 +4936,14 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "engines": { "node": ">=6" } }, "node_modules/envinfo": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", - "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", "bin": { "envinfo": "dist/cli.js" }, @@ -4581,56 +4955,57 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", + "integrity": "sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.2", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", + "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.8", "string.prototype.trimend": "^1.0.7", "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", + "typed-array-buffer": "^1.0.1", "typed-array-byte-length": "^1.0.0", "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4640,18 +5015,18 @@ } }, "node_modules/es-aggregate-error": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.11.tgz", - "integrity": "sha512-DCiZiNlMlbvofET/cE55My387NiLvuGToBEZDdK9U2G3svDCjL8WOgO5Il6lO83nQ8qmag/R9nArdpaFQ/m3lA==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.12.tgz", + "integrity": "sha512-j0PupcmELoVbYS2NNrsn5zcLLEsryQwP02x8fRawh7c2eEaPHwJFAxltZsqV7HJjsF57+SMpYyVRWgbVLfOagg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.0", + "define-data-property": "^1.1.1", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.1.0", + "function-bind": "^1.1.2", "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", + "has-property-descriptors": "^1.0.1", "set-function-name": "^2.0.1" }, "engines": { @@ -4661,6 +5036,59 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz", + "integrity": "sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.4", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.2", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", @@ -4680,6 +5108,15 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -4752,9 +5189,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -4852,6 +5289,18 @@ "eslint": ">=6.0.0" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.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", @@ -4910,6 +5359,92 @@ "eslint": ">=8.40.0" } }, + "node_modules/eslint-plugin-escompat": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-escompat/-/eslint-plugin-escompat-3.4.0.tgz", + "integrity": "sha512-ufTPv8cwCxTNoLnTZBFTQ5SxU2w7E7wiMIS7PSxsgP1eAxFjtSaoZ80LRn64hI8iYziE6kJG6gX/ZCJVxh48Bg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.0" + }, + "peerDependencies": { + "eslint": ">=5.14.1" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-filenames": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz", + "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==", + "dev": true, + "dependencies": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.upperfirst": "4.3.1" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/eslint-plugin-github": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.1.tgz", + "integrity": "sha512-1AqQBockOM+m0ZUpwfjWtX0lWdX5cRi/hwJnSNvXoOmz/Hh+ULH6QFz6ENWueTWjoWpgPv0af3bj+snps6o4og==", + "dev": true, + "dependencies": { + "@github/browserslist-config": "^1.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "aria-query": "^5.3.0", + "eslint-config-prettier": ">=8.0.0", + "eslint-plugin-escompat": "^3.3.3", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-filenames": "^1.3.2", + "eslint-plugin-i18n-text": "^1.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-no-only-tests": "^3.0.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-rule-documentation": ">=1.0.0", + "jsx-ast-utils": "^3.3.2", + "prettier": "^3.0.0", + "svg-element-attributes": "^1.3.1" + }, + "bin": { + "eslint-ignore-errors": "bin/eslint-ignore-errors.js" + }, + "peerDependencies": { + "eslint": "^8.0.1" + } + }, "node_modules/eslint-plugin-i": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i/-/eslint-plugin-i-2.29.1.tgz", @@ -4957,6 +5492,98 @@ "node": "*" } }, + "node_modules/eslint-plugin-i18n-text": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", + "integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==", + "dev": true, + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-jquery": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jquery/-/eslint-plugin-jquery-1.5.1.tgz", @@ -4966,6 +5593,64 @@ "eslint": ">=5.4.0" } }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-no-jquery": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz", @@ -4975,6 +5660,15 @@ "eslint": ">=2.3.0" } }, + "node_modules/eslint-plugin-no-only-tests": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.1.0.tgz", + "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==", + "dev": true, + "engines": { + "node": ">=5.0.0" + } + }, "node_modules/eslint-plugin-no-use-extend-native": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz", @@ -4990,6 +5684,36 @@ "node": ">=6.0.0" } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-regexp": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.2.0.tgz", @@ -5012,21 +5736,21 @@ } }, "node_modules/eslint-plugin-sonarjs": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.23.0.tgz", - "integrity": "sha512-z44T3PBf9W7qQ/aR+NmofOTyg6HLhSEZOPD4zhStqBpLoMp8GYhFksuUBnCxbnf1nfISpKBVkQhiBLFI/F4Wlg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.24.0.tgz", + "integrity": "sha512-87zp50mbbNrSTuoEOebdRQBPa0mdejA5UEjyuScyIw8hEpEjfWP89Qhkq5xVZfVyVSRQKZc9alVm7yRKQvvUmg==", "dev": true, "engines": { - "node": ">=14" + "node": ">=16" }, "peerDependencies": { "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/eslint-plugin-unicorn": { - "version": "50.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", - "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "version": "51.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", + "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -5057,12 +5781,12 @@ } }, "node_modules/eslint-plugin-vitest": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.21.tgz", - "integrity": "sha512-oYwR1MrwaBw/OG6CKU+SJYleAc442w6CWL1RTQl5WLwy8X3sh0bgHIQk5iEtmTak3Q+XAvZglr0bIoDOjFdkcw==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.22.tgz", + "integrity": "sha512-atkFGQ7aVgcuSeSMDqnyevIyUpfBPMnosksgEPrKE7Y8xQlqG/5z2IQ6UDau05zXaaFv7Iz8uzqvIuKshjZ0Zw==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^6.20.0" + "@typescript-eslint/utils": "^6.21.0" }, "engines": { "node": "^18.0.0 || >= 20.0.0" @@ -5146,6 +5870,15 @@ "eslint": ">=5" } }, + "node_modules/eslint-rule-documentation": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz", + "integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -5383,9 +6116,9 @@ } }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -5516,7 +6249,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -5565,7 +6297,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -5640,16 +6371,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5698,13 +6433,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -5747,7 +6483,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -5908,6 +6643,14 @@ "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz", "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==" }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -5926,12 +6669,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5962,12 +6705,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -5982,9 +6725,9 @@ "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==" }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", "dependencies": { "function-bind": "^1.1.2" }, @@ -6061,9 +6804,9 @@ "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA==" }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -6074,9 +6817,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -6117,6 +6860,11 @@ "postcss": "^8.1.0" } }, + "node_modules/idiomorph": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz", + "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6137,9 +6885,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -6159,7 +6907,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -6231,12 +6978,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -6261,14 +7008,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6277,8 +7026,22 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-bigint": { "version": "1.0.4", @@ -6292,6 +7055,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -6369,6 +7143,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -6377,6 +7163,21 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-get-set-prop": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-get-set-prop/-/is-get-set-prop-1.0.0.tgz", @@ -6407,6 +7208,15 @@ "js-types": "^1.0.0" } }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -6511,6 +7321,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -6566,12 +7385,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -6589,6 +7408,15 @@ "is-potential-custom-element-name": "^1.0.0" } }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -6601,6 +7429,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -6620,11 +7461,23 @@ "node": ">=0.10.0" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -6665,6 +7518,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", @@ -6677,9 +7538,9 @@ "dev": true }, "node_modules/js-tokens": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.2.tgz", - "integrity": "sha512-Olnt+V7xYdvGze9YTbGFZIfQXuGV4R3nQwwl8BrtgaPE/wq8UFpUHWuTNc05saowhSr1ZO6tx+V6RjE9D5YQog==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", "dev": true }, "node_modules/js-types": { @@ -6850,6 +7711,21 @@ "node": ">=0.10.0" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/just-extend": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", @@ -6906,6 +7782,24 @@ "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", "dev": true }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -6993,229 +7887,18 @@ "node": ">=8" } }, - "node_modules/lightningcss": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.23.0.tgz", - "integrity": "sha512-SEArWKMHhqn/0QzOtclIwH5pXIYQOUEkF8DgICd/105O+GCgd7jxjNod/QPnBCSWvpRHQBGVz5fQ9uScby03zA==", - "dependencies": { - "detect-libc": "^1.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.23.0", - "lightningcss-darwin-x64": "1.23.0", - "lightningcss-freebsd-x64": "1.23.0", - "lightningcss-linux-arm-gnueabihf": "1.23.0", - "lightningcss-linux-arm64-gnu": "1.23.0", - "lightningcss-linux-arm64-musl": "1.23.0", - "lightningcss-linux-x64-gnu": "1.23.0", - "lightningcss-linux-x64-musl": "1.23.0", - "lightningcss-win32-x64-msvc": "1.23.0" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.23.0.tgz", - "integrity": "sha512-kl4Pk3Q2lnE6AJ7Qaij47KNEfY2/UXRZBT/zqGA24B8qwkgllr/j7rclKOf1axcslNXvvUdztjo4Xqh39Yq1aA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.23.0.tgz", - "integrity": "sha512-KeRFCNoYfDdcolcFXvokVw+PXCapd2yHS1Diko1z1BhRz/nQuD5XyZmxjWdhmhN/zj5sH8YvWsp0/lPLVzqKpg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.23.0.tgz", - "integrity": "sha512-xhnhf0bWPuZxcqknvMDRFFo2TInrmQRWZGB0f6YoAsZX8Y+epfjHeeOIGCfAmgF0DgZxHwYc8mIR5tQU9/+ROA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.23.0.tgz", - "integrity": "sha512-fBamf/bULvmWft9uuX+bZske236pUZEoUlaHNBjnueaCTJ/xd8eXgb0cEc7S5o0Nn6kxlauMBnqJpF70Bgq3zg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.23.0.tgz", - "integrity": "sha512-RS7sY77yVLOmZD6xW2uEHByYHhQi5JYWmgVumYY85BfNoVI3DupXSlzbw+b45A9NnVKq45+oXkiN6ouMMtTwfg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.23.0.tgz", - "integrity": "sha512-cU00LGb6GUXCwof6ACgSMKo3q7XYbsyTj0WsKHLi1nw7pV0NCq8nFTn6ZRBYLoKiV8t+jWl0Hv8KkgymmK5L5g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.23.0.tgz", - "integrity": "sha512-q4jdx5+5NfB0/qMbXbOmuC6oo7caPnFghJbIAV90cXZqgV8Am3miZhC4p+sQVdacqxfd+3nrle4C8icR3p1AYw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.23.0.tgz", - "integrity": "sha512-G9Ri3qpmF4qef2CV/80dADHKXRAQeQXpQTLx7AiQrBYQHqBjB75oxqj06FCIe5g4hNCqLPnM9fsO4CyiT1sFSQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-loader": { + "node_modules/lilconfig": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lightningcss-loader/-/lightningcss-loader-2.1.0.tgz", - "integrity": "sha512-mB+M/lvs/GdXT4yc8ZiNgLUAbYpPI9grDyC3ybz/Zo6s4GZv53iZnLTnkJT/Qm3Sh89dbFUm+omoHFXCfZtcXw==", - "dependencies": { - "browserslist": "^4.21.4", - "lightningcss": "^1.16.0", - "webpack-sources": "^3.2.3" - }, - "peerDependencies": { - "webpack": ">=5" - } - }, - "node_modules/lightningcss-loader/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==", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.23.0.tgz", - "integrity": "sha512-1rcBDJLU+obPPJM6qR5fgBUiCdZwZLafZM5f9kwjFLkb/UBNIzmae39uCSmh71nzPCTXZqHbvwu23OWnWEz+eg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=10" } }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/linkify-it": { "version": "5.0.0", @@ -7293,12 +7976,30 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, "node_modules/lodash.sortedlastindex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/lodash.sortedlastindex/-/lodash.sortedlastindex-4.1.0.tgz", @@ -7334,6 +8035,12 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -7546,9 +8253,9 @@ "dev": true }, "node_modules/meow": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.1.0.tgz", - "integrity": "sha512-o5R/R3Tzxq0PJ3v3qcQJtSvSE9nKOLSAaDuuoMzDVuGTwHdccMWcYomh9Xolng2tjT6O/Y83d+0coVGof6tqmA==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, "engines": { "node": ">=18" @@ -7571,16 +8278,15 @@ } }, "node_modules/mermaid": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.7.0.tgz", - "integrity": "sha512-PsvGupPCkN1vemAAjScyw4pw34p4/0dZkSrqvAB26hUvJulOWGIwt35FZWmT9wPIi4r0QLa5X0PB4YLIGn0/YQ==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.8.0.tgz", + "integrity": "sha512-9CzfSreRjdDJxX796+jW4zjEq0DVw5xVF0nWsqff8OTbrt+ml0TZ5PyYUjjUZJa2NYxYJZZXewEquxGiM8qZEA==", "dependencies": { "@braintree/sanitize-url": "^6.0.1", "@types/d3-scale": "^4.0.3", "@types/d3-scale-chromatic": "^3.0.0", - "cytoscape": "^3.23.0", + "cytoscape": "^3.28.1", "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.1.0", "d3": "^7.4.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.10", @@ -8116,7 +8822,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -8134,9 +8839,9 @@ } }, "node_modules/monaco-editor": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz", - "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==" + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", + "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==" }, "node_modules/monaco-editor-webpack-plugin": { "version": "7.1.0", @@ -8168,6 +8873,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -8311,7 +9026,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8378,6 +9092,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -8414,6 +9136,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz", + "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==", + "dev": true, + "dependencies": { + "array.prototype.filter": "^1.0.3", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8496,7 +9279,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -8508,7 +9290,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -8578,7 +9359,6 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -8594,7 +9374,6 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, "engines": { "node": "14 || >=16.14" } @@ -8624,9 +9403,9 @@ } }, "node_modules/pdfobject": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.2.12.tgz", - "integrity": "sha512-D0oyD/sj8j82AMaJhoyMaY1aD5TkbpU3FbJC6w9/cpJlZRpYHqAkutXw1Ca/FKjYPZmTAu58uGIfgOEaDlbY8A==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.3.0.tgz", + "integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -8644,6 +9423,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -8721,12 +9516,12 @@ "dev": true }, "node_modules/playwright": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", - "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", "dev": true, "dependencies": { - "playwright-core": "1.41.1" + "playwright-core": "1.41.2" }, "bin": { "playwright": "cli.js" @@ -8739,9 +9534,9 @@ } }, "node_modules/playwright-core": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", - "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -8769,9 +9564,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -8810,6 +9605,70 @@ "node": "^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz", + "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", @@ -8865,6 +9724,24 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, "node_modules/postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -8958,6 +9835,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -9075,6 +9979,14 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -9177,6 +10089,17 @@ "node": ">=8" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -9200,6 +10123,27 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", + "integrity": "sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0", + "get-intrinsic": "^1.2.3", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -9228,14 +10172,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -9336,7 +10281,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -9490,13 +10434,13 @@ ] }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -9568,9 +10512,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9609,14 +10553,15 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.1" }, @@ -9669,14 +10614,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9692,7 +10641,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -9739,9 +10687,9 @@ } }, "node_modules/solid-js": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.12.tgz", - "integrity": "sha512-sLE/i6M9FSWlov3a2pTC5ISzanH2aKwqXTZj+bbFt4SUrVb4iGEa7fpILBMOxsQjkv3eXqEk6JVLlogOdTe0UQ==", + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.15.tgz", + "integrity": "sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==", "dependencies": { "csstype": "^3.1.0", "seroval": "^1.0.3", @@ -9822,9 +10770,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", - "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", @@ -9844,9 +10792,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==" + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" }, "node_modules/spdx-ranges": { "version": "2.1.1", @@ -9903,7 +10851,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9974,7 +10921,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -9982,6 +10928,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -10284,6 +11239,56 @@ "node": ">= 8" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/superstruct": { "version": "0.10.13", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz", @@ -10324,6 +11329,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-element-attributes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/svg-element-attributes/-/svg-element-attributes-1.3.1.tgz", + "integrity": "sha512-Bh05dSOnJBf3miNMqpsormfNtfidA/GxQVakhtn0T4DECWKeXQRQUceYjJ+OxYiiLdGe4Jo9iFV8wICFapFeIA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", @@ -10365,9 +11380,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.11.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", - "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + "version": "5.11.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.6.tgz", + "integrity": "sha512-K5BpYuMoPpJY7NwCHIWohH6tU9o0fs1+plNT5KJ+3BBlVEh4H1CpeKJV8o91lpscVY9oqb2jmaAassnW3wVoTg==" }, "node_modules/symbol-tree": { "version": "3.2.4", @@ -10387,6 +11402,22 @@ "node": ">=14" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -10403,6 +11434,87 @@ "node": ">=10.0.0" } }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -10412,9 +11524,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", + "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10517,6 +11629,25 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/throttle-debounce": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", @@ -10546,9 +11677,9 @@ } }, "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, "engines": { "node": ">=14.0.0" @@ -10620,12 +11751,12 @@ "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==" }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -10639,6 +11770,35 @@ "node": ">=6.10" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -10679,14 +11839,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz", + "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -10758,9 +11918,9 @@ } }, "node_modules/typo-js": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.3.tgz", - "integrity": "sha512-67Hyl94beZX8gmTap7IDPrG5hy2cHftgsCAcGvE1tzuxGT+kRB+zSBin0wIMwysYw8RUCBCvv9UfQl8TNM75dA==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz", + "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA==" }, "node_modules/uc.micro": { "version": "2.0.0", @@ -10769,9 +11929,9 @@ "dev": true }, "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", "dev": true }, "node_modules/uint8-to-base64": { @@ -10850,9 +12010,9 @@ } }, "node_modules/updates": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.1.tgz", - "integrity": "sha512-dMz/4251b0lV7yR58tuydCKaiWxOa18YM8fnRgtiDVzQ5ALopTZhMckv00w0nSMj6OFMFKLshTZGkX4dAebaaw==", + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.2.tgz", + "integrity": "sha512-+/JT4NChl82iexV9G80TY5HF3ubQ5O9UTOk3LlCo4Y4aRCYvo1h4bJE8YkP0PE7KiFRWIQq/rPmUYrY2QF8wVA==", "dev": true, "bin": { "updates": "bin/updates.js" @@ -10948,13 +12108,13 @@ } }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", + "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", "dev": true, "dependencies": { "esbuild": "^0.19.3", - "postcss": "^8.4.32", + "postcss": "^8.4.35", "rollup": "^4.2.0" }, "bin": { @@ -11025,9 +12185,9 @@ } }, "node_modules/vite-string-plugin": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.3.tgz", - "integrity": "sha512-uHL8BV2tBf32T2slYpS0vRzGVrAS3iuivtGknjzyecvpSq2AiBSkyLAjEvvIZuZGDDGFHyGX+5+yc3OBPjWDlA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.5.tgz", + "integrity": "sha512-KRCIFX3PWVUuEjpi9O7EKLT9E27OqOA3RimIvVx6cziLAUxvnk2VvHQfMrP+mKkqyqqSmnnYyTig3OyDnK/zlA==", "dev": true }, "node_modules/vite/node_modules/@types/estree": { @@ -11051,9 +12211,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.11.0.tgz", + "integrity": "sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -11066,19 +12226,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", + "@rollup/rollup-android-arm-eabi": "4.11.0", + "@rollup/rollup-android-arm64": "4.11.0", + "@rollup/rollup-darwin-arm64": "4.11.0", + "@rollup/rollup-darwin-x64": "4.11.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.11.0", + "@rollup/rollup-linux-arm64-gnu": "4.11.0", + "@rollup/rollup-linux-arm64-musl": "4.11.0", + "@rollup/rollup-linux-riscv64-gnu": "4.11.0", + "@rollup/rollup-linux-x64-gnu": "4.11.0", + "@rollup/rollup-linux-x64-musl": "4.11.0", + "@rollup/rollup-win32-arm64-msvc": "4.11.0", + "@rollup/rollup-win32-ia32-msvc": "4.11.0", + "@rollup/rollup-win32-x64-msvc": "4.11.0", "fsevents": "~2.3.2" } }, @@ -11149,9 +12309,9 @@ } }, "node_modules/vitest/node_modules/magic-string": { - "version": "0.30.6", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz", - "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -11161,15 +12321,15 @@ } }, "node_modules/vue": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz", - "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz", + "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==", "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-sfc": "3.4.15", - "@vue/runtime-dom": "3.4.15", - "@vue/server-renderer": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-dom": "3.4.19", + "@vue/compiler-sfc": "3.4.19", + "@vue/runtime-dom": "3.4.19", + "@vue/server-renderer": "3.4.19", + "@vue/shared": "3.4.19" }, "peerDependencies": { "typescript": "*" @@ -11189,6 +12349,15 @@ "vue": "^3.2.37" } }, + "node_modules/vue-chartjs": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz", + "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-eslint-parser": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", @@ -11294,9 +12463,9 @@ } }, "node_modules/webpack": { - "version": "5.90.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", - "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz", + "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -11555,17 +12724,58 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -11616,7 +12826,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -11754,6 +12963,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", diff --git a/package.json b/package.json index 3defca1a3d..3f0f9103cf 100644 --- a/package.json +++ b/package.json @@ -17,59 +17,69 @@ "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", "ansi_up": "6.0.2", - "asciinema-player": "3.6.3", + "asciinema-player": "3.6.4", + "chart.js": "4.4.1", + "chartjs-adapter-dayjs-4": "1.0.4", + "chartjs-plugin-zoom": "2.0.1", "clippie": "4.0.6", "css-loader": "6.10.0", + "css-variables-parser": "1.0.1", + "dayjs": "1.11.10", "dropzone": "6.0.0-beta.2", "easymde": "2.18.0", "esbuild-loader": "4.0.3", "escape-goat": "4.0.0", "fast-glob": "3.3.2", "htmx.org": "1.9.10", + "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.9", "license-checker-webpack-plugin": "0.2.1", - "lightningcss-loader": "2.1.0", - "mermaid": "10.7.0", + "mermaid": "10.8.0", "mini-css-extract-plugin": "2.8.0", "minimatch": "9.0.3", - "monaco-editor": "0.45.0", + "monaco-editor": "0.46.0", "monaco-editor-webpack-plugin": "7.1.0", - "pdfobject": "2.2.12", + "pdfobject": "2.3.0", + "postcss": "8.4.35", + "postcss-loader": "8.1.0", "pretty-ms": "9.0.0", "sortablejs": "1.15.2", - "swagger-ui-dist": "5.11.2", + "swagger-ui-dist": "5.11.6", + "tailwindcss": "3.4.1", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", - "vue": "3.4.15", + "vue": "3.4.19", "vue-bar-graph": "2.0.0", + "vue-chartjs": "5.3.0", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.90.1", + "webpack": "5.90.2", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.1.0", - "@playwright/test": "1.41.1", + "@playwright/test": "1.41.2", "@stoplight/spectral-cli": "6.11.0", - "@stylistic/eslint-plugin-js": "1.5.4", + "@stylistic/eslint-plugin-js": "1.6.2", "@stylistic/stylelint-plugin": "2.0.0", - "@vitejs/plugin-vue": "5.0.3", + "@vitejs/plugin-vue": "5.0.4", "eslint": "8.56.0", "eslint-plugin-array-func": "4.0.0", + "eslint-plugin-github": "4.10.1", "eslint-plugin-i": "2.29.1", "eslint-plugin-jquery": "1.5.1", "eslint-plugin-no-jquery": "2.7.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-regexp": "2.2.0", - "eslint-plugin-sonarjs": "0.23.0", - "eslint-plugin-unicorn": "50.0.1", - "eslint-plugin-vitest": "0.3.21", + "eslint-plugin-sonarjs": "0.24.0", + "eslint-plugin-unicorn": "51.0.1", + "eslint-plugin-vitest": "0.3.22", "eslint-plugin-vitest-globals": "1.4.0", "eslint-plugin-vue": "9.21.1", "eslint-plugin-vue-scoped-css": "2.7.2", @@ -81,13 +91,11 @@ "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.4", "svgo": "3.2.0", - "updates": "15.1.1", - "vite-string-plugin": "1.1.3", + "updates": "15.1.2", + "vite-string-plugin": "1.1.5", "vitest": "1.2.2" }, "browserslist": [ - "defaults", - "not ie > 0", - "not ie_mob > 0" + "defaults" ] } diff --git a/playwright.config.js b/playwright.config.js index b7badf1cc0..6595bc4312 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -11,6 +11,13 @@ export default { testDir: './tests/e2e/', testMatch: /.*\.test\.e2e\.js/, // Match any .test.e2e.js files + /** + * Only run one test at a time, running multiple could lead to a inconsistent + * database state. + */ + fullyParallel: false, + workers: 1, + /* Maximum time one test can run for. */ timeout: 30 * 1000, @@ -64,13 +71,12 @@ export default { }, }, - // disabled because of https://github.com/go-gitea/gitea/issues/21355 - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, { name: 'webkit', diff --git a/poetry.lock b/poetry.lock index 74d202c919..4cb58c6ef2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -342,13 +342,13 @@ telegram = ["requests"] [[package]] name = "yamllint" -version = "1.33.0" +version = "1.35.0" description = "A linter for YAML files." optional = false python-versions = ">=3.8" files = [ - {file = "yamllint-1.33.0-py3-none-any.whl", hash = "sha256:28a19f5d68d28d8fec538a1db21bb2d84c7dc2e2ea36266da8d4d1c5a683814d"}, - {file = "yamllint-1.33.0.tar.gz", hash = "sha256:2dceab9ef2d99518a2fcf4ffc964d44250ac4459be1ba3ca315118e4a1a81f7d"}, + {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"}, + {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"}, ] [package.dependencies] @@ -361,4 +361,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "175c87d138a47ba190a2c3f16b801f694915cc6f2367a358585df9cd1b17ff96" +content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece" diff --git a/public/assets/img/404.png b/public/assets/img/404.png deleted file mode 100644 index 8b66c971f4..0000000000 Binary files a/public/assets/img/404.png and /dev/null differ diff --git a/public/assets/img/500.png b/public/assets/img/500.png deleted file mode 100644 index dab69206ad..0000000000 Binary files a/public/assets/img/500.png and /dev/null differ diff --git a/public/assets/img/svg/gitea-discord.svg b/public/assets/img/svg/gitea-discord.svg index 6ebbdcdcc3..2edcb4fed7 100644 --- a/public/assets/img/svg/gitea-discord.svg +++ b/public/assets/img/svg/gitea-discord.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-forgejo.svg b/public/assets/img/svg/gitea-forgejo.svg index ef617c00f3..22ae790357 100644 --- a/public/assets/img/svg/gitea-forgejo.svg +++ b/public/assets/img/svg/gitea-forgejo.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/public/assets/img/svg/gitea-gitea.svg b/public/assets/img/svg/gitea-gitea.svg index 5d89fa1da9..61ef37074e 100644 --- a/public/assets/img/svg/gitea-gitea.svg +++ b/public/assets/img/svg/gitea-gitea.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cf65f0ddfa..ef763da24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ python = "^3.8" [tool.poetry.group.dev.dependencies] djlint = "1.34.1" -yamllint = "1.33.0" +yamllint = "1.35.0" [tool.djlint] profile="golang" diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 3363c4c0e8..9fbd3f045d 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -63,6 +63,7 @@ package actions import ( "crypto/md5" + "errors" "fmt" "net/http" "strconv" @@ -426,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { var items []downloadArtifactResponseItem for _, artifact := range artifacts { - downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download") + var downloadURL string + if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect { + u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName) + if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { + log.Error("Error getting serve direct url: %v", err) + } + if u != nil { + downloadURL = u.String() + } + } + if downloadURL == "" { + downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download") + } item := downloadArtifactResponseItem{ Path: util.PathJoinRel(itemPath, artifact.ArtifactPath), ItemType: "file", diff --git a/routers/api/actions/ping/ping.go b/routers/api/actions/ping/ping.go index 55219fe12b..13985c93a3 100644 --- a/routers/api/actions/ping/ping.go +++ b/routers/api/actions/ping/ping.go @@ -12,7 +12,7 @@ import ( pingv1 "code.gitea.io/actions-proto-go/ping/v1" "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" - "github.com/bufbuild/connect-go" + "connectrpc.com/connect" ) func NewPingServiceHandler() (string, http.Handler) { diff --git a/routers/api/actions/ping/ping_test.go b/routers/api/actions/ping/ping_test.go index f39e94a1f3..098b003ea2 100644 --- a/routers/api/actions/ping/ping_test.go +++ b/routers/api/actions/ping/ping_test.go @@ -11,7 +11,7 @@ import ( pingv1 "code.gitea.io/actions-proto-go/ping/v1" "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" - "github.com/bufbuild/connect-go" + "connectrpc.com/connect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go index ddc754dbc7..c2f4ade174 100644 --- a/routers/api/actions/runner/interceptor.go +++ b/routers/api/actions/runner/interceptor.go @@ -15,7 +15,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/bufbuild/connect-go" + "connectrpc.com/connect" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 8df6f297ce..caaad2b83b 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -16,7 +16,7 @@ import ( runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" - "github.com/bufbuild/connect-go" + "connectrpc.com/connect" gouuid "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index bb14c5163a..3701fc97c9 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -14,6 +14,7 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" + alpine_model "code.gitea.io/gitea/models/packages/alpine" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" packages_module "code.gitea.io/gitea/modules/packages" @@ -30,6 +31,53 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func createOrAddToExisting(ctx *context.Context, pck *alpine_module.Package, branch, repository, architecture string, buf packages_module.HashedSizeReader, fileMetadataRaw []byte) { + _, _, err := packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeAlpine, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s.apk", pck.Name, pck.Version), + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + alpine_module.PropertyBranch: branch, + alpine_module.PropertyRepository: repository, + alpine_module.PropertyArchitecture: architecture, + alpine_module.PropertyMetadata: string(fileMetadataRaw), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusConflict, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, pck.FileMetadata.Architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } +} + func GetRepositoryKey(ctx *context.Context) { _, pub, err := alpine_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID) if err != nil { @@ -133,49 +181,36 @@ func UploadPackageFile(ctx *context.Context) { return } - _, _, err = packages_service.CreatePackageOrAddFileToExisting( - ctx, - &packages_service.PackageCreationInfo{ - PackageInfo: packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeAlpine, - Name: pck.Name, - Version: pck.Version, - }, - Creator: ctx.Doer, - Metadata: pck.VersionMetadata, - }, - &packages_service.PackageFileCreationInfo{ - PackageFileInfo: packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s.apk", pck.Name, pck.Version), - CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, pck.FileMetadata.Architecture), - }, - Creator: ctx.Doer, - Data: buf, - IsLead: true, - Properties: map[string]string{ - alpine_module.PropertyBranch: branch, - alpine_module.PropertyRepository: repository, - alpine_module.PropertyArchitecture: pck.FileMetadata.Architecture, - alpine_module.PropertyMetadata: string(fileMetadataRaw), - }, - }, - ) - if err != nil { - switch err { - case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: - apiError(ctx, http.StatusConflict, err) - case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: - apiError(ctx, http.StatusForbidden, err) - default: + // Check whether the package being uploaded has no architecture defined. + // If true, loop through the available architectures in the repo and create + // the package file for the each architecture. If there are no architectures + // available on the repository, fallback to x86_64 + if pck.FileMetadata.Architecture == "noarch" { + architectures, err := alpine_model.GetArchitectures(ctx, ctx.Package.Owner.ID, repository) + if err != nil { apiError(ctx, http.StatusInternalServerError, err) + return } - return - } - if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, pck.FileMetadata.Architecture); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return + if len(architectures) == 0 { + architectures = []string{ + "x86_64", + } + } + + for _, arch := range architectures { + pck.FileMetadata.Architecture = arch + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + createOrAddToExisting(ctx, pck, branch, repository, pck.FileMetadata.Architecture, buf, fileMetadataRaw) + } + } else { + createOrAddToExisting(ctx, pck, branch, repository, pck.FileMetadata.Architecture, buf, fileMetadataRaw) } ctx.Status(http.StatusCreated) diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 427e262d06..6ad289e51e 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -157,7 +157,7 @@ func EnumeratePackageVersions(ctx *context.Context) { } type Resource struct { - Name string `json:"id"` + Name string `json:"name"` Type string `json:"type"` Checksum string `json:"checksum"` } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 272996f43d..2ce7651a09 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -21,7 +21,6 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/utils" @@ -117,11 +116,8 @@ func CreateUser(ctx *context.APIContext) { } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, - } - - if form.Restricted != nil { - overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted) + IsActive: optional.Some(true), + IsRestricted: optional.FromPtr(form.Restricted), } if form.Visibility != "" { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 6d56bb36ec..76c7992308 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -9,7 +9,7 @@ // // Schemes: https, http // BasePath: /api/v1 -// Version: {{AppVer | JSEscape | Safe}} +// Version: {{AppVer | JSEscape}} // License: MIT http://opensource.org/licenses/MIT // // Consumes: @@ -974,7 +974,9 @@ func Routes() *web.Route { m.Get("/{target}", user.CheckFollowing) }) - m.Get("/starred", user.GetStarredRepos) + if !setting.Repository.DisableStars { + m.Get("/starred", user.GetStarredRepos) + } m.Get("/subscriptions", user.GetWatchedRepos) }, context_service.UserAssignmentAPI()) @@ -1049,14 +1051,16 @@ func Routes() *web.Route { Post(bind(api.CreateRepoOption{}), repo.Create) // (repo scope) - m.Group("/starred", func() { - m.Get("", user.GetMyStarredRepos) - m.Group("/{username}/{reponame}", func() { - m.Get("", user.IsStarring) - m.Put("", user.Star) - m.Delete("", user.Unstar) - }, repoAssignment()) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + if !setting.Repository.DisableStars { + m.Group("/starred", func() { + m.Get("", user.GetMyStarredRepos) + m.Group("/{username}/{reponame}", func() { + m.Get("", user.IsStarring) + m.Put("", user.Star) + m.Delete("", user.Unstar) + }, repoAssignment()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + } m.Get("/times", repo.ListMyTrackedTimes) m.Get("/stopwatches", repo.GetStopwatches) m.Get("/subscriptions", user.GetMyWatchedRepos) @@ -1174,8 +1178,10 @@ func Routes() *web.Route { m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) - m.Combo("/forks").Get(repo.ListForks). - Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + if !setting.Repository.DisableForks { + m.Combo("/forks").Get(repo.ListForks). + Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + } m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) @@ -1219,7 +1225,9 @@ func Routes() *web.Route { m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) - m.Get("/stargazers", repo.ListStargazers) + if !setting.Repository.DisableStars { + m.Get("/stargazers", repo.ListStargazers) + } m.Get("/subscribers", repo.ListSubscribers) m.Group("/subscription", func() { m.Get("", user.IsWatching) @@ -1300,6 +1308,7 @@ func Routes() *web.Route { Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests). Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests) }) + m.Get("/{base}/*", repo.GetPullRequestByBaseHead) }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) m.Group("/statuses", func() { m.Combo("/{sha}").Get(repo.GetCommitStatuses). @@ -1310,6 +1319,7 @@ func Routes() *web.Route { m.Group("/{ref}", func() { m.Get("/status", repo.GetCombinedCommitStatusByRef) m.Get("/statuses", repo.GetCommitStatusesByRef) + m.Get("/pull", repo.GetCommitPullRequest) }, context.ReferencesGitRepo()) }, reqRepoReader(unit.TypeCode)) m.Group("/git", func() { diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index bd02a8afc4..2cdbcd25a2 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -17,9 +17,9 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" @@ -141,7 +141,7 @@ func DeleteBranch(ctx *context.APIContext) { // check whether branches of this repository has been synced totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{ RepoID: ctx.Repo.Repository.ID, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }) if err != nil { ctx.Error(http.StatusInternalServerError, "CountBranches", err) @@ -340,7 +340,7 @@ func ListBranches(ctx *context.APIContext) { branchOpts := git_model.FindBranchOptions{ ListOptions: listOptions, RepoID: ctx.Repo.Repository.ID, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), } var err error totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts) diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 43b6400009..d01cf6b8bc 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -10,6 +10,7 @@ import ( "net/http" "strconv" + issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -323,3 +324,53 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) { return } } + +// GetCommitPullRequest returns the pull request of the commit +func GetCommitPullRequest(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest + // --- + // summary: Get the pull request of the commit + // 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: sha + // in: path + // description: SHA of the commit to get + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/PullRequest" + // "404": + // "$ref": "#/responses/notFound" + + pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.Params(":sha")) + if err != nil { + if issues_model.IsErrPullRequestNotExist(err) { + ctx.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + } + return + } + + if err = pr.LoadBaseRepo(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + return + } + if err = pr.LoadHeadRepo(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + return + } + ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) +} diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 7988dec8d3..94a6381e25 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -425,7 +425,7 @@ func canReadFiles(r *context.Repository) bool { return r.Permission.CanRead(unit.TypeCode) } -func base64Reader(s string) (io.Reader, error) { +func base64Reader(s string) (io.ReadSeeker, error) { b, err := base64.StdEncoding.DecodeString(s) if err != nil { return nil, err @@ -779,13 +779,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch } message := "" if len(createFiles) != 0 { - message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n") + message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n") } if len(updateFiles) != 0 { - message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n") + message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n") } if len(deleteFiles) != 0 { - message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", ")) + message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", ")) } return strings.Trim(message, "\n") } diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 6f70e6bcec..35d7658b6c 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -395,7 +395,7 @@ func CreateIssueComment(ctx *context.APIContext) { } if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { - ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked"))) + ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index b1b486e017..ef9b4893fb 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -187,6 +187,91 @@ func GetPullRequest(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) } +// GetPullRequest returns a single PR based on index +func GetPullRequestByBaseHead(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/pulls/{base}/{head} repository repoGetPullRequestByBaseHead + // --- + // summary: Get a pull request by base and head + // 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: base + // in: path + // description: base of the pull request to get + // type: string + // required: true + // - name: head + // in: path + // description: head of the pull request to get + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/PullRequest" + // "404": + // "$ref": "#/responses/notFound" + + var headRepoID int64 + var headBranch string + head := ctx.Params("*") + if strings.Contains(head, ":") { + split := strings.SplitN(head, ":", 2) + headBranch = split[1] + var owner, name string + if strings.Contains(split[0], "/") { + split = strings.Split(split[0], "/") + owner = split[0] + name = split[1] + } else { + owner = split[0] + name = ctx.Repo.Repository.Name + } + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerName", err) + } + return + } + headRepoID = repo.ID + } else { + headRepoID = ctx.Repo.Repository.ID + headBranch = head + } + + pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.Params(":base"), headBranch) + if err != nil { + if issues_model.IsErrPullRequestNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetPullRequestByBaseHeadInfo", err) + } + return + } + + if err = pr.LoadBaseRepo(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + return + } + if err = pr.LoadHeadRepo(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + return + } + ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) +} + // DownloadPullDiffOrPatch render a pull's raw diff or patch func DownloadPullDiffOrPatch(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index ab8deab362..e39a096add 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -339,6 +339,7 @@ func CreatePullReviewComment(ctx *context.APIContext) { opts.Path, line, review.ID, + nil, ) if err != nil { ctx.InternalServerError(err) @@ -508,6 +509,7 @@ func CreatePullReview(ctx *context.APIContext) { true, // pending review 0, // no reply opts.CommitID, + nil, ); err != nil { ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err) return diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 08e79e544a..de105f474f 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -897,6 +897,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, + AllowFastForwardOnly: true, AllowManualMerge: true, AutodetectManualMerge: false, AllowRebaseUpdate: true, @@ -923,6 +924,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if opts.AllowSquash != nil { config.AllowSquash = *opts.AllowSquash } + if opts.AllowFastForwardOnly != nil { + config.AllowFastForwardOnly = *opts.AllowFastForwardOnly + } if opts.AllowManualMerge != nil { config.AllowManualMerge = *opts.AllowManualMerge } diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go index 29e2d1f21d..08ba7fabac 100644 --- a/routers/api/v1/repo/repo_test.go +++ b/routers/api/v1/repo/repo_test.go @@ -35,6 +35,7 @@ func TestRepoEdit(t *testing.T) { allowRebase := false allowRebaseMerge := false allowSquashMerge := false + allowFastForwardOnlyMerge := false archived := true opts := api.EditRepoOption{ Name: &ctx.Repo.Repository.Name, @@ -50,6 +51,7 @@ func TestRepoEdit(t *testing.T) { AllowRebase: &allowRebase, AllowRebaseMerge: &allowRebaseMerge, AllowSquash: &allowSquashMerge, + AllowFastForwardOnly: &allowFastForwardOnlyMerge, Archived: &archived, } diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go index 02bda1309d..957b839e66 100644 --- a/routers/api/v1/settings/settings.go +++ b/routers/api/v1/settings/settings.go @@ -61,6 +61,7 @@ func GetGeneralRepoSettings(ctx *context.APIContext) { HTTPGitDisabled: setting.Repository.DisableHTTPGit, MigrationsDisabled: setting.Repository.DisableMigrations, StarsDisabled: setting.Repository.DisableStars, + ForksDisabled: setting.Repository.DisableForks, TimeTrackingDisabled: !setting.Service.EnableTimetracking, LFSDisabled: !setting.LFS.StartServer, }) diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 263e335873..c7fa98a697 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -263,7 +263,7 @@ type swaggerChangedFileList struct { PerPage int `json:"X-PerPage"` // Total commit count - Total int `json:"X-Total"` + Total int `json:"X-Total-Count"` // Total number of pages PageCount int `json:"X-PageCount"` diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index 2299cdc247..5e80190017 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -72,7 +72,7 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str // ConvertToObjectID returns a full-length SHA1 from a potential ID string func ConvertToObjectID(ctx gocontext.Context, repo *context.Repository, commitID string) (git.ObjectID, error) { - objectFormat, _ := repo.GitRepo.GetObjectFormat() + objectFormat := repo.GetObjectFormat() if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) { sha, err := git.NewIDFromString(commitID) if err == nil { diff --git a/routers/install/install.go b/routers/install/install.go index 5c43bd486a..13504953ce 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -7,6 +7,7 @@ package install import ( "fmt" "net/http" + "net/mail" "os" "os/exec" "path/filepath" @@ -25,11 +26,11 @@ import ( "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/user" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/common" @@ -423,6 +424,11 @@ func SubmitInstall(ctx *context.Context) { } if len(strings.TrimSpace(form.SMTPAddr)) > 0 { + if _, err := mail.ParseAddress(form.SMTPFrom); err != nil { + ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form) + return + } + cfg.Section("mailer").Key("ENABLED").SetValue("true") cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr) cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort) @@ -537,8 +543,8 @@ func SubmitInstall(ctx *context.Context) { IsAdmin: true, } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsRestricted: util.OptionalBoolFalse, - IsActive: util.OptionalBoolTrue, + IsRestricted: optional.Some(false), + IsActive: optional.Some(true), } if err = user_model.CreateUser(ctx, u, overwriteDefault); err != nil { diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 90d8287f06..f28ae4c0eb 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -145,7 +145,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r repo := ctx.Repo.Repository gitRepo := ctx.Repo.GitRepo - objectFormat, _ := gitRepo.GetObjectFormat() + objectFormat := ctx.Repo.GetObjectFormat() if branchName == repo.DefaultBranch && newCommitID == objectFormat.EmptyObjectID().String() { log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index d31cb1cd25..58bb281731 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -7,8 +7,8 @@ package admin import ( "fmt" "net/http" + "reflect" "runtime" - "sort" "time" activities_model "code.gitea.io/gitea/models/activities" @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/updatechecker" @@ -28,13 +27,14 @@ import ( ) const ( - tplDashboard base.TplName = "admin/dashboard" - tplSelfCheck base.TplName = "admin/self_check" - tplCron base.TplName = "admin/cron" - tplQueue base.TplName = "admin/queue" - tplStacktrace base.TplName = "admin/stacktrace" - tplQueueManage base.TplName = "admin/queue_manage" - tplStats base.TplName = "admin/stats" + tplDashboard base.TplName = "admin/dashboard" + tplSystemStatus base.TplName = "admin/system_status" + tplSelfCheck base.TplName = "admin/self_check" + tplCron base.TplName = "admin/cron" + tplQueue base.TplName = "admin/queue" + tplStacktrace base.TplName = "admin/stacktrace" + tplQueueManage base.TplName = "admin/queue_manage" + tplStats base.TplName = "admin/stats" ) var sysStatus struct { @@ -72,7 +72,7 @@ var sysStatus struct { // Garbage collector statistics. NextGC string // next run in HeapAlloc time (bytes) - LastGC string // last run in absolute time (ns) + LastGCTime string // last run time PauseTotalNs string PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] NumGC uint32 @@ -110,7 +110,7 @@ func updateSystemStatus() { sysStatus.OtherSys = base.FileSize(int64(m.OtherSys)) sysStatus.NextGC = base.FileSize(int64(m.NextGC)) - sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) + sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339) sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) sysStatus.NumGC = m.NumGC @@ -132,7 +132,6 @@ func Dashboard(ctx *context.Context) { ctx.Data["PageIsAdminDashboard"] = true ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx) ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx) - // FIXME: update periodically updateSystemStatus() ctx.Data["SysStatus"] = sysStatus ctx.Data["SSH"] = setting.SSH @@ -140,6 +139,12 @@ func Dashboard(ctx *context.Context) { ctx.HTML(http.StatusOK, tplDashboard) } +func SystemStatus(ctx *context.Context) { + updateSystemStatus() + ctx.Data["SysStatus"] = sysStatus + ctx.HTML(http.StatusOK, tplSystemStatus) +} + // DashboardPost run an admin operation func DashboardPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.AdminDashboardForm) @@ -219,26 +224,22 @@ func CronTasks(ctx *context.Context) { func MonitorStats(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.monitor.stats") ctx.Data["PageIsAdminMonitorStats"] = true - bs, err := json.Marshal(activities_model.GetStatistic(ctx).Counter) - if err != nil { - ctx.ServerError("MonitorStats", err) - return - } - statsCounter := map[string]any{} - err = json.Unmarshal(bs, &statsCounter) - if err != nil { - ctx.ServerError("MonitorStats", err) - return - } - statsKeys := make([]string, 0, len(statsCounter)) - for k := range statsCounter { - if statsCounter[k] == nil { + modelStats := activities_model.GetStatistic(ctx).Counter + stats := map[string]any{} + + // To avoid manually converting the values of the stats struct to an map, + // and to avoid using JSON to do this for us (JSON encoder converts numbers to + // scientific notation). Use reflect to convert the struct to an map. + rv := reflect.ValueOf(modelStats) + for i := 0; i < rv.NumField(); i++ { + field := rv.Field(i) + // Preserve old behavior, do not show arrays that are empty. + if field.Kind() == reflect.Slice && field.Len() == 0 { continue } - statsKeys = append(statsKeys, k) + stats[rv.Type().Field(i).Name] = field.Interface() } - sort.Strings(statsKeys) - ctx.Data["StatsKeys"] = statsKeys - ctx.Data["StatsCounter"] = statsCounter + + ctx.Data["Stats"] = stats ctx.HTML(http.StatusOK, tplStats) } diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index 2b65ab3ea3..452291e179 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -6,6 +6,11 @@ package admin import ( "testing" + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/contexttest" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -66,3 +71,46 @@ func TestShadowPassword(t *testing.T) { assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem)) } } + +func TestMonitorStats(t *testing.T) { + unittest.PrepareTestEnv(t) + + t.Run("Normal", func(t *testing.T) { + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByLabel, false)() + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByRepository, false)() + + ctx, _ := contexttest.MockContext(t, "admin/stats") + MonitorStats(ctx) + + // Test some of the stats manually. + mappedStats := ctx.Data["Stats"].(map[string]any) + stats := activities_model.GetStatistic(ctx).Counter + + assert.EqualValues(t, stats.Comment, mappedStats["Comment"]) + assert.EqualValues(t, stats.Issue, mappedStats["Issue"]) + assert.EqualValues(t, stats.User, mappedStats["User"]) + assert.EqualValues(t, stats.Milestone, mappedStats["Milestone"]) + + // Ensure that these aren't set. + assert.Empty(t, stats.IssueByLabel) + assert.Empty(t, stats.IssueByRepository) + assert.Nil(t, mappedStats["IssueByLabel"]) + assert.Nil(t, mappedStats["IssueByRepository"]) + }) + + t.Run("IssueByX", func(t *testing.T) { + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByLabel, true)() + defer test.MockVariableValue(&setting.Metrics.EnabledIssueByRepository, true)() + + ctx, _ := contexttest.MockContext(t, "admin/stats") + MonitorStats(ctx) + + mappedStats := ctx.Data["Stats"].(map[string]any) + stats := activities_model.GetStatistic(ctx).Counter + + assert.NotEmpty(t, stats.IssueByLabel) + assert.NotEmpty(t, stats.IssueByRepository) + assert.EqualValues(t, stats.IssueByLabel, mappedStats["IssueByLabel"]) + assert.EqualValues(t, stats.IssueByRepository, mappedStats["IssueByRepository"]) + }) +} diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 2cf63c646d..7fdd18dfae 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) { if util.IsEmptyString(form.SSPISeparatorReplacement) { ctx.Data["Err_SSPISeparatorReplacement"] = true - return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error")) + return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error")) } if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) { ctx.Data["Err_SSPISeparatorReplacement"] = true - return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error")) + return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error")) } if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) { ctx.Data["Err_SSPIDefaultLanguage"] = true - return nil, errors.New(ctx.Tr("form.lang_select_error")) + return nil, errors.New(ctx.Locale.TrString("form.lang_select_error")) } return &sspi.Source{ diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index af184fa9eb..adb9799c01 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -140,7 +140,7 @@ func NewUserPost(ctx *context.Context) { } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), Visibility: &form.Visibility, } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 2adcb3aea0..4e4079d8ff 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -579,16 +579,8 @@ func GrantApplicationOAuth(ctx *context.Context) { // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities func OIDCWellKnown(ctx *context.Context) { - t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil) - if err != nil { - ctx.ServerError("unable to find template", err) - return - } - ctx.Resp.Header().Set("Content-Type", "application/json") ctx.Data["SigningKey"] = oauth2.DefaultSigningKey - if err = t.Execute(ctx.Resp, ctx.Data); err != nil { - ctx.ServerError("unable to execute template", err) - } + ctx.JSONTemplate("user/auth/oidc_wellknown") } // OIDCKeys generates the JSON Web Key Set @@ -990,7 +982,7 @@ func SignInOAuthCallback(ctx *context.Context) { } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm), + IsActive: optional.Some(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm), } source := authSource.Cfg.(*oauth2.Source) diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index 5af1696a64..1f2d133282 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -37,7 +37,7 @@ func ForgotPasswd(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title") if setting.MailService == nil { - log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin")) + log.Warn("no mail service configured") ctx.Data["IsResetDisable"] = true ctx.HTML(http.StatusOK, tplForgotPassword) return @@ -204,7 +204,7 @@ func ResetPasswdPost(ctx *context.Context) { Password: optional.Some(ctx.FormString("password")), MustChangePassword: optional.Some(false), } - if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil { + if err := user_service.UpdateAuth(ctx, u, opts); err != nil { ctx.Data["IsResetForm"] = true ctx.Data["Err_Password"] = true switch { diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 3cb3e62245..5186d1a524 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -6,6 +6,7 @@ package feed import ( "fmt" "html" + "html/template" "net/http" "net/url" "strconv" @@ -80,119 +81,120 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio // title title = act.ActUser.DisplayName() + " " + var titleExtra template.HTML switch act.OpType { case activities_model.ActionCreateRepo: - title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) link.Href = act.GetRepoAbsoluteLink(ctx) case activities_model.ActionRenameRepo: - title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) link.Href = act.GetRepoAbsoluteLink(ctx) case activities_model.ActionCommitRepo: link.Href = toBranchLink(ctx, act) if len(act.Content) != 0 { - title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx)) } else { - title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx)) } case activities_model.ActionCreateIssue: link.Href = toIssueLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionCreatePullRequest: link.Href = toPullLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionTransferRepo: link.Href = act.GetRepoAbsoluteLink(ctx) - title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) case activities_model.ActionPushTag: link.Href = toTagLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx)) case activities_model.ActionCommentIssue: issueLink := toIssueLink(ctx, act) if link.Href == "#" { link.Href = issueLink } - title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionMergePullRequest: pullLink := toPullLink(ctx, act) if link.Href == "#" { link.Href = pullLink } - title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionAutoMergePullRequest: pullLink := toPullLink(ctx, act) if link.Href == "#" { link.Href = pullLink } - title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionCloseIssue: issueLink := toIssueLink(ctx, act) if link.Href == "#" { link.Href = issueLink } - title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionReopenIssue: issueLink := toIssueLink(ctx, act) if link.Href == "#" { link.Href = issueLink } - title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionClosePullRequest: pullLink := toPullLink(ctx, act) if link.Href == "#" { link.Href = pullLink } - title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionReopenPullRequest: pullLink := toPullLink(ctx, act) if link.Href == "#" { link.Href = pullLink } - title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionDeleteTag: link.Href = act.GetRepoAbsoluteLink(ctx) - title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx)) case activities_model.ActionDeleteBranch: link.Href = act.GetRepoAbsoluteLink(ctx) - title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx)) case activities_model.ActionMirrorSyncPush: srcLink := toSrcLink(ctx, act) if link.Href == "#" { link.Href = srcLink } - title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx)) case activities_model.ActionMirrorSyncCreate: srcLink := toSrcLink(ctx, act) if link.Href == "#" { link.Href = srcLink } - title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx)) case activities_model.ActionMirrorSyncDelete: link.Href = act.GetRepoAbsoluteLink(ctx) - title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx)) case activities_model.ActionApprovePullRequest: pullLink := toPullLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionRejectPullRequest: pullLink := toPullLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionCommentPull: pullLink := toPullLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) case activities_model.ActionPublishRelease: releaseLink := toReleaseLink(ctx, act) if link.Href == "#" { link.Href = releaseLink } - title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content) + titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content) case activities_model.ActionPullReviewDismissed: pullLink := toPullLink(ctx, act) - title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1]) + titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1]) case activities_model.ActionStarRepo: link.Href = act.GetRepoAbsoluteLink(ctx) - title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx)) case activities_model.ActionWatchRepo: link.Href = act.GetRepoAbsoluteLink(ctx) - title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx)) + titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx)) default: return nil, fmt.Errorf("unknown action type: %v", act.OpType) } @@ -234,7 +236,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest: desc = act.GetIssueTitle(ctx) case activities_model.ActionPullReviewDismissed: - desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2] + desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2] } } if len(content) == 0 { @@ -243,7 +245,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio // It's a common practice for feed generators to use plain text titles. // See https://codeberg.org/forgejo/forgejo/pulls/1595 - plainTitle, err := html2text.FromString(title, html2text.Options{OmitLinks: true}) + plainTitle, err := html2text.FromString(title+" "+string(titleExtra), html2text.Options{OmitLinks: true}) if err != nil { return nil, err } diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 04f84c0c8d..3feca68d61 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -56,7 +56,7 @@ func showUserFeed(ctx *context.Context, formatType string) { } feed := &feeds.Feed{ - Title: ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()), + Title: ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()), Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()}, Description: ctxUserDescription, Created: time.Now(), diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go index 57b0c92766..558c03dba7 100644 --- a/routers/web/feed/release.go +++ b/routers/web/feed/release.go @@ -28,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas var link *feeds.Link if isReleasesOnly { - title = ctx.Tr("repo.release.releases_for", repo.FullName()) + title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName()) link = &feeds.Link{Href: repo.HTMLURL() + "/release"} } else { - title = ctx.Tr("repo.release.tags_for", repo.FullName()) + title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName()) link = &feeds.Link{Href: repo.HTMLURL() + "/tags"} } diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go index 5fcad26779..51c24510c7 100644 --- a/routers/web/feed/repo.go +++ b/routers/web/feed/repo.go @@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType } feed := &feeds.Feed{ - Title: ctx.Tr("home.feed_of", repo.FullName()), + Title: ctx.Locale.TrString("home.feed_of", repo.FullName()), Link: &feeds.Link{Href: repo.HTMLURL()}, Description: repo.Description, Created: time.Now(), diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 8bf02b2c42..36f543dc45 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -46,17 +45,6 @@ func Home(ctx *context.Context) { ctx.Data["PageIsUserProfile"] = true ctx.Data["Title"] = org.DisplayName() - if len(org.Description) != 0 { - desc, err := markdown.RenderString(&markup.RenderContext{ - Ctx: ctx, - Metas: map[string]string{"mode": "document"}, - }, org.Description) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - ctx.Data["RenderedDescription"] = desc - } var orderBy db.SearchOrderBy ctx.Data["SortType"] = ctx.FormString("sort") @@ -131,18 +119,12 @@ func Home(ctx *context.Context) { return } - var isFollowing bool - if ctx.Doer != nil { - isFollowing = user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) - } - ctx.Data["Repos"] = repos ctx.Data["Total"] = count ctx.Data["Members"] = members ctx.Data["Teams"] = ctx.Org.Teams ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull ctx.Data["PageIsViewRepositories"] = true - ctx.Data["IsFollowing"] = isFollowing err = shared_user.LoadHeaderCount(ctx) if err != nil { diff --git a/routers/web/org/org.go b/routers/web/org/org.go index 52f8df8a1c..1e4544730e 100644 --- a/routers/web/org/org.go +++ b/routers/web/org/org.go @@ -29,7 +29,7 @@ func Create(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_org") ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode if !ctx.Doer.CanCreateOrganization() { - ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed"))) + ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed"))) return } ctx.HTML(http.StatusOK, tplCreateOrg) @@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_org") if !ctx.Doer.CanCreateOrganization() { - ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed"))) + ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed"))) return } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 03798a712c..f062127d24 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -353,7 +353,7 @@ func ViewProject(ctx *context.Context) { } if boards[0].ID == 0 { - boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") + boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized") } issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) @@ -377,16 +377,16 @@ func ViewProject(ctx *context.Context) { linkedPrsMap := make(map[int64][]*issues_model.Issue) for _, issuesList := range issuesMap { for _, issue := range issuesList { - var referencedIds []int64 + var referencedIDs []int64 for _, comment := range issue.Comments { if comment.RefIssueID != 0 && comment.RefIsPull { - referencedIds = append(referencedIds, comment.RefIssueID) + referencedIDs = append(referencedIDs, comment.RefIssueID) } } - if len(referencedIds) > 0 { + if len(referencedIDs) > 0 { if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ - IssueIDs: referencedIds, + IssueIDs: referencedIDs, IsPull: util.OptionalBoolTrue, }); err == nil { linkedPrsMap[issue.ID] = linkedPrs @@ -679,7 +679,7 @@ func MoveIssues(ctx *context.Context) { board = &project_model.Board{ ID: 0, ProjectID: project.ID, - Title: ctx.Tr("repo.projects.type.uncategorized"), + Title: ctx.Locale.TrString("repo.projects.type.uncategorized"), } } else { board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index fe528a483b..19aca26711 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -61,17 +61,17 @@ func List(ctx *context.Context) { var workflows []Workflow if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("IsEmpty", err) return } else if !empty { commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("GetBranchCommit", err) return } entries, err := actions.ListWorkflows(commit) if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("ListWorkflows", err) return } @@ -95,12 +95,12 @@ func List(ctx *context.Context) { workflow := Workflow{Entry: *entry} content, err := actions.GetContentFromEntry(entry) if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("GetContentFromEntry", err) return } wf, err := model.ReadWorkflow(bytes.NewReader(content)) if err != nil { - workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error()) + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) workflows = append(workflows, workflow) continue } @@ -115,7 +115,7 @@ func List(ctx *context.Context) { continue } if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro) + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) break } } @@ -172,7 +172,7 @@ func List(ctx *context.Context) { runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("FindAndCount", err) return } @@ -181,7 +181,7 @@ func List(ctx *context.Context) { } if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("LoadTriggerUser", err) return } @@ -189,7 +189,7 @@ func List(ctx *context.Context) { actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.ServerError("GetActors", err) return } ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx.Doer, actors) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 63e3a352a5..05675d73c0 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -1,4 +1,5 @@ // Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions @@ -35,13 +36,19 @@ func View(ctx *context_module.Context) { ctx.Data["PageIsActions"] = true runIndex := ctx.ParamsInt64("run") jobIndex := ctx.ParamsInt64("job") + + job, _ := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + + workflowName := job.Run.WorkflowID + ctx.Data["RunIndex"] = runIndex ctx.Data["JobIndex"] = jobIndex ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions" - - if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() { - return - } + ctx.Data["WorkflowName"] = workflowName + ctx.Data["WorkflowURL"] = ctx.Repo.RepoLink + "/actions?workflow=" + workflowName ctx.HTML(http.StatusOK, tplViewActions) } @@ -60,6 +67,33 @@ func ViewLatest(ctx *context_module.Context) { ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect) } +func ViewLatestWorkflowRun(ctx *context_module.Context) { + branch := ctx.FormString("branch") + if branch == "" { + branch = ctx.Repo.Repository.DefaultBranch + } + branch = fmt.Sprintf("refs/heads/%s", branch) + event := ctx.FormString("event") + + workflowFile := ctx.Params("workflow_name") + run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound("GetLatestRunForBranchAndWorkflow", err) + } else { + ctx.ServerError("GetLatestRunForBranchAndWorkflow", err) + } + return + } + + err = run.LoadAttributes(ctx) + if err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect) +} + type ViewRequest struct { LogCursors []struct { Step int `json:"step"` @@ -71,15 +105,16 @@ type ViewRequest struct { type ViewResponse struct { State struct { Run struct { - Link string `json:"link"` - Title string `json:"title"` - Status string `json:"status"` - CanCancel bool `json:"canCancel"` - CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve - CanRerun bool `json:"canRerun"` - Done bool `json:"done"` - Jobs []*ViewJob `json:"jobs"` - Commit ViewCommit `json:"commit"` + Link string `json:"link"` + Title string `json:"title"` + Status string `json:"status"` + CanCancel bool `json:"canCancel"` + CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve + CanRerun bool `json:"canRerun"` + CanDeleteArtifact bool `json:"canDeleteArtifact"` + Done bool `json:"done"` + Jobs []*ViewJob `json:"jobs"` + Commit ViewCommit `json:"commit"` } `json:"run"` CurrentJob struct { Title string `json:"title"` @@ -103,6 +138,7 @@ type ViewJob struct { type ViewCommit struct { LocaleCommit string `json:"localeCommit"` LocalePushedBy string `json:"localePushedBy"` + LocaleWorkflow string `json:"localeWorkflow"` ShortSha string `json:"shortSHA"` Link string `json:"link"` Pusher ViewUser `json:"pusher"` @@ -160,6 +196,7 @@ func ViewPost(ctx *context_module.Context) { resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) + resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.Done = run.Status.IsDone() resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json resp.State.Run.Status = run.Status.String() @@ -182,8 +219,9 @@ func ViewPost(ctx *context_module.Context) { Link: run.RefLink(), } resp.State.Run.Commit = ViewCommit{ - LocaleCommit: ctx.Tr("actions.runs.commit"), - LocalePushedBy: ctx.Tr("actions.runs.pushed_by"), + LocaleCommit: ctx.Locale.TrString("actions.runs.commit"), + LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"), + LocaleWorkflow: ctx.Locale.TrString("actions.runs.workflow"), ShortSha: base.ShortSha(run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Pusher: pusher, @@ -208,7 +246,7 @@ func ViewPost(ctx *context_module.Context) { resp.State.CurrentJob.Title = current.Name resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale) if run.NeedApproval { - resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc") + resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc") } resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json @@ -549,6 +587,24 @@ func ArtifactsView(ctx *context_module.Context) { ctx.JSON(http.StatusOK, artifactsResponse) } +func ArtifactsDeleteView(ctx *context_module.Context) { + runIndex := ctx.ParamsInt64("run") + artifactName := ctx.Params("artifact_name") + + run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) + if err != nil { + ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } + if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + ctx.JSON(http.StatusOK, struct{}{}) +} + func ArtifactsDownloadView(ctx *context_module.Context) { runIndex := ctx.ParamsInt64("run") artifactName := ctx.Params("artifact_name") @@ -576,6 +632,14 @@ func ArtifactsDownloadView(ctx *context_module.Context) { return } + // if artifacts status is not uploaded-confirmed, treat it as not found + for _, art := range artifacts { + if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) { + ctx.Error(http.StatusNotFound, "artifact not found") + return + } + } + ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) writer := zip.NewWriter(ctx.Resp) diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go index 3d030edaca..af99c4ed98 100644 --- a/routers/web/repo/activity.go +++ b/routers/web/repo/activity.go @@ -22,6 +22,8 @@ func Activity(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.activity") ctx.Data["PageIsActivity"] = true + ctx.Data["PageIsPulse"] = true + ctx.Data["Period"] = ctx.Params("period") timeUntil := time.Now() diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index fa9dd8a08c..2c938dc966 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -16,9 +16,11 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + files_service "code.gitea.io/gitea/services/repository/files" ) type blameRow struct { @@ -68,6 +70,14 @@ func RefBlame(ctx *context.Context) { ctx.Data["FileSize"] = blob.Size() ctx.Data["FileName"] = blob.Name() + // Do not display a blame view if the size of the file is + // larger than what is configured as the maximum. + if blob.Size() >= setting.UI.MaxDisplayFileSize { + ctx.Data["IsFileTooLarge"] = true + ctx.HTML(http.StatusOK, tplRepoHome) + return + } + ctx.Data["NumLinesSet"] = true ctx.Data["NumLines"], err = blob.GetBlobLineCount() if err != nil { @@ -102,10 +112,7 @@ type blameResult struct { func performBlame(ctx *context.Context, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) { repoPath := ctx.Repo.Repository.RepoPath() - objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat() - if err != nil { - return nil, err - } + objectFormat := ctx.Repo.GetObjectFormat() blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore) if err != nil { @@ -218,31 +225,11 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) { repoLink := ctx.Repo.RepoLink - language := "" - - indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID) - if err == nil { - defer deleteTemporaryFile() - - filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{ - CachedOnly: true, - Attributes: []string{"linguist-language", "gitlab-language"}, - Filenames: []string{ctx.Repo.TreePath}, - IndexFile: indexFilename, - WorkTree: worktree, - }) - if err != nil { - log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) - } - - language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"] - if language == "" || language == "unspecified" { - language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"] - } - if language == "unspecified" { - language = "" - } + language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) + if err != nil { + log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) } + lines := make([]string, 0) rows := make([]*blameRow, 0) escapeStatus := &charset.EscapeStatus{} @@ -298,7 +285,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames lexerName = lexerNameForLine } - br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale) + br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale, charset.FileviewContext) rows = append(rows, br) escapeStatus = escapeStatus.Or(br.EscapeStatus) } diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 25dd881219..63516bb4d9 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) { message := strings.TrimSpace(form.CommitSummary) if message == "" { if form.Revert { - message = ctx.Tr("repo.commit.revert-header", sha) + message = ctx.Locale.TrString("repo.commit.revert-header", sha) } else { - message = ctx.Tr("repo.commit.cherry-pick-header", sha) + message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha) } } @@ -115,11 +115,18 @@ func CherryPickPost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitIdentity := getGitIdentity(ctx, form.CommitMailID, tplCherryPick, &form) + if ctx.Written() { + return + } + opts := &files.ApplyDiffPatchOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, NewBranch: branchName, Message: message, + Author: gitIdentity, + Committer: gitIdentity, } // First lets try the simple plain read-tree -m approach diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go new file mode 100644 index 0000000000..48ade655b7 --- /dev/null +++ b/routers/web/repo/code_frequency.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + contributors_service "code.gitea.io/gitea/services/repository" +) + +const ( + tplCodeFrequency base.TplName = "repo/activity" +) + +// CodeFrequency renders the page to show repository code frequency +func CodeFrequency(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency") + + ctx.Data["PageIsActivity"] = true + ctx.Data["PageIsCodeFrequency"] = true + ctx.PageData["repoLink"] = ctx.Repo.RepoLink + + ctx.HTML(http.StatusOK, tplCodeFrequency) +} + +// CodeFrequencyData returns JSON of code frequency data +func CodeFrequencyData(ctx *context.Context) { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if errors.Is(err, contributors_service.ErrAwaitGeneration) { + ctx.Status(http.StatusAccepted) + return + } + ctx.ServerError("GetCodeFrequencyData", err) + } else { + ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) + } +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index a3593815b8..535487d5fd 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/typesniffer" @@ -126,7 +127,7 @@ func setCsvCompareContext(ctx *context.Context) { return CsvDiffResult{nil, ""} } - errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large")) + errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large")) csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) { if blob == nil { @@ -311,14 +312,14 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch) baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch) baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch) - objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat() + if !baseIsCommit && !baseIsBranch && !baseIsTag { // Check if baseBranch is short sha commit hash if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil { ci.BaseBranch = baseCommit.ID.String() ctx.Data["BaseBranch"] = ci.BaseBranch baseIsCommit = true - } else if ci.BaseBranch == objectFormat.EmptyObjectID().String() { + } else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() { if isSameRepo { ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch)) } else { @@ -700,7 +701,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor ListOptions: db.ListOptions{ ListAll: true, }, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }) if err != nil { return nil, nil, err @@ -757,7 +758,7 @@ func CompareDiff(ctx *context.Context) { ListOptions: db.ListOptions{ ListAll: true, }, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }) if err != nil { ctx.ServerError("GetBranches", err) diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go new file mode 100644 index 0000000000..bcfef7580a --- /dev/null +++ b/routers/web/repo/contributors.go @@ -0,0 +1,44 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + contributors_service "code.gitea.io/gitea/services/repository" +) + +const ( + tplContributors base.TplName = "repo/activity" +) + +// Contributors render the page to show repository contributors graph +func Contributors(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors") + + ctx.Data["PageIsActivity"] = true + ctx.Data["PageIsContributors"] = true + + ctx.PageData["contributionType"] = "commits" + + ctx.PageData["repoLink"] = ctx.Repo.RepoLink + + ctx.HTML(http.StatusOK, tplContributors) +} + +// ContributorsData renders JSON of contributors along with their weekly commit statistics +func ContributorsData(ctx *context.Context) { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if errors.Is(err, contributors_service.ErrAwaitGeneration) { + ctx.Status(http.StatusAccepted) + return + } + ctx.ServerError("GetContributorStats", err) + } else { + ctx.JSON(http.StatusOK, contributorStats) + } +} diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 39d9967d02..075477e5f0 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -121,6 +121,18 @@ func getSelectableEmailAddresses(ctx *context.Context) ([]*user_model.ActivatedE return commitEmails, nil } +// CommonEditorData sets common context data that is used by the editor. +func CommonEditorData(ctx *context.Context) { + // Set context for selectable email addresses. + commitEmails, err := getSelectableEmailAddresses(ctx) + if err != nil { + ctx.ServerError("getSelectableEmailAddresses", err) + return + } + ctx.Data["CommitMails"] = commitEmails + ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() +} + func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile @@ -183,9 +195,6 @@ func editFile(ctx *context.Context, isNewFile bool) { } d, _ := io.ReadAll(dataRc) - if err := dataRc.Close(); err != nil { - log.Error("Error whilst closing blob data: %v", err) - } buf = append(buf, d...) if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { @@ -199,12 +208,6 @@ func editFile(ctx *context.Context, isNewFile bool) { treeNames = append(treeNames, fileName) } - commitEmails, err := getSelectableEmailAddresses(ctx) - if err != nil { - ctx.ServerError("getSelectableEmailAddresses", err) - return - } - ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() @@ -220,8 +223,6 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) - ctx.Data["CommitMails"] = commitEmails - ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() ctx.HTML(http.StatusOK, tplEditFile) } @@ -257,12 +258,6 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b branchName = form.NewBranchName } - commitEmails, err := getSelectableEmailAddresses(ctx) - if err != nil { - ctx.ServerError("getSelectableEmailAddresses", err) - return - } - ctx.Data["PageIsEdit"] = true ctx.Data["PageHasPosted"] = true ctx.Data["IsNewFile"] = isNewFile @@ -279,8 +274,6 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) - ctx.Data["CommitMails"] = commitEmails - ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail() if ctx.HasError() { ctx.HTML(http.StatusOK, tplEditFile) @@ -300,9 +293,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b message := strings.TrimSpace(form.CommitSummary) if len(message) == 0 { if isNewFile { - message = ctx.Tr("repo.editor.add", form.TreePath) + message = ctx.Locale.TrString("repo.editor.add", form.TreePath) } else { - message = ctx.Tr("repo.editor.update", form.TreePath) + message = ctx.Locale.TrString("repo.editor.update", form.TreePath) } } form.CommitMessage = strings.TrimSpace(form.CommitMessage) @@ -315,28 +308,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation = "create" } - gitIdentity := &files_service.IdentityOptions{ - Name: ctx.Doer.Name, - } - - // -1 is defined as placeholder email. - if form.CommitMailID == -1 { - gitIdentity.Email = ctx.Doer.GetPlaceholderEmail() - } else { - // Check if the given email is activated. - email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, form.CommitMailID) - if err != nil { - ctx.ServerError("GetEmailAddressByID", err) - return - } - - if email == nil || !email.IsActivated { - ctx.Data["Err_CommitMailID"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, &form) - return - } - - gitIdentity.Email = email.Email + gitIdentity := getGitIdentity(ctx, form.CommitMailID, tplEditFile, form) + if ctx.Written() { + return } if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ @@ -479,7 +453,7 @@ func DiffPreviewPost(ctx *context.Context) { } if diff.NumFiles == 0 { - ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show")) + ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show")) return } ctx.Data["File"] = diff.Files[0] @@ -546,13 +520,18 @@ func DeleteFilePost(ctx *context.Context) { message := strings.TrimSpace(form.CommitSummary) if len(message) == 0 { - message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath) + message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath) } form.CommitMessage = strings.TrimSpace(form.CommitMessage) if len(form.CommitMessage) > 0 { message += "\n\n" + form.CommitMessage } + gitIdentity := getGitIdentity(ctx, form.CommitMailID, tplDeleteFile, &form) + if ctx.Written() { + return + } + if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, @@ -563,8 +542,10 @@ func DeleteFilePost(ctx *context.Context) { TreePath: ctx.Repo.TreePath, }, }, - Message: message, - Signoff: form.Signoff, + Message: message, + Signoff: form.Signoff, + Author: gitIdentity, + Committer: gitIdentity, }); err != nil { // This is where we handle all the errors thrown by repofiles.DeleteRepoFile if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) { @@ -755,7 +736,7 @@ func UploadFilePost(ctx *context.Context) { if dir == "" { dir = "/" } - message = ctx.Tr("repo.editor.upload_files_to_dir", dir) + message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir) } form.CommitMessage = strings.TrimSpace(form.CommitMessage) @@ -763,6 +744,11 @@ func UploadFilePost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitIdentity := getGitIdentity(ctx, form.CommitMailID, tplUploadFile, &form) + if ctx.Written() { + return + } + if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ LastCommitID: ctx.Repo.CommitID, OldBranch: oldBranchName, @@ -771,6 +757,8 @@ func UploadFilePost(ctx *context.Context) { Message: message, Files: form.Files, Signoff: form.Signoff, + Author: gitIdentity, + Committer: gitIdentity, }); err != nil { if git_model.IsErrLFSFileLocked(err) { ctx.Data["Err_TreePath"] = true @@ -941,3 +929,33 @@ func GetClosestParentWithFiles(treePath string, commit *git.Commit) string { } return treePath } + +// getGitIdentity returns the Git identity that should be used for an Git +// operation, that takes into account an user's specified email. +func getGitIdentity(ctx *context.Context, commitMailID int64, tpl base.TplName, form any) *files_service.IdentityOptions { + gitIdentity := &files_service.IdentityOptions{ + Name: ctx.Doer.Name, + } + + // -1 is defined as placeholder email. + if commitMailID == -1 { + gitIdentity.Email = ctx.Doer.GetPlaceholderEmail() + } else { + // Check if the given email is activated. + email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, commitMailID) + if err != nil { + ctx.ServerError("GetEmailAddressByID", err) + return nil + } + + if email == nil || !email.IsActivated { + ctx.Data["Err_CommitMailID"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, form) + return nil + } + + gitIdentity.Email = email.Email + } + + return gitIdentity +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 3e7b099bba..963c6289b2 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" issue_template "code.gitea.io/gitea/modules/issue/template" @@ -717,16 +718,12 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is tmp.ItemID = -review.ReviewerTeamID } - if ctx.Repo.IsAdmin() { - // Admin can dismiss or re-request any review requests + if canChooseReviewer { + // Users who can choose reviewers can also remove review requests tmp.CanChange = true } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest { // A user can refuse review requests tmp.CanChange = true - } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest && - ctx.Doer.ID != review.ReviewerID { - // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers - tmp.CanChange = true } pullReviews = append(pullReviews, tmp) @@ -1042,7 +1039,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string }) if err != nil { log.Debug("render flash error: %v", err) - flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates") + flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates") } return flashError } @@ -1448,7 +1445,7 @@ func ViewIssue(ctx *context.Context) { return } - ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) + ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title)) iw := new(issues_model.IssueWatch) if ctx.Doer != nil { @@ -1534,18 +1531,9 @@ func ViewIssue(ctx *context.Context) { } if issue.IsPull { - canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests) + canChooseReviewer := false if ctx.Doer != nil && ctx.IsSigned { - if !canChooseReviewer { - canChooseReviewer = ctx.Doer.ID == issue.PosterID - } - if !canChooseReviewer { - canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer) - if err != nil { - ctx.ServerError("IsOfficialReviewer", err) - return - } - } + canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue) } RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer) @@ -1664,7 +1652,7 @@ func ViewIssue(ctx *context.Context) { } ghostMilestone := &issues_model.Milestone{ ID: -1, - Name: ctx.Tr("repo.issues.deleted_milestone"), + Name: ctx.Locale.TrString("repo.issues.deleted_milestone"), } if comment.OldMilestoneID > 0 && comment.OldMilestone == nil { comment.OldMilestone = ghostMilestone @@ -1681,7 +1669,7 @@ func ViewIssue(ctx *context.Context) { ghostProject := &project_model.Project{ ID: -1, - Title: ctx.Tr("repo.issues.deleted_project"), + Title: ctx.Locale.TrString("repo.issues.deleted_project"), } if comment.OldProjectID > 0 && comment.OldProject == nil { @@ -1739,6 +1727,10 @@ func ViewIssue(ctx *context.Context) { for _, codeComments := range comment.Review.CodeComments { for _, lineComments := range codeComments { for _, c := range lineComments { + if err := c.LoadAttachments(ctx); err != nil { + ctx.ServerError("LoadAttachments", err) + return + } // Check tag. role, ok = marked[c.PosterID] if ok { @@ -1871,6 +1863,8 @@ func ViewIssue(ctx *context.Context) { mergeStyle = repo_model.MergeStyleRebaseMerge } else if prConfig.AllowSquash { mergeStyle = repo_model.MergeStyleSquash + } else if prConfig.AllowFastForwardOnly { + mergeStyle = repo_model.MergeStyleFastForwardOnly } else if prConfig.AllowManualMerge { mergeStyle = repo_model.MergeStyleManuallyMerged } @@ -3026,27 +3020,34 @@ func NewComment(ctx *context.Context) { // check whether the ref of PR in base repo is consistent with the head commit of head branch in the head repo // get head commit of PR if pull.Flow == issues_model.PullRequestFlowGithub { - prHeadRef := pull.GetGitRefName() if err := pull.LoadBaseRepo(ctx); err != nil { ctx.ServerError("Unable to load base repo", err) return } + if err := pull.LoadHeadRepo(ctx); err != nil { + ctx.ServerError("Unable to load head repo", err) + return + } + + // Check if the base branch of the pull request still exists. + if ok := git.IsBranchExist(ctx, pull.BaseRepo.RepoPath(), pull.BaseBranch); !ok { + ctx.JSONError(ctx.Tr("repo.pulls.reopen_failed.base_branch")) + return + } + + // Check if the head branch of the pull request still exists. + if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch); !ok { + ctx.JSONError(ctx.Tr("repo.pulls.reopen_failed.head_branch")) + return + } + + prHeadRef := pull.GetGitRefName() prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef) if err != nil { ctx.ServerError("Get head commit Id of pr fail", err) return } - // get head commit of branch in the head repo - if err := pull.LoadHeadRepo(ctx); err != nil { - ctx.ServerError("Unable to load head repo", err) - return - } - if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok { - // todo localize - ctx.JSONError("The origin branch is delete, cannot reopen.") - return - } headBranchRef := pull.GetGitHeadBranchRefName() headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef) if err != nil { diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index 31e6ac608c..4dc537a06e 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) { for _, item := range items { var actionText string if item.IsDeleted { - actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted") + actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted") actionText = "" + actionTextDeleted + "" } else if item.IsFirstCreated { - actionText = ctx.Locale.Tr("repo.issues.content_history.created") + actionText = ctx.Locale.TrString("repo.issues.content_history.created") } else { - actionText = ctx.Locale.Tr("repo.issues.content_history.edited") + actionText = ctx.Locale.TrString("repo.issues.content_history.edited") } username := item.UserName diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index e0d49e44e1..742f12114d 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) { assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2}) - assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) + assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) } func TestUpdateIssueLabel_Clear(t *testing.T) { diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index c04435cf1b..03ea03467a 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) { // `message` will be both the summary and message combined message := strings.TrimSpace(form.CommitSummary) if len(message) == 0 { - message = ctx.Tr("repo.editor.patch") + message = ctx.Locale.TrString("repo.editor.patch") } form.CommitMessage = strings.TrimSpace(form.CommitMessage) @@ -87,12 +87,19 @@ func NewDiffPatchPost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitIdenitity := getGitIdentity(ctx, form.CommitMailID, tplPatchFile, &form) + if ctx.Written() { + return + } + fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, NewBranch: branchName, Message: message, Content: strings.ReplaceAll(form.Content, "\r", ""), + Author: gitIdenitity, + Committer: gitIdenitity, }) if err != nil { if git_model.IsErrBranchAlreadyExists(err) { diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 4908bb796d..cc0127e7e1 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -315,7 +315,7 @@ func ViewProject(ctx *context.Context) { } if boards[0].ID == 0 { - boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") + boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized") } issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) @@ -339,16 +339,16 @@ func ViewProject(ctx *context.Context) { linkedPrsMap := make(map[int64][]*issues_model.Issue) for _, issuesList := range issuesMap { for _, issue := range issuesList { - var referencedIds []int64 + var referencedIDs []int64 for _, comment := range issue.Comments { if comment.RefIssueID != 0 && comment.RefIsPull { - referencedIds = append(referencedIds, comment.RefIssueID) + referencedIDs = append(referencedIDs, comment.RefIssueID) } } - if len(referencedIds) > 0 { + if len(referencedIDs) > 0 { if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ - IssueIDs: referencedIds, + IssueIDs: referencedIDs, IsPull: util.OptionalBoolTrue, }); err == nil { linkedPrsMap[issue.ID] = linkedPrs @@ -633,7 +633,7 @@ func MoveIssues(ctx *context.Context) { board = &project_model.Board{ ID: 0, ProjectID: project.ID, - Title: ctx.Tr("repo.projects.type.uncategorized"), + Title: ctx.Locale.TrString("repo.projects.type.uncategorized"), } } else { board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 7830c17ced..561039c413 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1,5 +1,6 @@ -// Copyright 2018 The Gitea Authors. -// Copyright 2014 The Gogs Authors. +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // All rights reserved. // SPDX-License-Identifier: MIT @@ -28,10 +29,12 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" issue_template "code.gitea.io/gitea/modules/issue/template" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/upload" @@ -115,21 +118,21 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository { return repo } -func getForkRepository(ctx *context.Context) *repo_model.Repository { - forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid")) - if ctx.Written() { - return nil +func updateForkRepositoryInContext(ctx *context.Context, forkRepo *repo_model.Repository) bool { + if forkRepo == nil { + ctx.NotFound("No repository in context", nil) + return false } if forkRepo.IsEmpty { log.Trace("Empty repository %-v", forkRepo) - ctx.NotFound("getForkRepository", nil) - return nil + ctx.NotFound("updateForkRepositoryInContext", nil) + return false } if err := forkRepo.LoadOwner(ctx); err != nil { ctx.ServerError("LoadOwner", err) - return nil + return false } ctx.Data["repo_name"] = forkRepo.Name @@ -142,7 +145,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID) if err != nil { ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) - return nil + return false } var orgs []*organization.Organization for _, org := range ownedOrgs { @@ -170,7 +173,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID) if err != nil { ctx.ServerError("GetRepositoryByID", err) - return nil + return false } } @@ -184,7 +187,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { } else { ctx.Data["CanForkRepo"] = false ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true) - return nil + return false } branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ @@ -192,20 +195,25 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { ListOptions: db.ListOptions{ ListAll: true, }, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), // Add it as the first option ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch}, }) if err != nil { ctx.ServerError("FindBranchNames", err) - return nil + return false } ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) - return forkRepo + return true } -// Fork render repository fork page +// ForkByID redirects (with 301 Moved Permanently) to the repository's `/fork` page +func ForkByID(ctx *context.Context) { + ctx.Redirect(ctx.Repo.Repository.Link()+"/fork", http.StatusMovedPermanently) +} + +// Fork renders the repository fork page func Fork(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_fork") @@ -217,8 +225,7 @@ func Fork(ctx *context.Context) { ctx.Flash.Error(msg, true) } - getForkRepository(ctx) - if ctx.Written() { + if !updateForkRepositoryInContext(ctx, ctx.Repo.Repository) { return } @@ -236,8 +243,8 @@ func ForkPost(ctx *context.Context) { return } - forkRepo := getForkRepository(ctx) - if ctx.Written() { + forkRepo := ctx.Repo.Repository + if !updateForkRepositoryInContext(ctx, forkRepo) { return } @@ -340,7 +347,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { ctx.ServerError("LoadRepo", err) return nil, false } - ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) + ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title)) ctx.Data["Issue"] = issue if !issue.IsPull { @@ -377,6 +384,11 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) { } else { ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch } + + if pull.Flow == issues_model.PullRequestFlowAGit { + ctx.Data["MadeUsingAGit"] = true + } + ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["HeadBranchLink"] = pull.GetHeadBranchLink(ctx) ctx.Data["BaseBranchLink"] = pull.GetBaseBranchLink(ctx) @@ -658,6 +670,24 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C } if pb != nil && pb.EnableStatusCheck { + + var missingRequiredChecks []string + for _, requiredContext := range pb.StatusCheckContexts { + contextFound := false + matchesRequiredContext := createRequiredContextMatcher(requiredContext) + for _, presentStatus := range commitStatuses { + if matchesRequiredContext(presentStatus.Context) { + contextFound = true + break + } + } + + if !contextFound { + missingRequiredChecks = append(missingRequiredChecks, requiredContext) + } + } + ctx.Data["MissingRequiredChecks"] = missingRequiredChecks + ctx.Data["is_context_required"] = func(context string) bool { for _, c := range pb.StatusCheckContexts { if c == context { @@ -726,10 +756,22 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C return compareInfo } +func createRequiredContextMatcher(requiredContext string) func(string) bool { + if gp, err := glob.Compile(requiredContext); err == nil { + return func(contextToCheck string) bool { + return gp.Match(contextToCheck) + } + } + + return func(contextToCheck string) bool { + return requiredContext == contextToCheck + } +} + type pullCommitList struct { Commits []pull_service.CommitInfo `json:"commits"` LastReviewCommitSha string `json:"last_review_commit_sha"` - Locale map[string]string `json:"locale"` + Locale map[string]any `json:"locale"` } // GetPullCommits get all commits for given pull request @@ -747,7 +789,7 @@ func GetPullCommits(ctx *context.Context) { } // Get the needed locale - resp.Locale = map[string]string{ + resp.Locale = map[string]any{ "lang": ctx.Locale.Language(), "show_all_commits": ctx.Tr("repo.pulls.show_all_commits"), "stats_num_commits": ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)), @@ -944,6 +986,21 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi return } + for _, file := range diff.Files { + for _, section := range file.Sections { + for _, line := range section.Lines { + for _, comments := range line.Conversations { + for _, comment := range comments { + if err := comment.LoadAttachments(ctx); err != nil { + ctx.ServerError("LoadAttachments", err) + return + } + } + } + } + } + } + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) if err != nil { ctx.ServerError("LoadProtectedBranch", err) @@ -1271,19 +1328,19 @@ func MergePullRequest(ctx *context.Context) { return } ctx.Flash.Error(flashError) - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } else if models.IsErrMergeUnrelatedHistories(err) { log.Debug("MergeUnrelatedHistories error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories")) - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } else if git.IsErrPushOutOfDate(err) { log.Debug("MergePushOutOfDate error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date")) - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } else if models.IsErrSHADoesNotMatch(err) { log.Debug("MergeHeadOutOfDate error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date")) - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } else if git.IsErrPushRejected(err) { log.Debug("MergePushRejected error: %v", err) pushrejErr := err.(*git.ErrPushRejected) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index e399176a4a..798d3d5454 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" pull_service "code.gitea.io/gitea/services/pull" @@ -49,6 +50,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) { return } ctx.Data["AfterCommitID"] = pullHeadCommitID + ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled + upload.AddUploadContext(ctx, "comment") ctx.HTML(http.StatusOK, tplNewComment) } @@ -74,6 +77,11 @@ func CreateCodeComment(ctx *context.Context) { signedLine *= -1 } + var attachments []string + if setting.Attachment.Enabled { + attachments = form.Files + } + comment, err := pull_service.CreateCodeComment(ctx, ctx.Doer, ctx.Repo.GitRepo, @@ -84,6 +92,7 @@ func CreateCodeComment(ctx *context.Context) { !form.SingleReview, form.Reply, form.LatestCommitID, + attachments, ) if err != nil { ctx.ServerError("CreateCodeComment", err) @@ -153,12 +162,23 @@ func UpdateResolveConversation(ctx *context.Context) { } func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) { - comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, true) + comments, err := issues_model.FetchCodeConversation(ctx, comment, ctx.Doer) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) return } ctx.Data["PageIsPullFiles"] = (origin == "diff") + + for _, c := range comments { + if err := c.LoadAttachments(ctx); err != nil { + ctx.ServerError("LoadAttachments", err) + return + } + } + + ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled + upload.AddUploadContext(ctx, "comment") + ctx.Data["comments"] = comments if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer); err != nil { ctx.ServerError("CanMarkConversation", err) @@ -209,9 +229,9 @@ func SubmitReview(ctx *context.Context) { if issue.IsPoster(ctx.Doer.ID) { var translated string if reviewType == issues_model.ReviewTypeApprove { - translated = ctx.Tr("repo.issues.review.self.approval") + translated = ctx.Locale.TrString("repo.issues.review.self.approval") } else { - translated = ctx.Tr("repo.issues.review.self.rejection") + translated = ctx.Locale.TrString("repo.issues.review.self.rejection") } ctx.Flash.Error(translated) diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go new file mode 100644 index 0000000000..68f68d7bb0 --- /dev/null +++ b/routers/web/repo/pull_review_test.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http/httptest" + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/contexttest" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/pull" + + "github.com/stretchr/testify/assert" +) + +func TestRenderConversation(t *testing.T) { + unittest.PrepareTestEnv(t) + + pr, _ := issues_model.GetPullRequestByID(db.DefaultContext, 2) + _ = pr.LoadIssue(db.DefaultContext) + _ = pr.Issue.LoadPoster(db.DefaultContext) + _ = pr.Issue.LoadRepo(db.DefaultContext) + + run := func(name string, cb func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder)) { + t.Run(name, func(t *testing.T) { + ctx, resp := contexttest.MockContext(t, "/") + ctx.Render = templates.HTMLRenderer() + contexttest.LoadUser(t, ctx, pr.Issue.PosterID) + contexttest.LoadRepo(t, ctx, pr.BaseRepoID) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + cb(t, ctx, resp) + }) + } + + var preparedComment *issues_model.Comment + run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { + comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID, nil) + if !assert.NoError(t, err) { + return + } + comment.Invalidated = true + err = issues_model.UpdateCommentInvalidate(ctx, comment) + if !assert.NoError(t, err) { + return + } + preparedComment = comment + }) + if !assert.NotNil(t, preparedComment) { + return + } + run("diff with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { + ctx.Data["ShowOutdatedComments"] = true + renderConversation(ctx, preparedComment, "diff") + assert.Contains(t, resp.Body.String(), `
setting.Avatar.MaxFileSize { - return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) + return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) } data, err := io.ReadAll(r) @@ -47,7 +47,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error { } st := typesniffer.DetectContentType(data) if !(st.IsImage() && !st.IsSvgImage()) { - return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image")) } if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil { return fmt.Errorf("UploadAvatar: %w", err) diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index cd0f11d548..53e7b22d1b 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -307,7 +307,7 @@ func LFSFileGet(ctx *context.Context) { // Building code view blocks with line number on server side. escapedContent := &bytes.Buffer{} - ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale) + ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale, charset.FileviewContext) var output bytes.Buffer lines := strings.Split(escapedContent.String(), "\n") @@ -388,7 +388,7 @@ func LFSFileFind(ctx *context.Context) { sha := ctx.FormString("sha") ctx.Data["Title"] = oid ctx.Data["PageIsSettingsLFS"] = true - objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat() + objectFormat := ctx.Repo.GetObjectFormat() var objectID git.ObjectID if len(sha) == 0 { pointer := lfs.Pointer{Oid: oid, Size: size} diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 98d6977b81..827ebea7f8 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -68,7 +68,7 @@ func SettingsProtectedBranch(c *context.Context) { } c.Data["PageIsSettingsBranches"] = true - c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName + c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName users, err := access_model.GetRepoReaders(c, c.Repo.Repository) if err != nil { @@ -132,12 +132,15 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } } } else { - // FIXME: If a new ProtectBranch has a duplicate RuleName, an error should be returned. - // Currently, if a new ProtectBranch with a duplicate RuleName is created, the existing ProtectBranch will be updated. - // But we cannot modify this logic now because many unit tests rely on it. + // Check if a rule already exists with this rulename, if so redirect to it. protectBranch, err = git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName) if err != nil { - ctx.ServerError("GetProtectBranchOfRepoByName", err) + ctx.ServerError("GetProtectedBranchRuleByName", err) + return + } + if protectBranch != nil { + ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_duplicate_rule_name")) + ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName)) return } } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 5eda0a0ff0..18c7a97fe6 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -26,7 +26,6 @@ import ( "code.gitea.io/gitea/modules/indexer/stats" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -43,6 +42,7 @@ import ( const ( tplSettingsOptions base.TplName = "repo/settings/options" + tplSettingsUnits base.TplName = "repo/settings/units" tplCollaboration base.TplName = "repo/settings/collaboration" tplBranches base.TplName = "repo/settings/branches" tplGithooks base.TplName = "repo/settings/githooks" @@ -91,6 +91,202 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["PushMirrors"] = pushMirrors } +// Units show a repositorys unit settings page +func Units(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.settings.units.units") + ctx.Data["PageIsRepoSettingsUnits"] = true + + ctx.HTML(http.StatusOK, tplSettingsUnits) +} + +func UnitsPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoUnitSettingForm) + + repo := ctx.Repo.Repository + + var repoChanged bool + var units []repo_model.RepoUnit + var deleteUnitTypes []unit_model.Type + + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil + + if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { + repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch + repoChanged = true + } + + if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeCode, + }) + } else if !unit_model.TypeCode.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) + } + + if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + if !validation.IsValidExternalURL(form.ExternalWikiURL) { + ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) + ctx.Redirect(repo.Link() + "/settings/units") + return + } + + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeExternalWiki, + Config: &repo_model.ExternalWikiConfig{ + ExternalWikiURL: form.ExternalWikiURL, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + var wikiPermissions repo_model.UnitAccessMode + if form.GloballyWriteableWiki { + wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead + } + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: wikiPermissions, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } else { + if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } + if !unit_model.TypeWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } + } + + if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + if !validation.IsValidExternalURL(form.ExternalTrackerURL) { + ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) + ctx.Redirect(repo.Link() + "/settings/units") + return + } + if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { + ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) + ctx.Redirect(repo.Link() + "/settings/units") + return + } + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeExternalTracker, + Config: &repo_model.ExternalTrackerConfig{ + ExternalTrackerURL: form.ExternalTrackerURL, + ExternalTrackerFormat: form.TrackerURLFormat, + ExternalTrackerStyle: form.TrackerIssueStyle, + ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeIssues, + Config: &repo_model.IssuesConfig{ + EnableTimetracker: form.EnableTimetracker, + AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, + EnableDependencies: form.EnableIssueDependencies, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } else { + if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } + if !unit_model.TypeIssues.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } + } + + if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeProjects, + }) + } else if !unit_model.TypeProjects.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) + } + + if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeReleases, + }) + } else if !unit_model.TypeReleases.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) + } + + if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypePackages, + }) + } else if !unit_model.TypePackages.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) + } + + if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeActions, + }) + } else if !unit_model.TypeActions.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) + } + + if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypePullRequests, + Config: &repo_model.PullRequestsConfig{ + IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, + AllowMerge: form.PullsAllowMerge, + AllowRebase: form.PullsAllowRebase, + AllowRebaseMerge: form.PullsAllowRebaseMerge, + AllowSquash: form.PullsAllowSquash, + AllowFastForwardOnly: form.PullsAllowFastForwardOnly, + AllowManualMerge: form.PullsAllowManualMerge, + AutodetectManualMerge: form.EnableAutodetectManualMerge, + AllowRebaseUpdate: form.PullsAllowRebaseUpdate, + DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, + DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), + DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, + }, + }) + } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) + } + + if len(units) == 0 { + ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/units") + return + } + + if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + ctx.ServerError("UpdateRepositoryUnits", err) + return + } + if repoChanged { + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + } + log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/units") +} + // Settings show a repository's settings page func Settings(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsOptions) @@ -473,188 +669,6 @@ func SettingsPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(repo.Link() + "/settings") - case "advanced": - var repoChanged bool - var units []repo_model.RepoUnit - var deleteUnitTypes []unit_model.Type - - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil - - if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { - repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch - repoChanged = true - } - - if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeCode, - }) - } else if !unit_model.TypeCode.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) - } - - if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalWikiURL) { - ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeExternalWiki, - Config: &repo_model.ExternalWikiConfig{ - ExternalWikiURL: form.ExternalWikiURL, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { - var wikiPermissions repo_model.UnitAccessMode - if form.GloballyWriteableWiki { - wikiPermissions = repo_model.UnitAccessModeWrite - } else { - wikiPermissions = repo_model.UnitAccessModeRead - } - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeWiki, - Config: new(repo_model.UnitConfig), - DefaultPermissions: wikiPermissions, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } else { - if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } - if !unit_model.TypeWiki.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } - } - - if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalTrackerURL) { - ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { - ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ - ExternalTrackerURL: form.ExternalTrackerURL, - ExternalTrackerFormat: form.TrackerURLFormat, - ExternalTrackerStyle: form.TrackerIssueStyle, - ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeIssues, - Config: &repo_model.IssuesConfig{ - EnableTimetracker: form.EnableTimetracker, - AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, - EnableDependencies: form.EnableIssueDependencies, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } else { - if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } - if !unit_model.TypeIssues.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } - } - - if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeProjects, - }) - } else if !unit_model.TypeProjects.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) - } - - if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeReleases, - }) - } else if !unit_model.TypeReleases.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) - } - - if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypePackages, - }) - } else if !unit_model.TypePackages.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) - } - - if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeActions, - }) - } else if !unit_model.TypeActions.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) - } - - if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypePullRequests, - Config: &repo_model.PullRequestsConfig{ - IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, - AllowMerge: form.PullsAllowMerge, - AllowRebase: form.PullsAllowRebase, - AllowRebaseMerge: form.PullsAllowRebaseMerge, - AllowSquash: form.PullsAllowSquash, - AllowManualMerge: form.PullsAllowManualMerge, - AutodetectManualMerge: form.EnableAutodetectManualMerge, - AllowRebaseUpdate: form.PullsAllowRebaseUpdate, - DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, - DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), - DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, - }, - }) - } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) - } - - if len(units) == 0 { - ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return - } - - if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { - ctx.ServerError("UpdateRepositoryUnits", err) - return - } - if repoChanged { - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) - return - } - } - log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - case "signing": changed := false trustModel := repo_model.ToTrustModel(form.TrustModel) @@ -738,7 +752,7 @@ func SettingsPost(ctx *context.Context) { } repo.IsMirror = false - if _, err := repo_module.CleanUpMigrateInfo(ctx, repo); err != nil { + if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil { ctx.ServerError("CleanUpMigrateInfo", err) return } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil { diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 2ae4cf9f16..7bbe4d81a9 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -151,6 +151,7 @@ func WebhooksNew(ctx *context.Context) { } } ctx.Data["BaseLink"] = orCtx.LinkNew + ctx.Data["BaseLinkNew"] = orCtx.LinkNew ctx.HTML(http.StatusOK, orCtx.NewTemplate) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 9dc708bca4..32c09bb5f2 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -49,6 +49,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" issue_service "code.gitea.io/gitea/services/issue" + files_service "code.gitea.io/gitea/services/repository/files" "github.com/nektos/act/pkg/model" @@ -113,7 +114,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try log.Debug("Potential readme file: %s", entry.Name()) if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { if entry.IsLink() { - target, err := entry.FollowLinks() + target, _, err := entry.FollowLinks() if err != nil && !git.IsErrBadLink(err) { return "", nil, err } else if target != nil && (target.IsExecutable() || target.IsRegular()) { @@ -266,7 +267,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { target := readmeFile if readmeFile != nil && readmeFile.IsLink() { - target, _ = readmeFile.FollowLinks() + target, _, _ = readmeFile.FollowLinks() } if target == nil { // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) @@ -337,7 +338,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr log.Error("Read readme content failed: %v", err) } contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content)) - ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) + ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale, charset.FileviewContext) } if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { @@ -390,6 +391,15 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileName"] = blob.Name() ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + if entry.IsLink() { + _, link, err := entry.FollowLinks() + // Errors should be allowed, because this shouldn't + // block rendering invalid symlink files. + if err == nil { + ctx.Data["SymlinkURL"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(link) + } + } + commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetCommitByPath", err) @@ -557,31 +567,11 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { } ctx.Data["NumLinesSet"] = true - language := "" - - indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID) - if err == nil { - defer deleteTemporaryFile() - - filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{ - CachedOnly: true, - Attributes: []string{"linguist-language", "gitlab-language"}, - Filenames: []string{ctx.Repo.TreePath}, - IndexFile: indexFilename, - WorkTree: worktree, - }) - if err != nil { - log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) - } - - language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"] - if language == "" || language == "unspecified" { - language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"] - } - if language == "unspecified" { - language = "" - } + language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) + if err != nil { + log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) } + fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) ctx.Data["LexerName"] = lexerName if err != nil { @@ -591,7 +581,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { status := &charset.EscapeStatus{} statuses := make([]*charset.EscapeStatus, len(fileContent)) for i, line := range fileContent { - statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) + statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale, charset.FileviewContext) status = status.Or(statuses[i]) } ctx.Data["EscapeStatus"] = status @@ -697,7 +687,7 @@ func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input i go func() { sb := &strings.Builder{} // We allow NBSP here this is rendered - escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) + escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.FileviewContext, charset.RuneNBSP) output = template.HTML(sb.String()) close(done) }() @@ -762,7 +752,7 @@ func checkHomeCodeViewable(ctx *context.Context) { } } - ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo"))) + ctx.NotFound("Home", fmt.Errorf(ctx.Locale.TrString("units.error.no_unit_allowed_repo"))) } func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 2c445fe7fa..157ebd4d5d 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -254,7 +254,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { done := make(chan struct{}) go func() { // We allow NBSP here this is rendered - escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP) + escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.WikiContext, charset.RuneNBSP) output = buf.String() buf.Reset() close(done) @@ -714,7 +714,7 @@ func NewWikiPost(ctx *context.Context) { wikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { - form.Message = ctx.Tr("repo.editor.add", form.Title) + form.Message = ctx.Locale.TrString("repo.editor.add", form.Title) } if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil { @@ -766,7 +766,7 @@ func EditWikiPost(ctx *context.Context) { newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { - form.Message = ctx.Tr("repo.editor.update", form.Title) + form.Message = ctx.Locale.TrString("repo.editor.update", form.Title) } if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil { diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 203e5fa5a5..07026e484f 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -4,6 +4,8 @@ package user import ( + "net/url" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" @@ -15,8 +17,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -34,10 +34,9 @@ func prepareContextForCommonProfile(ctx *context.Context) { func PrepareContextForProfileBigAvatar(ctx *context.Context) { prepareContextForCommonProfile(ctx) - ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate - ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL + ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location) // Show OpenID URIs openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID) @@ -47,18 +46,6 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { } ctx.Data["OpenIDs"] = openIDs - if len(ctx.ContextUser.Description) != 0 { - content, err := markdown.RenderString(&markup.RenderContext{ - Metas: map[string]string{"mode": "document"}, - Ctx: ctx, - }, ctx.ContextUser.Description) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - ctx.Data["RenderedDescription"] = content - } - 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, diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go index 493c97aa67..42e9dbe967 100644 --- a/routers/web/swagger_json.go +++ b/routers/web/swagger_json.go @@ -4,22 +4,10 @@ package web import ( - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" ) -// tplSwaggerV1Json swagger v1 json template -const tplSwaggerV1Json base.TplName = "swagger/v1_json" - // SwaggerV1Json render swagger v1 json func SwaggerV1Json(ctx *context.Context) { - t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil) - if err != nil { - ctx.ServerError("unable to find template", err) - return - } - ctx.Resp.Header().Set("Content-Type", "application/json") - if err = t.Execute(ctx.Resp, ctx.Data); err != nil { - ctx.ServerError("unable to execute template", err) - } + ctx.JSONTemplate("swagger/v1_json") } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 79af74eb02..8759ba32b1 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -85,7 +85,7 @@ func Dashboard(ctx *context.Context) { page = 1 } - ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard") + ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard") ctx.Data["PageIsDashboard"] = true ctx.Data["PageIsNews"] = true cnt, _ := organization.GetOrganizationCount(ctx, ctxUser) @@ -296,17 +296,17 @@ func Milestones(ctx *context.Context) { } } - showRepoIds := make(container.Set[int64], len(showRepos)) + showRepoIDs := make(container.Set[int64], len(showRepos)) for _, repo := range showRepos { if repo.ID > 0 { - showRepoIds.Add(repo.ID) + showRepoIDs.Add(repo.ID) } } if len(repoIDs) == 0 { - repoIDs = showRepoIds.Values() + repoIDs = showRepoIDs.Values() } repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool { - return !showRepoIds.Contains(v) + return !showRepoIDs.Contains(v) }) var pagerCount int diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 7b04948ab0..4eec0e9905 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -31,6 +31,7 @@ import ( const ( tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar" + tplFollowUnfollow base.TplName = "org/follow_unfollow" ) // OwnerProfile render profile page for a user or a organization (aka, repo owner) @@ -349,6 +350,16 @@ func Action(ctx *context.Context) { return } - shared_user.PrepareContextForProfileBigAvatar(ctx) - ctx.HTML(http.StatusOK, tplProfileBigAvatar) + if ctx.ContextUser.IsIndividual() { + shared_user.PrepareContextForProfileBigAvatar(ctx) + ctx.HTML(http.StatusOK, tplProfileBigAvatar) + return + } else if ctx.ContextUser.IsOrganization() { + ctx.Data["Org"] = ctx.ContextUser + ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + ctx.HTML(http.StatusOK, tplFollowUnfollow) + return + } + log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type) + ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) } diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 371718ba23..6042e0b6cd 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -242,6 +242,11 @@ func DeleteEmail(ctx *context.Context) { // DeleteAccount render user suicide page and response for delete user himself func DeleteAccount(ctx *context.Context) { + if setting.Admin.UserDisabledFeatures.Contains(setting.UserFeatureDeletion) { + ctx.Error(http.StatusNotFound) + return + } + ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true @@ -308,6 +313,7 @@ func loadAccountData(ctx *context.Context) { ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm + ctx.Data["UserDisabledFeatures"] = &setting.Admin.UserDisabledFeatures if setting.Service.UserDeleteWithCommentsMaxTime != 0 { ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String() diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 95b350528c..24a807d518 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -126,7 +126,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser * defer fr.Close() if form.Avatar.Size > setting.Avatar.MaxFileSize { - return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) + return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) } data, err := io.ReadAll(fr) @@ -136,7 +136,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser * st := typesniffer.DetectContentType(data) if !(st.IsImage() && !st.IsSvgImage()) { - return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image")) } if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil { return fmt.Errorf("UploadAvatar: %w", err) @@ -389,7 +389,7 @@ func UpdateUserLang(ctx *context.Context) { middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0) log.Trace("User settings updated: %s", ctx.Doer.Name) - ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success")) + ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_language_success")) ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") } diff --git a/routers/web/user/task.go b/routers/web/user/task.go index f35f40e6a0..bec68c5f20 100644 --- a/routers/web/user/task.go +++ b/routers/web/user/task.go @@ -39,7 +39,7 @@ func TaskStatus(ctx *context.Context) { Args: []any{task.Message}, } } - message = ctx.Tr(translatableMessage.Format, translatableMessage.Args...) + message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...) } ctx.JSON(http.StatusOK, map[string]any{ diff --git a/routers/web/web.go b/routers/web/web.go index 1744ddb83a..fc09ed2b6b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -152,7 +152,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont if ctx.Doer.MustChangePassword { if ctx.Req.URL.Path != "/user/settings/change_password" { if strings.HasPrefix(ctx.Req.UserAgent(), "git") { - ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password")) + ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password")) return } ctx.Data["Title"] = ctx.Tr("auth.must_change_password") @@ -681,6 +681,7 @@ func registerRoutes(m *web.Route) { // ***** START: Admin ***** m.Group("/admin", func() { m.Get("", admin.Dashboard) + m.Get("/system_status", admin.SystemStatus) m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost) if setting.Database.Type.IsMySQL() || setting.Database.Type.IsMSSQL() { @@ -967,10 +968,9 @@ func registerRoutes(m *web.Route) { m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost) m.Get("/migrate", repo.Migrate) m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost) - m.Group("/fork", func() { - m.Combo("/{repoid}").Get(repo.Fork). - Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) - }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) + if !setting.Repository.DisableForks { + m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID) + } m.Get("/search", repo.SearchRepo) }, reqSignIn) @@ -1037,6 +1037,8 @@ func registerRoutes(m *web.Route) { m.Combo("").Get(repo_setting.Settings). Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) }, repo_setting.SettingsCtxData) + m.Combo("/units").Get(repo_setting.Units). + Post(web.Bind(forms.RepoUnitSettingForm{}), repo_setting.UnitsPost) m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar) m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar) @@ -1122,7 +1124,16 @@ func registerRoutes(m *web.Route) { }, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer)) }, reqSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoAdmin, context.RepoRef()) - m.Post("/{username}/{reponame}/action/{action}", reqSignIn, context.RepoAssignment, context.UnitTypes(), repo.Action) + m.Group("/{username}/{reponame}/action", func() { + m.Post("/watch", repo.ActionWatch(true)) + m.Post("/unwatch", repo.ActionWatch(false)) + m.Post("/accept_transfer", repo.ActionTransfer(true)) + m.Post("/reject_transfer", repo.ActionTransfer(false)) + if !setting.Repository.DisableStars { + m.Post("/star", repo.ActionStar(true)) + m.Post("/unstar", repo.ActionStar(false)) + } + }, reqSignIn, context.RepoAssignment, context.UnitTypes()) // Grouping for those endpoints not requiring authentication (but should respect ignSignIn) m.Group("/{username}/{reponame}", func() { @@ -1148,6 +1159,10 @@ func registerRoutes(m *web.Route) { // Grouping for those endpoints that do require authentication m.Group("/{username}/{reponame}", func() { + if !setting.Repository.DisableForks { + m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork). + Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) + } m.Group("/issues", func() { m.Group("/new", func() { m.Combo("").Get(context.RepoRef(), repo.NewIssue). @@ -1250,7 +1265,7 @@ func registerRoutes(m *web.Route) { Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick). Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, repo.MustBeEditable) + }, repo.MustBeEditable, repo.CommonEditorData) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) @@ -1346,7 +1361,9 @@ func registerRoutes(m *web.Route) { m.Get("/open.svg", badges.GetOpenPullsBadge) m.Get("/closed.svg", badges.GetClosedPullsBadge) }) - m.Get("/stars.svg", badges.GetStarsBadge) + if !setting.Repository.DisableStars { + m.Get("/stars.svg", badges.GetStarsBadge) + } m.Get("/release.svg", badges.GetLatestReleaseBadge) }) } @@ -1399,11 +1416,15 @@ func registerRoutes(m *web.Route) { m.Post("/approve", reqRepoActionsWriter, actions.Approve) m.Post("/artifacts", actions.ArtifactsView) m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) + m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) }) }) - m.Get("/workflows/{workflow_name}/badge.svg", badges.GetWorkflowBadge) + m.Group("/workflows/{workflow_name}", func() { + m.Get("/badge.svg", badges.GetWorkflowBadge) + m.Get("/runs/latest", actions.ViewLatestWorkflowRun) + }) }, reqRepoActionsReader, actions.MustEnableActions) m.Group("/wiki", func() { @@ -1427,6 +1448,18 @@ func registerRoutes(m *web.Route) { m.Group("/activity", func() { m.Get("", repo.Activity) m.Get("/{period}", repo.Activity) + m.Group("/contributors", func() { + m.Get("", repo.Contributors) + m.Get("/data", repo.ContributorsData) + }) + m.Group("/code-frequency", func() { + m.Get("", repo.CodeFrequency) + m.Get("/data", repo.CodeFrequencyData) + }) + m.Group("/recent-commits", func() { + m.Get("", repo.RecentCommits) + m.Get("/data", repo.RecentCommitsData) + }) }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases)) m.Group("/activity_author_data", func() { @@ -1550,16 +1583,20 @@ func registerRoutes(m *web.Route) { m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home) }, repo.SetEditorconfigIfExists) - m.Group("", func() { - m.Get("/forks", repo.Forks) - }, context.RepoRef(), reqRepoCodeReader) + if !setting.Repository.DisableForks { + m.Group("", func() { + m.Get("/forks", repo.Forks) + }, context.RepoRef(), reqRepoCodeReader) + } m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) }, ignSignIn, context.RepoAssignment, context.UnitTypes()) m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit) m.Group("/{username}/{reponame}", func() { - m.Get("/stars", repo.Stars) + if !setting.Repository.DisableStars { + m.Get("/stars", repo.Stars) + } m.Get("/watchers", repo.Watchers) m.Get("/search", reqRepoCodeReader, repo.Search) }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes()) diff --git a/services/actions/auth.go b/services/actions/auth.go index 53e68f0b71..e0f9a9015d 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -38,7 +38,7 @@ func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) { } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(setting.SecretKey)) + tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret()) if err != nil { return "", err } @@ -62,7 +62,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } - return []byte(setting.SecretKey), nil + return setting.GetGeneralTokenSigningSecret(), nil }) if err != nil { return 0, err diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go index f6288ccd5a..1f62f17f52 100644 --- a/services/actions/auth_test.go +++ b/services/actions/auth_test.go @@ -20,7 +20,7 @@ func TestCreateAuthorizationToken(t *testing.T) { assert.NotEqual(t, "", token) claims := jwt.MapClaims{} _, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { - return []byte(setting.SecretKey), nil + return setting.GetGeneralTokenSigningSecret(), nil }) assert.Nil(t, err) scp, ok := claims["scp"] diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index 785eeb5838..5376c2624c 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -20,23 +20,59 @@ func Cleanup(taskCtx context.Context, olderThan time.Duration) error { return CleanupArtifacts(taskCtx) } -// CleanupArtifacts removes expired artifacts and set records expired status +// CleanupArtifacts removes expired add need-deleted artifacts and set records expired status func CleanupArtifacts(taskCtx context.Context) error { + if err := cleanExpiredArtifacts(taskCtx); err != nil { + return err + } + return cleanNeedDeleteArtifacts(taskCtx) +} + +func cleanExpiredArtifacts(taskCtx context.Context) error { artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx) if err != nil { return err } log.Info("Found %d expired artifacts", len(artifacts)) for _, artifact := range artifacts { - if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { - log.Error("Cannot delete artifact %d: %v", artifact.ID, err) - continue - } if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil { log.Error("Cannot set artifact %d expired: %v", artifact.ID, err) continue } + if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { + log.Error("Cannot delete artifact %d: %v", artifact.ID, err) + continue + } log.Info("Artifact %d set expired", artifact.ID) } return nil } + +// deleteArtifactBatchSize is the batch size of deleting artifacts +const deleteArtifactBatchSize = 100 + +func cleanNeedDeleteArtifacts(taskCtx context.Context) error { + for { + artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize) + if err != nil { + return err + } + log.Info("Found %d artifacts pending deletion", len(artifacts)) + for _, artifact := range artifacts { + if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil { + log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err) + continue + } + if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { + log.Error("Cannot delete artifact %d: %v", artifact.ID, err) + continue + } + log.Info("Artifact %d set deleted", artifact.ID) + } + if len(artifacts) < deleteArtifactBatchSize { + log.Debug("No more artifacts pending deletion") + break + } + } + return nil +} diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 72a3ab7ac6..edd1fd1568 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -64,6 +64,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("head of pull request is missing in event payload") } sha = payload.PullRequest.Head.Sha + case webhook_module.HookEventRelease: + event = string(run.Event) + sha = run.CommitSHA default: return nil } diff --git a/services/actions/main_test.go b/services/actions/main_test.go new file mode 100644 index 0000000000..ea37ff507a --- /dev/null +++ b/services/actions/main_test.go @@ -0,0 +1,17 @@ +// Copyright 2024 The Forgejo Authors +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 0b4fed5db1..e144484dab 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -55,6 +55,47 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu }).Notify(withMethod(ctx, "NewIssue")) } +// IssueChangeContent notifies change content of issue +func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) { + ctx = withMethod(ctx, "IssueChangeContent") + + var err error + if err = issue.LoadRepo(ctx); err != nil { + log.Error("LoadRepo: %v", err) + return + } + + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + if issue.IsPull { + if err = issue.LoadPullRequest(ctx); err != nil { + log.Error("loadPullRequest: %v", err) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest). + WithDoer(doer). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueEdited, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), + Sender: convert.ToUser(ctx, doer, nil), + }). + WithPullRequest(issue.PullRequest). + Notify(ctx) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventIssues). + WithDoer(doer). + WithPayload(&api.IssuePayload{ + Action: api.HookIssueEdited, + Index: issue.Index, + Issue: convert.ToAPIIssue(ctx, issue), + Repository: convert.ToRepo(ctx, issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }). + Notify(ctx) +} + // IssueChangeStatus notifies close or reopen issue to notifiers func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) { ctx = withMethod(ctx, "IssueChangeStatus") @@ -101,11 +142,40 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode Notify(ctx) } +// IssueChangeAssignee notifies assigned or unassigned to notifiers +func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { + ctx = withMethod(ctx, "IssueChangeAssignee") + + var action api.HookIssueAction + if removed { + action = api.HookIssueUnassigned + } else { + action = api.HookIssueAssigned + } + notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestAssign, action) +} + +// IssueChangeMilestone notifies assignee to notifiers +func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) { + ctx = withMethod(ctx, "IssueChangeMilestone") + + var action api.HookIssueAction + if issue.MilestoneID > 0 { + action = api.HookIssueMilestoned + } else { + action = api.HookIssueDemilestoned + } + notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestMilestone, action) +} + func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, _, _ []*issues_model.Label, ) { ctx = withMethod(ctx, "IssueChangeLabels") + notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestLabel, api.HookIssueLabelUpdated) +} +func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) { var err error if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) @@ -117,20 +187,15 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode return } - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) return } - if err = issue.PullRequest.LoadIssue(ctx); err != nil { - log.Error("LoadIssue: %v", err) - return - } - newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel). + newNotifyInputFromIssue(issue, event). WithDoer(doer). WithPayload(&api.PullRequestPayload{ - Action: api.HookIssueLabelUpdated, + Action: action, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), @@ -140,10 +205,11 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode Notify(ctx) return } - newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel). + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + newNotifyInputFromIssue(issue, event). WithDoer(doer). WithPayload(&api.IssuePayload{ - Action: api.HookIssueLabelUpdated, + Action: action, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), Repository: convert.ToRepo(ctx, issue.Repo, permission), @@ -158,37 +224,88 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod ) { ctx = withMethod(ctx, "CreateIssueComment") - permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) - if issue.IsPull { - if err := issue.LoadPullRequest(ctx); err != nil { + notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentCreated) + return + } + notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentCreated) +} + +func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) { + ctx = withMethod(ctx, "UpdateComment") + + if err := c.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + + if c.Issue.IsPull { + notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited) + return + } + notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventIssueComment, api.HookIssueCommentEdited) +} + +func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) { + ctx = withMethod(ctx, "DeleteComment") + + if err := comment.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + + if comment.Issue.IsPull { + notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted) + return + } + notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentDeleted) +} + +func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) { + if err := comment.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + if err := comment.Issue.LoadAttributes(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer) + + payload := &api.IssueCommentPayload{ + Action: action, + Issue: convert.ToAPIIssue(ctx, comment.Issue), + Comment: convert.ToAPIComment(ctx, comment.Issue.Repo, comment), + Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + IsPull: comment.Issue.IsPull, + } + + if action == api.HookIssueCommentEdited { + payload.Changes = &api.ChangesPayload{ + Body: &api.ChangesFromPayload{ + From: oldContent, + }, + } + } + + if comment.Issue.IsPull { + if err := comment.Issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest: %v", err) return } - newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestComment). + newNotifyInputFromIssue(comment.Issue, event). WithDoer(doer). - WithPayload(&api.IssueCommentPayload{ - Action: api.HookIssueCommentCreated, - Issue: convert.ToAPIIssue(ctx, issue), - Comment: convert.ToAPIComment(ctx, repo, comment), - Repository: convert.ToRepo(ctx, repo, permission), - Sender: convert.ToUser(ctx, doer, nil), - IsPull: true, - }). - WithPullRequest(issue.PullRequest). + WithPayload(payload). + WithPullRequest(comment.Issue.PullRequest). Notify(ctx) return } - newNotifyInputFromIssue(issue, webhook_module.HookEventIssueComment). + + newNotifyInputFromIssue(comment.Issue, event). WithDoer(doer). - WithPayload(&api.IssueCommentPayload{ - Action: api.HookIssueCommentCreated, - Issue: convert.ToAPIIssue(ctx, issue), - Comment: convert.ToAPIComment(ctx, repo, comment), - Repository: convert.ToRepo(ctx, repo, permission), - Sender: convert.ToUser(ctx, doer, nil), - IsPull: false, - }). + WithPayload(payload). Notify(ctx) } @@ -305,6 +422,39 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode }).Notify(ctx) } +func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { + if !issue.IsPull { + log.Warn("PullRequestReviewRequest: issue is not a pull request: %v", issue.ID) + return + } + + ctx = withMethod(ctx, "PullRequestReviewRequest") + + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + if err := issue.LoadPullRequest(ctx); err != nil { + log.Error("LoadPullRequest failed: %v", err) + return + } + var action api.HookIssueAction + if isRequest { + action = api.HookIssueReviewRequested + } else { + action = api.HookIssueReviewRequestRemoved + } + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestReviewRequest). + WithDoer(doer). + WithPayload(&api.PullRequestPayload{ + Action: action, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + RequestedReviewer: convert.ToUser(ctx, reviewer, nil), + Repository: convert.ToRepo(ctx, issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }). + WithPullRequest(issue.PullRequest). + Notify(ctx) +} + func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { ctx = withMethod(ctx, "MergePullRequest") @@ -397,7 +547,6 @@ func (n *actionsNotifier) DeleteRef(ctx context.Context, pusher *user_model.User apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}) newNotifyInput(repo, pusher, webhook_module.HookEventDelete). - WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name WithPayload(&api.DeletePayload{ Ref: refFullName.ShortName(), RefType: refFullName.RefType(), diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index a5565495d9..2d138aaf33 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -133,12 +133,15 @@ func notify(ctx context.Context, input *notifyInput) error { defer gitRepo.Close() ref := input.Ref - if input.Event == webhook_module.HookEventDelete { - // The event is deleting a reference, so it will fail to get the commit for a deleted reference. - // Set ref to empty string to fall back to the default branch. - ref = "" + if ref != input.Repo.DefaultBranch && actions_module.IsDefaultBranchWorkflow(input.Event) { + if ref != "" { + log.Warn("Event %q should only trigger workflows on the default branch, but its ref is %q. Will fall back to the default branch", + input.Event, ref) + } + ref = input.Repo.DefaultBranch } if ref == "" { + log.Warn("Ref of event %q is empty, will fall back to the default branch", input.Event) ref = input.Repo.DefaultBranch } @@ -152,6 +155,11 @@ func notify(ctx context.Context, input *notifyInput) error { return nil } + if SkipPullRequestEvent(ctx, input.Event, input.Repo.ID, commit.ID.String()) { + log.Trace("repo %s with commit %s skip event %v", input.Repo.RepoPath(), commit.ID, input.Event) + return nil + } + var detectedWorkflows []*actions_module.DetectedWorkflow actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload) @@ -203,6 +211,24 @@ func notify(ctx context.Context, input *notifyInput) error { return handleWorkflows(ctx, detectedWorkflows, commit, input, ref) } +func SkipPullRequestEvent(ctx context.Context, event webhook_module.HookEventType, repoID int64, commitSHA string) bool { + if event != webhook_module.HookEventPullRequestSync { + return false + } + + run := actions_model.ActionRun{ + Event: webhook_module.HookEventPullRequest, + RepoID: repoID, + CommitSHA: commitSHA, + } + exist, err := db.GetEngine(ctx).Exist(&run) + if err != nil { + log.Error("Exist ActionRun %v: %v", run, err) + return false + } + return exist +} + func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool { // skip workflow runs with a configured skip-ci string in commit message if the event is push or pull_request(_sync) // https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs diff --git a/services/actions/notifier_helper_test.go b/services/actions/notifier_helper_test.go new file mode 100644 index 0000000000..3c23414b8e --- /dev/null +++ b/services/actions/notifier_helper_test.go @@ -0,0 +1,50 @@ +// Copyright 2024 The Forgejo Authors +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + webhook_module "code.gitea.io/gitea/modules/webhook" + + "github.com/stretchr/testify/assert" +) + +func Test_SkipPullRequestEvent(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repoID := int64(1) + commitSHA := "1234" + + // event is not webhook_module.HookEventPullRequestSync, never skip + assert.False(t, SkipPullRequestEvent(db.DefaultContext, webhook_module.HookEventPullRequest, repoID, commitSHA)) + + // event is webhook_module.HookEventPullRequestSync but there is nothing in the ActionRun table, do not skip + assert.False(t, SkipPullRequestEvent(db.DefaultContext, webhook_module.HookEventPullRequestSync, repoID, commitSHA)) + + // there is a webhook_module.HookEventPullRequest event but the SHA is different, do not skip + index := int64(1) + run := &actions_model.ActionRun{ + Index: index, + Event: webhook_module.HookEventPullRequest, + RepoID: repoID, + CommitSHA: "othersha", + } + unittest.AssertSuccessfulInsert(t, run) + assert.False(t, SkipPullRequestEvent(db.DefaultContext, webhook_module.HookEventPullRequestSync, repoID, commitSHA)) + + // there already is a webhook_module.HookEventPullRequest with the same SHA, skip + index++ + run = &actions_model.ActionRun{ + Index: index, + Event: webhook_module.HookEventPullRequest, + RepoID: repoID, + CommitSHA: commitSHA, + } + unittest.AssertSuccessfulInsert(t, run) + assert.True(t, SkipPullRequestEvent(db.DefaultContext, webhook_module.HookEventPullRequestSync, repoID, commitSHA)) +} diff --git a/services/agit/agit.go b/services/agit/agit.go index bc68372570..e46a5771e1 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -21,34 +21,33 @@ import ( // ProcReceive handle proc receive work func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { - // TODO: Add more options? - var ( - topicBranch string - title string - description string - forcePush bool - ) - results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) - ownerName := repo.OwnerName - repoName := repo.Name + topicBranch := opts.GitPushOptions["topic"] + _, forcePush := opts.GitPushOptions["force-push"] + title, hasTitle := opts.GitPushOptions["title"] + description, hasDesc := opts.GitPushOptions["description"] - topicBranch = opts.GitPushOptions["topic"] - _, forcePush = opts.GitPushOptions["force-push"] - objectFormat, _ := gitRepo.GetObjectFormat() + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + + pusher, err := user_model.GetUserByID(ctx, opts.UserID) + if err != nil { + return nil, fmt.Errorf("failed to get user[%d]: %w", opts.UserID, err) + } for i := range opts.OldCommitIDs { + // Avoid processing this change if the new commit is empty. if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() { results = append(results, private.HookProcReceiveRefResult{ OriginalRef: opts.RefFullNames[i], OldOID: opts.OldCommitIDs[i], NewOID: opts.NewCommitIDs[i], - Err: "Can't delete not exist branch", + Err: "Cannot delete a non-existent branch.", }) continue } + // Only process references that are in the form of refs/for/ if !opts.RefFullNames[i].IsFor() { results = append(results, private.HookProcReceiveRefResult{ IsNotMatched: true, @@ -57,10 +56,14 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. continue } + // Get the anything after the refs/for/ prefix. baseBranchName := opts.RefFullNames[i].ForBranchName() - curentTopicBranch := "" + curentTopicBranch := topicBranch + + // If the reference was given in the format of refs/for//, + // where and can contain slashes, we need to iteratively + // search for what the target and topic branch is. if !gitRepo.IsBranchExist(baseBranchName) { - // try match refs/for// for p, v := range baseBranchName { if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 { curentTopicBranch = baseBranchName[p+1:] @@ -70,55 +73,61 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. } } - if len(topicBranch) == 0 && len(curentTopicBranch) == 0 { + if len(curentTopicBranch) == 0 { results = append(results, private.HookProcReceiveRefResult{ OriginalRef: opts.RefFullNames[i], OldOID: opts.OldCommitIDs[i], NewOID: opts.NewCommitIDs[i], - Err: "topic-branch is not set", + Err: "The topic-branch option is not set", }) continue } - var headBranch string + // Include the user's name in the head branch, to avoid conflicts + // with other users. + headBranch := curentTopicBranch userName := strings.ToLower(opts.UserName) - - if len(curentTopicBranch) == 0 { - curentTopicBranch = topicBranch - } - - // because different user maybe want to use same topic, - // So it's better to make sure the topic branch name - // has user name prefix if !strings.HasPrefix(curentTopicBranch, userName+"/") { headBranch = userName + "/" + curentTopicBranch - } else { - headBranch = curentTopicBranch } + // Check if a AGit pull request already exist for this branch. pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { - return nil, fmt.Errorf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %w", ownerName, repoName, err) + return nil, fmt.Errorf("failed to get unmerged AGit flow pull request in repository %q: %w", repo.FullName(), err) } - // create a new pull request - if len(title) == 0 { - var has bool - title, has = opts.GitPushOptions["title"] - if !has || len(title) == 0 { - commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i]) - if err != nil { - return nil, fmt.Errorf("Failed to get commit %s in repository: %s/%s Error: %w", opts.NewCommitIDs[i], ownerName, repoName, err) - } - title = strings.Split(commit.CommitMessage, "\n")[0] + // Check if the changes are already in the target branch. + stdout, _, gitErr := git.NewCommand(ctx, "branch", "--contains").AddDynamicArguments(opts.NewCommitIDs[i], baseBranchName).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + if gitErr != nil { + return nil, fmt.Errorf("failed to check if the target branch already contains the new commit in repository %q: %w", repo.FullName(), err) + } + if len(stdout) > 0 { + results = append(results, private.HookProcReceiveRefResult{ + OriginalRef: opts.RefFullNames[i], + OldOID: opts.OldCommitIDs[i], + NewOID: opts.NewCommitIDs[i], + Err: "The target branch already contains this commit", + }) + continue + } + + // Automatically fill out the title and the description from the first commit. + shouldGetCommit := len(title) == 0 || len(description) == 0 + + var commit *git.Commit + if shouldGetCommit { + commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i]) + if err != nil { + return nil, fmt.Errorf("failed to get commit %s in repository %q: %w", opts.NewCommitIDs[i], repo.FullName(), err) } - description = opts.GitPushOptions["description"] } - - pusher, err := user_model.GetUserByID(ctx, opts.UserID) - if err != nil { - return nil, fmt.Errorf("Failed to get user. Error: %w", err) + if !hasTitle || len(title) == 0 { + title = strings.Split(commit.CommitMessage, "\n")[0] + } + if !hasDesc || len(description) == 0 { + _, description, _ = strings.Cut(commit.CommitMessage, "\n\n") } prIssue := &issues_model.Issue{ @@ -144,12 +153,11 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. } if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil { - return nil, err + return nil, fmt.Errorf("unable to create new pull request: %w", err) } log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID) - objectFormat, _ := gitRepo.GetObjectFormat() results = append(results, private.HookProcReceiveRefResult{ Ref: pr.GetGitRefName(), OriginalRef: opts.RefFullNames[i], @@ -159,55 +167,57 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. continue } - // update exist pull request + // Update an existing pull request. if err := pr.LoadBaseRepo(ctx); err != nil { - return nil, fmt.Errorf("Unable to load base repository for PR[%d] Error: %w", pr.ID, err) + return nil, fmt.Errorf("unable to load base repository for PR[%d]: %w", pr.ID, err) } oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { - return nil, fmt.Errorf("Unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err) + return nil, fmt.Errorf("unable to get commit id of reference[%s] in base repository for PR[%d]: %w", pr.GetGitRefName(), pr.ID, err) } + // Do not process this change if nothing was changed. if oldCommitID == opts.NewCommitIDs[i] { results = append(results, private.HookProcReceiveRefResult{ OriginalRef: opts.RefFullNames[i], OldOID: opts.OldCommitIDs[i], NewOID: opts.NewCommitIDs[i], - Err: "new commit is same with old commit", + Err: "The new commit is the same as the old commit", }) continue } + // If the force push option was not set, ensure that this change isn't a force push. if !forcePush { output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) if err != nil { - return nil, fmt.Errorf("Fail to detect force push: %w", err) + return nil, fmt.Errorf("failed to detect a force push: %w", err) } else if len(output) > 0 { results = append(results, private.HookProcReceiveRefResult{ OriginalRef: opts.RefFullNames[i], OldOID: opts.OldCommitIDs[i], NewOID: opts.NewCommitIDs[i], - Err: "request `force-push` push option", + Err: "Updates were rejected because the tip of your current branch is behind its remote counterpart. If this is intentional, set the `force-push` option by adding `-o force-push=true` to your `git push` command.", }) continue } } + // Set the new commit as reference of the pull request. pr.HeadCommitID = opts.NewCommitIDs[i] if err = pull_service.UpdateRef(ctx, pr); err != nil { - return nil, fmt.Errorf("Failed to update pull ref. Error: %w", err) + return nil, fmt.Errorf("failed to update the reference of the pull request: %w", err) } + // Add the pull request to the merge conflicting checker queue. pull_service.AddToTaskQueue(ctx, pr) - pusher, err := user_model.GetUserByID(ctx, opts.UserID) - if err != nil { - return nil, fmt.Errorf("Failed to get user. Error: %w", err) - } - err = pr.LoadIssue(ctx) - if err != nil { - return nil, fmt.Errorf("Failed to load pull issue. Error: %w", err) + + if err := pr.LoadIssue(ctx); err != nil { + return nil, fmt.Errorf("failed to load the issue of the pull request: %w", err) } + + // Create and notify about the new commits. comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) if err == nil && comment != nil { notify_service.PullRequestPushCommits(ctx, pusher, pr, comment) diff --git a/services/auth/auth.go b/services/auth/auth.go index 6e22d8298e..e53ff02dcf 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -40,6 +40,7 @@ func isContainerPath(req *http.Request) bool { var ( gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`) lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`) + archivePathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/archive/`) ) func isGitRawOrAttachPath(req *http.Request) bool { @@ -56,6 +57,10 @@ func isGitRawOrAttachOrLFSPath(req *http.Request) bool { return false } +func isArchivePath(req *http.Request) bool { + return archivePathRe.MatchString(req.URL.Path) +} + // handleSignIn clears existing session variables and stores new ones for the specified user object func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) { // We need to regenerate the session... diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index f2f7858a85..46d8510143 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -133,7 +133,7 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) && - !isGitRawOrAttachPath(req) { + !isGitRawOrAttachPath(req) && !isArchivePath(req) { return nil, nil } diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go index 359c1f2473..b6aeb0aed2 100644 --- a/services/auth/reverseproxy.go +++ b/services/auth/reverseproxy.go @@ -10,8 +10,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" gouuid "github.com/google/uuid" @@ -161,7 +161,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User { } overwriteDefault := user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), } if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil { diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go index 50eae27439..bb2270cbd6 100644 --- a/services/auth/source/db/source.go +++ b/services/auth/source/db/source.go @@ -18,7 +18,7 @@ func (source *Source) FromDB(bs []byte) error { return nil } -// ToDB exports an SMTPConfig to a serialized format. +// ToDB exports the config to a byte slice to be saved into database (this method is just dummy and does nothing for DB source) func (source *Source) ToDB() ([]byte, error) { return nil, nil } diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 8f641ed541..68ecd16342 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -13,7 +13,6 @@ import ( user_model "code.gitea.io/gitea/models/user" auth_module "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/util" source_service "code.gitea.io/gitea/services/auth/source" user_service "code.gitea.io/gitea/services/user" ) @@ -85,8 +84,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u IsAdmin: sr.IsAdmin, } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsRestricted: util.OptionalBoolOf(sr.IsRestricted), - IsActive: util.OptionalBoolTrue, + IsRestricted: optional.Some(sr.IsRestricted), + IsActive: optional.Some(true), } err := user_model.CreateUser(ctx, user, overwriteDefault) diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index eee7bb585a..62f052d68c 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/util" source_service "code.gitea.io/gitea/services/auth/source" user_service "code.gitea.io/gitea/services/user" ) @@ -125,8 +124,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { IsAdmin: su.IsAdmin, } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsRestricted: util.OptionalBoolOf(su.IsRestricted), - IsActive: util.OptionalBoolTrue, + IsRestricted: optional.Some(su.IsRestricted), + IsActive: optional.Some(true), } err = user_model.CreateUser(ctx, usr, overwriteDefault) diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go index eca0b8b7e1..070fffe60f 100644 --- a/services/auth/source/oauth2/jwtsigningkey.go +++ b/services/auth/source/oauth2/jwtsigningkey.go @@ -300,7 +300,7 @@ func InitSigningKey() error { case "HS384": fallthrough case "HS512": - key, err = loadSymmetricKey() + key = setting.GetGeneralTokenSigningSecret() case "RS256": fallthrough case "RS384": @@ -333,12 +333,6 @@ func InitSigningKey() error { return nil } -// loadSymmetricKey checks if the configured secret is valid. -// If it is not valid, it will return an error. -func loadSymmetricKey() (any, error) { - return util.Base64FixedDecode(base64.RawURLEncoding, []byte(setting.OAuth2.JWTSecretBase64), 32) -} - // loadOrCreateAsymmetricKey checks if the configured private key exists. // If it does not exist a new random key gets generated and saved on the configured path. func loadOrCreateAsymmetricKey() (any, error) { diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go index 0891a86392..addd1bd2c9 100644 --- a/services/auth/source/pam/source_authenticate.go +++ b/services/auth/source/pam/source_authenticate.go @@ -11,8 +11,8 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/pam" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/google/uuid" ) @@ -60,7 +60,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u LoginName: userName, // This is what the user typed in } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), } if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go index b244fc7d40..1f0a61c789 100644 --- a/services/auth/source/smtp/source_authenticate.go +++ b/services/auth/source/smtp/source_authenticate.go @@ -12,6 +12,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/util" ) @@ -75,7 +76,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u LoginName: userName, } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), } if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 0e974fde8f..8c0fc77a96 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/base" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" @@ -172,8 +173,8 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) ( } emailNotificationPreference := user_model.EmailNotificationsDisabled overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolOf(cfg.AutoActivateUsers), - KeepEmailPrivate: util.OptionalBoolTrue, + IsActive: optional.Some(cfg.AutoActivateUsers), + KeepEmailPrivate: optional.Some(true), EmailNotificationsPreference: &emailNotificationPreference, } if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { diff --git a/services/convert/repository.go b/services/convert/repository.go index b032d97d73..466d19d563 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -93,6 +93,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR allowRebase := false allowRebaseMerge := false allowSquash := false + allowFastForwardOnly := false allowRebaseUpdate := false defaultDeleteBranchAfterMerge := false defaultMergeStyle := repo_model.MergeStyleMerge @@ -105,6 +106,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR allowRebase = config.AllowRebase allowRebaseMerge = config.AllowRebaseMerge allowSquash = config.AllowSquash + allowFastForwardOnly = config.AllowFastForwardOnly allowRebaseUpdate = config.AllowRebaseUpdate defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge defaultMergeStyle = config.GetDefaultMergeStyle() @@ -220,6 +222,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR AllowRebase: allowRebase, AllowRebaseMerge: allowRebaseMerge, AllowSquash: allowSquash, + AllowFastForwardOnly: allowFastForwardOnly, AllowRebaseUpdate: allowRebaseUpdate, DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge, DefaultMergeStyle: string(defaultMergeStyle), diff --git a/services/cron/setting.go b/services/cron/setting.go index 0656307cba..6dad88830a 100644 --- a/services/cron/setting.go +++ b/services/cron/setting.go @@ -70,7 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool { // Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task. func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string { realArgs := make([]any, 0, len(args)+2) - realArgs = append(realArgs, locale.Tr("admin.dashboard."+name)) + realArgs = append(realArgs, locale.TrString("admin.dashboard."+name)) if doer == "" { realArgs = append(realArgs, "(Cron)") } else { @@ -80,7 +80,7 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer realArgs = append(realArgs, args...) } if doer == "" { - return locale.Tr("admin.dashboard.cron."+status, realArgs...) + return locale.TrString("admin.dashboard.cron."+status, realArgs...) } - return locale.Tr("admin.dashboard.task."+status, realArgs...) + return locale.TrString("admin.dashboard.task."+status, realArgs...) } diff --git a/services/cron/tasks.go b/services/cron/tasks.go index f0956a97d8..f8a7444c49 100644 --- a/services/cron/tasks.go +++ b/services/cron/tasks.go @@ -159,7 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo log.Debug("Registering task: %s", name) i18nKey := "admin.dashboard." + name - if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey { + if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey { return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey) } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c74c1582d3..0f1ff0efc0 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -132,6 +132,24 @@ type RepoSettingForm struct { EnablePrune bool // Advanced settings + IsArchived bool + + // Signing Settings + TrustModel string + + // Admin settings + EnableHealthCheck bool + RequestReindexType string +} + +// Validate validates the fields +func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + +// RepoUnitSettingForm form for changing repository unit settings +type RepoUnitSettingForm struct { EnableCode bool EnableWiki bool GloballyWriteableWiki bool @@ -154,6 +172,7 @@ type RepoSettingForm struct { PullsAllowRebase bool PullsAllowRebaseMerge bool PullsAllowSquash bool + PullsAllowFastForwardOnly bool PullsAllowManualMerge bool PullsDefaultMergeStyle string EnableAutodetectManualMerge bool @@ -163,18 +182,10 @@ type RepoSettingForm struct { EnableTimetracker bool AllowOnlyContributorsToTrackTime bool EnableIssueDependencies bool - IsArchived bool - - // Signing Settings - TrustModel string - - // Admin settings - EnableHealthCheck bool - RequestReindexType string } // Validate validates the fields -func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { +func (f *RepoUnitSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { ctx := context.GetValidateContext(req) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } @@ -316,7 +327,7 @@ func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) bind errs = append(errs, binding.Error{ FieldNames: []string{"Channel"}, Classification: "", - Message: ctx.Tr("repo.settings.add_webhook.invalid_channel_name"), + Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"), }) } return middleware.Validate(errs, ctx.Data, f, ctx.Locale) @@ -601,8 +612,8 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) // swagger:model MergePullRequestOption type MergePullRequestForm struct { // required: true - // enum: merge,rebase,rebase-merge,squash,manually-merged - Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"` + // enum: merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged + Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"` MergeTitleField string MergeMessageField string MergeCommitID string // only used for manually-merged @@ -628,6 +639,7 @@ type CodeCommentForm struct { SingleReview bool `form:"single_review"` Reply int64 `form:"reply"` LatestCommitID string + Files []string } // Validate validates the fields @@ -798,6 +810,7 @@ type CherryPickForm struct { CommitChoice string `binding:"Required;MaxSize(50)"` NewBranchName string `binding:"GitRefName;MaxSize(100)"` LastCommit string + CommitMailID int64 `binding:"Required"` Revert bool Signoff bool } @@ -824,6 +837,7 @@ type UploadRepoFileForm struct { CommitChoice string `binding:"Required;MaxSize(50)"` NewBranchName string `binding:"GitRefName;MaxSize(100)"` Files []string + CommitMailID int64 `binding:"Required"` Signoff bool } @@ -858,6 +872,7 @@ type DeleteRepoFileForm struct { CommitChoice string `binding:"Required;MaxSize(50)"` NewBranchName string `binding:"GitRefName;MaxSize(100)"` LastCommit string + CommitMailID int64 `binding:"Required"` Signoff bool } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 0f6e2b6c17..715c5bf9b8 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -13,7 +13,6 @@ import ( "html/template" "io" "net/url" - "sort" "strings" "time" @@ -75,13 +74,13 @@ const ( // DiffLine represents a line difference in a DiffSection. type DiffLine struct { - LeftIdx int - RightIdx int - Match int - Type DiffLineType - Content string - Comments []*issues_model.Comment - SectionInfo *DiffLineSectionInfo + LeftIdx int + RightIdx int + Match int + Type DiffLineType + Content string + Conversations []issues_model.CodeConversation + SectionInfo *DiffLineSectionInfo } // DiffLineSectionInfo represents diff line section meta data @@ -118,15 +117,15 @@ func (d *DiffLine) GetHTMLDiffLineType() string { // CanComment returns whether a line can get commented func (d *DiffLine) CanComment() bool { - return len(d.Comments) == 0 && d.Type != DiffLineSection + return len(d.Conversations) == 0 && d.Type != DiffLineSection } // GetCommentSide returns the comment side of the first comment, if not set returns empty string func (d *DiffLine) GetCommentSide() string { - if len(d.Comments) == 0 { + if len(d.Conversations) == 0 || len(d.Conversations[0]) == 0 { return "" } - return d.Comments[0].DiffSide() + return d.Conversations[0][0].DiffSide() } // GetLineTypeMarker returns the line type marker @@ -285,14 +284,14 @@ type DiffInline struct { // DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escaped func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline { - status, content := charset.EscapeControlHTML(s, locale) + status, content := charset.EscapeControlHTML(s, locale, charset.DiffContext) return DiffInline{EscapeStatus: status, Content: content} } // DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline { highlighted, _ := highlight.Code(fileName, language, code) - status, content := charset.EscapeControlHTML(highlighted, locale) + status, content := charset.EscapeControlHTML(highlighted, locale, charset.DiffContext) return DiffInline{EscapeStatus: status, Content: content} } @@ -467,23 +466,20 @@ type Diff struct { // LoadComments loads comments into each line func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error { - allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments) + allConversations, err := issues_model.FetchCodeConversations(ctx, issue, currentUser, showOutdatedComments) if err != nil { return err } for _, file := range diff.Files { - if lineCommits, ok := allComments[file.Name]; ok { + if lineCommits, ok := allConversations[file.Name]; ok { for _, section := range file.Sections { for _, line := range section.Lines { - if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { - line.Comments = append(line.Comments, comments...) + if conversations, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { + line.Conversations = append(line.Conversations, conversations...) } if comments, ok := lineCommits[int64(line.RightIdx)]; ok { - line.Comments = append(line.Comments, comments...) + line.Conversations = append(line.Conversations, comments...) } - sort.SliceStable(line.Comments, func(i, j int) bool { - return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix - }) } } } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index adcac355a7..8d6c376dce 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -601,7 +601,7 @@ func TestDiff_LoadCommentsNoOutdated(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff := setupDefaultDiff() assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, false)) - assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations, 2) } func TestDiff_LoadCommentsWithOutdated(t *testing.T) { @@ -611,20 +611,22 @@ func TestDiff_LoadCommentsWithOutdated(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff := setupDefaultDiff() assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, true)) - assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 3) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations, 2) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations[0], 2) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations[1], 1) } func TestDiffLine_CanComment(t *testing.T) { assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment()) - assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment()) + assert.False(t, (&DiffLine{Type: DiffLineAdd, Conversations: []issues_model.CodeConversation{{{Content: "bla"}}}}).CanComment()) assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment()) assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment()) assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment()) } func TestDiffLine_GetCommentSide(t *testing.T) { - assert.Equal(t, "previous", (&DiffLine{Comments: []*issues_model.Comment{{Line: -3}}}).GetCommentSide()) - assert.Equal(t, "proposed", (&DiffLine{Comments: []*issues_model.Comment{{Line: 3}}}).GetCommentSide()) + assert.Equal(t, "previous", (&DiffLine{Conversations: []issues_model.CodeConversation{{{Line: -3}}}}).GetCommentSide()) + assert.Equal(t, "proposed", (&DiffLine{Conversations: []issues_model.CodeConversation{{{Line: 3}}}}).GetCommentSide()) } func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { diff --git a/services/issue/assignee.go b/services/issue/assignee.go index 27fc695533..b5f472ba53 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -113,10 +114,10 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, return err } - var pemResult bool + canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue) + if isAdd { - pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) - if !pemResult { + if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) { return issues_model.ErrNotValidReviewRequest{ Reason: "Reviewer can't read", UserID: doer.ID, @@ -124,28 +125,6 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, } } - if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest { - return nil - } - - pemResult = doer.ID == issue.PosterID - if !pemResult { - pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests) - } - if !pemResult { - pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer) - if err != nil { - return err - } - if !pemResult { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer can't choose reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - } - if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { return issues_model.ErrNotValidReviewRequest{ Reason: "poster of pr can't be reviewer", @@ -153,22 +132,35 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, RepoID: issue.Repo.ID, } } - } else { - if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { + + if canDoerChangeReviewRequests { return nil } - pemResult = permDoer.IsAdmin() - if !pemResult { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer is not admin", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } + if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't choose reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, } } - return nil + if canDoerChangeReviewRequests { + return nil + } + + if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't remove reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } } // IsValidTeamReviewRequest Check permission for ReviewRequest Team @@ -181,11 +173,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, } } - permission, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) - if err != nil { - log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index) - return err - } + canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue) if isAdd { if issue.Repo.IsPrivate { @@ -200,30 +188,26 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, } } - doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests) - if !doerCanWrite && doer.ID != issue.PosterID { - official, err := issues_model.IsOfficialReviewer(ctx, issue, doer) - if err != nil { - log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index) - return err - } - if !official { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer can't choose reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } + if canDoerChangeReviewRequests { + return nil } - } else if !permission.IsAdmin() { + return issues_model.ErrNotValidReviewRequest{ - Reason: "Only admin users can remove team requests. Doer is not admin", + Reason: "Doer can't choose reviewer", UserID: doer.ID, RepoID: issue.Repo.ID, } } - return nil + if canDoerChangeReviewRequests { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't remove reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } } // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. @@ -264,3 +248,50 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use return comment, err } + +// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR +func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool { + // The poster of the PR can change the reviewers + if doer.ID == issue.PosterID { + return true + } + + // The owner of the repo can change the reviewers + if doer.ID == repo.OwnerID { + return true + } + + // Collaborators of the repo can change the reviewers + isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID) + if err != nil { + log.Error("IsCollaborator: %v", err) + return false + } + if isCollaborator { + return true + } + + // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers + if repo.Owner.IsOrganization() { + teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) + if err != nil { + log.Error("GetTeamsWithAccessToRepo: %v", err) + return false + } + for _, team := range teams { + if !team.UnitEnabled(ctx, unit.TypePullRequests) { + continue + } + isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID) + if err != nil { + log.Error("IsTeamMember: %v", err) + continue + } + if isMember { + return true + } + } + } + + return false +} diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go index 9682c52456..5ce2cd5fd5 100644 --- a/services/mailer/incoming/incoming_handler.go +++ b/services/mailer/incoming/incoming_handler.go @@ -130,6 +130,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u false, // not pending review but a single review comment.ReviewID, "", + nil, ) if err != nil { return fmt.Errorf("CreateCodeComment failed: %w", err) diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 7b64eead2f..f72ae45f89 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -94,7 +94,7 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) { // No mail service configured return } - sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account") + sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account") } // SendResetPasswordMail sends a password reset mail to the user @@ -104,7 +104,7 @@ func SendResetPasswordMail(u *user_model.User) { return } locale := translation.NewLocale(u.Language) - sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account") + sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account") } // SendActivateEmailMail sends confirmation email to confirm new email address @@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email string) { return } - msg := NewMessage(email, locale.Tr("mail.activate_email"), content.String()) + msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String()) msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) SendAsync(msg) @@ -158,7 +158,7 @@ func SendRegisterNotifyMail(u *user_model.User) { return } - msg := NewMessage(u.Email, locale.Tr("mail.register_notify"), content.String()) + msg := NewMessage(u.Email, locale.TrString("mail.register_notify"), content.String()) msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) SendAsync(msg) @@ -173,7 +173,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) locale := translation.NewLocale(u.Language) repoName := repo.FullName() - subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName) + subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName) data := map[string]any{ "locale": locale, "Subject": subject, diff --git a/services/mailer/mail_admin_new_user.go b/services/mailer/mail_admin_new_user.go index e9610e626a..ecf0ddf5fa 100644 --- a/services/mailer/mail_admin_new_user.go +++ b/services/mailer/mail_admin_new_user.go @@ -52,8 +52,8 @@ func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []str locale := translation.NewLocale(lang) manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10) - subject := locale.Tr("mail.admin.new_user.subject", u.Name) - body := locale.Tr("mail.admin.new_user.text", manageUserURL) + subject := locale.TrString("mail.admin.new_user.subject", u.Name) + body := locale.TrString("mail.admin.new_user.text", manageUserURL) mailMeta := map[string]any{ "NewUser": u, "NewUserUrl": u.HTMLURL(), diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 801c2476c2..6682774a04 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -68,12 +68,13 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo return } - subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) + subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) mailMeta := map[string]any{ "locale": locale, "Release": rel, "Subject": subject, "Language": locale.Language(), + "Link": rel.HTMLURL(), } var mailBody bytes.Buffer diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go index b89dcd43b5..e0d55bb120 100644 --- a/services/mailer/mail_repo.go +++ b/services/mailer/mail_repo.go @@ -56,11 +56,11 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U content bytes.Buffer ) - destination := locale.Tr("mail.repo.transfer.to_you") - subject := locale.Tr("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName()) + destination := locale.TrString("mail.repo.transfer.to_you") + subject := locale.TrString("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName()) if newOwner.IsOrganization() { destination = newOwner.DisplayName() - subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination) + subject = locale.TrString("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination) } data := map[string]any{ diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index ab32beefac..ceecefa50f 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -50,7 +50,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect) } - subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) + subject := locale.TrString("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) mailMeta := map[string]any{ "locale": locale, "Inviter": inviter, diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 0f0ca7e0f5..717be7b7f3 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -120,7 +120,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate r.DefaultBranch = repo.DefaultBranch r.Description = repo.Description - r, err = repo_module.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{ + r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{ RepoName: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, @@ -140,8 +140,18 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate if err != nil { return err } - g.gitRepo, err = gitrepo.OpenRepository(g.ctx, r) - return err + g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo) + if err != nil { + return err + } + + // detect object format from git repository and update to database + objectFormat, err := g.gitRepo.GetObjectFormat() + if err != nil { + return err + } + g.repo.ObjectFormatName = objectFormat.Name() + return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name") } // Close closes this uploader @@ -482,11 +492,19 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { } case issues_model.CommentTypeChangeTitle: if comment.Meta["OldTitle"] != nil { - cm.OldTitle = fmt.Sprintf("%s", comment.Meta["OldTitle"]) + cm.OldTitle = fmt.Sprint(comment.Meta["OldTitle"]) } if comment.Meta["NewTitle"] != nil { - cm.NewTitle = fmt.Sprintf("%s", comment.Meta["NewTitle"]) + cm.NewTitle = fmt.Sprint(comment.Meta["NewTitle"]) } + case issues_model.CommentTypeChangeTargetBranch: + if comment.Meta["OldRef"] != nil && comment.Meta["NewRef"] != nil { + cm.OldRef = fmt.Sprint(comment.Meta["OldRef"]) + cm.NewRef = fmt.Sprint(comment.Meta["NewRef"]) + cm.Content = "" + } + case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge: + cm.Content = "" default: } @@ -899,7 +917,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { comment.UpdatedAt = comment.CreatedAt } - objectFormat, _ := g.gitRepo.GetObjectFormat() + objectFormat := git.ObjectFormatFromName(g.repo.ObjectFormatName) if !objectFormat.IsValid(comment.CommitID) { log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID) comment.CommitID = headCommitID diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index 3db10465fc..5e49ae6d57 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -11,9 +11,11 @@ import ( "net/http" "net/url" "path" + "regexp" "strings" "time" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" @@ -506,30 +508,8 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err) } for _, comment := range comments { - // Flatten comment threads - if !comment.IndividualNote { - for _, note := range comment.Notes { - allComments = append(allComments, &base.Comment{ - IssueIndex: commentable.GetLocalIndex(), - Index: int64(note.ID), - PosterID: int64(note.Author.ID), - PosterName: note.Author.Username, - PosterEmail: note.Author.Email, - Content: note.Body, - Created: *note.CreatedAt, - }) - } - } else { - c := comment.Notes[0] - allComments = append(allComments, &base.Comment{ - IssueIndex: commentable.GetLocalIndex(), - Index: int64(c.ID), - PosterID: int64(c.Author.ID), - PosterName: c.Author.Username, - PosterEmail: c.Author.Email, - Content: c.Body, - Created: *c.CreatedAt, - }) + for _, note := range comment.Notes { + allComments = append(allComments, g.convertNoteToComment(commentable.GetLocalIndex(), note)) } } if resp.NextPage == 0 { @@ -540,6 +520,36 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co return allComments, true, nil } +var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$") + +func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment { + comment := &base.Comment{ + IssueIndex: localIndex, + Index: int64(note.ID), + PosterID: int64(note.Author.ID), + PosterName: note.Author.Username, + PosterEmail: note.Author.Email, + Content: note.Body, + Created: *note.CreatedAt, + Meta: map[string]any{}, + } + + // Try to find the underlying event of system notes. + if note.System { + if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil { + comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String() + comment.Meta["OldRef"] = match[1] + comment.Meta["NewRef"] = match[2] + } else if strings.HasPrefix(note.Body, "enabled an automatic merge") { + comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String() + } else if note.Body == "canceled the automatic merge" { + comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String() + } + } + + return comment +} + // GetPullRequests returns pull requests according page and perPage func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { if perPage > g.maxPerPage { diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index 25324a087f..6e5ab86720 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -558,6 +558,88 @@ func TestAwardsToReactions(t *testing.T) { }, reactions) } +func TestNoteToComment(t *testing.T) { + downloader := &GitlabDownloader{} + + now := time.Now() + makeTestNote := func(id int, body string, system bool) gitlab.Note { + return gitlab.Note{ + ID: id, + Author: struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + }{ + ID: 72, + Email: "test@example.com", + Username: "test", + }, + Body: body, + CreatedAt: &now, + System: system, + } + } + notes := []gitlab.Note{ + makeTestNote(1, "This is a regular comment", false), + makeTestNote(2, "enabled an automatic merge for abcd1234", true), + makeTestNote(3, "changed target branch from `master` to `main`", true), + makeTestNote(4, "canceled the automatic merge", true), + } + comments := []base.Comment{{ + IssueIndex: 17, + Index: 1, + PosterID: 72, + PosterName: "test", + PosterEmail: "test@example.com", + CommentType: "", + Content: "This is a regular comment", + Created: now, + Meta: map[string]any{}, + }, { + IssueIndex: 17, + Index: 2, + PosterID: 72, + PosterName: "test", + PosterEmail: "test@example.com", + CommentType: "pull_scheduled_merge", + Content: "enabled an automatic merge for abcd1234", + Created: now, + Meta: map[string]any{}, + }, { + IssueIndex: 17, + Index: 3, + PosterID: 72, + PosterName: "test", + PosterEmail: "test@example.com", + CommentType: "change_target_branch", + Content: "changed target branch from `master` to `main`", + Created: now, + Meta: map[string]any{ + "OldRef": "master", + "NewRef": "main", + }, + }, { + IssueIndex: 17, + Index: 4, + PosterID: 72, + PosterName: "test", + PosterEmail: "test@example.com", + CommentType: "pull_cancel_scheduled_merge", + Content: "canceled the automatic merge", + Created: now, + Meta: map[string]any{}, + }} + + for i, note := range notes { + actualComment := *downloader.convertNoteToComment(17, ¬e) + assert.EqualValues(t, actualComment, comments[i]) + } +} + func TestGitlabIIDResolver(t *testing.T) { r := gitlabIIDResolver{} r.recordIssueIID(1) diff --git a/services/packages/auth.go b/services/packages/auth.go index 2f78b26f50..8263c28bed 100644 --- a/services/packages/auth.go +++ b/services/packages/auth.go @@ -33,7 +33,7 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) { } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(setting.SecretKey)) + tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret()) if err != nil { return "", err } @@ -57,7 +57,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } - return []byte(setting.SecretKey), nil + return setting.GetGeneralTokenSigningSecret(), nil }) if err != nil { return 0, err diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go index 86c54e40c8..611faa6ade 100644 --- a/services/packages/debian/repository.go +++ b/services/packages/debian/repository.go @@ -342,7 +342,7 @@ func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " ")) fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " ")) fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123)) - fmt.Fprint(w, "Acquire-By-Hash: yes") + fmt.Fprint(w, "Acquire-By-Hash: yes\n") pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs) if err != nil { diff --git a/services/pull/check.go b/services/pull/check.go index dd6c3ed230..f4dd332b14 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -222,10 +222,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com } defer gitRepo.Close() - objectFormat, err := gitRepo.GetObjectFormat() - if err != nil { - return nil, fmt.Errorf("%-v GetObjectFormat: %w", pr.BaseRepo, err) - } + objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) // Get the commit from BaseBranch where the pull request got merged mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse"). diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 06e66fad77..27ee572640 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -52,6 +52,10 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, } } + if matchedCount != len(requiredContexts) { + return structs.CommitStatusPending + } + if matchedCount == 0 { status := git_model.CalcCommitStatus(commitStatuses) if status != nil { diff --git a/services/pull/merge.go b/services/pull/merge.go index 718e964014..2112108d45 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -273,6 +273,10 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use if err := doMergeStyleSquash(mergeCtx, message); err != nil { return "", err } + case repo_model.MergeStyleFastForwardOnly: + if err := doMergeStyleFastForwardOnly(mergeCtx); err != nil { + return "", err + } default: return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} } @@ -383,6 +387,13 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g StdErr: ctx.errbuf.String(), Err: err, } + } else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") { + log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + return models.ErrMergeDivergingFastForwardOnly{ + StdOut: ctx.outbuf.String(), + StdErr: ctx.errbuf.String(), + Err: err, + } } log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) @@ -492,7 +503,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged} } - objectFormat, _ := baseGitRepo.GetObjectFormat() + objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) if len(commitID) != objectFormat.FullLength() { return fmt.Errorf("Wrong commit ID") } diff --git a/services/pull/merge_ff_only.go b/services/pull/merge_ff_only.go new file mode 100644 index 0000000000..f57c732104 --- /dev/null +++ b/services/pull/merge_ff_only.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch) +func doMergeStyleFastForwardOnly(ctx *mergeContext) error { + cmd := git.NewCommand(ctx, "merge", "--ff-only").AddDynamicArguments(trackingBranch) + if err := runMergeCommand(ctx, repo_model.MergeStyleFastForwardOnly, cmd); err != nil { + log.Error("%-v Unable to merge tracking into base: %v", ctx.pr, err) + return err + } + + return nil +} diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go index 0f7664297a..bf56c071db 100644 --- a/services/pull/merge_merge.go +++ b/services/pull/merge_merge.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/modules/log" ) -// doMergeStyleMerge merges the tracking into the current HEAD - which is assumed to tbe staging branch (equal to the pr.BaseBranch) +// doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch) func doMergeStyleMerge(ctx *mergeContext, message string) error { cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch) if err := runMergeCommand(ctx, repo_model.MergeStyleMerge, cmd); err != nil { diff --git a/services/pull/review.go b/services/pull/review.go index 7e9e7d40a6..39644479f9 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -71,7 +71,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis } // CreateCodeComment creates a comment on the code line -func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) { +func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) { var ( existsReview bool err error @@ -104,6 +104,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. treePath, line, replyReviewID, + attachments, ) if err != nil { return nil, err @@ -144,6 +145,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. treePath, line, review.ID, + attachments, ) if err != nil { return nil, err @@ -161,8 +163,8 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git. return comment, nil } -// createCodeComment creates a plain code comment at the specified line / path -func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) { +// CreateCodeCommentKnownReviewID creates a plain code comment at the specified line / path +func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) { var commitID, patch string if err := issue.LoadPullRequest(ctx); err != nil { return nil, fmt.Errorf("LoadPullRequest: %w", err) @@ -260,6 +262,7 @@ func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, ReviewID: reviewID, Patch: patch, Invalidated: invalidated, + Attachments: attachments, }) } diff --git a/services/release/release.go b/services/release/release.go index 4c522c18be..a359e5078e 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -88,7 +88,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel created = true rel.LowerTagName = strings.ToLower(rel.TagName) - objectFormat, _ := gitRepo.GetObjectFormat() + objectFormat := git.ObjectFormatFromName(rel.Repo.ObjectFormatName) commits := repository.NewPushCommits() commits.HeadCommit = repository.CommitToPushCommit(commit) commits.CompareURL = rel.Repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), commit.ID.String()) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index bfb965063f..7ca68776b5 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -154,7 +155,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r ListOptions: db.ListOptions{ ListAll: true, }, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }) found := false diff --git a/services/repository/branch.go b/services/repository/branch.go index d7ac25b7c9..39be3984ae 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/timeutil" @@ -59,7 +60,7 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git branchOpts := git_model.FindBranchOptions{ RepoID: repo.ID, - IsDeletedBranch: isDeletedBranch, + IsDeletedBranch: isDeletedBranch.ToGeneric(), ListOptions: db.ListOptions{ Page: page, PageSize: pageSize, @@ -243,7 +244,7 @@ func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName stri // we cannot simply insert the branch but need to check we have branches or not hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{ RepoID: repoID, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }.ToConds()) if err != nil { return err @@ -368,11 +369,6 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R return fmt.Errorf("GetBranch: %v", err) } - objectFormat, err := gitRepo.GetObjectFormat() - if err != nil { - return err - } - if rawBranch.IsDeleted { return nil } @@ -394,6 +390,8 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R return err } + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + // Don't return error below this if err := PushUpdate( &repo_module.PushUpdateOptions{ diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go new file mode 100644 index 0000000000..7c9f535ae0 --- /dev/null +++ b/services/repository/contributors_graph.go @@ -0,0 +1,317 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "bufio" + "context" + "errors" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "code.gitea.io/gitea/models/avatars" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + + "gitea.com/go-chi/cache" +) + +const ( + contributorStatsCacheKey = "GetContributorStats/%s/%s" + contributorStatsCacheTimeout int64 = 60 * 10 +) + +var ( + ErrAwaitGeneration = errors.New("generation took longer than ") + awaitGenerationTime = time.Second * 5 + generateLock = sync.Map{} +) + +type WeekData struct { + Week int64 `json:"week"` // Starting day of the week as Unix timestamp + Additions int `json:"additions"` // Number of additions in that week + Deletions int `json:"deletions"` // Number of deletions in that week + Commits int `json:"commits"` // Number of commits in that week +} + +// ContributorData represents statistical git commit count data +type ContributorData struct { + Name string `json:"name"` // Display name of the contributor + Login string `json:"login"` // Login name of the contributor in case it exists + AvatarLink string `json:"avatar_link"` + HomeLink string `json:"home_link"` + TotalCommits int64 `json:"total_commits"` + Weeks map[int64]*WeekData `json:"weeks"` +} + +// ExtendedCommitStats contains information for commit stats with author data +type ExtendedCommitStats struct { + Author *api.CommitUser `json:"author"` + Stats *api.CommitStats `json:"stats"` +} + +const layout = time.DateOnly + +func findLastSundayBeforeDate(dateStr string) (string, error) { + date, err := time.Parse(layout, dateStr) + if err != nil { + return "", err + } + + weekday := date.Weekday() + daysToSubtract := int(weekday) - int(time.Sunday) + if daysToSubtract < 0 { + daysToSubtract += 7 + } + + lastSunday := date.AddDate(0, 0, -daysToSubtract) + return lastSunday.Format(layout), nil +} + +// GetContributorStats returns contributors stats for git commits for given revision or default branch +func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) { + // as GetContributorStats is resource intensive we cache the result + cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision) + if !cache.IsExist(cacheKey) { + genReady := make(chan struct{}) + + // dont start multible async generations + _, run := generateLock.Load(cacheKey) + if run { + return nil, ErrAwaitGeneration + } + + generateLock.Store(cacheKey, struct{}{}) + // run generation async + go generateContributorStats(genReady, cache, cacheKey, repo, revision) + + select { + case <-time.After(awaitGenerationTime): + return nil, ErrAwaitGeneration + case <-genReady: + // we got generation ready before timeout + break + } + } + // TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout) + + switch v := cache.Get(cacheKey).(type) { + case error: + return nil, v + case map[string]*ContributorData: + return v, nil + default: + return nil, fmt.Errorf("unexpected type in cache detected") + } +} + +// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision +func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) { + baseCommit, err := repo.GetCommit(revision) + if err != nil { + return nil, err + } + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + + gitCmd := git.NewCommand(repo.Ctx, "log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse") + // AddOptionFormat("--max-count=%d", limit) + gitCmd.AddDynamicArguments(baseCommit.ID.String()) + + var extendedCommitStats []*ExtendedCommitStats + stderr := new(strings.Builder) + err = gitCmd.Run(&git.RunOpts{ + Dir: repo.Path, + Stdout: stdoutWriter, + Stderr: stderr, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + scanner := bufio.NewScanner(stdoutReader) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "---" { + continue + } + scanner.Scan() + authorName := strings.TrimSpace(scanner.Text()) + scanner.Scan() + authorEmail := strings.TrimSpace(scanner.Text()) + scanner.Scan() + date := strings.TrimSpace(scanner.Text()) + scanner.Scan() + stats := strings.TrimSpace(scanner.Text()) + if authorName == "" || authorEmail == "" || date == "" || stats == "" { + // FIXME: find a better way to parse the output so that we will handle this properly + log.Warn("Something is wrong with git log output, skipping...") + log.Warn("authorName: %s, authorEmail: %s, date: %s, stats: %s", authorName, authorEmail, date, stats) + continue + } + // 1 file changed, 1 insertion(+), 1 deletion(-) + fields := strings.Split(stats, ",") + + commitStats := api.CommitStats{} + for _, field := range fields[1:] { + parts := strings.Split(strings.TrimSpace(field), " ") + value, contributionType := parts[0], parts[1] + amount, _ := strconv.Atoi(value) + + if strings.HasPrefix(contributionType, "insertion") { + commitStats.Additions = amount + } else { + commitStats.Deletions = amount + } + } + commitStats.Total = commitStats.Additions + commitStats.Deletions + scanner.Text() // empty line at the end + + res := &ExtendedCommitStats{ + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: authorName, + Email: authorEmail, + }, + Date: date, + }, + Stats: &commitStats, + } + extendedCommitStats = append(extendedCommitStats, res) + + } + _ = stdoutReader.Close() + return nil + }, + }) + if err != nil { + return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr) + } + + return extendedCommitStats, nil +} + +func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) { + ctx := graceful.GetManager().HammerContext() + + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + err := fmt.Errorf("OpenRepository: %w", err) + _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) + return + } + defer closer.Close() + + if len(revision) == 0 { + revision = repo.DefaultBranch + } + extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision) + if err != nil { + err := fmt.Errorf("ExtendedCommitStats: %w", err) + _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) + return + } + if len(extendedCommitStats) == 0 { + err := fmt.Errorf("no commit stats returned for revision '%s'", revision) + _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) + return + } + + layout := time.DateOnly + + unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0) + contributorsCommitStats := make(map[string]*ContributorData) + contributorsCommitStats["total"] = &ContributorData{ + Name: "Total", + Weeks: make(map[int64]*WeekData), + } + total := contributorsCommitStats["total"] + + for _, v := range extendedCommitStats { + userEmail := v.Author.Email + if len(userEmail) == 0 { + continue + } + u, _ := user_model.GetUserByEmail(ctx, userEmail) + if u != nil { + // update userEmail with user's primary email address so + // that different mail addresses will linked to same account + userEmail = u.GetEmail() + } + // duplicated logic + if _, ok := contributorsCommitStats[userEmail]; !ok { + if u == nil { + avatarLink := avatars.GenerateEmailAvatarFastLink(ctx, userEmail, 0) + if avatarLink == "" { + avatarLink = unknownUserAvatarLink + } + contributorsCommitStats[userEmail] = &ContributorData{ + Name: v.Author.Name, + AvatarLink: avatarLink, + Weeks: make(map[int64]*WeekData), + } + } else { + contributorsCommitStats[userEmail] = &ContributorData{ + Name: u.DisplayName(), + Login: u.LowerName, + AvatarLink: u.AvatarLinkWithSize(ctx, 0), + HomeLink: u.HomeLink(), + Weeks: make(map[int64]*WeekData), + } + } + } + // Update user statistics + user := contributorsCommitStats[userEmail] + startingOfWeek, _ := findLastSundayBeforeDate(v.Author.Date) + + val, _ := time.Parse(layout, startingOfWeek) + week := val.UnixMilli() + + if user.Weeks[week] == nil { + user.Weeks[week] = &WeekData{ + Additions: 0, + Deletions: 0, + Commits: 0, + Week: week, + } + } + if total.Weeks[week] == nil { + total.Weeks[week] = &WeekData{ + Additions: 0, + Deletions: 0, + Commits: 0, + Week: week, + } + } + user.Weeks[week].Additions += v.Stats.Additions + user.Weeks[week].Deletions += v.Stats.Deletions + user.Weeks[week].Commits++ + user.TotalCommits++ + + // Update overall statistics + total.Weeks[week].Additions += v.Stats.Additions + total.Weeks[week].Deletions += v.Stats.Deletions + total.Weeks[week].Commits++ + total.TotalCommits++ + } + + _ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout) + generateLock.Delete(cacheKey) + if genDone != nil { + genDone <- struct{}{} + } +} diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go new file mode 100644 index 0000000000..3801a5eee4 --- /dev/null +++ b/services/repository/contributors_graph_test.go @@ -0,0 +1,87 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "slices" + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + + "gitea.com/go-chi/cache" + "github.com/stretchr/testify/assert" +) + +func TestRepository_ContributorsGraph(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + assert.NoError(t, repo.LoadOwner(db.DefaultContext)) + mockCache, err := cache.NewCacher(cache.Options{ + Adapter: "memory", + Interval: 24 * 60, + }) + assert.NoError(t, err) + + generateContributorStats(nil, mockCache, "key", repo, "404ref") + err, isErr := mockCache.Get("key").(error) + assert.True(t, isErr) + assert.ErrorAs(t, err, &git.ErrNotExist{}) + + generateContributorStats(nil, mockCache, "key2", repo, "master") + data, isData := mockCache.Get("key2").(map[string]*ContributorData) + assert.True(t, isData) + var keys []string + for k := range data { + keys = append(keys, k) + } + slices.Sort(keys) + assert.EqualValues(t, []string{ + "ethantkoenig@gmail.com", + "jimmy.praet@telenet.be", + "jon@allspice.io", + "total", // generated summary + }, keys) + + assert.EqualValues(t, &ContributorData{ + Name: "Ethan Koenig", + AvatarLink: "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon", + TotalCommits: 1, + Weeks: map[int64]*WeekData{ + 1511654400000: { + Week: 1511654400000, // sunday 2017-11-26 + Additions: 3, + Deletions: 0, + Commits: 1, + }, + }, + }, data["ethantkoenig@gmail.com"]) + assert.EqualValues(t, &ContributorData{ + Name: "Total", + AvatarLink: "", + TotalCommits: 3, + Weeks: map[int64]*WeekData{ + 1511654400000: { + Week: 1511654400000, // sunday 2017-11-26 (2017-11-26 20:31:18 -0800) + Additions: 3, + Deletions: 0, + Commits: 1, + }, + 1607817600000: { + Week: 1607817600000, // sunday 2020-12-13 (2020-12-15 15:23:11 -0500) + Additions: 10, + Deletions: 0, + Commits: 1, + }, + 1624752000000: { + Week: 1624752000000, // sunday 2021-06-27 (2021-06-29 21:54:09 +0200) + Additions: 2, + Deletions: 0, + Commits: 1, + }, + }, + }, data["total"]) +} diff --git a/services/repository/create_test.go b/services/repository/create_test.go index b3e1f0550c..131249ad9c 100644 --- a/services/repository/create_test.go +++ b/services/repository/create_test.go @@ -52,12 +52,12 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") // Create repos. - repoIds := make([]int64, 0) + repoIDs := make([]int64, 0) for i := 0; i < 3; i++ { r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) assert.NoError(t, err, "CreateRepository %d", i) if r != nil { - repoIds = append(repoIds, r.ID) + repoIDs = append(repoIDs, r.ID) } } // Get fresh copy of Owner team after creating repos. @@ -93,10 +93,10 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { }, } teamRepos := [][]int64{ - repoIds, - repoIds, + repoIDs, + repoIDs, {}, - repoIds, + repoIDs, {}, } for i, team := range teams { @@ -109,7 +109,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { // Update teams and check repositories. teams[3].IncludesAllRepositories = false teams[4].IncludesAllRepositories = true - teamRepos[4] = repoIds + teamRepos[4] = repoIDs for i, team := range teams { assert.NoError(t, models.UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name) testTeamRepositories(team.ID, teamRepos[i]) @@ -119,27 +119,27 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) assert.NoError(t, err, "CreateRepository last") if r != nil { - repoIds = append(repoIds, r.ID) + repoIDs = append(repoIDs, r.ID) } - teamRepos[0] = repoIds - teamRepos[1] = repoIds - teamRepos[4] = repoIds + teamRepos[0] = repoIDs + teamRepos[1] = repoIDs + teamRepos[4] = repoIDs for i, team := range teams { testTeamRepositories(team.ID, teamRepos[i]) } // Remove repo and check teams repositories. - assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIds[0]), "DeleteRepository") - teamRepos[0] = repoIds[1:] - teamRepos[1] = repoIds[1:] - teamRepos[3] = repoIds[1:3] - teamRepos[4] = repoIds[1:] + assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository") + teamRepos[0] = repoIDs[1:] + teamRepos[1] = repoIDs[1:] + teamRepos[3] = repoIDs[1:3] + teamRepos[4] = repoIDs[1:] for i, team := range teams { testTeamRepositories(team.ID, teamRepos[i]) } // Wipe created items. - for i, rid := range repoIds { + for i, rid := range repoIDs { if i > 0 { // first repo already deleted. assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i) } diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go index 16a15e06a7..512aec7c81 100644 --- a/services/repository/files/commit.go +++ b/services/repository/files/commit.go @@ -30,10 +30,8 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato } defer closer.Close() - objectFormat, err := gitRepo.GetObjectFormat() - if err != nil { - return fmt.Errorf("GetObjectFormat[%s]: %w", repoPath, err) - } + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + commit, err := gitRepo.GetCommit(sha) if err != nil { gitRepo.Close() diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 8adec4aff8..009e5cfa2a 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -270,3 +270,34 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git Content: content, }, nil } + +// TryGetContentLanguage tries to get the (linguist) language of the file content +func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) { + indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID) + if err != nil { + return "", err + } + + defer deleteTemporaryFile() + + filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ + CachedOnly: true, + Attributes: []string{"linguist-language", "gitlab-language"}, + Filenames: []string{treePath}, + IndexFile: indexFilename, + WorkTree: worktree, + }) + if err != nil { + return "", err + } + + language := filename2attribute2info[treePath]["linguist-language"] + if language == "" || language == "unspecified" { + language = filename2attribute2info[treePath]["gitlab-language"] + } + if language == "unspecified" { + language = "" + } + + return language, nil +} diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go index 91c878e505..fbd2f3e70f 100644 --- a/services/repository/files/diff_test.go +++ b/services/repository/files/diff_test.go @@ -53,11 +53,11 @@ func TestGetDiffPreview(t *testing.T) { Name: "", Lines: []*gitdiff.DiffLine{ { - LeftIdx: 0, - RightIdx: 0, - Type: 4, - Content: "@@ -1,3 +1,4 @@", - Comments: nil, + LeftIdx: 0, + RightIdx: 0, + Type: 4, + Content: "@@ -1,3 +1,4 @@", + Conversations: nil, SectionInfo: &gitdiff.DiffLineSectionInfo{ Path: "README.md", LastLeftIdx: 0, @@ -69,42 +69,42 @@ func TestGetDiffPreview(t *testing.T) { }, }, { - LeftIdx: 1, - RightIdx: 1, - Type: 1, - Content: " # repo1", - Comments: nil, + LeftIdx: 1, + RightIdx: 1, + Type: 1, + Content: " # repo1", + Conversations: nil, }, { - LeftIdx: 2, - RightIdx: 2, - Type: 1, - Content: " ", - Comments: nil, + LeftIdx: 2, + RightIdx: 2, + Type: 1, + Content: " ", + Conversations: nil, }, { - LeftIdx: 3, - RightIdx: 0, - Match: 4, - Type: 3, - Content: "-Description for repo1", - Comments: nil, + LeftIdx: 3, + RightIdx: 0, + Match: 4, + Type: 3, + Content: "-Description for repo1", + Conversations: nil, }, { - LeftIdx: 0, - RightIdx: 3, - Match: 3, - Type: 2, - Content: "+Description for repo1", - Comments: nil, + LeftIdx: 0, + RightIdx: 3, + Match: 3, + Type: 2, + Content: "+Description for repo1", + Conversations: nil, }, { - LeftIdx: 0, - RightIdx: 4, - Match: -1, - Type: 2, - Content: "+this is a new line", - Comments: nil, + LeftIdx: 0, + RightIdx: 4, + Match: -1, + Type: 2, + Content: "+this is a new line", + Conversations: nil, }, }, }, diff --git a/services/repository/files/search.go b/services/repository/files/search.go new file mode 100644 index 0000000000..f8317c4892 --- /dev/null +++ b/services/repository/files/search.go @@ -0,0 +1,90 @@ +package files + +import ( + "context" + "html/template" + "strconv" + "strings" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/go-enry/go-enry/v2" +) + +type Result struct { + RepoID int64 // ignored + Filename string + CommitID string // branch + UpdatedUnix timeutil.TimeStamp // ignored + Language string + Color string + LineNumbers []int64 + FormattedLines template.HTML +} + +const pHEAD = "HEAD:" + +func NewRepoGrep(ctx context.Context, repo *repo_model.Repository, keyword string) ([]*Result, error) { + t, _, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return nil, err + } + + data := []*Result{} + + stdout, _, err := git.NewCommand(ctx, + "grep", + "-1", // n before and after lines + "-z", + "--heading", + "--break", // easier parsing + "--fixed-strings", // disallow regex for now + "-n", // line nums + "-i", // ignore case + "--full-name", // full file path, rel to repo + //"--column", // for adding better highlighting support + ). + AddDynamicArguments(keyword). + AddArguments("HEAD"). + RunStdString(&git.RunOpts{Dir: t.Path}) + if err != nil { + return data, nil // non zero exit code when there are no results + } + + for _, block := range strings.Split(stdout, "\n\n") { + res := Result{CommitID: repo.DefaultBranch} + code := []string{} + + for _, line := range strings.Split(block, "\n") { + if strings.HasPrefix(line, pHEAD) { + res.Filename = strings.TrimPrefix(line, pHEAD) + continue + } + + if ln, after, ok := strings.Cut(line, "\x00"); ok { + i, err := strconv.ParseInt(ln, 10, 64) + if err != nil { + continue + } + + res.LineNumbers = append(res.LineNumbers, i) + code = append(code, after) + } + } + + if res.Filename == "" || len(code) == 0 || len(res.LineNumbers) == 0 { + continue + } + + res.FormattedLines, res.Language = highlight.Code(res.Filename, "", strings.Join(code, "\n")) + res.Color = enry.GetColor(res.Language) + + data = append(data, &res) + } + + return data, nil +} diff --git a/services/repository/files/search_test.go b/services/repository/files/search_test.go new file mode 100644 index 0000000000..c24bb731a8 --- /dev/null +++ b/services/repository/files/search_test.go @@ -0,0 +1,48 @@ +package files + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestNewRepoGrep(t *testing.T) { + unittest.PrepareTestEnv(t) + ctx, _ := contexttest.MockContext(t, "user2/repo1") + ctx.SetParams(":id", "1") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + + t.Run("with result", func(t *testing.T) { + res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "Description") + assert.NoError(t, err) + + expected := []*Result{ + { + RepoID: 0, + Filename: "README.md", + CommitID: "master", + UpdatedUnix: 0, + Language: "Markdown", + Color: "#083fa1", + LineNumbers: []int64{2, 3}, + FormattedLines: "\nDescription for repo1", + }, + } + + assert.EqualValues(t, res, expected) + }) + + t.Run("empty result", func(t *testing.T) { + res, err := NewRepoGrep(ctx, ctx.Repo.Repository, "keyword that does not match in the repo") + assert.NoError(t, err) + + assert.EqualValues(t, res, []*Result{}) + }) +} diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 9d3185c3fc..e3a7f3b8b0 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -37,7 +37,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git } apiURL := repo.APIURL() apiURLLen := len(apiURL) - objectFormat, _ := gitRepo.GetObjectFormat() + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) hashLen := objectFormat.FullLength() const gitBlobsPath = "/git/blobs/" diff --git a/services/repository/files/update.go b/services/repository/files/update.go index f223daf3a9..4f7178184b 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -40,7 +40,7 @@ type ChangeRepoFile struct { Operation string TreePath string FromTreePath string - ContentReader io.Reader + ContentReader io.ReadSeeker SHA string Options *RepoFileOptions } @@ -448,6 +448,10 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file return err } if !exist { + _, err := file.ContentReader.Seek(0, io.SeekStart) + if err != nil { + return err + } if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil { if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil { return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index cbfaf49d13..e8c149f77f 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -25,6 +25,8 @@ type UploadRepoFileOptions struct { NewBranch string TreePath string Message string + Author *IdentityOptions + Committer *IdentityOptions Files []string // In UUID format. Signoff bool } @@ -128,9 +130,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return err } - // make author and committer the doer - author := doer - committer := doer + author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) // Now commit the tree commitHash, err := t.CommitTree(opts.LastCommitID, author, committer, treeHash, opts.Message, opts.Signoff) diff --git a/services/repository/lfs.go b/services/repository/lfs.go index 4504f796bd..4d48881b87 100644 --- a/services/repository/lfs.go +++ b/services/repository/lfs.go @@ -79,7 +79,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R store := lfs.NewContentStore() errStop := errors.New("STOPERR") - objectFormat, _ := gitRepo.GetObjectFormat() + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject, count int64) error { if opts.NumberToCheckPerRepo > 0 && total > opts.NumberToCheckPerRepo { diff --git a/services/repository/migrate.go b/services/repository/migrate.go new file mode 100644 index 0000000000..b218a2ef46 --- /dev/null +++ b/services/repository/migrate.go @@ -0,0 +1,287 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migration" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// MigrateRepositoryGitData starts migrating git related data after created migrating repository +func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, + repo *repo_model.Repository, opts migration.MigrateOptions, + httpTransport *http.Transport, +) (*repo_model.Repository, error) { + repoPath := repo_model.RepoPath(u.Name, opts.RepoName) + + if u.IsOrganization() { + t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) + if err != nil { + return nil, err + } + repo.NumWatches = t.NumMembers + } else { + repo.NumWatches = 1 + } + + migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second + + var err error + if err = util.RemoveAll(repoPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err) + } + + if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{ + Mirror: true, + Quiet: true, + Timeout: migrateTimeout, + SkipTLSVerify: setting.Migrations.SkipTLSVerify, + }); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err) + } + return repo, fmt.Errorf("Clone: %w", err) + } + + if err := git.WriteCommitGraph(ctx, repoPath); err != nil { + return repo, err + } + + if opts.Wiki { + wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) + wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) + if len(wikiRemotePath) > 0 { + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + + if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ + Mirror: true, + Quiet: true, + Timeout: migrateTimeout, + SkipTLSVerify: setting.Migrations.SkipTLSVerify, + }); err != nil { + log.Warn("Clone wiki: %v", err) + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + } else { + // Figure out the branch of the wiki we just cloned. We assume + // that the default branch is to be used, and we'll use the same + // name as the source. + gitRepo, err := git.OpenRepository(ctx, wikiPath) + if err != nil { + log.Warn("Failed to open wiki repository during migration: %v", err) + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + return repo, err + } + defer gitRepo.Close() + + branch, err := gitRepo.GetDefaultBranch() + if err != nil { + log.Warn("Failed to get the default branch of a migrated wiki repo: %v", err) + if err := util.RemoveAll(wikiPath); err != nil { + return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) + } + + return repo, err + } + repo.WikiBranch = branch + + if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { + return repo, err + } + } + } + } + + if repo.OwnerID == u.ID { + repo.Owner = u + } + + if err = repo_module.CheckDaemonExportOK(ctx, repo); err != nil { + return repo, fmt.Errorf("checkDaemonExportOK: %w", err) + } + + if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err) + } + + gitRepo, err := git.OpenRepository(ctx, repoPath) + if err != nil { + return repo, fmt.Errorf("OpenRepository: %w", err) + } + defer gitRepo.Close() + + repo.IsEmpty, err = gitRepo.IsEmpty() + if err != nil { + return repo, fmt.Errorf("git.IsEmpty: %w", err) + } + + if !repo.IsEmpty { + if len(repo.DefaultBranch) == 0 { + // Try to get HEAD branch and set it as default branch. + headBranch, err := gitRepo.GetHEADBranch() + if err != nil { + return repo, fmt.Errorf("GetHEADBranch: %w", err) + } + if headBranch != nil { + repo.DefaultBranch = headBranch.Name + } + } + + if _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { + return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) + } + + if !opts.Releases { + // note: this will greatly improve release (tag) sync + // for pull-mirrors with many tags + repo.IsMirror = opts.Mirror + if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { + log.Error("Failed to synchronize tags to releases for repository: %v", err) + } + } + + if opts.LFS { + endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint) + lfsClient := lfs.NewClient(endpoint, httpTransport) + if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil { + log.Error("Failed to store missing LFS objects for repository: %v", err) + } + } + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + + if opts.Mirror { + remoteAddress, err := util.SanitizeURL(opts.CloneAddr) + if err != nil { + return repo, err + } + mirrorModel := repo_model.Mirror{ + RepoID: repo.ID, + Interval: setting.Mirror.DefaultInterval, + EnablePrune: true, + NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval), + LFS: opts.LFS, + RemoteAddress: remoteAddress, + } + if opts.LFS { + mirrorModel.LFSEndpoint = opts.LFSEndpoint + } + + if opts.MirrorInterval != "" { + parsedInterval, err := time.ParseDuration(opts.MirrorInterval) + if err != nil { + log.Error("Failed to set Interval: %v", err) + return repo, err + } + if parsedInterval == 0 { + mirrorModel.Interval = 0 + mirrorModel.NextUpdateUnix = 0 + } else if parsedInterval < setting.Mirror.MinInterval { + err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval) + log.Error("Interval: %s is too frequent", opts.MirrorInterval) + return repo, err + } else { + mirrorModel.Interval = parsedInterval + mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval) + } + } + + if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil { + return repo, fmt.Errorf("InsertOne: %w", err) + } + + repo.IsMirror = true + if err = UpdateRepository(ctx, repo, false); err != nil { + return nil, err + } + + // this is necessary for sync local tags from remote + configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName()) + if stdout, _, err := git.NewCommand(ctx, "config"). + AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err) + return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*): %w", err) + } + } else { + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil { + return nil, err + } + } + + return repo, committer.Commit() +} + +// cleanUpMigrateGitConfig removes mirror info which prevents "push --all". +// This also removes possible user credentials. +func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { + cmd := git.NewCommand(ctx, "remote", "rm", "origin") + // if the origin does not exist + _, stderr, err := cmd.RunStdString(&git.RunOpts{ + Dir: repoPath, + }) + if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + return err + } + return nil +} + +// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. +func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { + repoPath := repo.RepoPath() + if err := repo_module.CreateDelegateHooks(repoPath); err != nil { + return repo, fmt.Errorf("createDelegateHooks: %w", err) + } + if repo.HasWiki() { + if err := repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { + return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) + } + } + + _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) + if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) + } + + if repo.HasWiki() { + if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil { + return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err) + } + } + + return repo, UpdateRepository(ctx, repo, false) +} diff --git a/services/repository/push.go b/services/repository/push.go index 2ef8cac95e..bb080e30cc 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -93,11 +93,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } defer gitRepo.Close() - objectFormat, err := gitRepo.GetObjectFormat() - if err != nil { - return fmt.Errorf("unknown repository ObjectFormat [%s]: %w", repo.FullName(), err) - } - if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { return fmt.Errorf("Failed to update size for repository: %v", err) } @@ -105,6 +100,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { addTags := make([]string, 0, len(optsList)) delTags := make([]string, 0, len(optsList)) var pusher *user_model.User + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) for _, opts := range optsList { log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName) @@ -321,14 +317,9 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo return nil } - lowerTags := make([]string, 0, len(tags)) - for _, tag := range tags { - lowerTags = append(lowerTags, strings.ToLower(tag)) - } - releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ RepoID: repo.ID, - TagNames: lowerTags, + TagNames: tags, }) if err != nil { return fmt.Errorf("db.Find[repo_model.Release]: %w", err) @@ -338,6 +329,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo relMap[rel.LowerTagName] = rel } + lowerTags := make([]string, 0, len(tags)) + for _, tag := range tags { + lowerTags = append(lowerTags, strings.ToLower(tag)) + } + newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap)) emailToUser := make(map[string]*user_model.User) diff --git a/services/user/email.go b/services/user/email.go index 12ac1f6ca4..0b579cf792 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -153,9 +153,7 @@ func ReplaceInactivePrimaryEmail(ctx context.Context, oldEmail string, email *us return err } else if !has { return user_model.ErrUserNotExist{ - UID: email.UID, - Name: "", - KeyID: 0, + UID: email.UID, } } diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000..8c474c33a8 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,39 @@ +import {readFileSync} from 'node:fs'; +import {env} from 'node:process'; +import {parse} from 'css-variables-parser'; + +const isProduction = env.NODE_ENV !== 'development'; + +export default { + prefix: 'tw-', + content: [ + isProduction && '!./templates/devtest/**/*', + isProduction && '!./web_src/js/standalone/devtest.js', + './templates/**/*.tmpl', + './web_src/**/*.{js,vue}', + ].filter(Boolean), + blocklist: [ + // classes that don't work without CSS variables from "@tailwind base" which we don't use + 'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter', + 'backdrop-filter', + // unneeded classes + '[-a-zA-Z:0-9_.]', + ], + theme: { + colors: { + // make `tw-bg-red` etc work with our CSS variables + ...Object.fromEntries( + Object.keys(parse([ + readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'), + readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'), + ].join('\n'), {})).filter((prop) => prop.startsWith('color-')).map((prop) => { + const color = prop.substring(6); + return [color, `var(--color-${color})`]; + }) + ), + inherit: 'inherit', + current: 'currentcolor', + transparent: 'transparent', + }, + }, +}; diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index f43b4c5385..8088315f17 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -75,69 +75,9 @@

{{ctx.Locale.Tr "admin.dashboard.system_status"}}

-
-
-
{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}
-
{{.SysStatus.StartTime}}
-
{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}
-
{{.SysStatus.NumGoroutine}}
-
-
{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}
-
{{.SysStatus.MemAllocated}}
-
{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}
-
{{.SysStatus.MemTotal}}
-
{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}
-
{{.SysStatus.MemSys}}
-
{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}
-
{{.SysStatus.Lookups}}
-
{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}
-
{{.SysStatus.MemMallocs}}
-
{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}
-
{{.SysStatus.MemFrees}}
-
-
{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}
-
{{.SysStatus.HeapAlloc}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}
-
{{.SysStatus.HeapSys}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}
-
{{.SysStatus.HeapIdle}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}
-
{{.SysStatus.HeapInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}
-
{{.SysStatus.HeapReleased}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}
-
{{.SysStatus.HeapObjects}}
-
-
{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}
-
{{.SysStatus.StackInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}
-
{{.SysStatus.StackSys}}
-
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}
-
{{.SysStatus.MSpanInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}
-
{{.SysStatus.MSpanSys}}
-
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}
-
{{.SysStatus.MCacheInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}
-
{{.SysStatus.MCacheSys}}
-
{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}
-
{{.SysStatus.BuckHashSys}}
-
{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}
-
{{.SysStatus.GCSys}}
-
{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}
-
{{.SysStatus.OtherSys}}
-
-
{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}
-
{{.SysStatus.NextGC}}
-
{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}
-
{{.SysStatus.LastGC}}
-
{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}
-
{{.SysStatus.PauseTotalNs}}
-
{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}
-
{{.SysStatus.PauseNs}}
-
{{ctx.Locale.Tr "admin.dashboard.gc_times"}}
-
{{.SysStatus.NumGC}}
-
+ {{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}} +
+ {{template "admin/system_status" .}}
{{template "admin/layout_footer" .}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 5cfd9ddefa..cf860dab2a 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -88,7 +88,7 @@ {{ctx.Locale.Tr "packages.settings.delete"}}
- {{ctx.Locale.Tr "packages.settings.delete.notice" `` `` | Safe}} + {{ctx.Locale.Tr "packages.settings.delete.notice" (``|SafeHTML) (``|SafeHTML)}}
{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index fdba0734a2..e11247aed4 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -101,7 +101,7 @@

{{ctx.Locale.Tr "repo.settings.delete_desc"}}

- {{ctx.Locale.Tr "repo.settings.delete_notices_2" `` | Safe}}
+ {{ctx.Locale.Tr "repo.settings.delete_notices_2" (``|SafeHTML)}}
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 894e41f8d7..42944615c3 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -39,7 +39,7 @@ {{ctx.Locale.Tr "admin.monitor.process.cancel"}}
-

{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" `` | Safe}}

+

{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (``|SafeHTML)}}

{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/stats.tmpl b/templates/admin/stats.tmpl index 04fa862a85..70f2aa7fb4 100644 --- a/templates/admin/stats.tmpl +++ b/templates/admin/stats.tmpl @@ -5,10 +5,10 @@
- {{range $statsKey := .StatsKeys}} + {{range $statsKey, $statsValue := .Stats}} - + {{end}}
{{$statsKey}}{{index $.StatsCounter $statsKey}}{{$statsValue}}
diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl new file mode 100644 index 0000000000..7b5c9be6cc --- /dev/null +++ b/templates/admin/system_status.tmpl @@ -0,0 +1,62 @@ +
+
{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}
+
{{.SysStatus.StartTime}}
+
{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}
+
{{.SysStatus.NumGoroutine}}
+
+
{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}
+
{{.SysStatus.MemAllocated}}
+
{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}
+
{{.SysStatus.MemTotal}}
+
{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}
+
{{.SysStatus.MemSys}}
+
{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}
+
{{.SysStatus.Lookups}}
+
{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}
+
{{.SysStatus.MemMallocs}}
+
{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}
+
{{.SysStatus.MemFrees}}
+
+
{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}
+
{{.SysStatus.HeapAlloc}}
+
{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}
+
{{.SysStatus.HeapSys}}
+
{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}
+
{{.SysStatus.HeapIdle}}
+
{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}
+
{{.SysStatus.HeapInuse}}
+
{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}
+
{{.SysStatus.HeapReleased}}
+
{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}
+
{{.SysStatus.HeapObjects}}
+
+
{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}
+
{{.SysStatus.StackInuse}}
+
{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}
+
{{.SysStatus.StackSys}}
+
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}
+
{{.SysStatus.MSpanInuse}}
+
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}
+
{{.SysStatus.MSpanSys}}
+
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}
+
{{.SysStatus.MCacheInuse}}
+
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}
+
{{.SysStatus.MCacheSys}}
+
{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}
+
{{.SysStatus.BuckHashSys}}
+
{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}
+
{{.SysStatus.GCSys}}
+
{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}
+
{{.SysStatus.OtherSys}}
+
+
{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}
+
{{.SysStatus.NextGC}}
+
{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}
+
{{.SysStatus.LastGCTime}}
+
{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}
+
{{.SysStatus.PauseTotalNs}}
+
{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}
+
{{.SysStatus.PauseNs}}
+
{{ctx.Locale.Tr "admin.dashboard.gc_times"}}
+
{{.SysStatus.NumGC}}
+
diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 81f70511d0..bcb53d8131 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -26,7 +26,7 @@
{{else if .SearchResults}}

- {{ctx.Locale.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html}} + {{ctx.Locale.Tr "explore.code_search_results" .Keyword}}

{{template "code/searchresults" .}} {{else if .Keyword}} diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index 73293ddf48..ccf188609c 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -275,6 +275,12 @@
ps: no JS code attached, so just a layout
{{template "shared/combomarkdowneditor" .}}
+ +

Tailwind CSS Demo

+
+ +
+
{{template "base/footer" .}} diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index c51dcaa3ff..848afd305c 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -39,7 +39,9 @@ {{if not $.DisableStars}} {{svg "octicon-star" 16}}{{.NumStars}} {{end}} - {{svg "octicon-git-branch" 16}}{{.NumForks}} + {{if not $.DisableForks}} + {{svg "octicon-git-branch" 16}}{{.NumForks}} + {{end}} {{$description := .DescriptionHTML $.Context}} diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl index eaf2e7a090..96f34d508a 100644 --- a/templates/explore/repo_search.tmpl +++ b/templates/explore/repo_search.tmpl @@ -2,6 +2,7 @@
+ {{if and .PageIsExploreRepositories .OnlyShowRelevant}}
- {{ctx.Locale.Tr "explore.relevant_repositories" ((printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))|Escape) | Safe}} + {{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}}
{{end}}
diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl index 9abbff6d9c..0d661d53cb 100644 --- a/templates/explore/user_list.tmpl +++ b/templates/explore/user_list.tmpl @@ -21,7 +21,7 @@ {{.Email}} {{end}} - {{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}} + {{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}} diff --git a/templates/install.tmpl b/templates/install.tmpl index 42842084b7..c18f25f79c 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -8,7 +8,7 @@
{{template "base/alert" .}} -

{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker" | Safe}}

+

{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}

@@ -72,7 +72,7 @@
- {{ctx.Locale.Tr "install.sqlite_helper" | Safe}} + {{ctx.Locale.Tr "install.sqlite_helper"}}
diff --git a/templates/mail/auth/register_notify.tmpl b/templates/mail/auth/register_notify.tmpl index 3cdb456fb3..224c845452 100644 --- a/templates/mail/auth/register_notify.tmpl +++ b/templates/mail/auth/register_notify.tmpl @@ -11,7 +11,7 @@

{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}


{{.locale.Tr "mail.register_notify.text_1" AppName}}


{{.locale.Tr "mail.register_notify.text_2" .Username}}

{{AppUrl}}user/login


-

{{.locale.Tr "mail.register_notify.text_3" ($set_pwd_url | Escape) | Str2html}}


+

{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}


© {{AppName}}

diff --git a/templates/mail/issue/assigned.tmpl b/templates/mail/issue/assigned.tmpl index d02ea39918..5720319ee8 100644 --- a/templates/mail/issue/assigned.tmpl +++ b/templates/mail/issue/assigned.tmpl @@ -8,14 +8,14 @@ {{.Subject}} -{{$repo_url := printf "%s" (Escape .Issue.Repo.HTMLURL) (Escape .Issue.Repo.FullName)}} -{{$link := printf "#%d" (Escape .Link) .Issue.Index}} +{{$repo_url := HTMLFormat "%s" .Issue.Repo.HTMLURL .Issue.Repo.FullName}} +{{$link := HTMLFormat "#%d" .Link .Issue.Index}}

{{if .IsPull}} - {{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url | Str2html}} + {{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url}} {{else}} - {{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url | Str2html}} + {{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url}} {{end}}

diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index f4ceada2a7..53c909ee9c 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -49,7 +49,7 @@ {{ctx.Locale.Tr "org.teams.leave"}}
-

{{ctx.Locale.Tr "org.teams.leave.detail" `` | Safe}}

+

{{ctx.Locale.Tr "org.teams.leave.detail" (``|SafeHTML)}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/package/content/alpine.tmpl b/templates/package/content/alpine.tmpl index 48e1dff939..496ffbc7b5 100644 --- a/templates/package/content/alpine.tmpl +++ b/templates/package/content/alpine.tmpl @@ -3,12 +3,12 @@
- +
/$branch/$repository
-

{{ctx.Locale.Tr "packages.alpine.registry.info" | Safe}}

+

{{ctx.Locale.Tr "packages.alpine.registry.info"}}

- +
curl -JO 
@@ -18,7 +18,7 @@
- +
diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl index 5b4c809096..53b2ef1152 100644 --- a/templates/package/content/cargo.tmpl +++ b/templates/package/content/cargo.tmpl @@ -3,7 +3,7 @@
- +
[registry]
 default = "forgejo"
 
@@ -19,7 +19,7 @@ git-fetch-with-cli = true
cargo add {{.PackageDescriptor.Package.Name}}@{{.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl index a2f69b8f84..0f7694edc8 100644 --- a/templates/package/content/chef.tmpl +++ b/templates/package/content/chef.tmpl @@ -3,7 +3,7 @@
- +
knife[:supermarket_site] = ''
@@ -11,7 +11,7 @@
knife supermarket install {{.PackageDescriptor.Package.Name}} {{.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl index 3a4025fb33..7da94095dd 100644 --- a/templates/package/content/composer.tmpl +++ b/templates/package/content/composer.tmpl @@ -3,7 +3,7 @@
- +
{
 	"repositories": [{
 			"type": "composer",
@@ -17,7 +17,7 @@
 				
composer require {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/conan.tmpl b/templates/package/content/conan.tmpl index fbbb44d8b7..0a9f508dcc 100644 --- a/templates/package/content/conan.tmpl +++ b/templates/package/content/conan.tmpl @@ -11,7 +11,7 @@
conan install --remote=forgejo {{.PackageDescriptor.Package.Name}}/{{.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl index d5905c76fb..313b05ffe9 100644 --- a/templates/package/content/conda.tmpl +++ b/templates/package/content/conda.tmpl @@ -3,7 +3,7 @@
- +
channel_alias: 
 channels:
   - 
@@ -16,7 +16,7 @@ default_channels:
 				
conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index b8d2768fa7..0d5f0f09d5 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -19,7 +19,7 @@
{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}
- +
diff --git a/templates/package/content/cran.tmpl b/templates/package/content/cran.tmpl index 54ead877ba..766dd43a4c 100644 --- a/templates/package/content/cran.tmpl +++ b/templates/package/content/cran.tmpl @@ -3,7 +3,7 @@
- +
options("repos" = c(getOption("repos"), c(forgejo="")))
@@ -11,7 +11,7 @@
install.packages("{{.PackageDescriptor.Package.Name}}")
- +
diff --git a/templates/package/content/debian.tmpl b/templates/package/content/debian.tmpl index 460e7cc3c5..3c03eec396 100644 --- a/templates/package/content/debian.tmpl +++ b/templates/package/content/debian.tmpl @@ -7,7 +7,7 @@
sudo curl  -o /etc/apt/keyrings/forgejo-{{$.PackageDescriptor.Owner.Name}}.asc
 echo "deb [signed-by=/etc/apt/keyrings/forgejo-{{$.PackageDescriptor.Owner.Name}}.asc]  $distribution $component" | sudo tee -a /etc/apt/sources.list.d/forgejo.list
 sudo apt update
-

{{ctx.Locale.Tr "packages.debian.registry.info" | Safe}}

+

{{ctx.Locale.Tr "packages.debian.registry.info"}}

@@ -16,7 +16,7 @@ sudo apt update
- +
diff --git a/templates/package/content/generic.tmpl b/templates/package/content/generic.tmpl index 70218ddf6b..aec8eb314e 100644 --- a/templates/package/content/generic.tmpl +++ b/templates/package/content/generic.tmpl @@ -11,7 +11,7 @@ curl -OJ - + diff --git a/templates/package/content/go.tmpl b/templates/package/content/go.tmpl index 3b2b478e0c..853218e51c 100644 --- a/templates/package/content/go.tmpl +++ b/templates/package/content/go.tmpl @@ -7,7 +7,7 @@
GOPROXY= go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl index 7f39a0e78f..59f89be637 100644 --- a/templates/package/content/helm.tmpl +++ b/templates/package/content/helm.tmpl @@ -12,7 +12,7 @@ helm repo update
helm install {{.PackageDescriptor.Package.Name}} {{AppDomain}}/{{.PackageDescriptor.Package.Name}}
- +
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl index 7880eb63d8..e764684595 100644 --- a/templates/package/content/maven.tmpl +++ b/templates/package/content/maven.tmpl @@ -3,7 +3,7 @@
- +
<repositories>
 	<repository>
 		<id>gitea</id>
@@ -24,7 +24,7 @@
 </distributionManagement>
- +
<dependency>
 	<groupId>{{.PackageDescriptor.Metadata.GroupID}}</groupId>
 	<artifactId>{{.PackageDescriptor.Metadata.ArtifactID}}</artifactId>
@@ -40,7 +40,7 @@
 				
mvn dependency:get -DremoteRepositories= -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}
- +
diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl index 3c493d38e6..cfd7595bfc 100644 --- a/templates/package/content/npm.tmpl +++ b/templates/package/content/npm.tmpl @@ -3,7 +3,7 @@
- +
{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=
@@ -15,7 +15,7 @@
"{{.PackageDescriptor.Package.Name}}": "{{.PackageDescriptor.Version.Version}}"
- +
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl index 80c7917ae2..f4807b0ad9 100644 --- a/templates/package/content/nuget.tmpl +++ b/templates/package/content/nuget.tmpl @@ -11,7 +11,7 @@
dotnet add package --source {{.PackageDescriptor.Owner.Name}} --version {{.PackageDescriptor.Version.Version}} {{.PackageDescriptor.Package.Name}}
- +
diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl index 316001a961..e83b0d3570 100644 --- a/templates/package/content/pub.tmpl +++ b/templates/package/content/pub.tmpl @@ -7,7 +7,7 @@
dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=
- +
diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl index c4bfe487e0..e0353c91c6 100644 --- a/templates/package/content/pypi.tmpl +++ b/templates/package/content/pypi.tmpl @@ -7,7 +7,7 @@
pip install --index-url  {{.PackageDescriptor.Package.Name}}
- +
diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl index aedac6ca43..a7e2141ccd 100644 --- a/templates/package/content/rpm.tmpl +++ b/templates/package/content/rpm.tmpl @@ -31,7 +31,7 @@ zypper install {{$.PackageDescriptor.Package.Name}}
- +
diff --git a/templates/package/content/rubygems.tmpl b/templates/package/content/rubygems.tmpl index bc99af4a70..412c3a2954 100644 --- a/templates/package/content/rubygems.tmpl +++ b/templates/package/content/rubygems.tmpl @@ -3,7 +3,7 @@
- +
gem install {{.PackageDescriptor.Package.Name}} --version "{{.PackageDescriptor.Version.Version}}" --source ""
@@ -13,7 +13,7 @@ end
- +
diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl index 9c2e35ebae..471f5b5457 100644 --- a/templates/package/content/swift.tmpl +++ b/templates/package/content/swift.tmpl @@ -7,7 +7,7 @@
swift package-registry set 
- +
dependencies: [
 	.package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}")
 ]
@@ -17,7 +17,7 @@
swift package resolve
- +
diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl index 61a4ae8b2d..79b4d2f054 100644 --- a/templates/package/content/vagrant.tmpl +++ b/templates/package/content/vagrant.tmpl @@ -7,7 +7,7 @@
vagrant box add --box-version {{.PackageDescriptor.Version.Version}} ""
- +
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index 6ef62753e2..10e26c7010 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -1,8 +1,14 @@ {{template "base/head" .}} -
- {{template "shared/user/org_profile_avatar" .}} +
+ {{if .ContextUser.IsOrganization}} + {{template "org/header" .}} + {{else}} + {{template "shared/user/org_profile_avatar" .}} + {{end}}
- {{template "user/overview/header" .}} + {{if not .ContextUser.IsOrganization}} + {{template "user/overview/header" .}} + {{end}} {{template "base/alert" .}}

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}}) / {{ctx.Locale.Tr "repo.settings"}}

diff --git a/templates/package/shared/cargo.tmpl b/templates/package/shared/cargo.tmpl index 9015bc6616..401d909002 100644 --- a/templates/package/shared/cargo.tmpl +++ b/templates/package/shared/cargo.tmpl @@ -18,7 +18,7 @@
- +

diff --git a/templates/package/shared/cleanup_rules/edit.tmpl b/templates/package/shared/cleanup_rules/edit.tmpl index 8729494412..138a90791c 100644 --- a/templates/package/shared/cleanup_rules/edit.tmpl +++ b/templates/package/shared/cleanup_rules/edit.tmpl @@ -40,7 +40,7 @@
-

{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container" | Safe}}

+

{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container"}}

{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.remove.title"}}

diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 299c182dc9..67c686675c 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -30,9 +30,9 @@ {{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}} {{end}} {{if $hasRepositoryAccess}} - {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.Link (.Repository.FullName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}} {{else}} - {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}} {{end}}
@@ -45,9 +45,9 @@

{{ctx.Locale.Tr "packages.empty"}}

{{if and .Repository .CanWritePackages}} {{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}} -

{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl | Safe}}

+

{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl}}

{{end}} -

{{ctx.Locale.Tr "packages.empty.documentation" "https://forgejo.org/docs/latest/user/packages/" | Safe}}

+

{{ctx.Locale.Tr "packages.empty.documentation" "https://forgejo.org/docs/latest/user/packages/"}}

{{else}}

{{ctx.Locale.Tr "packages.filter.no_result"}}

diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl index fcf3030fe6..eee952c096 100644 --- a/templates/package/shared/versionlist.tmpl +++ b/templates/package/shared/versionlist.tmpl @@ -25,7 +25,7 @@
{{.Version.LowerVersion}}
- {{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 553a46cfad..0fa23d67fd 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -10,9 +10,9 @@
{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}} {{if .HasRepositoryAccess}} - {{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName .PackageDescriptor.Repository.Link .PackageDescriptor.Repository.FullName}} {{else}} - {{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName}} {{end}}
diff --git a/templates/repo/actions/no_workflows.tmpl b/templates/repo/actions/no_workflows.tmpl index af1f28e8cf..009313581e 100644 --- a/templates/repo/actions/no_workflows.tmpl +++ b/templates/repo/actions/no_workflows.tmpl @@ -2,7 +2,7 @@ {{svg "octicon-no-entry" 48}}

{{ctx.Locale.Tr "actions.runs.no_workflows"}}

{{if and .CanWriteCode .CanWriteActions}} -

{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/" | Safe}}

+

{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/"}}

{{end}} -

{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/" | Safe}}

+

{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/"}}

diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl index 6b07e7000a..b787762719 100644 --- a/templates/repo/actions/view.tmpl +++ b/templates/repo/actions/view.tmpl @@ -6,6 +6,8 @@ data-run-index="{{.RunIndex}}" data-job-index="{{.JobIndex}}" data-actions-url="{{.ActionsURL}}" + data-workflow-name="{{.WorkflowName}}" + data-workflow-url="{{.WorkflowURL}}" data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}" data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" @@ -19,6 +21,7 @@ data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}" data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}" data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" + data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}" diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index 3149f20670..a19fb66261 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -1,235 +1,17 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
-

- {{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}} - - -

-
- - {{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}} -

{{ctx.Locale.Tr "repo.activity.overview"}}

-
- {{if .Permission.CanRead $.UnitTypePullRequests}} -
- {{if gt .Activity.ActivePRCount 0}} -
- - -
- {{else}} -
- -
- {{end}} - {{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}} -
- {{end}} - {{if .Permission.CanRead $.UnitTypeIssues}} -
- {{if gt .Activity.ActiveIssueCount 0}} -
- - -
- {{else}} -
- -
- {{end}} - {{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}} -
- {{end}} +
+
+ {{template "repo/navbar" .}}
-
- {{if .Permission.CanRead $.UnitTypePullRequests}} - - {{svg "octicon-git-pull-request"}} {{.Activity.MergedPRCount}}
- {{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}} -
- - {{svg "octicon-git-branch"}} {{.Activity.OpenedPRCount}}
- {{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}} -
- {{end}} - {{if .Permission.CanRead $.UnitTypeIssues}} - - {{svg "octicon-issue-closed"}} {{.Activity.ClosedIssueCount}}
- {{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}} -
- - {{svg "octicon-issue-opened"}} {{.Activity.OpenedIssueCount}}
- {{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}} -
- {{end}} +
+ {{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}} + {{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}} + {{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}} + {{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
- {{end}} - - {{if .Permission.CanRead $.UnitTypeCode}} - {{if eq .Activity.Code.CommitCountInAllBranches 0}} -
-

{{ctx.Locale.Tr "repo.activity.no_git_activity"}}

-
- {{end}} - {{if gt .Activity.Code.CommitCountInAllBranches 0}} -
-
- {{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}} - {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}} - {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}} - {{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}} - {{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}} - {{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}} - {{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}} - {{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}} - {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}} - {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}} - {{ctx.Locale.Tr "repo.activity.git_stats_additions"}} - {{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}} - {{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}} - {{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}. -
-
-
-
-
- {{end}} - {{end}} - - {{if gt .Activity.PublishedReleaseCount 0}} -

- {{svg "octicon-tag" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.releases_published_by" - (ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount) - (ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount) - }} -

-
- {{range .Activity.PublishedReleases}} -

- {{ctx.Locale.Tr "repo.activity.published_release_label"}} - {{.TagName}} - {{if not .IsTag}} - {{.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{end}} - {{TimeSinceUnix .CreatedUnix ctx.Locale}} -

- {{end}} -
- {{end}} - - {{if gt .Activity.MergedPRCount 0}} -

- {{svg "octicon-git-pull-request" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.prs_merged_by" - (ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount) - (ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount) - }} -

-
- {{range .Activity.MergedPRs}} -

- {{ctx.Locale.Tr "repo.activity.merged_prs_label"}} - #{{.Index}} {{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{TimeSinceUnix .MergedUnix ctx.Locale}} -

- {{end}} -
- {{end}} - - {{if gt .Activity.OpenedPRCount 0}} -

- {{svg "octicon-git-branch" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.prs_opened_by" - (ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount) - (ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount) - }} -

-
- {{range .Activity.OpenedPRs}} -

- {{ctx.Locale.Tr "repo.activity.opened_prs_label"}} - #{{.Index}} {{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}} -

- {{end}} -
- {{end}} - - {{if gt .Activity.ClosedIssueCount 0}} -

- {{svg "octicon-issue-closed" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.issues_closed_from" - (ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount) - (ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount) - }} -

-
- {{range .Activity.ClosedIssues}} -

- {{ctx.Locale.Tr "repo.activity.closed_issue_label"}} - #{{.Index}} {{.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{TimeSinceUnix .ClosedUnix ctx.Locale}} -

- {{end}} -
- {{end}} - - {{if gt .Activity.OpenedIssueCount 0}} -

- {{svg "octicon-issue-opened" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.issues_created_by" - (ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount) - (ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount) - }} -

-
- {{range .Activity.OpenedIssues}} -

- {{ctx.Locale.Tr "repo.activity.new_issue_label"}} - #{{.Index}} {{.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{TimeSinceUnix .CreatedUnix ctx.Locale}} -

- {{end}} -
- {{end}} - - {{if gt .Activity.UnresolvedIssueCount 0}} -

- {{svg "octicon-comment-discussion" 16 "gt-mr-3"}} - {{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}} -

-
- {{range .Activity.UnresolvedIssues}} -

- {{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}} - #{{.Index}} - {{if .IsPull}} - {{.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{else}} - {{.Title | RenderEmoji $.Context | RenderCodeBlock}} - {{end}} - {{TimeSinceUnix .UpdatedUnix ctx.Locale}} -

- {{end}} -
- {{end}}
{{template "base/footer" .}} + diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 31cd5b23f6..3bd5e36949 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -30,6 +30,15 @@
+ {{if .IsFileTooLarge}} + + + + + + +
{{ctx.Locale.Tr "repo.file_too_large"}}
+ {{else}} {{range $row := .BlameRows}} @@ -75,6 +84,7 @@ {{end}}
+ {{end}}
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index bee5363296..8a5cdc7cc7 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -1,7 +1,7 @@ {{/* Attributes: * root * ContainerClasses -* (TODO: search "branch_dropdown" in the template direcotry) +* (TODO: search "branch_dropdown" in the template directory) */}} {{$defaultSelectedRefName := $.root.BranchName}} {{if and .root.IsViewTag (not .noTag)}} diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl index 176ae3df00..eac9d0e243 100644 --- a/templates/repo/code/recently_pushed_new_branches.tmpl +++ b/templates/repo/code/recently_pushed_new_branches.tmpl @@ -8,7 +8,7 @@ {{$name = (print $repo.FullName ":" .Name)}} {{end}} {{$branchLink := (print ($repo.Link) "/src/branch/" (PathEscapeSegments .Name))}} - {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape $name) $timeSince $branchLink | Safe}} + {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $name $timeSince $branchLink}}
{{ctx.Locale.Tr "repo.pulls.compare_changes"}} diff --git a/templates/repo/code_frequency.tmpl b/templates/repo/code_frequency.tmpl new file mode 100644 index 0000000000..50ec1beb6b --- /dev/null +++ b/templates/repo/code_frequency.tmpl @@ -0,0 +1,9 @@ +{{if .Permission.CanRead $.UnitTypeCode}} +
+
+{{end}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 01fa45babe..115ee92955 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -88,7 +88,7 @@ {{.CsrfTokenHtml}}
@@ -113,7 +113,7 @@
@@ -184,7 +184,7 @@
{{if .Commit.Signature}} -
+
{{if .Verification.Verified}} {{if ne .Verification.SigningUser.ID 0}} diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl index ec2be6c38d..74c20a6a2c 100644 --- a/templates/repo/commit_statuses.tmpl +++ b/templates/repo/commit_statuses.tmpl @@ -1,10 +1,10 @@ {{if .Statuses}} {{if and (eq (len .Statuses) 1) .Status.TargetURL}} - + {{template "repo/commit_status" .Status}} {{else}} - + {{template "repo/commit_status" .Status}} {{end}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 7702770c40..4eb31e0e8e 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -78,9 +78,12 @@ {{end}} - {{if $.FileName}} - {{svg "octicon-file-code"}} - {{end}} + + {{svg "octicon-file-code"}} + {{end}} diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl new file mode 100644 index 0000000000..4a258e5b70 --- /dev/null +++ b/templates/repo/contributors.tmpl @@ -0,0 +1,13 @@ +{{if .Permission.CanRead $.UnitTypeCode}} +
+
+{{end}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 66f73fb398..d6ff22b7ab 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -51,10 +51,10 @@
{{if .IsForcedPrivate}} - + {{else}} - + {{end}}
{{ctx.Locale.Tr "repo.visibility_description"}} diff --git a/templates/repo/create_helper.tmpl b/templates/repo/create_helper.tmpl index 653955efc9..70c28b72e8 100644 --- a/templates/repo/create_helper.tmpl +++ b/templates/repo/create_helper.tmpl @@ -1,3 +1,3 @@ {{if not $.DisableMigrations}} -

{{ctx.Locale.Tr "repo.new_repo_helper" ((print AppSubUrl "/repo/migrate")|Escape) | Safe}}

+

{{ctx.Locale.Tr "repo.new_repo_helper" (print AppSubUrl "/repo/migrate")}}

{{end}} diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl index 2dff28a965..353f6db705 100644 --- a/templates/repo/diff/blob_excerpt.tmpl +++ b/templates/repo/diff/blob_excerpt.tmpl @@ -5,17 +5,17 @@
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - {{end}} {{if eq $line.GetExpandDirection 2}} - {{end}} @@ -51,17 +51,17 @@
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - {{end}} {{if eq $line.GetExpandDirection 2}} - {{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 05559fc9a7..9dcc59cea7 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,7 +1,7 @@ {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
-
+
{{if $showFileTree}} diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl index 767c2613a0..54817d4740 100644 --- a/templates/repo/diff/comment_form.tmpl +++ b/templates/repo/diff/comment_form.tmpl @@ -19,6 +19,12 @@ "DisableAutosize" "true" )}} + {{if $.root.IsAttachmentEnabled}} +
+ {{template "repo/upload" $.root}} +
+ {{end}} +
{{.Content}}
-
+
+ {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{end}}
{{$reactions := .Reactions.GroupByType}} {{if $reactions}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index c2a114ba4e..65b85b9686 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -194,7 +194,7 @@ {{if .HasPullRequest}}
- {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}} + {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print $.RepoLink "/pulls/" .PullRequest.Issue.Index) $.RepoRelPath .PullRequest.Index}}

{{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}} #{{.PullRequest.Issue.Index}} @@ -202,11 +202,11 @@

@@ -220,7 +220,7 @@ {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} - {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}} {{end}}
{{end}} diff --git a/templates/repo/diff/conversations.tmpl b/templates/repo/diff/conversations.tmpl new file mode 100644 index 0000000000..5945337cd3 --- /dev/null +++ b/templates/repo/diff/conversations.tmpl @@ -0,0 +1,3 @@ +{{range .conversations}} + {{template "repo/diff/conversation" dict "." $ "comments" .}} +{{end}} diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl index 3bcb877cc6..b7c46dd846 100644 --- a/templates/repo/diff/options_dropdown.tmpl +++ b/templates/repo/diff/options_dropdown.tmpl @@ -13,7 +13,7 @@ {{ctx.Locale.Tr "repo.diff.download_diff"}} {{end}} {{ctx.Locale.Tr "repo.pulls.expand_files"}} - {{ctx.Locale.Tr "repo.pulls.collapse_files"}} + {{ctx.Locale.Tr "repo.pulls.collapse_files"}} {{if .Issue.Index}} {{if .ShowOutdatedComments}} diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl index 5b0d982e96..5a1c8cce64 100644 --- a/templates/repo/diff/section_split.tmpl +++ b/templates/repo/diff/section_split.tmpl @@ -18,17 +18,17 @@
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - {{end}} {{if eq $line.GetExpandDirection 2}} - {{end}} @@ -108,44 +108,44 @@ {{if and (eq .GetType 3) $hasmatch}} {{$match := index $section.Lines $line.Match}} - {{if or $line.Comments $match.Comments}} + {{if or $line.Conversations $match.Conversations}} - {{if $line.Comments}} + {{if $line.Conversations}} {{if eq $line.GetCommentSide "previous"}} - {{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}} {{end}} {{end}} - {{if $match.Comments}} + {{if $match.Conversations}} {{if eq $match.GetCommentSide "previous"}} - {{template "repo/diff/conversation" dict "." $.root "comments" $match.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $match.Conversations}} {{end}} {{end}} - {{if $line.Comments}} + {{if $line.Conversations}} {{if eq $line.GetCommentSide "proposed"}} - {{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}} {{end}} {{end}} - {{if $match.Comments}} + {{if $match.Conversations}} {{if eq $match.GetCommentSide "proposed"}} - {{template "repo/diff/conversation" dict "." $.root "comments" $match.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $match.Conversations}} {{end}} {{end}} {{end}} - {{else if $line.Comments}} + {{else if $line.Conversations}} {{if eq $line.GetCommentSide "previous"}} - {{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}} {{end}} {{if eq $line.GetCommentSide "proposed"}} - {{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}} {{end}} diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index 2b901411e2..ae97dc6db6 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -14,17 +14,17 @@
{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - {{end}} {{if eq $line.GetExpandDirection 2}} - {{end}} @@ -60,10 +60,10 @@ */}} {{end}} - {{if $line.Comments}} + {{if $line.Conversations}} - {{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}} + {{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}} {{end}} diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index ab2c3c3349..f9c9eef5aa 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -11,11 +11,11 @@
diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index d362a5602a..0a7c49dae3 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -13,7 +13,7 @@ {{range $i, $v := .TreeNames}} {{if eq $i $l}} - + {{svg "octicon-info"}} {{else}} {{$v}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index c1ec483b77..62194abe50 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -10,7 +10,7 @@ {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} - {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}} {{end}}
{{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index ef3e40eea8..286eedff18 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -5,9 +5,9 @@
{{template "repo/icon" .}}
{{if .IsArchived}} @@ -62,55 +62,8 @@ {{if not $.DisableStars}} {{template "repo/star_unstar" $}} {{end}} - {{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}} -
- - {{svg "octicon-repo-forked"}}{{ctx.Locale.Tr "repo.fork"}} - - - - {{CountFmt .NumForks}} - -
+ {{if not $.DisableForks}} + {{template "repo/header_fork" $}} {{end}}
{{end}} @@ -219,7 +172,12 @@ {{end}} {{if .Permission.IsAdmin}} - + {{if not (.Repository.AllUnitsEnabled ctx)}} + + {{svg "octicon-diff-added"}} {{ctx.Locale.Tr "repo.settings.units.add_more"}} + + {{end}} + {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} {{end}} diff --git a/templates/repo/header_fork.tmpl b/templates/repo/header_fork.tmpl new file mode 100644 index 0000000000..e248a77850 --- /dev/null +++ b/templates/repo/header_fork.tmpl @@ -0,0 +1,52 @@ +{{with .Repository}} +{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}} +
+ + {{svg "octicon-repo-forked"}}{{ctx.Locale.Tr "repo.fork"}} + + + + {{CountFmt .NumForks}} + +
+{{end}} +{{end}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 5e27d9160c..f9a4599b92 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -11,23 +11,21 @@ {{if $description}}{{$description | RenderCodeBlock}}{{else if .IsRepositoryAdmin}}{{ctx.Locale.Tr "repo.no_desc"}}{{end}} {{.Repository.Website}}
- {{if .RepoSearchEnabled}} -
{{range .Topics}}{{.Name}}{{end}} @@ -65,7 +63,7 @@ {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} - {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}} {{end}}
{{end}} diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl index 5a80b959d0..a001f81891 100644 --- a/templates/repo/icon.tmpl +++ b/templates/repo/icon.tmpl @@ -1,10 +1,10 @@ {{$avatarLink := (.RelAvatarLink ctx)}} {{if $avatarLink}} - {{.FullName}} + {{.FullName}} {{else if $.IsMirror}} - {{svg "octicon-mirror" 32}} + {{svg "octicon-mirror" 24}} {{else if $.IsFork}} - {{svg "octicon-repo-forked" 32}} + {{svg "octicon-repo-forked" 24}} {{else}} - {{svg "octicon-repo" 32}} + {{svg "octicon-repo" 24}} {{end}} diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 14d08fc0ef..7fb3d82827 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -23,11 +23,11 @@ {{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}} {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} {{if .OriginalAuthor}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} {{else if gt .Poster.ID 0}} - {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}} {{else}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} {{end}}
diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl index 127b9d7d87..a8037482be 100644 --- a/templates/repo/issue/choose.tmpl +++ b/templates/repo/issue/choose.tmpl @@ -11,8 +11,8 @@
- {{.Name | RenderEmojiPlain}} -
{{.About | RenderEmojiPlain}} + {{.Name}} +
{{.About}}
{{ctx.Locale.Tr "repo.issues.choose.get_started"}} @@ -24,8 +24,8 @@
- {{.Name | RenderEmojiPlain}} -
{{.About | RenderEmojiPlain}} + {{.Name}} +
{{.About}}
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues.choose.open_external_link"}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 511ef7f397..f9f635f7cd 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -21,10 +21,10 @@
- {{ctx.Locale.Tr "repo.issues.filter_label_exclude" | Safe}} + {{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}
- {{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} - {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} + {{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} + {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} @@ -32,7 +32,7 @@
{{end}} {{$previousExclusiveScope = $exclusiveScope}} - + {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} @@ -62,13 +62,13 @@
-
{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}} - {{ctx.Locale.Tr "repo.issues.filter_milestone_none"}} + {{ctx.Locale.Tr "repo.issues.filter_milestone_all"}} + {{ctx.Locale.Tr "repo.issues.filter_milestone_none"}} {{if .OpenMilestones}}
{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}
{{range .OpenMilestones}} - + {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} @@ -78,7 +78,7 @@
{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}
{{range .ClosedMilestones}} - + {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} @@ -99,15 +99,15 @@ {{svg "octicon-search" 16}}
- {{ctx.Locale.Tr "repo.issues.filter_project_all"}} - {{ctx.Locale.Tr "repo.issues.filter_project_none"}} + {{ctx.Locale.Tr "repo.issues.filter_project_all"}} + {{ctx.Locale.Tr "repo.issues.filter_project_none"}} {{if .OpenProjects}}
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
{{range .OpenProjects}} - + {{svg .IconName 18 "gt-mr-3 gt-shrink-0"}}{{.Title}} {{end}} @@ -118,7 +118,7 @@ {{ctx.Locale.Tr "repo.issues.new.closed_projects"}}
{{range .ClosedProjects}} - + {{svg .IconName 18 "gt-mr-3"}}{{.Title}} {{end}} @@ -156,11 +156,11 @@ {{svg "octicon-search" 16}}
- {{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}} - {{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}} + {{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}} + {{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}
{{range .Assignees}} - + {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} {{end}} @@ -175,14 +175,14 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{end}} @@ -194,13 +194,13 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl index f41b4ee2c6..7ddc38a387 100644 --- a/templates/repo/issue/labels/edit_delete_label.tmpl +++ b/templates/repo/issue/labels/edit_delete_label.tmpl @@ -29,9 +29,9 @@

- {{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}} + {{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}
- {{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning" | Safe}} + {{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}

diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl index e7fb1e5ff6..2b2b2336c4 100644 --- a/templates/repo/issue/labels/label_new.tmpl +++ b/templates/repo/issue/labels/label_new.tmpl @@ -17,7 +17,7 @@

- {{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}} + {{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index ea19518efa..d9495d9b77 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -31,7 +31,7 @@
{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}} {{if .IsClosed}} - {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}} + {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}} {{else}} {{if .Milestone.DeadlineString}} @@ -45,7 +45,7 @@ {{end}} {{end}}
-
{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}
+
{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}
{{if .TotalTrackedTime}}
{{svg "octicon-clock"}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index 3d4bbfd8b1..698e3fffba 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -47,14 +47,14 @@ {{if .UpdatedUnix}}
{{svg "octicon-clock"}} - {{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}} + {{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}}
{{end}}
{{if .IsClosed}} {{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}} {{svg "octicon-clock" 14}} - {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}} + {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}} {{else}} {{if .DeadlineString}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 04ae8456bb..8e4310f0bb 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -13,7 +13,7 @@
{{if .PageIsComparePull}} -
{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}
+
{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}
{{end}}
{{if .Fields}} diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index ed444f6dce..91db68c810 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -28,10 +28,10 @@ {{.Issue.OriginalAuthor}}
- {{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}} - {{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" (.Repository.OriginalURL|Escape) (.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" .Repository.OriginalURL .Repository.GetOriginalURLHostname}}){{end}} {{else}} @@ -39,7 +39,7 @@ {{template "shared/user/authorlink" .Issue.Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}} {{end}}
@@ -133,7 +133,7 @@
{{else}}
- {{ctx.Locale.Tr "repo.issues.sign_in_require_desc" (.SignInLink|Escape) | Safe}} + {{ctx.Locale.Tr "repo.issues.sign_in_require_desc" .SignInLink}}
{{end}} {{end}}{{/* end if: .IsSigned */}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 53c20a68bb..b37d1e069f 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -33,10 +33,10 @@ {{.OriginalAuthor}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}} {{else}} {{if gt .Poster.ID 0}} @@ -46,7 +46,7 @@ {{end}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} {{end}}
@@ -85,9 +85,9 @@ {{template "shared/user/authorlink" .Poster}} {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr}} {{end}}
@@ -98,9 +98,9 @@ {{template "shared/user/authorlink" .Poster}} {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr}} {{end}}
@@ -112,16 +112,16 @@ {{template "shared/user/authorlink" .Poster}} {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} {{if eq $.Issue.PullRequest.Status 3}} - {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (HTMLFormat `%[2]s` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "%[1]s" $.BaseTarget) $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `%[2]s` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "%[1]s" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (HTMLFormat `%[2]s` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "%[1]s" $.BaseTarget) $createdStr}} {{end}}
{{else if eq .Type 3 5 6}} {{$refFrom:= ""}} {{if ne .RefRepoID .Issue.RepoID}} - {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} + {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" .RefRepo.FullName}} {{end}} {{$refTr := "repo.issues.ref_issue_from"}} {{if .Issue.IsPull}} @@ -138,7 +138,7 @@ {{if eq .RefAction 3}}{{end}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} + {{ctx.Locale.Tr $refTr .EventTag $createdStr (.RefCommentLink ctx) $refFrom}} {{if eq .RefAction 3}}{{end}} @@ -153,9 +153,9 @@ {{template "shared/user/authorlink" .Poster}} {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.commit_ref_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.pulls.commit_ref_at" .EventTag $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}} {{end}}
@@ -171,11 +171,11 @@ {{template "shared/user/authorlink" .Poster}} {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr}} {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}} {{end}}
@@ -186,7 +186,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} + {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" .OldMilestone.Name .Milestone.Name $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" .OldMilestone.Name $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" .Milestone.Name $createdStr}}{{end}}
{{else if and (eq .Type 9) (gt .AssigneeID 0)}} @@ -197,9 +197,9 @@ {{template "shared/user/authorlink" .Assignee}} {{if eq .Poster.ID .Assignee.ID}} - {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_assignee_at" .Poster.GetDisplayName $createdStr}} {{end}} {{else}} @@ -207,9 +207,9 @@ {{template "shared/user/authorlink" .Assignee}} {{if eq .Poster.ID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_assignee_at" .Poster.GetDisplayName $createdStr}} {{end}} {{end}} @@ -220,7 +220,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr}}
{{else if eq .Type 11}} @@ -229,7 +229,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.delete_branch_at" .OldRef $createdStr}} {{else if eq .Type 12}} @@ -238,7 +238,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr}} {{else if eq .Type 13}} @@ -247,7 +247,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}} {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
@@ -266,7 +266,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}} {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
@@ -285,7 +285,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr}}
{{else if eq .Type 16}} @@ -294,7 +294,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr}}
{{else if eq .Type 17}} @@ -307,7 +307,7 @@ {{if eq (len $parsedDeadline) 2}} {{$from := DateTime "long" (index $parsedDeadline 1)}} {{$to := DateTime "long" (index $parsedDeadline 0)}} - {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr}} {{end}} @@ -317,7 +317,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr}} {{else if eq .Type 19}} @@ -326,7 +326,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr}} {{if .DependentIssue}}
@@ -349,7 +349,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr}} {{if .DependentIssue}}
@@ -369,9 +369,8 @@ {{else if eq .Type 22}}
- {{if .OriginalAuthor}} - {{else}} - {{/* Some timeline avatars need a offset to correctly allign with their speech + {{if not .OriginalAuthor}} + {{/* Some timeline avatars need a offset to correctly align with their speech bubble. The condition depends on review type and for positive reviews whether there is a comment element or not */}} @@ -385,20 +384,21 @@ {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} {{.OriginalAuthor}} - {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}) + {{end}} {{else}} {{template "shared/user/authorlink" .Poster}} {{end}} {{if eq .Review.Type 1}} - {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}} {{else if eq .Review.Type 2}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}} {{else if eq .Review.Type 3}} - {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}} {{end}} {{if .Review.Dismissed}}
{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}
@@ -422,12 +422,12 @@ {{.OriginalAuthor}} {{if $.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}} {{else}} {{template "shared/user/authorlink" .Poster}} {{end}} - {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} + {{ctx.Locale.Tr "repo.issues.review.left_comment"}}
@@ -477,12 +477,12 @@ {{if .Content}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr}} {{else}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr}} {{end}}
@@ -492,16 +492,28 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr}}
{{else if eq .Type 25}}
{{svg "octicon-git-branch"}} - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if not .OriginalAuthor}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} - {{.Poster.Name}} - {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}) + {{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" .OldRef .NewRef $createdStr}}
{{else if eq .Type 26}} @@ -511,7 +523,7 @@ {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr}}
{{svg "octicon-clock"}} @@ -532,12 +544,12 @@ {{if (gt .AssigneeID 0)}} {{if .RemovedAssignee}} {{if eq .PosterID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" .Assignee.GetDisplayName $createdStr}} {{end}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" .Assignee.GetDisplayName $createdStr}} {{end}} {{else}} @@ -546,9 +558,9 @@ {{$teamName = .AssigneeTeam.Name}} {{end}} {{if .RemovedAssignee}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" $teamName $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" $teamName $createdStr}} {{end}} {{end}} @@ -563,9 +575,9 @@ {{template "shared/user/authorlink" .Poster}} {{if .IsForcePush}} - {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.force_push_codes" $.Issue.PullRequest.HeadBranch (ShortSha .OldCommit) ($.Issue.Repo.CommitLink .OldCommit) (ShortSha .NewCommit) ($.Issue.Repo.CommitLink .NewCommit) $createdStr}} {{else}} - {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} + {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr}} {{end}} {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} @@ -587,19 +599,19 @@ {{$oldProjectDisplayHtml := "Unknown Project"}} {{if .OldProject}} {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} - {{$oldProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} + {{$oldProjectDisplayHtml = HTMLFormat `%s` (ctx.Locale.Tr $trKey) .OldProject.Title}} {{end}} {{$newProjectDisplayHtml := "Unknown Project"}} {{if .Project}} {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} - {{$newProjectDisplayHtml = printf `%s` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} + {{$newProjectDisplayHtml = HTMLFormat `%s` (ctx.Locale.Tr $trKey) .Project.Title}} {{end}} {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} - {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr}} {{else if gt .OldProjectID 0}} - {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr}} {{else if gt .ProjectID 0}} - {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr}} {{end}}
@@ -619,7 +631,7 @@ {{else}} {{$reviewerName = .Review.OriginalAuthor}} {{end}} - {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr}}
{{if .Content}} @@ -655,11 +667,11 @@ {{template "shared/user/authorlink" .Poster}} {{if and .OldRef .NewRef}} - {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.change_ref_at" .OldRef .NewRef $createdStr}} {{else if .OldRef}} - {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_ref_at" .OldRef $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_ref_at" .NewRef $createdStr}} {{end}}
@@ -667,9 +679,19 @@
{{svg "octicon-git-merge" 16}} - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} + {{if .OriginalAuthor}} + + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + + {{if $.Repository.OriginalURL}} + ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}) + {{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr}} + {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr}}{{end}}
{{else if or (eq .Type 36) (eq .Type 37)}} @@ -678,8 +700,8 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} + {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr}} + {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl index 7c01bb4228..95121b0dc7 100644 --- a/templates/repo/issue/view_content/comments_delete_time.tmpl +++ b/templates/repo/issue/view_content/comments_delete_time.tmpl @@ -1,4 +1,4 @@ -{{if .comment.Time}} {{/* compatibility with time comments made before v1.14 */}} +{{if and .comment.Time (.ctxData.Repository.IsTimetrackerEnabled ctx)}} {{/* compatibility with time comments made before v1.14 */}} {{if (not .comment.Time.Deleted)}} {{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}} diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl index 0e978dafcd..1bad0e9b55 100644 --- a/templates/repo/issue/view_content/conversation.tmpl +++ b/templates/repo/issue/view_content/conversation.tmpl @@ -1,7 +1,6 @@ {{$invalid := (index .comments 0).Invalidated}} {{$resolved := (index .comments 0).IsResolved}} {{$resolveDoer := (index .comments 0).ResolveDoer}} -{{$hideByDefault := or $resolved (and $invalid (not .ShowOutdatedComments))}} {{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
@@ -15,7 +14,7 @@
{{if or $invalid $resolved}} - -