diff --git a/.deadcode-out b/.deadcode-out index e63e4a3dc3..61c5bcb055 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -13,6 +13,13 @@ forgejo.org/models IsErrSHANotFound IsErrMergeDivergingFastForwardOnly +forgejo.org/models/activities + GetActivityByID + NewFederatedUserActivity + CreateUserActivity + GetFollowingFeeds + FederatedUserActivity.loadActor + forgejo.org/models/auth WebAuthnCredentials @@ -54,9 +61,17 @@ forgejo.org/models/user IsErrExternalLoginUserAlreadyExist IsErrExternalLoginUserNotExist NewFederatedUser + NewFederatedUserFollower IsErrUserSettingIsNotExist GetUserAllSettings DeleteUserSetting + GetFederatedUser + GetFederatedUserByUserID + UpdateFederatedUser + GetFollowersForUser + AddFollower + RemoveFollower + IsFollowingAp forgejo.org/modules/activitypub NewContext diff --git a/.forgejo/testdata/build-release/Dockerfile b/.forgejo/testdata/build-release/Dockerfile index d10564359e..09cce06c47 100644 --- a/.forgejo/testdata/build-release/Dockerfile +++ b/.forgejo/testdata/build-release/Dockerfile @@ -1,4 +1,4 @@ -FROM data.forgejo.org/oci/alpine:3.21 +FROM data.forgejo.org/oci/alpine:3.22 ARG RELEASE_VERSION=unkown LABEL maintainer="contact@forgejo.org" \ org.opencontainers.image.version="${RELEASE_VERSION}" diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index 98b93b5757..5aa6c8cd98 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -28,7 +28,7 @@ jobs: runs-on: docker container: - image: data.forgejo.org/renovate/renovate:40.57.1 + image: data.forgejo.org/renovate/renovate:41.1.4 steps: - name: Load renovate repo cache diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml index 86e74591f1..7a93bb66a8 100644 --- a/.forgejo/workflows/testing.yml +++ b/.forgejo/workflows/testing.yml @@ -115,6 +115,11 @@ jobs: run: | su forgejo -c 'make deps-frontend frontend' - uses: ./.forgejo/workflows-composite/build-backend + - name: Decide to run all tests + id: run-all + if: contains(github.event.pull_request.labels.*.name, 'run-all-playwright-tests') || contains(github.event.pull_request.title, 'playwright') + run: | + echo "all=1" >> "$GITHUB_OUTPUT" - name: Get changed files id: changed-files uses: https://data.forgejo.org/tj-actions/changed-files@v46 @@ -127,6 +132,7 @@ jobs: USE_REPO_TEST_DIR: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}} + RUN_ALL: ${{steps.run-all.all}} - name: Upload test artifacts on failure if: failure() uses: https://data.forgejo.org/forgejo/upload-artifact@v4 diff --git a/Dockerfile b/Dockerfile index 397e97acb1..322e2c61a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx -FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env +FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct} @@ -51,7 +51,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /go/src/forgejo.org/environment-to-ini RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete -FROM data.forgejo.org/oci/alpine:3.21 +FROM data.forgejo.org/oci/alpine:3.22 ARG RELEASE_VERSION LABEL maintainer="contact@forgejo.org" \ org.opencontainers.image.authors="Forgejo" \ diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 9ee71f64e0..6a3abaa4b9 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx -FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env +FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct} @@ -49,7 +49,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ /go/src/forgejo.org/environment-to-ini RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete -FROM data.forgejo.org/oci/alpine:3.21 +FROM data.forgejo.org/oci/alpine:3.22 ARG RELEASE_VERSION LABEL maintainer="contact@forgejo.org" \ org.opencontainers.image.authors="Forgejo" \ diff --git a/Makefile b/Makefile index e6416d5a6f..e770f2a989 100644 --- a/Makefile +++ b/Makefile @@ -47,8 +47,7 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasour GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@40.57.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... @@ -222,7 +221,6 @@ help: @echo " - lint-go lint go files" @echo " - lint-go-fix lint go files and fix issues" @echo " - lint-go-vet lint go files with vet" - @echo " - lint-go-gopls lint go files with gopls" @echo " - lint-js lint js files" @echo " - lint-js-fix lint js files and fix issues" @echo " - lint-css lint css files" @@ -487,11 +485,6 @@ lint-go-vet: @echo "Running go vet..." @$(GO) vet ./... -.PHONY: lint-go-gopls -lint-go-gopls: - @echo "Running gopls check..." - @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA) - .PHONY: lint-editorconfig lint-editorconfig: $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .forgejo/workflows @@ -932,7 +925,6 @@ deps-tools: $(GO) install $(GO_LICENSES_PACKAGE) $(GO) install $(GOVULNCHECK_PACKAGE) $(GO) install $(GOMOCK_PACKAGE) - $(GO) install $(GOPLS_PACKAGE) node_modules: package-lock.json npm install --no-save diff --git a/eslint.config.mjs b/eslint.config.mjs index 5737bed623..28cfa80089 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,5 @@ import eslintCommunityEslintPluginEslintComments from '@eslint-community/eslint-plugin-eslint-comments'; -import stylisticEslintPluginJs from '@stylistic/eslint-plugin-js'; +import stylisticEslintPlugin from '@stylistic/eslint-plugin'; import vitest from '@vitest/eslint-plugin'; import arrayFunc from 'eslint-plugin-array-func'; import eslintPluginImportX from 'eslint-plugin-import-x'; @@ -26,7 +26,7 @@ export default tseslint.config( { plugins: { '@eslint-community/eslint-comments': eslintCommunityEslintPluginEslintComments, - '@stylistic/js': stylisticEslintPluginJs, + '@stylistic': stylisticEslintPlugin, '@vitest': vitest, 'array-func': arrayFunc, 'no-jquery': noJquery, @@ -69,62 +69,62 @@ export default tseslint.config( '@eslint-community/eslint-comments/no-unused-enable': [2], '@eslint-community/eslint-comments/no-use': [0], '@eslint-community/eslint-comments/require-description': [0], - '@stylistic/js/array-bracket-newline': [0], - '@stylistic/js/array-bracket-spacing': [2, 'never'], - '@stylistic/js/array-element-newline': [0], - '@stylistic/js/arrow-parens': [2, 'always'], + '@stylistic/array-bracket-newline': [0], + '@stylistic/array-bracket-spacing': [2, 'never'], + '@stylistic/array-element-newline': [0], + '@stylistic/arrow-parens': [2, 'always'], - '@stylistic/js/arrow-spacing': [2, { + '@stylistic/arrow-spacing': [2, { before: true, after: true, }], - '@stylistic/js/block-spacing': [0], + '@stylistic/block-spacing': [0], - '@stylistic/js/brace-style': [2, '1tbs', { + '@stylistic/brace-style': [2, '1tbs', { allowSingleLine: true, }], - '@stylistic/js/comma-dangle': [2, 'always-multiline'], + '@stylistic/comma-dangle': [2, 'always-multiline'], - '@stylistic/js/comma-spacing': [2, { + '@stylistic/comma-spacing': [2, { before: false, after: true, }], - '@stylistic/js/comma-style': [2, 'last'], - '@stylistic/js/computed-property-spacing': [2, 'never'], - '@stylistic/js/dot-location': [2, 'property'], - '@stylistic/js/eol-last': [2], - '@stylistic/js/function-call-spacing': [2, 'never'], - '@stylistic/js/function-call-argument-newline': [0], - '@stylistic/js/function-paren-newline': [0], - '@stylistic/js/generator-star-spacing': [0], - '@stylistic/js/implicit-arrow-linebreak': [0], + '@stylistic/comma-style': [2, 'last'], + '@stylistic/computed-property-spacing': [2, 'never'], + '@stylistic/dot-location': [2, 'property'], + '@stylistic/eol-last': [2], + '@stylistic/function-call-spacing': [2, 'never'], + '@stylistic/function-call-argument-newline': [0], + '@stylistic/function-paren-newline': [0], + '@stylistic/generator-star-spacing': [0], + '@stylistic/implicit-arrow-linebreak': [0], - '@stylistic/js/indent': [2, 2, { + '@stylistic/indent': [2, 2, { ignoreComments: true, SwitchCase: 1, }], - '@stylistic/js/key-spacing': [2], - '@stylistic/js/keyword-spacing': [2], - '@stylistic/js/linebreak-style': [2, 'unix'], - '@stylistic/js/lines-around-comment': [0], - '@stylistic/js/lines-between-class-members': [0], - '@stylistic/js/max-len': [0], - '@stylistic/js/max-statements-per-line': [0], - '@stylistic/js/multiline-ternary': [0], - '@stylistic/js/new-parens': [2], - '@stylistic/js/newline-per-chained-call': [0], - '@stylistic/js/no-confusing-arrow': [0], - '@stylistic/js/no-extra-parens': [0], - '@stylistic/js/no-extra-semi': [2], - '@stylistic/js/no-floating-decimal': [0], - '@stylistic/js/no-mixed-operators': [0], - '@stylistic/js/no-mixed-spaces-and-tabs': [2], + '@stylistic/key-spacing': [2], + '@stylistic/keyword-spacing': [2], + '@stylistic/linebreak-style': [2, 'unix'], + '@stylistic/lines-around-comment': [0], + '@stylistic/lines-between-class-members': [0], + '@stylistic/max-len': [0], + '@stylistic/max-statements-per-line': [0], + '@stylistic/multiline-ternary': [0], + '@stylistic/new-parens': [2], + '@stylistic/newline-per-chained-call': [0], + '@stylistic/no-confusing-arrow': [0], + '@stylistic/no-extra-parens': [0], + '@stylistic/no-extra-semi': [2], + '@stylistic/no-floating-decimal': [0], + '@stylistic/no-mixed-operators': [0], + '@stylistic/no-mixed-spaces-and-tabs': [2], - '@stylistic/js/no-multi-spaces': [2, { + '@stylistic/no-multi-spaces': [2, { ignoreEOLComments: true, exceptions: { @@ -132,60 +132,60 @@ export default tseslint.config( }, }], - '@stylistic/js/no-multiple-empty-lines': [2, { + '@stylistic/no-multiple-empty-lines': [2, { max: 1, maxEOF: 0, maxBOF: 0, }], - '@stylistic/js/no-tabs': [2], - '@stylistic/js/no-trailing-spaces': [2], - '@stylistic/js/no-whitespace-before-property': [2], - '@stylistic/js/nonblock-statement-body-position': [2], - '@stylistic/js/object-curly-newline': [0], - '@stylistic/js/object-curly-spacing': [2, 'never'], - '@stylistic/js/object-property-newline': [0], - '@stylistic/js/one-var-declaration-per-line': [0], - '@stylistic/js/operator-linebreak': [2, 'after'], - '@stylistic/js/padded-blocks': [2, 'never'], - '@stylistic/js/padding-line-between-statements': [0], - '@stylistic/js/quote-props': [0], + '@stylistic/no-tabs': [2], + '@stylistic/no-trailing-spaces': [2], + '@stylistic/no-whitespace-before-property': [2], + '@stylistic/nonblock-statement-body-position': [2], + '@stylistic/object-curly-newline': [0], + '@stylistic/object-curly-spacing': [2, 'never'], + '@stylistic/object-property-newline': [0], + '@stylistic/one-var-declaration-per-line': [0], + '@stylistic/operator-linebreak': [2, 'after'], + '@stylistic/padded-blocks': [2, 'never'], + '@stylistic/padding-line-between-statements': [0], + '@stylistic/quote-props': [0], - '@stylistic/js/quotes': [2, 'single', { + '@stylistic/quotes': [2, 'single', { avoidEscape: true, allowTemplateLiterals: true, }], - '@stylistic/js/rest-spread-spacing': [2, 'never'], + '@stylistic/rest-spread-spacing': [2, 'never'], - '@stylistic/js/semi': [2, 'always', { + '@stylistic/semi': [2, 'always', { omitLastInOneLineBlock: true, }], - '@stylistic/js/semi-spacing': [2, { + '@stylistic/semi-spacing': [2, { before: false, after: true, }], - '@stylistic/js/semi-style': [2, 'last'], - '@stylistic/js/space-before-blocks': [2, 'always'], + '@stylistic/semi-style': [2, 'last'], + '@stylistic/space-before-blocks': [2, 'always'], - '@stylistic/js/space-before-function-paren': [2, { + '@stylistic/space-before-function-paren': [2, { anonymous: 'ignore', named: 'never', asyncArrow: 'always', }], - '@stylistic/js/space-in-parens': [2, 'never'], - '@stylistic/js/space-infix-ops': [2], - '@stylistic/js/space-unary-ops': [2], - '@stylistic/js/spaced-comment': [2, 'always'], - '@stylistic/js/switch-colon-spacing': [2], - '@stylistic/js/template-curly-spacing': [2, 'never'], - '@stylistic/js/template-tag-spacing': [2, 'never'], - '@stylistic/js/wrap-iife': [2, 'inside'], - '@stylistic/js/wrap-regex': [0], - '@stylistic/js/yield-star-spacing': [2, 'after'], + '@stylistic/space-in-parens': [2, 'never'], + '@stylistic/space-infix-ops': [2], + '@stylistic/space-unary-ops': [2], + '@stylistic/spaced-comment': [2, 'always'], + '@stylistic/switch-colon-spacing': [2], + '@stylistic/template-curly-spacing': [2, 'never'], + '@stylistic/template-tag-spacing': [2, 'never'], + '@stylistic/wrap-iife': [2, 'inside'], + '@stylistic/wrap-regex': [0], + '@stylistic/yield-star-spacing': [2, 'after'], 'accessor-pairs': [2], 'array-callback-return': [2, { diff --git a/go.mod b/go.mod index b993bcbe3b..bb2be827eb 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.2.1 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.2 @@ -76,7 +76,7 @@ require ( github.com/meilisearch/meilisearch-go v0.31.0 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/minio/minio-go/v7 v7.0.93 + github.com/minio/minio-go/v7 v7.0.94 github.com/msteinert/pam/v2 v2.1.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.8.0 @@ -242,7 +242,7 @@ require ( replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 -replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.26.0 +replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.28.0 replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1 diff --git a/go.sum b/go.sum index d6c7b2c6a5..639880e2ce 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ code.forgejo.org/f3/gof3/v3 v3.11.0 h1:f/xToKwqTgxG6PYxvewywjDQyCcyHEEJ6sZqUitFs code.forgejo.org/f3/gof3/v3 v3.11.0/go.mod h1:4FaRUNSQGBiD1M0DuB0yNv+Z2wMtlOeckgygHSSq4KQ= code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 h1:HTZl3CBk3ABNYtFI6TPLvJgGKFIhKT5CBk0sbOtkDKU= code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:PphB88CPbx601QrWPMZATeorACeVmQlyv3u+uUMbSaM= -code.forgejo.org/forgejo/act v1.26.0 h1:6mTmoaw7d/WpYiw/Pw6AaypxFdgJog5OFi/PMEgEbxs= -code.forgejo.org/forgejo/act v1.26.0/go.mod h1:HFDFrXPrqfM9aH2RCnMiBdo/3ThxDmZjp58InPjGOfo= +code.forgejo.org/forgejo/act v1.28.0 h1:96njNC7C1YNyjWq5OWvLZMF/nw0PMthzIA8Nwbnn7jo= +code.forgejo.org/forgejo/act v1.28.0/go.mod h1:dFuiwAmD5vyrzecysHB2kL/GM3wRpoVPl+WdbCTC8Bs= code.forgejo.org/forgejo/archiver/v3 v3.5.1 h1:UmmbA7D5550uf71SQjarmrn6yKwOGxtEjb3jaYYtmSE= code.forgejo.org/forgejo/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= code.forgejo.org/forgejo/go-rpmutils v1.0.0 h1:RZGGeKt70p/WaIEL97pyT6uiiEIoN8/aLmS5Z6WmX0M= @@ -213,8 +213,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= @@ -411,8 +411,8 @@ github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.93 h1:lAB4QJp8Nq3vDMOU0eKgMuyBiEGMNlXQ5Glc8qAxqSU= -github.com/minio/minio-go/v7 v7.0.93/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc= +github.com/minio/minio-go/v7 v7.0.94 h1:1ZoksIKPyaSt64AVOyaQvhDOgVC3MfZsWM6mZXRUGtM= +github.com/minio/minio-go/v7 v7.0.94/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/models/actions/run.go b/models/actions/run.go index 48756b7a08..55def805ed 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -55,6 +55,7 @@ type ActionRun struct { PreviousDuration time.Duration Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` + NotifyEmail bool } func init() { diff --git a/models/activities/action.go b/models/activities/action.go index 1e40546b97..8592f81414 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -442,6 +442,12 @@ func (a *Action) GetIssueContent(ctx context.Context) string { return a.Issue.Content } +func GetActivityByID(ctx context.Context, id int64) (*Action, error) { + var act Action + _, err := db.GetEngine(ctx).ID(id).Get(&act) + return &act, err +} + // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { db.ListOptions @@ -595,13 +601,14 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) } // NotifyWatchers creates batch of actions for every watcher. -func NotifyWatchers(ctx context.Context, actions ...*Action) error { +func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) { var watchers []*repo_model.Watch var repo *repo_model.Repository var err error var permCode []bool var permIssue []bool var permPR []bool + var out []Action e := db.GetEngine(ctx) @@ -612,14 +619,14 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { // Add feeds for user self and all watchers. watchers, err = repo_model.GetWatchers(ctx, act.RepoID) if err != nil { - return fmt.Errorf("get watchers: %w", err) + return nil, fmt.Errorf("get watchers: %w", err) } // Be aware that optimizing this correctly into the `GetWatchers` SQL // query is for most cases less performant than doing this. blockedDoerUserIDs, err := user_model.ListBlockedByUsersID(ctx, act.ActUserID) if err != nil { - return fmt.Errorf("user_model.ListBlockedByUsersID: %w", err) + return nil, fmt.Errorf("user_model.ListBlockedByUsersID: %w", err) } if len(blockedDoerUserIDs) > 0 { @@ -634,8 +641,9 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { // Add feed for actioner. act.UserID = act.ActUserID if _, err = e.Insert(act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) + return nil, fmt.Errorf("insert new actioner: %w", err) } + out = append(out, *act) if repoChanged { act.loadRepo(ctx) @@ -643,7 +651,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { // check repo owner exist. if err := act.Repo.LoadOwner(ctx); err != nil { - return fmt.Errorf("can't get repo owner: %w", err) + return nil, fmt.Errorf("can't get repo owner: %w", err) } } else if act.Repo == nil { act.Repo = repo @@ -654,7 +662,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { act.ID = 0 act.UserID = act.Repo.Owner.ID if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) + return nil, fmt.Errorf("insert new actioner: %w", err) } } @@ -707,26 +715,29 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error { } if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new action: %w", err) + return nil, fmt.Errorf("insert new action: %w", err) } } } - return nil + return out, nil } // NotifyWatchersActions creates batch of actions for every watcher. -func NotifyWatchersActions(ctx context.Context, acts []*Action) error { +func NotifyWatchersActions(ctx context.Context, acts []*Action) ([]Action, error) { ctx, committer, err := db.TxContext(ctx) if err != nil { - return err + return nil, err } defer committer.Close() + var out []Action for _, act := range acts { - if err := NotifyWatchers(ctx, act); err != nil { - return err + as, err := NotifyWatchers(ctx, act) + if err != nil { + return nil, err } + out = append(out, as...) } - return committer.Commit() + return out, committer.Commit() } // DeleteIssueActions delete all actions related with issueID diff --git a/models/activities/action_test.go b/models/activities/action_test.go index bcc9c98cec..47dbd8ac2d 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -197,7 +197,8 @@ func TestNotifyWatchers(t *testing.T) { RepoID: 1, OpType: activities_model.ActionStarRepo, } - require.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action)) + _, err := activities_model.NotifyWatchers(db.DefaultContext, action) + require.NoError(t, err) // One watchers are inactive, thus action is only created for user 8, 1, 4, 11 unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ diff --git a/models/activities/federated_user_activity.go b/models/activities/federated_user_activity.go new file mode 100644 index 0000000000..1ff3a855d0 --- /dev/null +++ b/models/activities/federated_user_activity.go @@ -0,0 +1,106 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "context" + "fmt" + + "forgejo.org/models/db" + user_model "forgejo.org/models/user" + "forgejo.org/modules/json" + "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" + "forgejo.org/modules/validation" + + ap "github.com/go-ap/activitypub" +) + +type FederatedUserActivity struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL"` + ActorID int64 + ActorURI string + Actor *user_model.User `xorm:"-"` // transient + NoteContent string `xorm:"TEXT"` + NoteURL string `xorm:"VARCHAR(255)"` + OriginalNote string `xorm:"TEXT"` + Created timeutil.TimeStamp `xorm:"created"` +} + +func init() { + db.RegisterModel(new(FederatedUserActivity)) +} + +func NewFederatedUserActivity(userID, actorID int64, actorURI, noteContent, noteURL string, originalNote ap.Activity) (FederatedUserActivity, error) { + jsonString, err := json.Marshal(originalNote) + if err != nil { + return FederatedUserActivity{}, err + } + result := FederatedUserActivity{ + UserID: userID, + ActorID: actorID, + ActorURI: actorURI, + NoteContent: noteContent, + NoteURL: noteURL, + OriginalNote: string(jsonString), + } + if valid, err := validation.IsValid(result); !valid { + return FederatedUserActivity{}, err + } + return result, nil +} + +func (federatedUserActivity FederatedUserActivity) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.UserID, "UserID")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorID, "ActorID")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorURI, "ActorURI")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteContent, "NoteContent")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteURL, "NoteURL")...) + result = append(result, validation.ValidateNotEmpty(federatedUserActivity.OriginalNote, "OriginalNote")...) + return result +} + +func CreateUserActivity(ctx context.Context, federatedUserActivity *FederatedUserActivity) error { + if valid, err := validation.IsValid(federatedUserActivity); !valid { + return err + } + _, err := db.GetEngine(ctx).Insert(federatedUserActivity) + return err +} + +type GetFollowingFeedsOptions struct { + db.ListOptions +} + +func GetFollowingFeeds(ctx context.Context, actorID int64, opts GetFollowingFeedsOptions) ([]*FederatedUserActivity, int64, error) { + log.Debug("user_id = %s", actorID) + sess := db.GetEngine(ctx).Where("user_id = ?", actorID) + opts.SetDefaultValues() + sess = db.SetSessionPagination(sess, &opts) + + actions := make([]*FederatedUserActivity, 0, opts.PageSize) + count, err := sess.FindAndCount(&actions) + if err != nil { + return nil, 0, fmt.Errorf("FindAndCount: %w", err) + } + for _, act := range actions { + if err := act.loadActor(ctx); err != nil { + return nil, 0, err + } + } + return actions, count, err +} + +func (federatedUserActivity *FederatedUserActivity) loadActor(ctx context.Context) error { + log.Debug("for activity %s", federatedUserActivity) + actorUser, _, err := user_model.GetFederatedUserByUserID(ctx, federatedUserActivity.ActorID) + if err != nil { + return err + } + federatedUserActivity.Actor = actorUser + + return nil +} diff --git a/models/activities/federated_user_activity_test.go b/models/activities/federated_user_activity_test.go new file mode 100644 index 0000000000..9bf4f77984 --- /dev/null +++ b/models/activities/federated_user_activity_test.go @@ -0,0 +1,24 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "testing" + + "forgejo.org/modules/validation" +) + +func Test_FederatedUserActivityValidation(t *testing.T) { + sut := FederatedUserActivity{} + sut.UserID = 13 + sut.ActorID = 33 + sut.ActorURI = "33" + sut.NoteContent = "Any content!" + sut.NoteURL = "https://example.org/note/17" + sut.OriginalNote = "federatedUserActivityNote-17" + + if res, _ := validation.IsValid(sut); !res { + t.Errorf("sut expected to be valid: %v\n", sut.Validate()) + } +} diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index f4121284a6..34407d6f81 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -113,3 +113,344 @@ review_id: 22 assignee_id: 5 created_unix: 946684817 + +- + id: 13 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["4ca8bcaf27e28504df7bf996819665986b01c847","96cef4a7b72b3c208340ae6f0cf55a93e9077c93","c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2"]}' + created_unix: 1688672373 + +- + id: 14 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["23576dd018294e476c06e569b6b0f170d0558705"]}' + created_unix: 1688672374 + +- + id: 15 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["3e64625bd6eb5bcba69ac97de6c8f507402df861", "c704db5794097441aa2d9dd834d5b7e2f8f08108"]}' + created_unix: 1688672375 + +- + id: 16 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":false,"commit_ids":["811d46c7e518f4f180afb862c0db5cb8c80529ce", "747ddb3506a4fa04a7747808eb56ae16f9e933dc", "837d5c8125633d7d258f93b998e867eab0145520", "1978192d98bb1b65e11c2cf37da854fbf94bffd6"]}' + created_unix: 1688672376 + +- + id: 17 + type: 29 # push + poster_id: 2 + issue_id: 19 # in repo_id 58 + content: '{"is_force_push":true,"commit_ids":["1978192d98bb1b65e11c2cf37da854fbf94bffd6", "9b93963cf6de4dc33f915bb67f192d099c301f43"]}' + created_unix: 1749734240 + +- + id: 2000 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 1 + old_milestone_id: 0 + created_unix: 946684820 + +- + id: 2001 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 2 + old_milestone_id: 1 + created_unix: 946684920 + +- + id: 2002 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 0 + old_milestone_id: 2 + created_unix: 946685020 + +- + id: 2003 + type: 8 # milestone + poster_id: 1 + issue_id: 1 # in repo_id 1 + milestone_id: 10 # not exsting milestone + old_milestone_id: 0 + created_unix: 946685080 + +- + id: 2010 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 1 + old_project_id: 0 + created_unix: 946685120 + +- + id: 2011 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 2 + old_project_id: 1 + created_unix: 946685220 + +- + id: 2012 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 0 + old_project_id: 2 + created_unix: 946685320 + +- + id: 2013 + type: 30 # project + poster_id: 1 + issue_id: 1 # in repo_id 1 + project_id: 10 # not existing project + old_project_id: 0 + created_unix: 946685420 + +- + id: 2020 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 1 # add label + created_unix: 946685520 + +- + id: 2021 + type: 7 # label + poster_id: 1 + issue_id: 1 + label_id: 2 + content: 1 # add label + created_unix: 946685620 + +- + id: 2022 + type: 7 # label + poster_id: 2 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 0 # remove label + created_unix: 946685720 + +- + id: 2023 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 1 # add label + created_unix: 946685720 + +- + id: 2024 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 2 + content: 0 # remove label + created_unix: 946685720 + +- + id: 2025 + type: 7 # label + poster_id: 2 + issue_id: 1 # in repo_id 1 + label_id: 2 + content: 1 # add label + created_unix: 946685820 + +- + id: 2026 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 1 + content: 0 # remove label + created_unix: 946685920 + +- + id: 2027 + type: 7 # label + poster_id: 1 + issue_id: 1 # in repo_id 1 + label_id: 2 + content: 0 # remove label + created_unix: 946685920 + +- id: 2040 + type: 9 # assignee + poster_id: 1 + issue_id: 1 # in repo_id 1 + assignee_id: 1 # self + removed_assignee: false # add assignee + created_unix: 946688020 + +- id: 2041 + type: 9 # assignee + poster_id: 2 + issue_id: 1 # in repo_id 1 + assignee_id: 1 + removed_assignee: true # remove assignee + created_unix: 946688120 + +- id: 2042 + type: 9 # assignee + poster_id: 1 + issue_id: 1 # in repo_id 1 + assignee_id: 2 + removed_assignee: false # add assignee + created_unix: 946688220 + +- id: 2043 + type: 9 # assignee + poster_id: 2 + issue_id: 1 # in repo_id 1 + assignee_id: 2 # self + removed_assignee: true # remove assignee + created_unix: 946688320 + +- id: 2050 + type: 23 # lock + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688420 + +- id: 2051 + type: 24 # unlock + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688520 + +- id: 2052 + type: 23 # lock + poster_id: 1 + issue_id: 1 # in repo_id 1 + content: "Too heated" + created_unix: 946688620 + +- id: 2053 + type: 24 # unlock + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688720 + +- id: 2060 + type: 36 # pin + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688820 + +- id: 2061 + type: 37 # unpin + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946688920 + +- id: 2070 + type: 2 # close + poster_id: 1 + issue_id: 1 # in repo_id 1 + created_unix: 946689020 + +- id: 2071 + type: 1 # reopen + poster_id: 2 + issue_id: 1 # in repo_id 1 + created_unix: 946689220 + +- id: 2072 + type: 2 # close + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + created_unix: 946689320 + +- id: 2073 + type: 1 # reopen + poster_id: 2 + issue_id: 2 # pull in repo_id 1 + created_unix: 946689420 + +- id: 2080 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 5 # issue in repo_id 1 + created_unix: 946689500 + +- id: 2081 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 2 # pull in repo_id 1 + created_unix: 946689600 + +- id: 2082 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 32 + ref_issue_id: 16 # issue in repo_id 32 + created_unix: 946689700 + +- id: 2083 + type: 3 # issue reference + poster_id: 1 + issue_id: 1 # issue in repo_id 1 + ref_repo_id: 10 + ref_issue_id: 8 # pull in repo_id 10 + created_unix: 946689800 + +- id: 2090 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 1 # issue in repo_id 1 + created_unix: 946689900 + +- id: 2091 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 1 + ref_issue_id: 2 # pull in repo_id 1 + created_unix: 946690000 + +- id: 2092 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 32 + ref_issue_id: 16 # issue in repo_id 32 + created_unix: 946690050 + +- id: 2093 + type: 6 # pull reference + poster_id: 1 + issue_id: 2 # pull in repo_id 1 + ref_repo_id: 10 + ref_issue_id: 8 # pull in repo_id 10 + created_unix: 946690100 diff --git a/models/fixtures/pull_auto_merge.yml b/models/fixtures/pull_auto_merge.yml new file mode 100644 index 0000000000..ca780a73aa --- /dev/null +++ b/models/fixtures/pull_auto_merge.yml @@ -0,0 +1 @@ +[] # empty diff --git a/models/forgefed/federationhost.go b/models/forgefed/federationhost.go index 29f1b7d28e..978847bd95 100644 --- a/models/forgefed/federationhost.go +++ b/models/forgefed/federationhost.go @@ -6,6 +6,7 @@ package forgefed import ( "database/sql" "fmt" + "net/url" "strings" "time" @@ -17,9 +18,9 @@ import ( // swagger:model type FederationHost struct { ID int64 `xorm:"pk autoincr"` - HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"` + HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` + HostPort uint16 `xorm:" UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` NodeInfo NodeInfo `xorm:"extends NOT NULL"` - HostPort uint16 `xorm:"NOT NULL DEFAULT 443"` HostSchema string `xorm:"NOT NULL DEFAULT 'https'"` LatestActivity time.Time `xorm:"NOT NULL"` KeyID sql.NullString `xorm:"key_id UNIQUE"` @@ -42,6 +43,13 @@ func NewFederationHost(hostFqdn string, nodeInfo NodeInfo, port uint16, schema s return result, nil } +func (host FederationHost) AsURL() url.URL { + return url.URL{ + Scheme: host.HostSchema, + Host: fmt.Sprintf("%v:%v", host.HostFqdn, host.HostPort), + } +} + // Validate collects error strings in a slice and returns this func (host FederationHost) Validate() []string { var result []string diff --git a/models/forgefed/nodeinfo.go b/models/forgefed/nodeinfo.go index 2461b5e499..38f51304c5 100644 --- a/models/forgefed/nodeinfo.go +++ b/models/forgefed/nodeinfo.go @@ -17,12 +17,14 @@ type ( ) const ( - ForgejoSourceType SoftwareNameType = "forgejo" - GiteaSourceType SoftwareNameType = "gitea" + ForgejoSourceType SoftwareNameType = "forgejo" + GiteaSourceType SoftwareNameType = "gitea" + MastodonSourceType SoftwareNameType = "mastodon" + GoToSocialSourceType SoftwareNameType = "gotosocial" ) var KnownSourceTypes = []any{ - ForgejoSourceType, GiteaSourceType, + ForgejoSourceType, GiteaSourceType, MastodonSourceType, GoToSocialSourceType, } // ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------ diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 21a2077d06..737350b019 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -103,6 +103,12 @@ var migrations = []*Migration{ NewMigration("Normalize repository.topics to empty slice instead of null", SetTopicsAsEmptySlice), // v31 -> v32 NewMigration("Migrate maven package name concatenation", ChangeMavenArtifactConcatenation), + // v32 -> v33 + NewMigration("Add federated user activity tables, update the `federated_user` table & add indexes", FederatedUserActivityMigration), + // v33 -> v34 + NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun), + // v34 -> v35 + NewMigration("Add index to `stopped` column in `action_run` table", AddIndexToActionRunStopped), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v33.go b/models/forgejo_migrations/v33.go new file mode 100644 index 0000000000..272035fc23 --- /dev/null +++ b/models/forgejo_migrations/v33.go @@ -0,0 +1,126 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgejo_migrations //nolint:revive + +import ( + "fmt" + + "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func dropOldFederationHostIndexes(x *xorm.Engine) { + // drop unique index on HostFqdn + type FederationHost struct { + ID int64 `xorm:"pk autoincr"` + HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"` + } + + err := x.DropIndexes(FederationHost{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } +} + +func addFederatedUserActivityTables(x *xorm.Engine) { + type FederatedUserActivity struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` + ActorID int64 + ActorURI string + NoteContent string `xorm:"TEXT"` + NoteURL string `xorm:"VARCHAR(255)"` + OriginalNote string `xorm:"TEXT"` + Created timeutil.TimeStamp `xorm:"created"` + } + + // add unique index on HostFqdn+HostPort + type FederationHost struct { + ID int64 `xorm:"pk autoincr"` + HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` + HostPort uint16 `xorm:"UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` + } + + type FederatedUserFollower struct { + ID int64 `xorm:"pk autoincr"` + + FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` + FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` + } + + // Add InboxPath to FederatedUser & add index fo UserID + type FederatedUser struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` + InboxPath string + } + + var err error + + err = x.Sync(&FederationHost{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + err = x.Sync(&FederatedUserActivity{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + err = x.Sync(&FederatedUserFollower{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + err = x.Sync(&FederatedUser{}) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + // Migrate + sessMigration := x.NewSession() + defer sessMigration.Close() + if err := sessMigration.Begin(); err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + federatedUsers := make([]*FederatedUser, 0) + err = sessMigration.OrderBy("id").Find(&federatedUsers) + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + + for _, federatedUser := range federatedUsers { + if federatedUser.InboxPath != "" { + log.Info("migration[33]: This user was already migrated: %v", federatedUser) + } else { + // Migrate User.InboxPath + sql := "UPDATE `federated_user` SET `inbox_path` = ? WHERE `id` = ?" + if _, err := sessMigration.Exec(sql, fmt.Sprintf("/api/v1/activitypub/user-id/%v/inbox", federatedUser.UserID), federatedUser.ID); err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } + } + } + + err = sessMigration.Commit() + if err != nil { + log.Warn("migration[33]: There was an issue: %v", err) + return + } +} + +func FederatedUserActivityMigration(x *xorm.Engine) error { + dropOldFederationHostIndexes(x) + addFederatedUserActivityTables(x) + return nil +} diff --git a/models/forgejo_migrations/v33_test.go b/models/forgejo_migrations/v33_test.go new file mode 100644 index 0000000000..664c704bbc --- /dev/null +++ b/models/forgejo_migrations/v33_test.go @@ -0,0 +1,46 @@ +// Copyright 2025 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "testing" + "time" + + migration_tests "forgejo.org/models/migrations/test" + "forgejo.org/modules/log" + ft "forgejo.org/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_FederatedUserActivityMigration(t *testing.T) { + lc, cl := ft.NewLogChecker(log.DEFAULT, log.WARN) + lc.Filter("migration[33]") + defer cl() + + // intentionally conflicting definition + type FederatedUser struct { + ID int64 `xorm:"pk autoincr"` + UserID string + } + + // Prepare TestEnv + x, deferable := migration_tests.PrepareTestEnv(t, 0, + new(FederatedUser), + ) + sessTest := x.NewSession() + sessTest.Insert(FederatedUser{UserID: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"}) + sessTest.Commit() + defer deferable() + if x == nil || t.Failed() { + return + } + + require.NoError(t, FederatedUserActivityMigration(x)) + logFiltered, _ := lc.Check(5 * time.Second) + assert.NotEmpty(t, logFiltered) +} diff --git a/models/forgejo_migrations/v34.go b/models/forgejo_migrations/v34.go new file mode 100644 index 0000000000..9e958b934f --- /dev/null +++ b/models/forgejo_migrations/v34.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import "xorm.io/xorm" + +func AddNotifyEmailToActionRun(x *xorm.Engine) error { + type ActionRun struct { + ID int64 + NotifyEmail bool + } + return x.Sync(new(ActionRun)) +} diff --git a/models/forgejo_migrations/v35.go b/models/forgejo_migrations/v35.go new file mode 100644 index 0000000000..0fb3b43e2c --- /dev/null +++ b/models/forgejo_migrations/v35.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations //nolint:revive + +import ( + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func AddIndexToActionRunStopped(x *xorm.Engine) error { + type ActionRun struct { + ID int64 + Stopped timeutil.TimeStamp `xorm:"index"` + } + + return x.Sync(&ActionRun{}) +} diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index bf4b89ee0b..91a69c26a7 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -66,6 +66,8 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { sess.Asc("issue.created_unix").Asc("issue.id") case "recentupdate": sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") + case "recentclose": + sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id") case "leastupdate": sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") case "mostcomment": diff --git a/models/issues/pull.go b/models/issues/pull.go index 0781fd0a2d..188ef00814 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -5,6 +5,7 @@ package issues import ( + "bufio" "context" "errors" "fmt" @@ -923,31 +924,30 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr * return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } -// GetCodeOwnersFromContent returns the code owners configuration -// Return empty slice if files missing +// GetCodeOwnersFromReader returns the code owners configuration // Return warning messages on parsing errors // We're trying to do the best we can when parsing a file. // Invalid lines are skipped. Non-existent users and teams too. -func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) { - if len(data) == 0 { - return nil, nil - } +func GetCodeOwnersFromReader(ctx context.Context, rc io.ReadCloser, truncated bool) ([]*CodeOwnerRule, []string) { + defer rc.Close() + scanner := bufio.NewScanner(rc) - rules := make([]*CodeOwnerRule, 0) - lines := strings.Split(data, "\n") - warnings := make([]string, 0) + var rules []*CodeOwnerRule + var warnings []string + line := 0 + for scanner.Scan() { + line++ - for i, line := range lines { - tokens := TokenizeCodeOwnersLine(line) + tokens := TokenizeCodeOwnersLine(scanner.Text()) if len(tokens) == 0 { continue } else if len(tokens) < 2 { - warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1)) + warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", line)) continue } rule, wr := ParseCodeOwnersLine(ctx, tokens) for _, w := range wr { - warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) + warnings = append(warnings, fmt.Sprintf("Line: %d: %s", line, w)) } if rule == nil { continue @@ -955,6 +955,12 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul rules = append(rules, rule) } + if err := scanner.Err(); err != nil { + warnings = append(warnings, err.Error()) + } + if truncated { + warnings = append(warnings, fmt.Sprintf("File too big: truncated while on line %d", line)) + } return rules, warnings } diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index a448673454..8fc0491026 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -152,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio applySorts(findSession, opts.SortType, 0) findSession = db.SetSessionPagination(findSession, opts) prs := make([]*PullRequest, 0, opts.PageSize) - return prs, maxResults, findSession.Find(&prs) + found := findSession.Find(&prs) + return prs, maxResults, found } // PullRequestList defines a list of pull requests diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 5a8e8d8aaf..3682f6fd25 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -79,6 +79,47 @@ func TestPullRequestsNewest(t *testing.T) { } } +func TestPullRequests_Closed_RecentSortType(t *testing.T) { + // Issue ID | Closed At. | Updated At + // 2 | 1707270001 | 1707270001 + // 3 | 1707271000 | 1707279999 + // 11 | 1707279999 | 1707275555 + tests := []struct { + sortType string + expectedIssueIDOrder []int64 + }{ + {"recentupdate", []int64{3, 11, 2}}, + {"recentclose", []int64{11, 3, 2}}, + } + + require.NoError(t, unittest.PrepareTestDatabase()) + _, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2") + require.NoError(t, err) + _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3") + require.NoError(t, err) + _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11") + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.sortType, func(t *testing.T) { + prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + }, + State: "closed", + SortType: test.sortType, + }) + require.NoError(t, err) + + if assert.Len(t, prs, len(test.expectedIssueIDOrder)) { + for i := range test.expectedIssueIDOrder { + assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID) + } + } + }) + } +} + func TestLoadRequestedReviewers(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/pull/automerge.go b/models/pull/automerge.go index 63f572309b..dcc1f39271 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -10,6 +10,7 @@ import ( "forgejo.org/models/db" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/log" "forgejo.org/modules/timeutil" ) @@ -58,13 +59,15 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, return ErrAlreadyScheduledToAutoMerge{PullID: pullID} } - _, err := db.GetEngine(ctx).Insert(&AutoMerge{ + scheduledPRM, err := db.GetEngine(ctx).Insert(&AutoMerge{ DoerID: doer.ID, PullID: pullID, MergeStyle: style, Message: message, DeleteBranchAfterMerge: deleteBranch, }) + log.Trace("ScheduleAutoMerge %+v for PR %d", scheduledPRM, pullID) + return err } @@ -81,6 +84,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe return false, nil, err } + log.Trace("GetScheduledMergeByPullID found %+v for PR %d", scheduledPRM, pullID) + scheduledPRM.Doer = doer return true, scheduledPRM, nil } @@ -94,6 +99,8 @@ func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error { return db.ErrNotExist{Resource: "auto_merge", ID: pullID} } + log.Trace("DeleteScheduledAutoMerge %+v for PR %d", scheduledPRM, pullID) + _, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{}) return err } diff --git a/models/user/federated_user.go b/models/user/federated_user.go index c1833c7de3..d2a9c34c9e 100644 --- a/models/user/federated_user.go +++ b/models/user/federated_user.go @@ -11,19 +11,21 @@ import ( type FederatedUser struct { ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"NOT NULL"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` KeyID sql.NullString `xorm:"key_id UNIQUE"` PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"` - NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! + InboxPath string + NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! } -func NewFederatedUser(userID int64, externalID string, federationHostID int64, normalizedOriginalURL string) (FederatedUser, error) { +func NewFederatedUser(userID int64, externalID string, federationHostID int64, inboxPath, normalizedOriginalURL string) (FederatedUser, error) { result := FederatedUser{ UserID: userID, ExternalID: externalID, FederationHostID: federationHostID, + InboxPath: inboxPath, NormalizedOriginalURL: normalizedOriginalURL, } if valid, err := validation.IsValid(result); !valid { @@ -32,10 +34,11 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64, n return result, nil } -func (user FederatedUser) Validate() []string { +func (federatedUser FederatedUser) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...) - result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...) - result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.UserID, "UserID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.ExternalID, "ExternalID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.FederationHostID, "FederationHostID")...) + result = append(result, validation.ValidateNotEmpty(federatedUser.InboxPath, "InboxPath")...) return result } diff --git a/models/user/federated_user_follower.go b/models/user/federated_user_follower.go new file mode 100644 index 0000000000..db72c9b5ce --- /dev/null +++ b/models/user/federated_user_follower.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import "forgejo.org/modules/validation" + +type FederatedUserFollower struct { + ID int64 `xorm:"pk autoincr"` + FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` + FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` +} + +func NewFederatedUserFollower(followedUserID, federatedUserID int64) (FederatedUserFollower, error) { + result := FederatedUserFollower{ + FollowedUserID: followedUserID, + FollowingUserID: federatedUserID, + } + if valid, err := validation.IsValid(result); !valid { + return FederatedUserFollower{}, err + } + return result, nil +} + +func (user FederatedUserFollower) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(user.FollowedUserID, "FollowedUserID")...) + result = append(result, validation.ValidateNotEmpty(user.FollowingUserID, "FollowingUserID")...) + return result +} diff --git a/models/user/federated_user_follower_test.go b/models/user/federated_user_follower_test.go new file mode 100644 index 0000000000..e57ba01308 --- /dev/null +++ b/models/user/federated_user_follower_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "forgejo.org/modules/validation" + + "github.com/stretchr/testify/assert" +) + +func Test_FederatedUserFollowerValidation(t *testing.T) { + sut := FederatedUserFollower{ + FollowedUserID: 12, + FollowingUserID: 1, + } + res, err := validation.IsValid(sut) + assert.Truef(t, res, "sut should be valid but was %q", err) + + sut = FederatedUserFollower{ + FollowedUserID: 1, + } + res, _ = validation.IsValid(sut) + assert.False(t, res, "sut should be invalid") +} diff --git a/models/user/federated_user_test.go b/models/user/federated_user_test.go index 542798c9bc..be18339670 100644 --- a/models/user/federated_user_test.go +++ b/models/user/federated_user_test.go @@ -14,6 +14,7 @@ func Test_FederatedUserValidation(t *testing.T) { UserID: 12, ExternalID: "12", FederationHostID: 1, + InboxPath: "/api/v1/activitypub/user-id/12/inbox", } if res, err := validation.IsValid(sut); !res { t.Errorf("sut should be valid but was %q", err) @@ -22,6 +23,7 @@ func Test_FederatedUserValidation(t *testing.T) { sut = FederatedUser{ ExternalID: "12", FederationHostID: 1, + InboxPath: "/api/v1/activitypub/user-id/12/inbox", } if res, _ := validation.IsValid(sut); res { t.Error("sut should be invalid") diff --git a/models/user/follow.go b/models/user/follow.go index 5be0f73c35..e32c226385 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -11,6 +11,7 @@ import ( ) // Follow represents relations of user and their followers. +// TODO: We should unify Activity-pub-following and classical following (see models/user/user_repository.go#IsFollowingAp) type Follow struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"UNIQUE(follow)"` diff --git a/models/user/user_repository.go b/models/user/user_repository.go index 299d3af64a..3f24efb1fb 100644 --- a/models/user/user_repository.go +++ b/models/user/user_repository.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package user @@ -8,12 +8,14 @@ import ( "fmt" "forgejo.org/models/db" + "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/validation" ) func init() { db.RegisterModel(new(FederatedUser)) + db.RegisterModel(new(FederatedUserFollower)) } func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error { @@ -30,7 +32,12 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat if err != nil { return err } - defer committer.Close() + defer func() { + err := committer.Close() + if err != nil { + log.Error("Error closing committer: %v", err) + } + }() if err := CreateUser(ctx, user, &overwrite); err != nil { return err @@ -50,6 +57,14 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat return committer.Commit() } +func (federatedUser *FederatedUser) UpdateFederatedUser(ctx context.Context) error { + if _, err := validation.IsValid(federatedUser); err != nil { + return err + } + _, err := db.GetEngine(ctx).ID(federatedUser.ID).Cols("inbox_path").Update(federatedUser) + return err +} + func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { federatedUser := new(FederatedUser) user := new(User) @@ -75,6 +90,41 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID return user, federatedUser, nil } +func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { + user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID) + if err != nil { + return nil, nil, err + } else if federatedUser == nil { + return nil, nil, fmt.Errorf("FederatedUser for externalId = %v and federationHostId = %v does not exist", externalID, federationHostID) + } + return user, federatedUser, nil +} + +func GetFederatedUserByUserID(ctx context.Context, userID int64) (*User, *FederatedUser, error) { + federatedUser := new(FederatedUser) + user := new(User) + has, err := db.GetEngine(ctx).Where("user_id=?", userID).Get(federatedUser) + if err != nil { + return nil, nil, err + } else if !has { + return nil, nil, fmt.Errorf("Federated user %v does not exist", federatedUser.UserID) + } + has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user) + if err != nil { + return nil, nil, err + } else if !has { + return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID) + } + + if res, err := validation.IsValid(*user); !res { + return nil, nil, err + } + if res, err := validation.IsValid(*federatedUser); !res { + return nil, nil, err + } + return user, federatedUser, nil +} + func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *FederatedUser, error) { federatedUser := new(FederatedUser) user := new(User) @@ -101,7 +151,85 @@ func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *Federa return user, federatedUser, nil } +func UpdateFederatedUser(ctx context.Context, federatedUser *FederatedUser) error { + if res, err := validation.IsValid(federatedUser); !res { + return err + } + _, err := db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser) + return err +} + func DeleteFederatedUser(ctx context.Context, userID int64) error { _, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID}) return err } + +func GetFollowersForUser(ctx context.Context, user *User) ([]*FederatedUserFollower, error) { + if res, err := validation.IsValid(user); !res { + return nil, err + } + followers := make([]*FederatedUserFollower, 0, 8) + + err := db.GetEngine(ctx). + Where("followed_user_id = ?", user.ID). + Find(&followers) + if err != nil { + return nil, err + } + for _, element := range followers { + if res, err := validation.IsValid(*element); !res { + return nil, err + } + } + return followers, nil +} + +func AddFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) (*FederatedUserFollower, error) { + if res, err := validation.IsValid(followedUser); !res { + return nil, err + } + if res, err := validation.IsValid(followingUser); !res { + return nil, err + } + + federatedUserFollower, err := NewFederatedUserFollower(followedUser.ID, followingUser.UserID) + if err != nil { + return nil, err + } + _, err = db.GetEngine(ctx).Insert(&federatedUserFollower) + if err != nil { + return nil, err + } + + return &federatedUserFollower, err +} + +func RemoveFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) error { + if res, err := validation.IsValid(followedUser); !res { + return err + } + if res, err := validation.IsValid(followingUser); !res { + return err + } + + _, err := db.GetEngine(ctx).Delete(&FederatedUserFollower{ + FollowedUserID: followedUser.ID, + FollowingUserID: followingUser.UserID, + }) + return err +} + +// TODO: We should unify Activity-pub-following and classical following (see models/user/follow.go) +func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) { + if res, err := validation.IsValid(followedUser); !res { + return false, err + } + if res, err := validation.IsValid(followingUser); !res { + return false, err + } + + return db.GetEngine(ctx).Get(&FederatedUserFollower{ + FollowedUserID: followedUser.ID, + FollowingUserID: followingUser.UserID, + }) +} diff --git a/models/user/user_system.go b/models/user/user_system.go index 82805cc8ee..11f54591b7 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -12,6 +12,13 @@ import ( "forgejo.org/modules/structs" ) +// IsSystem returns true if the user has a fixed +// negative ID, is never stored in the database and +// is generated on the fly when needed. +func (u *User) IsSystem() bool { + return u.IsGhost() || u.IsActions() +} + const ( GhostUserID = -1 GhostUserName = "Ghost" diff --git a/models/user/user_test.go b/models/user/user_test.go index 2a9e652a35..fd9d05653f 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -148,7 +148,7 @@ func TestAPActorID_APActorID(t *testing.T) { assert.Equal(t, expected, url) } -func TestAPActorKeyID(t *testing.T) { +func TestKeyID(t *testing.T) { user := user_model.User{ID: 1} url := user.APActorKeyID() expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key" diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index daf453fb47..7ae4557ed6 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -323,6 +323,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ } case "paths": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) @@ -336,6 +340,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa } } case "paths-ignore": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index b85ed7fd56..9068ce31c3 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -150,6 +150,24 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on: workflow_dispatch", expected: true, }, + { + desc: "push to tag matches workflow with paths condition (should skip paths check)", + triggeredEvent: webhook_module.HookEventPush, + payload: &api.PushPayload{ + Ref: "refs/tags/v1.0.0", + Before: "0000000", + Commits: []*api.PayloadCommit{ + { + ID: "abcdef123456", + Added: []string{"src/main.go"}, + Message: "Release v1.0.0", + }, + }, + }, + commit: nil, + yamlOn: "on:\n push:\n paths:\n - src/**", + expected: true, + }, } for _, tc := range testCases { diff --git a/modules/git/blob.go b/modules/git/blob.go index 3fda358938..8c5c275146 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -12,7 +12,6 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/typesniffer" - "forgejo.org/modules/util" ) // Blob represents a Git object. @@ -25,42 +24,25 @@ type Blob struct { repo *Repository } -// DataAsync gets a ReadCloser for the contents of a blob without reading it all. -// Calling the Close function on the result will discard all unread output. -func (b *Blob) DataAsync() (io.ReadCloser, error) { +func (b *Blob) newReader() (*bufio.Reader, int64, func(), error) { wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) if err != nil { - return nil, err + return nil, 0, nil, err } _, err = wr.Write([]byte(b.ID.String() + "\n")) if err != nil { cancel() - return nil, err + return nil, 0, nil, err } _, _, size, err := ReadBatchLine(rd) if err != nil { cancel() - return nil, err + return nil, 0, nil, err } b.gotSize = true b.size = size - - if size < 4096 { - bs, err := io.ReadAll(io.LimitReader(rd, size)) - defer cancel() - if err != nil { - return nil, err - } - _, err = rd.Discard(1) - return io.NopCloser(bytes.NewReader(bs)), err - } - - return &blobReader{ - rd: rd, - n: size, - cancel: cancel, - }, nil + return rd, size, cancel, err } // Size returns the uncompressed size of the blob @@ -91,10 +73,36 @@ func (b *Blob) Size() int64 { return b.size } +// DataAsync gets a ReadCloser for the contents of a blob without reading it all. +// Calling the Close function on the result will discard all unread output. +func (b *Blob) DataAsync() (io.ReadCloser, error) { + rd, size, cancel, err := b.newReader() + if err != nil { + return nil, err + } + + if size < 4096 { + bs, err := io.ReadAll(io.LimitReader(rd, size)) + defer cancel() + if err != nil { + return nil, err + } + _, err = rd.Discard(1) + return io.NopCloser(bytes.NewReader(bs)), err + } + + return &blobReader{ + rd: rd, + n: size, + cancel: cancel, + }, nil +} + type blobReader struct { - rd *bufio.Reader - n int64 - cancel func() + rd *bufio.Reader + n int64 // number of bytes to read + additionalDiscard int64 // additional number of bytes to discard + cancel func() } func (b *blobReader) Read(p []byte) (n int, err error) { @@ -117,7 +125,8 @@ func (b *blobReader) Close() error { defer b.cancel() - if err := DiscardFull(b.rd, b.n+1); err != nil { + // discard the unread bytes, the truncated bytes and the trailing newline + if err := DiscardFull(b.rd, b.n+b.additionalDiscard+1); err != nil { return err } @@ -131,17 +140,35 @@ func (b *Blob) Name() string { return b.name } -// GetBlobContent Gets the limited content of the blob as raw text +// NewTruncatedReader return a blob-reader which silently truncates when the limit is reached (io.EOF will be returned) +func (b *Blob) NewTruncatedReader(limit int64) (rc io.ReadCloser, fullSize int64, err error) { + r, fullSize, cancel, err := b.newReader() + if err != nil { + return nil, fullSize, err + } + + limit = min(limit, fullSize) + return &blobReader{ + rd: r, + n: limit, + additionalDiscard: fullSize - limit, + cancel: cancel, + }, fullSize, nil +} + +// GetBlobContent Gets the truncated content of the blob as raw text func (b *Blob) GetBlobContent(limit int64) (string, error) { if limit <= 0 { return "", nil } - dataRc, err := b.DataAsync() + rc, fullSize, err := b.NewTruncatedReader(limit) if err != nil { return "", err } - defer dataRc.Close() - buf, err := util.ReadWithLimit(dataRc, int(limit)) + defer rc.Close() + + buf := make([]byte, min(fullSize, limit)) + _, err = io.ReadFull(rc, buf) return string(buf), err } diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index 810964b33d..54115013d3 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -35,6 +35,106 @@ func TestBlob_Data(t *testing.T) { assert.Equal(t, output, string(data)) } +func TestBlob(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + repo, err := openRepositoryWithDefaultContext(bareRepo1Path) + require.NoError(t, err) + + defer repo.Close() + + testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375") + require.NoError(t, err) + + t.Run("GetBlobContent", func(t *testing.T) { + r, err := testBlob.GetBlobContent(100) + require.NoError(t, err) + require.Equal(t, "file2\n", r) + + r, err = testBlob.GetBlobContent(-1) + require.NoError(t, err) + require.Empty(t, r) + + r, err = testBlob.GetBlobContent(4) + require.NoError(t, err) + require.Equal(t, "file", r) + + r, err = testBlob.GetBlobContent(6) + require.NoError(t, err) + require.Equal(t, "file2\n", r) + }) + + t.Run("NewTruncatedReader", func(t *testing.T) { + // read fewer than available + rc, size, err := testBlob.NewTruncatedReader(100) + require.NoError(t, err) + require.Equal(t, int64(6), size) + + buf := make([]byte, 1) + n, err := rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Equal(t, "f", string(buf)) + n, err = rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 1, n) + require.Equal(t, "i", string(buf)) + + require.NoError(t, rc.Close()) + + // read more than available + rc, size, err = testBlob.NewTruncatedReader(100) + require.NoError(t, err) + require.Equal(t, int64(6), size) + + buf = make([]byte, 100) + n, err = rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 6, n) + require.Equal(t, "file2\n", string(buf[:n])) + + n, err = rc.Read(buf) + require.Error(t, err) + require.Equal(t, io.EOF, err) + require.Equal(t, 0, n) + + require.NoError(t, rc.Close()) + + // read more than truncated + rc, size, err = testBlob.NewTruncatedReader(4) + require.NoError(t, err) + require.Equal(t, int64(6), size) + + buf = make([]byte, 10) + n, err = rc.Read(buf) + require.NoError(t, err) + require.Equal(t, 4, n) + require.Equal(t, "file", string(buf[:n])) + + n, err = rc.Read(buf) + require.Error(t, err) + require.Equal(t, io.EOF, err) + require.Equal(t, 0, n) + + require.NoError(t, rc.Close()) + }) + + t.Run("NonExisting", func(t *testing.T) { + nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000") + require.NoError(t, err) + + r, err := nonExistingBlob.GetBlobContent(100) + require.Error(t, err) + require.IsType(t, ErrNotExist{}, err) + require.Empty(t, r) + + rc, size, err := nonExistingBlob.NewTruncatedReader(100) + require.Error(t, err) + require.IsType(t, ErrNotExist{}, err) + require.Empty(t, rc) + require.Empty(t, size) + }) +} + func Benchmark_Blob_Data(b *testing.B) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := openRepositoryWithDefaultContext(bareRepo1Path) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 717da464b9..e811d29994 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -267,8 +267,13 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error // RenderString renders Markdown string to HTML with all specific handling stuff and return string func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) { + return RenderReader(ctx, strings.NewReader(content)) +} + +// RenderReader renders Markdown io.Reader to HTML with all specific handling stuff and return string +func RenderReader(ctx *markup.RenderContext, input io.Reader) (template.HTML, error) { var buf strings.Builder - if err := Render(ctx, strings.NewReader(content), &buf); err != nil { + if err := Render(ctx, input, &buf); err != nil { return "", err } return template.HTML(buf.String()), nil diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index 32bdf3bfa2..87ee2854ae 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -23,6 +23,11 @@ var wellKnownMimeTypesLower = map[string]string{ ".wasm": "application/wasm", ".webp": "image/webp", ".xml": "text/xml; charset=utf-8", + ".glb": "model/gltf-binary", + ".gltf": "model/gltf+json", + ".obj": "model/obj", + ".stl": "model/stl", + ".3mf": "model/3mf", // well, there are some types missing from the builtin list ".txt": "text/plain; charset=utf-8", diff --git a/modules/structs/action.go b/modules/structs/action.go index 2c42365c19..f47b228d75 100644 --- a/modules/structs/action.go +++ b/modules/structs/action.go @@ -78,3 +78,9 @@ type ActionRun struct { // the url of this action run HTMLURL string `json:"html_url"` } + +// ListActionRunResponse return a list of ActionRun +type ListActionRunResponse struct { + Entries []*ActionRun `json:"workflow_runs"` + TotalCount int64 `json:"total_count"` +} diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 505367336c..b13f344738 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -32,23 +32,3 @@ type ActionTaskResponse struct { Entries []*ActionTask `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } - -// ActionRun represents an ActionRun -type RepoActionRun struct { - ID int64 `json:"id"` - Name string `json:"name"` - RunNumber int64 `json:"run_number"` - Event string `json:"event"` - Status string `json:"status"` - HeadBranch string `json:"head_branch"` - HeadSHA string `json:"head_sha"` - WorkflowID string `json:"workflow_id"` - URL string `json:"url"` - TriggeringActor *User `json:"triggering_actor"` -} - -// ListActionRunResponse return a list of ActionRun -type ListRepoActionRunResponse struct { - Entries []*RepoActionRun `json:"workflow_runs"` - TotalCount int64 `json:"total_count"` -} diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index a8fc70e54c..262feb2b05 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -24,6 +24,16 @@ const ( AvifMimeType = "image/avif" // ApplicationOctetStream MIME type of binary files. ApplicationOctetStream = "application/octet-stream" + // GLTFMimeType MIME type of GLTF files. + GLTFMimeType = "model/gltf+json" + // GLBMimeType MIME type of GLB files. + GLBMimeType = "model/gltf-binary" + // OBJMimeType MIME type of OBJ files. + OBJMimeType = "model/obj" + // STLMimeType MIME type of STL files. + STLMimeType = "model/stl" + // 3MFMimeType MIME type of 3MF files. + ThreeMFMimeType = "model/3mf" ) var ( @@ -67,6 +77,36 @@ func (ct SniffedType) IsAudio() bool { return strings.Contains(ct.contentType, "audio/") } +// Is3DModel detects if data is a 3D format +func (ct SniffedType) Is3DModel() bool { + return strings.Contains(ct.contentType, "model/") +} + +// IsGLTFFile detects if data is an SVG image format +func (ct SniffedType) IsGLTF() bool { + return strings.Contains(ct.contentType, GLTFMimeType) +} + +// IsGLBFile detects if data is an GLB image format +func (ct SniffedType) IsGLB() bool { + return strings.Contains(ct.contentType, GLBMimeType) +} + +// IsOBJFile detects if data is an OBJ image format +func (ct SniffedType) IsOBJ() bool { + return strings.Contains(ct.contentType, OBJMimeType) +} + +// IsSTLTextFile detects if data is an STL text format +func (ct SniffedType) IsSTL() bool { + return strings.Contains(ct.contentType, STLMimeType) +} + +// Is3MFFile detects if data is an 3MF image format +func (ct SniffedType) Is3MF() bool { + return strings.Contains(ct.contentType, ThreeMFMimeType) +} + // IsRepresentableAsText returns true if file content can be represented as // plain text or is empty. func (ct SniffedType) IsRepresentableAsText() bool { @@ -75,7 +115,7 @@ func (ct SniffedType) IsRepresentableAsText() bool { // IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser func (ct SniffedType) IsBrowsableBinaryType() bool { - return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() + return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() || ct.Is3DModel() } // GetMimeType returns the mime type @@ -135,6 +175,13 @@ func DetectContentType(data []byte) SniffedType { ct = "audio/ogg" // for most cases, it is used as an audio container } } + + // GLTF is unsupported by http.DetectContentType + // hexdump -n 4 -C glTF.glb + if bytes.HasPrefix(data, []byte("glTF")) { + ct = GLBMimeType + } + return SniffedType{ct} } diff --git a/modules/typesniffer/typesniffer_test.go b/modules/typesniffer/typesniffer_test.go index 8d80b4ddb4..176d3658bb 100644 --- a/modules/typesniffer/typesniffer_test.go +++ b/modules/typesniffer/typesniffer_test.go @@ -117,6 +117,14 @@ func TestIsAudio(t *testing.T) { assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char } +func TestIsGLB(t *testing.T) { + glb, _ := hex.DecodeString("676c5446") + assert.True(t, DetectContentType(glb).IsGLB()) + assert.True(t, DetectContentType(glb).Is3DModel()) + assert.False(t, DetectContentType([]byte("plain text")).IsGLB()) + assert.False(t, DetectContentType([]byte("plain text")).Is3DModel()) +} + func TestDetectContentTypeFromReader(t *testing.T) { mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") st, err := DetectContentTypeFromReader(bytes.NewReader(mp3)) @@ -145,3 +153,15 @@ func TestDetectContentTypeAvif(t *testing.T) { assert.True(t, st.IsImage()) } + +func TestDetectContentTypeModelGLB(t *testing.T) { + glb, err := hex.DecodeString("676c5446") + require.NoError(t, err) + + st, err := DetectContentTypeFromReader(bytes.NewReader(glb)) + require.NoError(t, err) + + // print st for debugging + assert.Equal(t, "model/gltf-binary", st.GetMimeType()) + assert.True(t, st.IsGLB()) +} diff --git a/modules/util/io.go b/modules/util/io.go index 1559b019a0..4c99004c0c 100644 --- a/modules/util/io.go +++ b/modules/util/io.go @@ -4,7 +4,6 @@ package util import ( - "bytes" "errors" "io" ) @@ -20,42 +19,6 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { return n, err } -// ReadWithLimit reads at most "limit" bytes from r into buf. -// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil. -func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) { - return readWithLimit(r, 1024, n) -} - -func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) { - if limit <= batch { - buf := make([]byte, limit) - n, err := ReadAtMost(r, buf) - if err != nil { - return nil, err - } - return buf[:n], nil - } - res := bytes.NewBuffer(make([]byte, 0, batch)) - bufFix := make([]byte, batch) - eof := false - for res.Len() < limit && !eof { - bufTmp := bufFix - if res.Len()+batch > limit { - bufTmp = bufFix[:limit-res.Len()] - } - n, err := io.ReadFull(r, bufTmp) - if err == io.EOF || err == io.ErrUnexpectedEOF { - eof = true - } else if err != nil { - return nil, err - } - if _, err = res.Write(bufTmp[:n]); err != nil { - return nil, err - } - } - return res.Bytes(), nil -} - // ErrNotEmpty is an error reported when there is a non-empty reader var ErrNotEmpty = errors.New("not-empty") diff --git a/modules/util/io_test.go b/modules/util/io_test.go deleted file mode 100644 index 870e713646..0000000000 --- a/modules/util/io_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package util - -import ( - "bytes" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type readerWithError struct { - buf *bytes.Buffer -} - -func (r *readerWithError) Read(p []byte) (n int, err error) { - if r.buf.Len() < 2 { - return 0, errors.New("test error") - } - return r.buf.Read(p) -} - -func TestReadWithLimit(t *testing.T) { - bs := []byte("0123456789abcdef") - - // normal test - buf, err := readWithLimit(bytes.NewBuffer(bs), 5, 2) - require.NoError(t, err) - assert.Equal(t, []byte("01"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 5) - require.NoError(t, err) - assert.Equal(t, []byte("01234"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 6) - require.NoError(t, err) - assert.Equal(t, []byte("012345"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, len(bs)) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789abcdef"), buf) - - buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 100) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789abcdef"), buf) - - // test with error - buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 10) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789"), buf) - - buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 100) - require.ErrorContains(t, err, "test error") - assert.Empty(t, buf) - - // test public function - buf, err = ReadWithLimit(bytes.NewBuffer(bs), 2) - require.NoError(t, err) - assert.Equal(t, []byte("01"), buf) - - buf, err = ReadWithLimit(bytes.NewBuffer(bs), 9999999) - require.NoError(t, err) - assert.Equal(t, []byte("0123456789abcdef"), buf) -} diff --git a/modules/util/truncate.go b/modules/util/truncate.go index f2edbdc673..7207a89177 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -54,3 +54,12 @@ func SplitTrimSpace(input, sep string) []string { return stringList } + +// TruncateRunes returns a truncated string with given rune limit, +// it returns input string if its rune length doesn't exceed the limit. +func TruncateRunes(str string, limit int) string { + if utf8.RuneCountInString(str) < limit { + return str + } + return string([]rune(str)[:limit]) +} diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index dfe1230fd4..8187b13eb2 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -44,3 +44,18 @@ func TestSplitString(t *testing.T) { } test(tc, SplitStringAtByteN) } + +func TestTruncateRunes(t *testing.T) { + assert.Empty(t, TruncateRunes("", 0)) + assert.Empty(t, TruncateRunes("", 1)) + + assert.Empty(t, TruncateRunes("ab", 0)) + assert.Equal(t, "a", TruncateRunes("ab", 1)) + assert.Equal(t, "ab", TruncateRunes("ab", 2)) + assert.Equal(t, "ab", TruncateRunes("ab", 3)) + + assert.Empty(t, TruncateRunes("测试", 0)) + assert.Equal(t, "测", TruncateRunes("测试", 1)) + assert.Equal(t, "测试", TruncateRunes("测试", 2)) + assert.Equal(t, "测试", TruncateRunes("测试", 3)) +} diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 5f25cdea43..9448dd8e7e 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -2922,6 +2922,7 @@ settings.event_action_success = Úspěch settings.event_action_success_desc = Běh akce byl úspěšný. settings.event_header_action = Události běhu akce settings.event_action_recover_desc = Běh akce byl úspěšný, předchozí běh akce ve stejném workflow selhal. +issues.filter_type.all_pull_requests = Všechny žádosti o sloučení [graphs] component_loading_info = Tohle může chvíli trvat… diff --git a/options/locale/locale_da.ini b/options/locale/locale_da.ini index 6ec3a10e28..ea22f49e77 100644 --- a/options/locale/locale_da.ini +++ b/options/locale/locale_da.ini @@ -2735,6 +2735,7 @@ settings.event_action_success = Success settings.event_action_recover_desc = Handlingskørsel lykkedes efter at den sidste handlingskørsel i samme arbejdsgang mislykkedes. settings.event_action_failure_desc = Handlingskørsel sluttede som en fejl. settings.event_action_recover = Gendan +issues.filter_type.all_pull_requests = Alle pull-anmodninger [notification] watching = Overvåger diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 4b2e215107..829912b3cc 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -2924,6 +2924,7 @@ settings.event_action_success = Erfolg settings.event_header_action = Action-Run-Ereignisse settings.event_action_recover_desc = Action-Run war erfolgreich, nachdem der letzte Action-Run im selben Arbeitsablauf fehlgeschlagen ist. settings.event_action_recover = Wiederherstellen +issues.filter_type.all_pull_requests = Alle Pull-Requests [graphs] component_loading_failed = Konnte %s nicht laden diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7479ab80af..0bb128f8b3 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -768,8 +768,8 @@ update_profile_success = Your profile has been updated. change_username = Your username has been changed. change_username_prompt = Note: Changing your username also changes your account URL. change_username_redirect_prompt = The old username will redirect until someone claims it. -change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old username during the cooldown period. -change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old username during the cooldown period. +change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old username during the cooldown period. +change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old username during the cooldown period. continue = Continue cancel = Cancel language = Language @@ -1628,6 +1628,7 @@ issues.filter_poster = Author issues.filter_poster_no_select = All authors issues.filter_type = Type issues.filter_type.all_issues = All issues +issues.filter_type.all_pull_requests = All pull requests issues.filter_type.assigned_to_you = Assigned to you issues.filter_type.created_by_you = Created by you issues.filter_type.mentioning_you = Mentioning you @@ -1693,15 +1694,13 @@ issues.close_comment_issue = Close with comment issues.reopen_issue = Reopen issues.reopen_comment_issue = Reopen with comment issues.create_comment = Comment -issues.closed_at = `closed this issue %[2]s` -issues.reopened_at = `reopened this issue %[2]s` -issues.commit_ref_at = `referenced this issue from a commit %[2]s` -issues.ref_issue_from = `referenced this issue %[4]s %[2]s` -issues.ref_pull_from = `referenced this pull request %[4]s %[2]s` -issues.ref_closing_from = `referenced this issue from a pull request %[4]s that will close it, %[2]s` -issues.ref_reopening_from = `referenced this issue from a pull request %[4]s that will reopen it, %[2]s` -issues.ref_closed_from = `closed this issue %[4]s %[2]s` -issues.ref_reopened_from = `reopened this issue %[4]s %[2]s` +issues.closed_at = `closed this issue %s` +issues.reopened_at = `reopened this issue %s` +issues.commit_ref_at = `referenced this issue from a commit %s` +issues.ref_issue_from = `referenced this issue %[3]s %[1]s` +issues.ref_pull_from = `referenced this pull request %[3]s %[1]s` +issues.ref_closing_from = `referenced this issue from a pull request %[3]s that will close it, %[1]s` +issues.ref_reopening_from = `referenced this issue from a pull request %[3]s that will reopen it, %[1]s` issues.ref_from = `from %[1]s` issues.author = Author issues.author.tooltip.issue = This user is the author of this issue. @@ -2013,9 +2012,9 @@ pulls.update_branch_success = Branch update was successful pulls.update_not_allowed = You are not allowed to update branch pulls.outdated_with_base_branch = This branch is out-of-date with the base branch pulls.close = Close pull request -pulls.closed_at = `closed this pull request %[2]s` -pulls.reopened_at = `reopened this pull request %[2]s` -pulls.commit_ref_at = `referenced this pull request from a commit %[2]s` +pulls.closed_at = `closed this pull request %s` +pulls.reopened_at = `reopened this pull request %s` +pulls.commit_ref_at = `referenced this pull request from a commit %s` pulls.cmd_instruction_hint = View command line instructions pulls.cmd_instruction_checkout_title = Checkout pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. @@ -2932,8 +2931,8 @@ settings.update_settings = Update settings settings.update_setting_success = Organization settings have been updated. settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name. settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. -settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old name during the cooldown period. -settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old name during the cooldown period. +settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old name during the cooldown period. +settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old name during the cooldown period. settings.update_avatar_success = The organization's avatar has been updated. settings.delete = Delete organization settings.delete_account = Delete this organization diff --git a/options/locale/locale_fil.ini b/options/locale/locale_fil.ini index 1dbfb588f0..7d1405f633 100644 --- a/options/locale/locale_fil.ini +++ b/options/locale/locale_fil.ini @@ -1500,7 +1500,7 @@ issues.content_history.created = ginawa editor.patching = Pina-patch: editor.fail_to_apply_patch = Hindi malapat ang patch na "%s" settings.danger_zone = Mapanganib na lugar -issues.closed_at = `isinara ang isyung %[2]s` +issues.closed_at = `isinara ang isyung ito %[2]s` settings.collaboration.admin = Tagapangasiwa settings.admin_settings = Mga setting ng tagapangasiwa issues.start_tracking_history = `sinimulan ang trabaho %s` @@ -2777,6 +2777,9 @@ settings.event_header_action = Mga event sa run ng aksyon settings.event_action_failure = Pagkabigo settings.event_action_failure_desc = Natapos ang action run bilang pagkabigo. settings.event_action_recover = I-recover +settings.event_action_success = Matagumpay +settings.event_action_success_desc = Matagumpay na natapos ang Action Run. +settings.event_action_recover_desc = Matagumpay na natapos ang Action Run pagkatapos na nabigo ang huling Action Run sa katulad na workflow. [search] commit_kind = Maghanap ng mga commit… diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 9ba91fc3a9..3fcfda18bd 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -2920,6 +2920,7 @@ settings.event_header_action = Événements d'exécution d'action settings.event_action_success_desc = L'exécution de l'action a réussi. settings.event_action_failure_desc = L'exécution de l'action a échoué. settings.event_action_recover_desc = L'exécution de l'action a réussi après l'échec de la dernière exécution de l'action dans le même workflow. +issues.filter_type.all_pull_requests = Toutes les demandes d'ajout [graphs] component_loading = Chargement %s… diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index b53c0faa9b..789871f3c3 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -2920,6 +2920,7 @@ settings.event_action_recover = Atgūt settings.event_action_recover_desc = Darbības izpilde bija sekmīga pēc kļūmes iepriekšējā darbības izpildē tajā pašā darbplūsmā. settings.event_action_success = Sekmīgi settings.event_action_success_desc = Darbības izpilde bija sekmīga. +issues.filter_type.all_pull_requests = Visi izmaiņu pieprasījumi [graphs] component_loading=Ielādē %s… diff --git a/options/locale/locale_nds.ini b/options/locale/locale_nds.ini index 2175e11b02..57985942ed 100644 --- a/options/locale/locale_nds.ini +++ b/options/locale/locale_nds.ini @@ -2621,6 +2621,7 @@ settings.event_action_recover = Verhaalt settings.event_header_action = Aktioons-Loop-Vörfallen settings.event_action_failure_desc = Aktioons-Loop is as fehlslagen ennt. settings.event_action_recover_desc = Aktioons-Loop is daankregen worden, nadeem de leste Aktioons-Loop in de sülven Warkwies fehlslagen is. +issues.filter_type.all_pull_requests = All Haalvörslagen [repo.permissions] code.read = Lesen: De Quelltext vun deesem Repositorium ankieken un klonen. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index bcb62eb23b..9158329978 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1408,7 +1408,7 @@ editor.fail_to_update_file=Не удалось обновить/создать editor.fail_to_update_file_summary=Ошибка: editor.push_rejected_no_message=Изменение отклонено сервером без сообщения. Пожалуйста, проверьте Git-хуки. editor.push_rejected=Изменение отклонено сервером. Пожалуйста, проверьте Git-хуки. -editor.push_rejected_summary=Причина отклонения: +editor.push_rejected_summary=Полная причина отклонения: editor.add_subdir=Добавить каталог… editor.unable_to_upload_files=Не удалось загрузить файлы в «%s» из-за ошибки: %v editor.upload_file_is_locked=Файл «%s» заблокирован %s. @@ -1545,27 +1545,27 @@ issues.add_labels=добавлены метки %s %s issues.remove_label=удалил(а) метку %s %s issues.remove_labels=удалил(а) метки %s %s issues.add_remove_labels=добавлены метки %s и убраны метки %s %s -issues.add_milestone_at=`добавлено в этап %s %s` -issues.add_project_at=`добавлено в проект %s %s` -issues.change_milestone_at=`изменил(а) целевой этап с %s на %s %s` -issues.change_project_at=`изменил(а) проект с %s на %s %s` -issues.remove_milestone_at=`удалил(а) это из этапа %s %s` -issues.remove_project_at=`удалил(а) это из проекта %s %s` +issues.add_milestone_at=`добавление в этап %s %s` +issues.add_project_at=`добавление в проект %s %s` +issues.change_milestone_at=`этап изменён с %s на %s %s` +issues.change_project_at=`проект изменён с %s на %s %s` +issues.remove_milestone_at=`удаление из этапа %s %s` +issues.remove_project_at=`удаление из проекта %s %s` issues.deleted_milestone=`(удалено)` issues.deleted_project=`(удалено)` -issues.self_assign_at=`назначил(а) на себя %s` -issues.add_assignee_at=`был(а) назначен(а) %s %s` -issues.remove_assignee_at=`был снят с назначения %s %s` -issues.remove_self_assignment=`убрал(а) их назначение %s` -issues.change_title_at=`изменил(а) заголовок с %s на %s %s` -issues.change_ref_at=`изменил(а) ссылку с %s на %s %s` -issues.remove_ref_at=`убрал(а) ссылку %s %s` -issues.add_ref_at=`добавлена ссылка %s %s` +issues.self_assign_at=`назначение себя %s` +issues.add_assignee_at=`назначение %s %s` +issues.remove_assignee_at=`снятие с назначения %s %s` +issues.remove_self_assignment=`снято назначение с себя %s` +issues.change_title_at=`заголовок изменён с %s на %s %s` +issues.change_ref_at=`изменена ссылка с %s на %s %s` +issues.remove_ref_at=`убрана ссылка на %s %s` +issues.add_ref_at=`добавлена ссылка на %s %s` issues.delete_branch_at=`удалена ветвь %s %s` -issues.filter_label=Метка -issues.filter_label_exclude=`Используйте alt + click/enter, чтобы исключить метки` -issues.filter_label_no_select=Все метки -issues.filter_label_select_no_label=Нет метки +issues.filter_label=Метки +issues.filter_label_exclude=`Исключайте метки с помощью alt + лкм/enter` +issues.filter_label_no_select=Любые метки +issues.filter_label_select_no_label=Без меток issues.filter_milestone=Этап issues.filter_milestone_all=Все этапы issues.filter_milestone_none=Нет этапов @@ -2923,6 +2923,7 @@ settings.event_action_recover = Восстановлен settings.event_action_recover_desc = После неудачи повторное выполнение рабочего потока было успешно. settings.event_action_success = Успех settings.event_action_success_desc = Выполнение завершилось успешно. +issues.filter_type.all_pull_requests = Все запросы на слияние [graphs] component_loading_failed = Не удалось загрузить %s diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index a4999e9751..541acbf408 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -201,6 +201,9 @@ table_modal.placeholder.content = Innehåll table_modal.label.rows = Rader table_modal.label.columns = Kolumner buttons.switch_to_legacy.tooltip = Använd legacy-redigeraren istället +link_modal.url = Url +link_modal.description = Beskrivning +link_modal.header = Lägg till en länk [filter] string.asc = A - Ö @@ -329,6 +332,7 @@ invalid_app_data_path = Sökvägen för appdata är ogiltig: %v internal_token_failed = Misslyckades att generera intern token: %v password_algorithm = Hashalgoritm för lösenord invalid_password_algorithm = Ogiltig hashalgoritm för lösenord +env_config_keys_prompt = Följande miljövariabler kommer också att tillämpas på din konfigurationsfil: [home] uname_holder=Användarnamn eller e-postadress @@ -462,6 +466,41 @@ reply = eller svara på detta e-postmeddelande direkt hi_user_x = Hej %s, admin.new_user.user_info = Användarinformation admin.new_user.text = Vänligen klicka här för att hantera denna användare från administratörspanelen. +admin.new_user.subject = Ny användare %s har just registrerat sig +totp_disabled.no_2fa = Det finns inga andra 2FA-metoder konfigurerade längre, vilket innebär att det inte längre är nödvändigt att logga in på ditt konto med 2FA. +removed_security_key.text_1 = Säkerhetsnyckeln ”%[1]s” har just tagits bort från ditt konto. +repo.transfer.to_you = dig +repo.transfer.body = För att acceptera eller avvisa det, besök %s eller ignorera det helt enkelt. +removed_security_key.no_2fa = Det finns inga andra 2FA-metoder konfigurerade längre, vilket innebär att det inte längre är nödvändigt att logga in på ditt konto med 2FA. +release.note = Notera: +totp_enrolled.subject = Du har aktiverat TOTP som 2FA-metod +totp_enrolled.text_1.no_webauthn = Du har just aktiverat TOTP för ditt konto. Det innebär att du måste använda TOTP som 2FA-metod vid alla framtida inloggningar på ditt konto. +totp_enrolled.text_1.has_webauthn = Du har just aktiverat TOTP för ditt konto. Det innebär att du vid alla framtida inloggningar på ditt konto kan använda TOTP som 2FA-metod eller någon av dina säkerhetsnycklar. +link_not_working_do_paste = Fungerar inte länken? Prova att kopiera och klistra in den i webbläsarens adressfält. +primary_mail_change.text_1 = Den primära e-postadressen för ditt konto har just ändrats till %[1]s. Det innebär att denna e-postadress inte längre kommer att ta emot e-postmeddelanden för ditt konto. +totp_disabled.subject = TOTP har inaktiverats +totp_disabled.text_1 = Tidsbaserat engångslösenord (TOTP) på ditt konto har just inaktiverats. +account_security_caution.text_2 = Om detta inte var du, har ditt konto blivit kompromitterat. Kontakta administratören för denna webbplats. +account_security_caution.text_1 = Om detta var du, kan du tryggt ignorera detta meddelande. +activate_account.text_2 = Klicka på följande länk för att aktivera ditt konto inom %s: +activate_email.text = Klicka på följande länk för att verifiera din e-postadress inom %s: +register_notify.text_3 = Om någon annan har skapat det här kontot åt dig måste du först ställa in ditt lösenord. +issue.x_mentioned_you = @%s2 nämnde dig: +repo.collaborator.added.subject = %s har lagt till dig som medarbetare i %s +repo.collaborator.added.text = Du har lagts till som medarbetare i förrådet: +team_invite.subject = %[1]s har bjudit in dig att gå med i organisationen %[2]s +register_notify.text_1 = detta är din registreringsbekräftelse via e-post för %s! +release.downloads = Hämtningar: +release.download.zip = Källkod (ZIP) +release.download.targz = Källkod (TAR.GZ) +repo.transfer.subject_to = %s vill överföra förrådet ”%s” till %s +removed_security_key.subject = En säkerhetsnyckel har tagits bort +issue_assigned.pull = @%[1] har tilldelat dig pull-begäran %[2]s i förrådet %[3]s. +issue_assigned.issue = @%[1] har tilldelat dig ärendet %[2] i förrådet %[3]. +register_notify.text_2 = Du kan logga in på ditt konto med ditt användarnamn: %s +reset_password.text = Om detta var du, klicka på följande länk för att återställa ditt konto inom %s: +issue.action.force_push = %[1]s2 gjorde en force-push av %[2]s från %[3]s till %[4]s. +repo.transfer.subject_to_you = %s vill överföra förrådet ”%s” till dig @@ -545,6 +584,12 @@ auth_failed=Autentisering misslyckades: %v target_branch_not_exist=Målgrenen finns inte. +org_still_own_repo = Denna organisation äger fortfarande ett eller flera förråd, ta bort eller överför dem först. +must_use_public_key = Den nyckel du angav är en privat nyckel. Skicka inte upp din privata nyckel någonstans. Använd istället din publika nyckel. +unable_verify_ssh_key = SSH-nyckeln kan inte verifieras. Kontrollera att den är korrekt. +still_own_repo = Ditt konto äger ett eller flera förråd, ta bort eller överför dem först. +still_has_org = Ditt konto är medlem i en eller flera organisationer. Lämna dem först. +still_own_packages = Ditt konto har ett eller flera paket, ta bort dem först. [user] @@ -560,6 +605,13 @@ follow=Följ unfollow=Sluta följa user_bio=Biografi disabled_public_activity=Den här användaren har inaktiverat den publika synligheten av aktiviteten. +code = Kod +watched = Övervakade förråd +unblock = Avblockera +email_visibility.limited = Din e-postadress är synlig för alla autentiserade användare +show_on_map = Visa denna plats på en karta +settings = Användarinställningar +block = Blockera [settings] @@ -760,6 +812,17 @@ email_notifications.submit=Ställ in e-postpreferenser visibility.public=Offentlig visibility.private=Privat change_password = Byt lösenord +user_block_success = Användaren har blockerats. +blocked_since = Blockerad sedan %s +user_unblock_success = Användaren har blivit avblockerad. +visibility.limited = Begränsad +visibility.limited_tooltip = Synlig endast för inloggade användare +visibility.private_tooltip = Synlig endast för medlemmar i organisationer som du har gått med i +select_permissions = Välj behörigheter +permission_no_access = Ingen åtkomst +permission_write = Läs och skriv +user_block_yourself = Du kan inte blockera dig själv. +gpg_token_help = Du kan skapa en signatur med hjälp av: [repo] owner=Ägare @@ -1693,6 +1756,21 @@ topic.manage_topics=Hantera ämnen topic.done=Klar topic.count_prompt=Du kan inte välja fler än 25 ämnen settings.enter_repo_name = Ange ägar- och utvecklingskatalog-namnet exakt som det visas: +release = Utgåva +commitstatus.success = Lyckades +visibility_helper = Gör förrådet privat +download_bundle = Hämta BUNDLE +download_zip = Hämta ZIP +download_tar = Hämta TAR.GZ +repo_desc_helper = Ange kort beskrivning (valfritt) +all_branches = Alla grenar +fork_no_valid_owners = Detta förråd kan inte förgrenas eftersom det inte finns några giltiga ägare. +fork_to_different_account = Förgrena till ett annat konto +size_format = %[1]s: %[2]s, %[3]s: %[4]s +already_forked = Du har redan förgrenat %s +commitstatus.failure = Fel +ext_issues = Externa fel +open_with_editor = Öppna med %s @@ -2255,7 +2333,7 @@ project_kind = Sök projekt... search = Sök… type_tooltip = Söktyp team_kind = Sök lag... -org_kind = Sök organisationer... +org_kind = Sök organisationer… issue_kind = Sök ärenden... regexp_tooltip = Tolka söktermen som ett reguljärt uttryck code_search_unavailable = Kodsökning är för närvarande inte tillgänglig. Vänligen kontakta webbplatsadministratören. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 2f749dfd1e..4f51ddcc7e 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -668,6 +668,7 @@ username_error_no_dots = ` sadece alfanumerik karakterler ("0-9","a-z","A-Z"), t unset_password = Oturum açma kullanıcısı parola belirlemedi. unsupported_login_type = Oturum açma türü hesap silmeyi desteklemiyor. +email_domain_is_not_allowed = Kullanıcı e-posta adresi %s alan adı EMAIL_DOMAIN_ALLOWLIST veya EMAIL_DOMAIN_BLOCKLIST ile çelişiyor. Lütfen işleminizin beklendiğinden emin olun. [user] change_avatar=Profil resmini değiştir… @@ -2163,7 +2164,7 @@ settings.pulls.allow_rebase_update=Değişiklik isteği dalının yeniden yapıl settings.pulls.default_delete_branch_after_merge=Varsayılan olarak birleştirmeden sonra değişiklik isteği dalını sil settings.pulls.default_allow_edits_from_maintainers=Bakımcıların düzenlemelerine izin ver settings.releases_desc=Depo Sürümlerini Etkinleştir -settings.packages_desc=Depo Paket Kütüğünü Etkinleştir +settings.packages_desc=Depo paket kütüğünü etkinleştir settings.projects_desc=Depo Projelerini Etkinleştir settings.actions_desc=Depo İşlemlerini Etkinleştir settings.admin_settings=Yönetici Ayarları @@ -3492,7 +3493,7 @@ error.unit_not_allowed=Bu depo bölümüne erişme izniniz yok. title=Paketler desc=Depo paketlerini yönet. empty=Henüz hiçbir paket yok. -empty.documentation=Paket kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz. +empty.documentation=Paket deposu hakkında daha fazla bilgi için, belgeye bakabilirsiniz. empty.repo=Bir paket yüklediniz ama burada gösterilmiyor mu? Paket ayarlarına gidin ve bu depoya bağlantı verin. registry.documentation=%s kütüğü hakkında daha fazla bilgi için, belgeye bakabilirsiniz. filter.type=Tür @@ -3635,9 +3636,9 @@ owner.settings.cleanuprules.remove.days=Şundan eski sürümleri kaldır owner.settings.cleanuprules.remove.pattern=Eşleşen sürümlari kaldır owner.settings.cleanuprules.success.update=Temizleme kuralı güncellendi. owner.settings.cleanuprules.success.delete=Temizleme kuralı silindi. -owner.settings.chef.title=Chef Kütüğü +owner.settings.chef.title=Chef deposu owner.settings.chef.keypair=Anahtar çifti üret -owner.settings.chef.keypair.description=Chef kütüğünde kimlik doğrulaması için bir anahtar çifti gereklidir. Eğer daha önce bir anahtar çifti ürettiyseniz, yeni bir anahtar çifti üretmek eski anahtar çiftini ıskartaya çıkartacaktır. +owner.settings.chef.keypair.description=Chef kayıt defterine gönderilen istekler kimlik doğrulama yöntemi olarak kriptografik olarak imzalanmalıdır. Bir anahtar çifti oluştururken, yalnızca genel anahtar Forgejo'da saklanır. Özel anahtar size knife ile kullanılmak üzere sağlanır. Yeni bir anahtar çifti oluşturmak öncekini geçersiz kılar. npm.dependencies.bundle = Paketlenmiş Bağımlılıklar rpm.repository.multiple_groups = Bu paket birçok grupta mevcut. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index d2d7c58edc..2e536c3d1a 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -2668,6 +2668,7 @@ settings.event_action_success = Успіх settings.event_action_recover = Відновлено commitstatus.success = Успіх commitstatus.failure = Збій +issues.filter_type.all_pull_requests = Усі запити на злиття [graphs] contributors.what = внески @@ -3231,7 +3232,7 @@ notices.view_detail_header=Переглянути деталі повідомл notices.select_all=Вибрати все notices.deselect_all=Скасувати виділення notices.inverse_selection=Інвертувати виділене -notices.delete_selected=Видалити обране +notices.delete_selected=Видалити вибране notices.delete_all=Видалити всі cповіщення notices.type=Тип notices.type_1=Репозиторій diff --git a/options/locale/locale_vi.ini b/options/locale/locale_vi.ini index 57e592a209..28577bc3f7 100644 --- a/options/locale/locale_vi.ini +++ b/options/locale/locale_vi.ini @@ -9,7 +9,7 @@ sign_up = Đăng ký link_account = Liên kết tài khoản register = Đăng ký version = Phiên bản -powered_by = Sử dụng %s +powered_by = Được cung cấp bởi %s page = Trang template = Mẫu language = Ngôn ngữ @@ -25,7 +25,7 @@ access_token = Mã truy cập captcha = CAPTCHA twofa = Xác thực hai lớp webauthn_insert_key = Cắm khóa bảo mật của bạn vào -copy_hash = Chép chuỗi băm +copy_hash = Sao chép chuỗi băm sign_in_with_provider = Đăng nhập bằng %s webauthn_press_button = Hãy nhấn nút trên khóa bảo mật… webauthn_use_twofa = Dùng mã xác thực hai lớp ở trên điện thoại @@ -36,7 +36,7 @@ webauthn_error_insecure = WebAuthn chỉ hỗ trợ kết nối mã hóa. Nếu webauthn_error_unable_to_process = Máy chủ không thể xử lý yêu cầu của bạn. webauthn_error_empty = Bạn phải đặt tên cho khóa này. webauthn_error_timeout = Hết thời gian đọc khóa mất rồi. Hãy tải lại trang và thử lại. -copy_type_unsupported = Không chép được +copy_type_unsupported = Không thể sao chép loại tệp này repository = Kho mã organization = Tổ chức new_fork = Tạo một nhánh mới @@ -55,17 +55,17 @@ all = Tất cả sources = Nguồn forks = Các phân nhánh activities = Hoạt động -pull_requests = Yêu cầu thêm mã +pull_requests = Yêu cầu kéo mã save = Lưu -issues = +issues =Vấn đề enabled = Bật disabled = Tắt -copy = Chép -copy_generic = Chép vào bộ nhớ tạm -copy_url = Chép URL -copy_content = Chép nội dung -copy_success = Đã chép! -copy_error = Không chép được +copy = Sao chép +copy_generic = Sao chép vào bộ nhớ tạm +copy_url = Sao chép URL +copy_content = Sao chép nội dung +copy_success = Đã sao chép! +copy_error = Sao chép thất bại write = Viết preview = Xem trước error = Lỗi @@ -73,7 +73,7 @@ error413 = Bạn đã dùng hết định mức. go_back = Quay lại invalid_data = Dữ liệu không hợp lệ: %v never = Không bao giờ -unknown = Không biết +unknown = Không xác định unpin = Bỏ ghim pin = Ghim archived = Đã lưu trữ @@ -81,6 +81,60 @@ signed_in_as = Đăng nhập bằng re_type = Xác nhận mật khẩu webauthn_sign_in = Nhấn nút trên khóa bảo mật, nếu không có nút thì bạn hãy rút ra rồi cắm lại. new_org.link = Tạo tổ chức -error404 = Trang bạn đang tìm không tồn tại hoặc bạn không có quyền xem. +error404 = Trang bạn đang tìm không tồn tại, đã bị xoá hoặc bạn không có quyền để xem nó. edit = Chỉnh sửa -filter = Lọc \ No newline at end of file +filter = Bộ lọc +dashboard = Trang quản lý +logo = Logo +toc = Mục lục +user_profile_and_more = Hồ sơ và cài đặt… +passcode = Mã xác thực +webauthn_error_duplicated = Khóa bảo mật không được phép cho yêu cầu này. Vui lòng đảm bảo rằng khóa chưa được đăng ký trước đó. +mirror = Bản sao +new_mirror = Tạo bản sao mới +your_starred = Đã đánh sao +mirrors = Các bản sao +concept_system_global = Chung +concept_user_individual = Cá nhân +show_log_seconds = Hiện giây +show_full_screen = Toàn màn hình +download_logs = Tải xuống nhật ký +confirm_delete_selected = Xác nhận xoá tất cả mục được chọn? +name = Tên +filter.clear = Xoá bộ lọc +filter.not_fork = Không phải phân nhánh +filter.not_archived = Không bị lưu trữ +filter.is_archived = Bị lưu trữ +filter.is_fork = Phân nhánh +filter.is_mirror = Bản sao +filter.is_template = Mẫu +filter.not_template = Không phải mẫu +filter.public = Công khai +filter.private = Riêng tư +twofa_scratch = Mã xác thực 2 lớp dự phòng +collaborative = Cộng tác +milestones = Cột mốc +cancel = Huỷ bỏ +retry = Thử lại +rerun = Chạy lại +rerun_all = Chạy lại tất cả +ok = Đồng ý +add = Thêm +add_all = Thêm tất cả +remove = Xoá +remove_all = Xoá tất cả +remove_label_str = Xoá "%s" +locked = Bị khoá +copy_branch = Sao chép tên nhánh +loading = Đang tải… +rss_feed = Nguồn RSS +confirm_delete_artifact = Bạn có chắc muốn xoá "%s" ? +value = Giá trị +copy_path = Sao chép đường dẫn +filter.not_mirror = Không phải bản sao +show_timestamps = Hiện mốc thời gian +concept_code_repository = Kho mã +concept_user_organization = Tổ chức + +[search] +search = Tìm kiếm… \ No newline at end of file diff --git a/options/locale_next/locale_cs-CZ.json b/options/locale_next/locale_cs-CZ.json index 0e3ea260de..97a8536d4f 100644 --- a/options/locale_next/locale_cs-CZ.json +++ b/options/locale_next/locale_cs-CZ.json @@ -102,5 +102,8 @@ "editor.textarea.tab_hint": "Řádek je již odsazen. Pro opuštění editoru stiskněte znovu Tab nebo Escape.", "editor.textarea.shift_tab_hint": "Na tomto řádku není žádné odsazení. Pro opuštění editoru stiskněte znovu Shift + Tab nebo Escape.", "admin.dashboard.cleanup_offline_runners": "Vymazat offline runnery", - "settings.visibility.description": "Viditelnost profilu ovlivňuje možnost ostatních přistupovat k vašim veřejným repozitářům. Zjistit více" + "settings.visibility.description": "Viditelnost profilu ovlivňuje možnost ostatních přistupovat k vašim veřejným repozitářům. Zjistit více", + "avatar.constraints_hint": "Velikost vlastního avataru nesmí překročit %[1]s nebo být větší než %[2]dx%[3]d pixelů", + "repo.diff.commit.next-short": "Další", + "repo.diff.commit.previous-short": "Předchozí" } diff --git a/options/locale_next/locale_da.json b/options/locale_next/locale_da.json index fe3c25ab2e..8315e06bcc 100644 --- a/options/locale_next/locale_da.json +++ b/options/locale_next/locale_da.json @@ -92,5 +92,10 @@ "followers.outgoing.list.self.none": "Du følger ikke nogen.", "followers.outgoing.list.none": "%s følger ikke nogen.", "editor.textarea.tab_hint": "Linjen er allerede indrykket. Tryk på Tab igen eller Escape for at forlade editoren.", - "editor.textarea.shift_tab_hint": "Ingen indrykning på denne linje. Tryk på Shift + Tab igen eller Escape for at forlade editoren." + "editor.textarea.shift_tab_hint": "Ingen indrykning på denne linje. Tryk på Shift + Tab igen eller Escape for at forlade editoren.", + "admin.dashboard.cleanup_offline_runners": "Ryd op offline runners", + "settings.visibility.description": "Profilsynlighed påvirker andres adgang til dine ikke-private depoter. Læs mere", + "avatar.constraints_hint": "Brugerdefineret avatar må ikke overstige %[1]s i størrelse eller være større end %[2]dx%[3]d pixels", + "repo.diff.commit.next-short": "Næste", + "repo.diff.commit.previous-short": "Forrige" } diff --git a/options/locale_next/locale_de-DE.json b/options/locale_next/locale_de-DE.json index 846e85d094..94ab12f180 100644 --- a/options/locale_next/locale_de-DE.json +++ b/options/locale_next/locale_de-DE.json @@ -93,5 +93,9 @@ "followers.incoming.list.none": "Niemand folgt diesem Benutzer.", "editor.textarea.tab_hint": "Zeile bereits eingerückt. Drücke nochmals Tab oder Escape um den Editor zu verlassen.", "editor.textarea.shift_tab_hint": "Keine Einrückung auf dieser Zeile. Drücke nochmals Shift + Tab oder Escape um den Editor zu verlassen.", - "admin.dashboard.cleanup_offline_runners": "Aufräumen der offline Runner" + "admin.dashboard.cleanup_offline_runners": "Aufräumen der offline Runner", + "settings.visibility.description": "Die Profilsichtbarkeit beeinflusst die Möglichkeit anderer, auf deine nicht-privaten Repositorys zuzugreifen. Erfahre mehr", + "avatar.constraints_hint": "Individuelles Profilbild darf %[1]s in der Größe nicht überschreiten, und nicht größer als %[2]dx%[3]d Pixel sein", + "repo.diff.commit.next-short": "Nächste", + "repo.diff.commit.previous-short": "Vorherige" } diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index 356aaaead0..368152095e 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -89,6 +89,8 @@ "mail.actions.run_info_previous_status": "Previous Run's Status: %[1]s", "mail.actions.run_info_ref": "Branch: %[1]s (%[2]s)", "mail.actions.run_info_trigger": "Triggered because: %[1]s by: %[2]s", + "repo.diff.commit.next-short": "Next", + "repo.diff.commit.previous-short": "Prev", "discussion.locked": "This discussion has been locked. Commenting is limited to contributors.", "editor.textarea.tab_hint": "Line already indented. Press Tab again or Escape to leave the editor.", "editor.textarea.shift_tab_hint": "No indentation on this line. Press Shift + Tab again or Escape to leave the editor.", diff --git a/options/locale_next/locale_fr-FR.json b/options/locale_next/locale_fr-FR.json index 0792804bf2..a9035f0848 100644 --- a/options/locale_next/locale_fr-FR.json +++ b/options/locale_next/locale_fr-FR.json @@ -97,5 +97,11 @@ "repo.issue_indexer.title": "Indexeur de problèmes", "stars.list.none": "Personne n'a mis d'étoiles sur ce dépôt.", "watch.list.none": "Personne ne consulte ce dépôt.", - "repo.form.cannot_create": "Tous les espaces dans lesquels vous pouvez créer des dépôts ont atteint la limite de dépôts." + "repo.form.cannot_create": "Tous les espaces dans lesquels vous pouvez créer des dépôts ont atteint la limite de dépôts.", + "admin.dashboard.cleanup_offline_runners": "Nettoyer les exécuteurs hors ligne", + "mail.actions.run_info_trigger": "Déclenché parce que : %[1]s par : %[2]s", + "settings.visibility.description": "La visibilité du profil affecte la capacité des autres à accéder à vos dépôts non-privés. Voir plus", + "editor.textarea.shift_tab_hint": "Pas d'indentation sur cette ligne. Appuyez sur Maj + Tab une nouvelle fois ou sur Échap pour quitter l'éditeur.", + "avatar.constraints_hint": "L'avatar personnalisé ne doit pas dépasser une taille de %[1]s ou être plus grand que %[2]dx%[3]d pixels", + "editor.textarea.tab_hint": "Ligne déjà indentée. Appuyez sur Tab une nouvelle fois ou sur Échap pour quitter l'éditeur." } diff --git a/options/locale_next/locale_lv-LV.json b/options/locale_next/locale_lv-LV.json index 6386f2163b..f71cfa227d 100644 --- a/options/locale_next/locale_lv-LV.json +++ b/options/locale_next/locale_lv-LV.json @@ -102,5 +102,8 @@ "editor.textarea.tab_hint": "Rinda jau ir ar atkāpi. Spied Tab vēlreiz vai Escape, lai izietu no redaktora!", "editor.textarea.shift_tab_hint": "Šajā rindā nav atkāpes. Spied Shift + Tab vēlreiz vai Escape, lai izietu no redaktora!", "admin.dashboard.cleanup_offline_runners": "Notīrīt bezsaistes izpildītājus", - "settings.visibility.description": "Profila redzamība ietekmē iespēju citiem piekļūt Tavām glabātavām, kas nav privātas. Uzzināt vairāk" + "settings.visibility.description": "Profila redzamība ietekmē iespēju citiem piekļūt Tavām glabātavām, kas nav privātas. Uzzināt vairāk", + "avatar.constraints_hint": "Pielāgots profila attēls nevar pārsniegt %[1]s vai būt lielāks par %[2]dx%[3]d pikseļiem", + "repo.diff.commit.next-short": "Nāk.", + "repo.diff.commit.previous-short": "Iepr." } diff --git a/options/locale_next/locale_nds.json b/options/locale_next/locale_nds.json index ba8d66a2da..9a3884a87f 100644 --- a/options/locale_next/locale_nds.json +++ b/options/locale_next/locale_nds.json @@ -94,5 +94,8 @@ "editor.textarea.tab_hint": "Rieg al inschuven. Drück weer Tab of Esc, um de Bewarker to verlaten.", "editor.textarea.shift_tab_hint": "Keen Inschuuv in deeser Rieg. Drück weer Umschalt+Tab of Esc, um de Bewarker to verlaten.", "admin.dashboard.cleanup_offline_runners": "Nich verbunnen Lopers uprümen", - "settings.visibility.description": "De Profil-Sichtbaarkeid maakt daar wat an, of un wo anner Lüü diene nich-privaaten Repositoriums ankieken könen. Mehr unnerhören" + "settings.visibility.description": "De Profil-Sichtbaarkeid maakt daar wat an, of un wo anner Lüü diene nich-privaaten Repositoriums ankieken könen. Mehr unnerhören", + "avatar.constraints_hint": "Dat eegene Kontobill düür nich groter as %[1]s wesen of groter as %[2]d×%[3]d Billtüttels wesen", + "repo.diff.commit.next-short": "Anner", + "repo.diff.commit.previous-short": "Vörig" } diff --git a/options/locale_next/locale_pt-BR.json b/options/locale_next/locale_pt-BR.json index 909c9339d3..92e140878e 100644 --- a/options/locale_next/locale_pt-BR.json +++ b/options/locale_next/locale_pt-BR.json @@ -100,5 +100,8 @@ "stars.list.none": "Ninguém favoritou este repositório.", "followers.outgoing.list.self.none": "Você não está seguindo ninguém.", "editor.textarea.tab_hint": "Linha já indentada. Pressione Tab novamente ou Esc para sair do editor.", - "editor.textarea.shift_tab_hint": "Sem indentação nesta linha. Pressione Shift + Tab novamente ou Esc para sair do editor." + "editor.textarea.shift_tab_hint": "Sem indentação nesta linha. Pressione Shift + Tab novamente ou Esc para sair do editor.", + "admin.dashboard.cleanup_offline_runners": "Limpar runners desconectados", + "avatar.constraints_hint": "Imagem de perfil personalizada não pode exceder %[1]s em tamanho ou ser maior que %[2]dx%[3]d pixels", + "settings.visibility.description": "A visibilidade do perfil afeta a habilidade de acessarem seus repositórios não-privados. Saiba mais" } diff --git a/options/locale_next/locale_pt-PT.json b/options/locale_next/locale_pt-PT.json index da27faa2ce..78e6dc4493 100644 --- a/options/locale_next/locale_pt-PT.json +++ b/options/locale_next/locale_pt-PT.json @@ -101,5 +101,7 @@ "editor.textarea.tab_hint": "Linha já indentada. Pressione Tab novamente ou Escape para sair do editor.", "editor.textarea.shift_tab_hint": "Sem indentação nesta linha. Pressione Shift + Tab novamente ou Escape para sair do editor.", "stars.list.none": "Ninguém juntou este repositório aos favoritos.", - "admin.dashboard.cleanup_offline_runners": "Limpeza de executores offline" + "admin.dashboard.cleanup_offline_runners": "Limpeza de executores offline", + "settings.visibility.description": "A visibilidade do perfil afecta a capacidade de outros acederem aos seus repositórios não privados. Ler mais", + "avatar.constraints_hint": "O avatar personalizado não pode exceder %[1]s de tamanho ou ser maior do que %[2]dx%[3]d pixéis" } diff --git a/options/locale_next/locale_ru-RU.json b/options/locale_next/locale_ru-RU.json index 6dd2b6fb7e..8992cc6abd 100644 --- a/options/locale_next/locale_ru-RU.json +++ b/options/locale_next/locale_ru-RU.json @@ -101,5 +101,9 @@ "moderation.report_abuse_form.header": "Жалоба администрации", "editor.textarea.tab_hint": "Отступ уже добавлен. Нажмите Tab снова или Escape, чтобы покинуть редактор.", "editor.textarea.shift_tab_hint": "В строке нет отступов. Нажмите Shift + Tab снова или Escape, чтобы покинуть редактор.", - "admin.dashboard.cleanup_offline_runners": "Удалить недоступных исполнителей" + "admin.dashboard.cleanup_offline_runners": "Удалить недоступных исполнителей", + "avatar.constraints_hint": "Изображение профиля не может быть более %[1]s и крупнее %[2]dx%[3]d пикселей", + "settings.visibility.description": "Видимость профиля влияет на доступ других до ваших не частных репозиториев. Подробнее", + "repo.diff.commit.previous-short": "Предыдущий", + "repo.diff.commit.next-short": "Далее" } diff --git a/options/locale_next/locale_sv-SE.json b/options/locale_next/locale_sv-SE.json index 53b25dffcb..9a8762212c 100644 --- a/options/locale_next/locale_sv-SE.json +++ b/options/locale_next/locale_sv-SE.json @@ -56,7 +56,7 @@ "mail.actions.successful_run_after_failure": "Arbetsflöde %[1]s återställdes i förrådet %[2]s", "mail.actions.not_successful_run": "Arbetsflödet %[1]s misslyckades i förrådet %[2]s", "editor.textarea.shift_tab_hint": "Ingen indragning på den här raden. Tryck på Shift + Tab igen eller Escape för att lämna redigeringsläget.", - "meta.last_line": "Tack för att du översatte Forgejo! Daniel Nylander heter jag. https://www.danielnylander.se", + "meta.last_line": "Daniel Nylander heter jag och har översatt Forgejo. Mer information om mig på https://www.danielnylander.se", "editor.textarea.tab_hint": "Raden är redan indragen. Tryck på Tab igen eller Escape för att lämna redigeringsläget.", "home.welcome.no_activity": "Ingen aktivitet", "home.welcome.activity_hint": "Det finns inget i ditt flöde ännu. Dina åtgärder och aktivitet från förråd som du bevakar kommer att visas här.", @@ -93,5 +93,7 @@ "moderation.abuse_category": "Kategori", "moderation.abuse_category.placeholder": "Välj en kategori", "moderation.abuse_category.spam": "Skräppost", - "moderation.abuse_category.malware": "Skadlig kod" + "moderation.abuse_category.malware": "Skadlig kod", + "settings.visibility.description": "Profilens synlighet påverkar andras möjlighet att komma åt dina icke-privata förråd. Läs mer", + "avatar.constraints_hint": "Anpassade avatarer får inte vara större än %[1] eller %[2]dx%[3] bildpunkter" } diff --git a/options/locale_next/locale_uk-UA.json b/options/locale_next/locale_uk-UA.json index a00d368f4a..c3bcf5397c 100644 --- a/options/locale_next/locale_uk-UA.json +++ b/options/locale_next/locale_uk-UA.json @@ -88,10 +88,10 @@ "moderation.report_abuse_form.details": "Використовуйте цю форму, щоб повідомити про користувачів, які створюють спам-профілі, репозиторії, задачі, коментарі або поводяться неналежним чином.", "moderation.abuse_category": "Категорія", "moderation.abuse_category.other_violations": "Інші порушення правил платформи", - "moderation.reporting_failed": "Не вдалося надіслати нове повідомлення про порушення: %v", + "moderation.reporting_failed": "Не вдалося надіслати повідомлення про порушення: %v", "moderation.report_remarks": "Подробиці", "moderation.reported_thank_you": "Дякуємо за ваше повідомлення. Адміністрацію поінформовано про нього.", - "repo.form.cannot_create": "Усі простори, в яких ви можете створювати репозиторії, досягли максимальної кількості репозиторіїв.", + "repo.form.cannot_create": "У всіх просторах, де ви можете створювати репозиторії, досягнуто обмеження кількості репозиторіїв.", "repo.issue_indexer.title": "Індексатор задач", "followers.incoming.list.self.none": "Ніхто не стежить за вашим профілем.", "followers.incoming.list.none": "Ніхто не стежить за цим користувачем.", @@ -102,5 +102,8 @@ "editor.textarea.tab_hint": "У рядку вже є відступ. Натисніть Tab ще раз або Esc, щоб вийти з редактора.", "editor.textarea.shift_tab_hint": "У цьому рядку немає відступів. Натисніть Shift + Tab ще раз або Esc, щоб вийти з редактора.", "admin.dashboard.cleanup_offline_runners": "Очистити неактивні раннери", - "settings.visibility.description": "Видимість профілю впливає на можливість інших користувачів отримати доступ до ваших неприватних репозиторіїв. Дізнатися більше" + "settings.visibility.description": "Видимість профілю впливає на можливість інших користувачів отримати доступ до ваших неприватних репозиторіїв. Дізнатися більше", + "avatar.constraints_hint": "Розмір користувацького аватара не може перевищувати %[1]s або бути більшим за %[2]d×%[3]d пікселів", + "repo.diff.commit.next-short": "Наступний", + "repo.diff.commit.previous-short": "Попередній" } diff --git a/options/locale_next/locale_zh-CN.json b/options/locale_next/locale_zh-CN.json index 721e2ad839..e7c1bad81e 100644 --- a/options/locale_next/locale_zh-CN.json +++ b/options/locale_next/locale_zh-CN.json @@ -69,5 +69,7 @@ "followers.incoming.list.self.none": "没有人关注你的个人资料。", "editor.textarea.tab_hint": "此行已缩进。再次按 Tab 或按 Escape 退出编辑器。", "editor.textarea.shift_tab_hint": "此行无缩进。再次按 Shift + Tab 或按 Escape 退出编辑器。", - "admin.dashboard.cleanup_offline_runners": "清理离线运行器" + "admin.dashboard.cleanup_offline_runners": "清理离线运行器", + "settings.visibility.description": "个人资料可见性设置会影响他人对您的非私有仓库的访问。了解更多", + "avatar.constraints_hint": "自定义头像大小不得超过 %[1]s,或大于 %[2]d×%[3]d 像素" } diff --git a/package-lock.json b/package-lock.json index 4a91eeecc2..9de06a8055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,12 @@ "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", "@github/text-expander-element": "2.8.0", + "@google/model-viewer": "4.1.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "ansi_up": "6.0.5", "asciinema-player": "3.8.2", - "chart.js": "4.4.9", + "chart.js": "4.5.0", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", "clippie": "4.1.7", @@ -62,9 +63,9 @@ "devDependencies": { "@axe-core/playwright": "4.10.2", "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.53.0", + "@playwright/test": "1.52.0", "@stoplight/spectral-cli": "6.15.0", - "@stylistic/eslint-plugin-js": "4.4.1", + "@stylistic/eslint-plugin": "4.4.1", "@stylistic/stylelint-plugin": "3.1.2", "@vitejs/plugin-vue": "5.2.4", "@vitest/coverage-v8": "3.2.3", @@ -84,7 +85,7 @@ "eslint-plugin-vitest-globals": "1.5.0", "eslint-plugin-vue": "10.2.0", "eslint-plugin-vue-scoped-css": "2.10.0", - "eslint-plugin-wc": "2.2.1", + "eslint-plugin-wc": "3.0.1", "globals": "16.1.0", "happy-dom": "18.0.0", "license-checker-rseidelsohn": "4.4.2", @@ -1222,6 +1223,22 @@ "dom-input-range": "^1.2.0" } }, + "node_modules/@google/model-viewer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.1.0.tgz", + "integrity": "sha512-7WB/jS6wfBfRl/tWhsUUvDMKFE1KlKME97coDLlZQfvJD0nCwjhES1lJ+k7wnmf7T3zMvCfn9mIjM/mgZapuig==", + "license": "Apache-2.0", + "dependencies": { + "@monogrid/gainmap-js": "^3.1.0", + "lit": "^3.2.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "three": "^0.172.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2004,6 +2021,21 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", + "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz", + "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, "node_modules/@mcaptcha/core-glue": { "version": "0.1.0-alpha-5", "resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz", @@ -2064,6 +2096,18 @@ "langium": "3.3.1" } }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz", + "integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", @@ -2143,13 +2187,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", - "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.53.0" + "playwright": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -3011,15 +3055,18 @@ "node": "^12.20 || >=14.13" } }, - "node_modules/@stylistic/eslint-plugin-js": { + "node_modules/@stylistic/eslint-plugin": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-4.4.1.tgz", - "integrity": "sha512-eLisyHvx7Sel8vcFZOEwDEBGmYsYM1SqDn81BWgmbqEXfXRf8oe6Rwp+ryM/8odNjlxtaaxp0Ihmt86CnLAxKg==", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", + "integrity": "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ==", "dev": true, "license": "MIT", "dependencies": { + "@typescript-eslint/utils": "^8.32.1", "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0" + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3028,6 +3075,19 @@ "eslint": ">=9.0.0" } }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@stylistic/stylelint-plugin": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.2.tgz", @@ -3477,8 +3537,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@types/unist": { "version": "2.0.11", @@ -5373,9 +5432,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", - "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -7683,14 +7742,14 @@ } }, "node_modules/eslint-plugin-wc": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-2.2.1.tgz", - "integrity": "sha512-KstLqGmyQz088DvFlDYHg0sHih+w2QeulreCi1D1ftr357klO2zqHdG/bbnNMmuQdVFDuNkopNIyNhmG0XCT/g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-3.0.1.tgz", + "integrity": "sha512-0p1wkSlA2Ue3FA4qW+5LZ+15sy0p1nUyVl1eyBMLq4rtN1LtE9IdI49BXNWMz8N8bM/y7Ulx8SWGAni5f8XO5g==", "dev": true, "license": "MIT", "dependencies": { "is-valid-element-name": "^1.0.0", - "js-levenshtein-esm": "^1.2.0" + "js-levenshtein-esm": "^2.0.0" }, "peerDependencies": { "eslint": ">=8.40.0" @@ -8752,6 +8811,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -9291,6 +9356,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-proto-prop": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-proto-prop/-/is-proto-prop-3.0.1.tgz", @@ -9692,9 +9763,9 @@ } }, "node_modules/js-levenshtein-esm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", - "integrity": "sha512-fzreKVq1eD7eGcQr7MtRpQH94f8gIfhdrc7yeih38xh684TNMK9v5aAu2wxfIRMk/GpAJRrzcirMAPIaSDaByQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-2.0.0.tgz", + "integrity": "sha512-1n4LEPOL4wRXY8rOQcuA7Iuaphe5xCMayvufCzlLAi+hRsnBRDbSS6XPuV58CBVJxj5D9ApFLyjQ7KzFToyHBw==", "dev": true, "license": "MIT" }, @@ -10007,6 +10078,15 @@ "npm": ">=8" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10035,6 +10115,37 @@ "uc.micro": "^2.0.0" } }, + "node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz", + "integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz", + "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -11843,13 +11954,13 @@ } }, "node_modules/playwright": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", - "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.53.0" + "playwright-core": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -11862,9 +11973,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", - "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -12347,6 +12458,16 @@ "dev": true, "license": "Unlicense" }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -14409,6 +14530,13 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.172.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz", + "integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==", + "license": "MIT", + "peer": true + }, "node_modules/throttle-debounce": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", diff --git a/package.json b/package.json index f7f04b1c0b..f7df1b3f38 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,12 @@ "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", "@github/text-expander-element": "2.8.0", + "@google/model-viewer": "4.1.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "ansi_up": "6.0.5", "asciinema-player": "3.8.2", - "chart.js": "4.4.9", + "chart.js": "4.5.0", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", "clippie": "4.1.7", @@ -61,9 +62,9 @@ "devDependencies": { "@axe-core/playwright": "4.10.2", "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.53.0", + "@playwright/test": "1.52.0", "@stoplight/spectral-cli": "6.15.0", - "@stylistic/eslint-plugin-js": "4.4.1", + "@stylistic/eslint-plugin": "4.4.1", "@stylistic/stylelint-plugin": "3.1.2", "@vitejs/plugin-vue": "5.2.4", "@vitest/coverage-v8": "3.2.3", @@ -78,12 +79,12 @@ "eslint-plugin-playwright": "2.2.0", "eslint-plugin-regexp": "2.9.0", "eslint-plugin-sonarjs": "3.0.2", - "eslint-plugin-unicorn": "59.0.1", "eslint-plugin-toml": "0.12.0", + "eslint-plugin-unicorn": "59.0.1", "eslint-plugin-vitest-globals": "1.5.0", "eslint-plugin-vue": "10.2.0", "eslint-plugin-vue-scoped-css": "2.10.0", - "eslint-plugin-wc": "2.2.1", + "eslint-plugin-wc": "3.0.1", "globals": "16.1.0", "happy-dom": "18.0.0", "license-checker-rseidelsohn": "4.4.2", diff --git a/release-notes-published/11.0.2.md b/release-notes-published/11.0.2.md new file mode 100644 index 0000000000..a1a8549984 --- /dev/null +++ b/release-notes-published/11.0.2.md @@ -0,0 +1,33 @@ + + + + +## Release notes + +- Features + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7986) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7991)): feat: make Forgejo Actions server logs less noisy +- Bug fixes + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8155) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8167)): fix: do not fail when release or wiki is set in `/repos/migrate` API + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7976) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7985)): fix: ignore expired artifacts for quota calculation + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7979) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7983)): fix: pull request cross references + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7883) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7886)): fix: quote reply in Chromium + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7775) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7779)): fix: make hash pattern more strict +- Included for completeness but not worth a release note + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8112) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8120)): fix: remove download attribute from external assets + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8110): Update bleve to v2.5.2 with changes made in backport of 2.5.0 + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8094) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8095)): fix: show membership of limited orgs + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8059): Update dependency go to v1.24.3 (v11.0/forgejo) + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8057): chore: drop unused `@typescript-eslint/parser` package + - [PR](https://codeberg.org/forgejo/forgejo/pulls/8021) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8022)): chore(cleanup): suppress non actionable XORM warnings + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7987) ([backported](https://codeberg.org/forgejo/forgejo/pulls/8000)): fix: aggregate deleted team as ghost team + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7925) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7937)): fix(ui): center footer links + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7894) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7903)): fix(ui): fix force-push compare line layout + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7884) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7887)): fix: parse `change-id` in the git commit header + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7885): Update module github.com/blevesearch/bleve/v2 to v2.5.1 (v11.0/forgejo) - abandoned + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7746) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7871)): fix(ui): improve force-push compare line layout + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7640) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7869)): fix: Remove "create branch" button on mirrored repos + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7858): Update module github.com/msteinert/pam/v2 to v2.1.0 (v11.0/forgejo) + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7817) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7821)): fix: replace ß with ss in normalizeUserName + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7784) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7786)): fix(api): document `is_system_webhook` field + - [PR](https://codeberg.org/forgejo/forgejo/pulls/7773) ([backported](https://codeberg.org/forgejo/forgejo/pulls/7774)): fix: remove artificial delay for PR update + diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 03089a18d3..dbc4933de6 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -748,7 +748,7 @@ func ListActionRuns(ctx *context.APIContext) { // type: string // responses: // "200": - // "$ref": "#/responses/RepoActionRunList" + // "$ref": "#/responses/ActionRunList" // "400": // "$ref": "#/responses/error" // "403": @@ -779,16 +779,16 @@ func ListActionRuns(ctx *context.APIContext) { return } - res := new(api.ListRepoActionRunResponse) + res := new(api.ListActionRunResponse) res.TotalCount = total - res.Entries = make([]*api.RepoActionRun, len(runs)) + res.Entries = make([]*api.ActionRun, len(runs)) for i, r := range runs { - cr, err := convert.ToRepoActionRun(ctx, r) - if err != nil { - ctx.Error(http.StatusInternalServerError, "ToActionRun", err) + if err := r.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return } + cr := convert.ToActionRun(ctx, r, ctx.Doer) res.Entries[i] = cr } @@ -821,7 +821,7 @@ func GetActionRun(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/responses/RepoActionRun" + // "$ref": "#/responses/ActionRun" // "400": // "$ref": "#/responses/error" // "403": @@ -839,16 +839,17 @@ func GetActionRun(ctx *context.APIContext) { return } + // Action runs lives in its own table, therefore we check that the + // run with the requested ID is owned by the repository if ctx.Repo.Repository.ID != run.RepoID { ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist) return } - res, err := convert.ToRepoActionRun(ctx, run) - if err != nil { - ctx.Error(http.StatusInternalServerError, "ToRepoActionRun", err) + if err := run.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return } - ctx.JSON(http.StatusOK, res) + ctx.JSON(http.StatusOK, convert.ToActionRun(ctx, run, ctx.Doer)) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 75b4870e51..c9dda124de 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -71,7 +71,7 @@ func ListPullRequests(ctx *context.APIContext) { // in: query // description: Type of sort // type: string - // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority] + // enum: [oldest, recentupdate, recentclose, leastupdate, mostcomment, leastcomment, priority] // - name: milestone // in: query // description: ID of the milestone diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index bde0efea4e..cd4832e15f 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -463,16 +463,16 @@ type swaggerSyncForkInfo struct { Body []api.SyncForkInfo `json:"body"` } -// RepoActionRunList -// swagger:response RepoActionRunList -type swaggerRepoActionRunList struct { +// ActionRunList +// swagger:response ActionRunList +type swaggerActionRunList struct { // in:body - Body api.ListRepoActionRunResponse `json:"body"` + Body api.ListActionRunResponse `json:"body"` } -// RepoActionRun -// swagger:response RepoActionRun -type swaggerRepoActionRun struct { +// ActionRun +// swagger:response ActionRun +type swaggerActionRun struct { // in:body - Body api.RepoActionRun `json:"body"` + Body api.ActionRun `json:"body"` } diff --git a/routers/web/org/home.go b/routers/web/org/home.go index a3823565ed..8f14f8899c 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -175,10 +175,12 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor return } - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) + if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to NewTruncatedReader: %v", err) } else { - if profileContent, err := markdown.RenderString(&markup.RenderContext{ + defer rc.Close() + + if profileContent, err := markdown.RenderReader(&markup.RenderContext{ Ctx: ctx, GitRepo: profileGitRepo, Links: markup.Links{ @@ -188,7 +190,7 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, Metas: map[string]string{"mode": "document"}, - }, bytes); err != nil { + }, rc); err != nil { log.Error("failed to RenderString: %v", err) } else { ctx.Data["ProfileReadme"] = profileContent diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d8f3bd8d9f..3f17e30cec 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1308,7 +1308,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use } // Special user that can't have associated contributions and permissions in the repo. - if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() { + if poster.IsSystem() || poster.IsAPServerActor() { return roleDescriptor, nil } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 2db982d3b6..fd18646211 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -10,13 +10,16 @@ import ( "errors" "fmt" "html" + "html/template" "net/http" "net/url" + "path" "strconv" "strings" "forgejo.org/models" activities_model "forgejo.org/models/activities" + asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/db" git_model "forgejo.org/models/git" issues_model "forgejo.org/models/issues" @@ -28,11 +31,13 @@ import ( "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/base" + "forgejo.org/modules/charset" "forgejo.org/modules/emoji" "forgejo.org/modules/git" "forgejo.org/modules/gitrepo" issue_template "forgejo.org/modules/issue/template" "forgejo.org/modules/log" + "forgejo.org/modules/markup" "forgejo.org/modules/optional" "forgejo.org/modules/setting" "forgejo.org/modules/structs" @@ -498,6 +503,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -508,6 +514,12 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) ctx.Data["NumCommits"] = len(compareInfo.Commits) ctx.Data["NumFiles"] = compareInfo.NumFiles + commitIDs := map[string]bool{} + for _, commit := range compareInfo.Commits { + commitIDs[commit.ID.String()] = true + } + ctx.Data["CommitIDs"] = commitIDs + if len(compareInfo.Commits) != 0 { sha := compareInfo.Commits[0].ID.String() commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll) @@ -591,6 +603,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -601,6 +614,13 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["NumCommits"] = len(compareInfo.Commits) ctx.Data["NumFiles"] = compareInfo.NumFiles + + commitIDs := map[string]bool{} + for _, commit := range compareInfo.Commits { + commitIDs[commit.ID.String()] = true + } + ctx.Data["CommitIDs"] = commitIDs + return compareInfo } @@ -659,6 +679,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C } ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -736,6 +757,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 + ctx.Data["CommitIDs"] = map[string]bool{} ctx.Data["NumFiles"] = 0 return nil } @@ -760,6 +782,13 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.Data["NumCommits"] = len(compareInfo.Commits) ctx.Data["NumFiles"] = compareInfo.NumFiles + + commitIDs := map[string]bool{} + for _, commit := range compareInfo.Commits { + commitIDs[commit.ID.String()] = true + } + ctx.Data["CommitIDs"] = commitIDs + return compareInfo } @@ -919,7 +948,78 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit - if willShowSpecifiedCommit || willShowSpecifiedCommitRange { + if willShowSpecifiedCommit { + commitID := specifiedEndCommit + + ctx.Data["CommitID"] = commitID + + var prevCommit, curCommit, nextCommit *git.Commit + + // Iterate in reverse to properly map "previous" and "next" buttons + for i := len(prInfo.Commits) - 1; i >= 0; i-- { + commit := prInfo.Commits[i] + + if curCommit != nil { + nextCommit = commit + break + } + + if commit.ID.String() == commitID { + curCommit = commit + } else { + prevCommit = commit + } + } + + if curCommit == nil { + ctx.ServerError("Repo.GitRepo.viewPullFiles", git.ErrNotExist{ID: commitID}) + return + } + + ctx.Data["Commit"] = curCommit + if prevCommit != nil { + ctx.Data["PrevCommitLink"] = path.Join(ctx.Repo.RepoLink, "pulls", strconv.FormatInt(issue.Index, 10), "commits", prevCommit.ID.String()) + } + if nextCommit != nil { + ctx.Data["NextCommitLink"] = path.Join(ctx.Repo.RepoLink, "pulls", strconv.FormatInt(issue.Index, 10), "commits", nextCommit.ID.String()) + } + + statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) + if err != nil { + log.Error("GetLatestCommitStatus: %v", err) + } + + ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses) + ctx.Data["CommitStatuses"] = statuses + + verification := asymkey_model.ParseCommitWithSignature(ctx, curCommit) + ctx.Data["Verification"] = verification + ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, curCommit) + + note := &git.Note{} + err = git.GetNote(ctx, ctx.Repo.GitRepo, specifiedEndCommit, note) + if err == nil { + ctx.Data["NoteCommit"] = note.Commit + ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) + ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)), + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + if err != nil { + ctx.ServerError("RenderCommitMessage", err) + return + } + } + + endCommitID = commitID + startCommitID = prInfo.MergeBase + ctx.Data["IsShowingAllCommits"] = false + } else if willShowSpecifiedCommitRange { if len(specifiedEndCommit) > 0 { endCommitID = specifiedEndCommit } else { @@ -930,6 +1030,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi } else { startCommitID = prInfo.MergeBase } + ctx.Data["IsShowingAllCommits"] = false } else { endCommitID = headCommitID @@ -937,10 +1038,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi ctx.Data["IsShowingAllCommits"] = true } - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["AfterCommitID"] = endCommitID ctx.Data["BeforeCommitID"] = startCommitID + ctx.Data["Username"] = ctx.Repo.Owner.Name + ctx.Data["Reponame"] = ctx.Repo.Repository.Name fileOnly := ctx.FormBool("file-only") diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 2e9c34e8a7..b9cb86bd08 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -342,6 +342,20 @@ func LFSFileGet(ctx *context.Context) { ctx.Data["IsVideoFile"] = true case st.IsAudio(): ctx.Data["IsAudioFile"] = true + case st.Is3DModel(): + ctx.Data["Is3DModelFile"] = true + switch { + case st.IsGLB(): + ctx.Data["IsGLBFile"] = true + case st.IsSTL(): + ctx.Data["IsSTLFile"] = true + case st.IsGLTF(): + ctx.Data["IsGLTFFile"] = true + case st.IsOBJ(): + ctx.Data["IsOBJFile"] = true + case st.Is3MF(): + ctx.Data["Is3MFFile"] = true + } case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()): ctx.Data["IsImageFile"] = true } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index b65d1fbc92..6f35e19880 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -153,11 +153,9 @@ func UnitsPost(ctx *context.Context) { }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { - var wikiPermissions repo_model.UnitAccessMode + wikiPermissions := repo_model.UnitAccessModeUnset if form.GloballyWriteableWiki { wikiPermissions = repo_model.UnitAccessModeWrite - } else { - wikiPermissions = repo_model.UnitAccessModeRead } units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 2d4acff302..88687c14b5 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -439,8 +439,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { - if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { - _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) + if rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err == nil { + _, warnings := issue_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize) if len(warnings) > 0 { ctx.Data["FileWarning"] = strings.Join(warnings, "\n") } @@ -624,6 +624,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsVideoFile"] = true case fInfo.st.IsAudio(): ctx.Data["IsAudioFile"] = true + case fInfo.st.Is3DModel(): + ctx.Data["Is3DModelFile"] = true + switch { + case fInfo.st.IsGLB(): + ctx.Data["IsGLBFile"] = true + case fInfo.st.IsSTL(): + ctx.Data["IsSTLFile"] = true + case fInfo.st.IsGLTF(): + ctx.Data["IsGLTFFile"] = true + case fInfo.st.IsOBJ(): + ctx.Data["IsOBJFile"] = true + case fInfo.st.Is3MF(): + ctx.Data["Is3MFFile"] = true + } case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): ctx.Data["IsImageFile"] = true ctx.Data["CanCopyContent"] = true diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 8068c1c6bc..78dd6c5e7c 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -70,17 +70,6 @@ func userProfile(ctx *context.Context) { ctx.Data["OpenGraphURL"] = ctx.ContextUser.HTMLURL() ctx.Data["OpenGraphDescription"] = ctx.ContextUser.Description - // prepare heatmap data - if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserHeatmapDataByUser", err) - return - } - ctx.Data["HeatmapData"] = data - ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) - } - profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) defer profileClose() @@ -186,6 +175,17 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.none", ctx.ContextUser.Name) } case "activity": + // prepare heatmap data + if setting.Service.EnableUserHeatmap { + data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserHeatmapDataByUser", err) + return + } + ctx.Data["HeatmapData"] = data + ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) + } + date := ctx.FormString("date") pagingNum = setting.UI.FeedPagingNum items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ @@ -264,10 +264,12 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb total = int(count) case "overview": - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) + if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("failed to NewTruncatedReader: %v", err) } else { - if profileContent, err := markdown.RenderString(&markup.RenderContext{ + defer rc.Close() + + if profileContent, err := markdown.RenderReader(&markup.RenderContext{ Ctx: ctx, GitRepo: profileGitRepo, Links: markup.Links{ @@ -280,7 +282,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }, Metas: map[string]string{"mode": "document"}, - }, bytes); err != nil { + }, rc); err != nil { log.Error("failed to RenderString: %v", err) } else { ctx.Data["ProfileReadme"] = profileContent diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 0ddb1b21f5..400ee71f08 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -66,6 +66,9 @@ func ProfilePost(ctx *context.Context) { ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod ctx.Data["CommonPronouns"] = commonPronouns + ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize + ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth + ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) diff --git a/routers/web/web.go b/routers/web/web.go index 4db663856f..12d6b321e2 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1512,7 +1512,10 @@ func registerRoutes(m *web.Route) { m.Group("/commits", func() { m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits) m.Get("/list", context.RepoRef(), repo.GetPullCommits) - m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) + m.Group("/{sha:[a-f0-9]{4,40}}", func() { + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) + m.Post("/reviews/submit", context.RepoMustNotBeArchived(), web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) + }) }) m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest) m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 9654186fbb..e240c996b5 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -345,6 +345,14 @@ func handleWorkflows( Status: actions_model.StatusWaiting, } + if workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content)); err == nil { + notifications, err := workflow.Notifications() + if err != nil { + log.Error("Notifications: %w", err) + } + run.NotifyEmail = notifications + } + need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer) if err != nil { log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err) diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 3ec0807d5f..cf8b29ead7 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -4,6 +4,7 @@ package actions import ( + "bytes" "context" "errors" "fmt" @@ -18,6 +19,7 @@ import ( webhook_module "forgejo.org/modules/webhook" "github.com/nektos/act/pkg/jobparser" + act_model "github.com/nektos/act/pkg/model" "xorm.io/builder" ) @@ -140,6 +142,16 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) return err } + workflow, err := act_model.ReadWorkflow(bytes.NewReader(cron.Content)) + if err != nil { + return err + } + notifications, err := workflow.Notifications() + if err != nil { + return err + } + run.NotifyEmail = notifications + // Parse the workflow specification from the cron schedule workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars)) if err != nil { diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go new file mode 100644 index 0000000000..7073985252 --- /dev/null +++ b/services/actions/schedule_tasks_test.go @@ -0,0 +1,121 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package actions + +import ( + "testing" + + actions_model "forgejo.org/models/actions" + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + webhook_module "forgejo.org/modules/webhook" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateScheduleTask(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2}) + + assertConstant := func(t *testing.T, cron *actions_model.ActionSchedule, run *actions_model.ActionRun) { + t.Helper() + assert.Equal(t, cron.Title, run.Title) + assert.Equal(t, cron.RepoID, run.RepoID) + assert.Equal(t, cron.OwnerID, run.OwnerID) + assert.Equal(t, cron.WorkflowID, run.WorkflowID) + assert.Equal(t, cron.TriggerUserID, run.TriggerUserID) + assert.Equal(t, cron.Ref, run.Ref) + assert.Equal(t, cron.CommitSHA, run.CommitSHA) + assert.Equal(t, cron.Event, run.Event) + assert.Equal(t, cron.EventPayload, run.EventPayload) + assert.Equal(t, cron.ID, run.ScheduleID) + assert.Equal(t, actions_model.StatusWaiting, run.Status) + } + + assertMutable := func(t *testing.T, expected, run *actions_model.ActionRun) { + t.Helper() + assert.Equal(t, expected.NotifyEmail, run.NotifyEmail) + } + + testCases := []struct { + name string + cron actions_model.ActionSchedule + want []actions_model.ActionRun + }{ + { + name: "simple", + cron: actions_model.ActionSchedule{ + Title: "scheduletitle1", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: "some.yml", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", + Content: []byte( + ` +name: test +on: push +jobs: + job2: + runs-on: ubuntu-latest + steps: + - run: true +`), + }, + want: []actions_model.ActionRun{ + { + Title: "scheduletitle1", + NotifyEmail: false, + }, + }, + }, + { + name: "enable-email-notifications is true", + cron: actions_model.ActionSchedule{ + Title: "scheduletitle2", + RepoID: repo.ID, + OwnerID: repo.OwnerID, + WorkflowID: "some.yml", + TriggerUserID: repo.OwnerID, + Ref: "branch", + CommitSHA: "fakeSHA", + Event: webhook_module.HookEventSchedule, + EventPayload: "fakepayload", + Content: []byte( + ` +name: test +enable-email-notifications: true +on: push +jobs: + job2: + runs-on: ubuntu-latest + steps: + - run: true +`), + }, + want: []actions_model.ActionRun{ + { + Title: "scheduletitle2", + NotifyEmail: true, + }, + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.NoError(t, CreateScheduleTask(t.Context(), &testCase.cron)) + require.Equal(t, len(testCase.want), unittest.GetCount(t, actions_model.ActionRun{RepoID: repo.ID})) + for _, expected := range testCase.want { + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{Title: expected.Title}) + assertConstant(t, &testCase.cron, run) + assertMutable(t, &expected, run) + } + unittest.AssertSuccessfulDelete(t, actions_model.ActionRun{RepoID: repo.ID}) + }) + } +} diff --git a/services/actions/workflows.go b/services/actions/workflows.go index 7ec7c3abed..fbba3fd667 100644 --- a/services/actions/workflows.go +++ b/services/actions/workflows.go @@ -111,6 +111,11 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette return nil, nil, err } + notifications, err := wf.Notifications() + if err != nil { + return nil, nil, err + } + run := &actions_model.ActionRun{ Title: title, RepoID: repo.ID, @@ -125,6 +130,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette EventPayload: string(p), TriggerEvent: string(webhook.HookEventWorkflowDispatch), Status: actions_model.StatusWaiting, + NotifyEmail: notifications, } vars, err := actions_model.GetVariablesOfRun(ctx, run) diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index f183136907..cbfe3bd54e 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -107,6 +107,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { return } if !exists { + log.Trace("GetScheduledMergeByPullID found nothing for PR %d", pullID) return } @@ -204,6 +205,10 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { return } + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { + log.Error("DeleteScheduledAutoMerge[%d]: %v", pr.ID, err) + } + if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil { log.Error("pull_service.Merge: %v", err) // FIXME: if merge failed, we should display some error message to the pull request page. diff --git a/services/convert/action.go b/services/convert/action.go index 5e17172b45..703c1f1261 100644 --- a/services/convert/action.go +++ b/services/convert/action.go @@ -8,22 +8,17 @@ import ( actions_model "forgejo.org/models/actions" access_model "forgejo.org/models/perm/access" + user_model "forgejo.org/models/user" api "forgejo.org/modules/structs" ) // ToActionRun convert actions_model.User to api.ActionRun // the run needs all attributes loaded -func ToActionRun(ctx context.Context, run *actions_model.ActionRun) *api.ActionRun { +func ToActionRun(ctx context.Context, run *actions_model.ActionRun, doer *user_model.User) *api.ActionRun { if run == nil { return nil } - // The doer is the one whose perspective is used to view this ActionRun. - // In the best case we use the user that created the webhook. - // Unfortunately we don't know who that was. - // So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins. - // This is pretty close to perfect. - doer := run.Repo.Owner permissionInRepo, _ := access_model.GetUserRepoPermission(ctx, run.Repo, doer) return &api.ActionRun{ diff --git a/services/convert/convert.go b/services/convert/convert.go index 48da9d7623..2ea24a1b51 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -222,29 +222,6 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action }, nil } -// ToRepoActionRun convert a actions_model.ActionRun to an api.RepoActionRun -func ToRepoActionRun(ctx context.Context, r *actions_model.ActionRun) (*api.RepoActionRun, error) { - if err := r.LoadAttributes(ctx); err != nil { - return nil, err - } - - url := strings.TrimSuffix(setting.AppURL, "/") + r.Link() - actor := ToUser(ctx, r.TriggerUser, nil) - - return &api.RepoActionRun{ - ID: r.ID, - Name: r.Title, - HeadBranch: r.PrettyRef(), - HeadSHA: r.CommitSHA, - RunNumber: r.Index, - Event: r.TriggerEvent, - Status: r.Status.String(), - WorkflowID: r.WorkflowID, - URL: url, - TriggeringActor: actor, - }, nil -} - // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { verif := asymkey_model.ParseCommitWithSignature(ctx, c) diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go index 174c175f86..a3b719d1a7 100644 --- a/services/federation/federation_service.go +++ b/services/federation/federation_service.go @@ -211,6 +211,11 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI return nil, nil, err } + inbox, err := url.ParseRequestURI(person.Inbox.GetLink().String()) + if err != nil { + return nil, nil, err + } + newUser := user.User{ LowerName: strings.ToLower(name), Name: name, @@ -227,6 +232,7 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI federatedUser := user.FederatedUser{ ExternalID: personID.ID, FederationHostID: federationHostID, + InboxPath: inbox.Path, NormalizedOriginalURL: personID.AsURI(), } diff --git a/services/feed/action.go b/services/feed/action.go index a2cd0551a3..7d179bd1c8 100644 --- a/services/feed/action.go +++ b/services/feed/action.go @@ -39,6 +39,24 @@ func NewNotifier() notify_service.Notifier { return &actionNotifier{} } +func notifyAll(ctx context.Context, action *activities_model.Action) error { + _, err := activities_model.NotifyWatchers(ctx, action) + if err != nil { + return err + } + return err + // return federation_service.NotifyActivityPubFollowers(ctx, out) +} + +func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error { + _, err := activities_model.NotifyWatchersActions(ctx, acts) + if err != nil { + return err + } + return nil + // return federation_service.NotifyActivityPubFollowers(ctx, out) +} + func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) { if err := issue.LoadPoster(ctx); err != nil { log.Error("issue.LoadPoster: %v", err) @@ -50,7 +68,7 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue } repo := issue.Repo - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: issue.Poster.ID, ActUser: issue.Poster, OpType: activities_model.ActionCreateIssue, @@ -91,7 +109,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model } // Notify watchers for whatever action comes in, ignore if no action type. - if err := activities_model.NotifyWatchers(ctx, act); err != nil { + if err := notifyAll(ctx, act); err != nil { log.Error("NotifyWatchers: %v", err) } } @@ -127,7 +145,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode } // Notify watchers for whatever action comes in, ignore if no action type. - if err := activities_model.NotifyWatchers(ctx, act); err != nil { + if err := notifyAll(ctx, act); err != nil { log.Error("NotifyWatchers: %v", err) } } @@ -146,7 +164,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: pull.Issue.Poster.ID, ActUser: pull.Issue.Poster, OpType: activities_model.ActionCreatePullRequest, @@ -160,7 +178,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model. } func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionRenameRepo, @@ -174,7 +192,7 @@ func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model. } func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionTransferRepo, @@ -188,7 +206,7 @@ func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_mode } func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -201,7 +219,7 @@ func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_mod } func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionCreateRepo, @@ -266,13 +284,13 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model actions = append(actions, action) } - if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil { + if err := notifyAllActions(ctx, actions); err != nil { log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err) } } func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionMergePullRequest, @@ -286,7 +304,7 @@ func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.Us } func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionAutoMergePullRequest, @@ -304,7 +322,7 @@ func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_ if len(review.OriginalAuthor) > 0 { reviewerName = review.OriginalAuthor } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: activities_model.ActionPullReviewDismissed, @@ -342,7 +360,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use opType = activities_model.ActionDeleteBranch } - if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err = notifyAll(ctx, &activities_model.Action{ ActUserID: pusher.ID, ActUser: pusher, OpType: opType, @@ -362,7 +380,7 @@ func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -381,7 +399,7 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r // has sent same action in `PushCommits`, so skip it. return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: doer.ID, ActUser: doer, OpType: opType, @@ -405,7 +423,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncPush, @@ -420,7 +438,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model } func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncCreate, @@ -434,7 +452,7 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use } func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), OpType: activities_model.ActionMirrorSyncDelete, @@ -452,7 +470,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release log.Error("LoadAttributes: %v", err) return } - if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ + if err := notifyAll(ctx, &activities_model.Action{ ActUserID: rel.PublisherID, ActUser: rel.Publisher, OpType: activities_model.ActionPublishRelease, diff --git a/services/issue/pull.go b/services/issue/pull.go index b0a0c47d88..2eef1fbfa8 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -43,8 +43,6 @@ type ReviewRequestNotifier struct { } func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { - files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} - if pr.IsWorkInProgress(ctx) { return nil, nil } @@ -72,18 +70,17 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, return nil, err } - var data string - for _, file := range files { + var rules []*issues_model.CodeOwnerRule + for _, file := range []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} { if blob, err := commit.GetBlobByPath(file); err == nil { - data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) + rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize) if err == nil { + rules, _ = issues_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize) break } } } - rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data) - // get the mergebase mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) if err != nil { diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go index 7c63603a98..09763e164e 100644 --- a/services/mailer/mail_actions.go +++ b/services/mailer/mail_actions.go @@ -23,19 +23,24 @@ func MailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Statu return nil } - if run.TriggerUser.Email != "" && run.TriggerUser.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { - if err := sendMailActionRun(run.TriggerUser, run, priorStatus, lastRun); err != nil { - return err - } + if !run.NotifyEmail { + return nil } - if run.Repo.Owner.Email != "" && run.Repo.Owner.Email != run.TriggerUser.Email && run.Repo.Owner.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { - if err := sendMailActionRun(run.Repo.Owner, run, priorStatus, lastRun); err != nil { - return err - } + user := run.TriggerUser + // this happens e.g. when this is a scheduled run + if user.IsSystem() { + user = run.Repo.Owner + } + if user.IsSystem() || user.Email == "" { + return nil } - return nil + if user.EmailNotificationsPreference == user_model.EmailNotificationsDisabled { + return nil + } + + return sendMailActionRun(user, run, priorStatus, lastRun) } func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error { diff --git a/services/mailer/mail_actions_now_done_test.go b/services/mailer/mail_actions_now_done_test.go index 0d832f2b36..6a01ea7631 100644 --- a/services/mailer/mail_actions_now_done_test.go +++ b/services/mailer/mail_actions_now_done_test.go @@ -4,42 +4,53 @@ package mailer import ( + "slices" "testing" actions_model "forgejo.org/models/actions" "forgejo.org/models/db" + organization_model "forgejo.org/models/organization" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" "forgejo.org/modules/setting" + "forgejo.org/modules/test" notify_service "forgejo.org/services/notify" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func getActionsNowDoneTestUsers(t *testing.T) []*user_model.User { +func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User { t.Helper() - newTriggerUser := new(user_model.User) - newTriggerUser.Name = "new_trigger_user" - newTriggerUser.Language = "en_US" - newTriggerUser.IsAdmin = false - newTriggerUser.Email = "new_trigger_user@example.com" - newTriggerUser.LastLoginUnix = 1693648327 - newTriggerUser.CreatedUnix = 1693648027 - newTriggerUser.EmailNotificationsPreference = user_model.EmailNotificationsEnabled - require.NoError(t, user_model.CreateUser(db.DefaultContext, newTriggerUser)) + user := new(user_model.User) + user.Name = name + user.Language = "en_US" + user.IsAdmin = false + user.Email = email + user.LastLoginUnix = 1693648327 + user.CreatedUnix = 1693648027 + opts := user_model.CreateUserOverwriteOptions{ + AllowCreateOrganization: optional.Some(true), + EmailNotificationsPreference: ¬ifications, + } + require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts)) + return user +} - newOwner := new(user_model.User) - newOwner.Name = "new_owner" - newOwner.Language = "en_US" - newOwner.IsAdmin = false - newOwner.Email = "new_owner@example.com" - newOwner.LastLoginUnix = 1693648329 - newOwner.CreatedUnix = 1693648029 - newOwner.EmailNotificationsPreference = user_model.EmailNotificationsEnabled - require.NoError(t, user_model.CreateUser(db.DefaultContext, newOwner)) - - return []*user_model.User{newTriggerUser, newOwner} +func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User { + t.Helper() + org := new(organization_model.Organization) + org.Name = name + org.Language = "en_US" + org.IsAdmin = false + // contact email for the organization, for display purposes but otherwise not used as of v12 + org.Email = email + org.LastLoginUnix = 1693648327 + org.CreatedUnix = 1693648027 + org.Email = email + require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner)) + return (*user_model.User)(org) } func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) { @@ -49,98 +60,181 @@ func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) { func TestActionRunNowDoneNotificationMail(t *testing.T) { ctx := t.Context() - users := getActionsNowDoneTestUsers(t) - defer CleanUpUsers(ctx, users) - triggerUser := users[0] - ownerUser := users[1] + defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)() + + actionsUser := user_model.NewActionsUser() + require.NotEmpty(t, actionsUser.Email) repo := repo_model.Repository{ Name: "some repo", Description: "rockets are cool", - Owner: ownerUser, - OwnerID: ownerUser.ID, } // Do some funky stuff with the action run's ids: // The run with the larger ID finished first. // This is odd but something that must work. - run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} - run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} + run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} + run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} + + assignUsers := func(triggerUser, owner *user_model.User) { + for _, run := range []*actions_model.ActionRun{run1, run2} { + run.TriggerUser = triggerUser + run.TriggerUserID = triggerUser.ID + run.NotifyEmail = true + } + repo.Owner = owner + repo.OwnerID = owner.ID + } notify_service.RegisterNotifier(NewNotifier()) + orgOwner := getActionsNowDoneTestUser(t, "org_owner", "org_owner@example.com", "disabled") + defer CleanUpUsers(ctx, []*user_model.User{orgOwner}) + t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) { + user := getActionsNowDoneTestUser(t, "new_user", "new_user@example.com", "enabled") + defer CleanUpUsers(ctx, []*user_model.User{user}) + assignUsers(user, user) defer MockMailSettings(func(msgs ...*Message) { assert.Fail(t, "no mail should be sent") })() notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) }) - t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { - mailSentToOwner := false - mailSentToTriggerUser := false + t.Run("WorkflowEnableEmailNotificationIsFalse", func(t *testing.T) { + user := getActionsNowDoneTestUser(t, "new_user1", "new_user1@example.com", "enabled") + defer CleanUpUsers(ctx, []*user_model.User{user}) + assignUsers(user, user) defer MockMailSettings(func(msgs ...*Message) { - assert.LessOrEqual(t, len(msgs), 2) - for _, msg := range msgs { - switch msg.To { - case triggerUser.EmailTo(): - assert.False(t, mailSentToTriggerUser, "sent mail twice") - mailSentToTriggerUser = true - case ownerUser.EmailTo(): - assert.False(t, mailSentToOwner, "sent mail twice") - mailSentToOwner = true - default: - assert.Fail(t, "sent mail to unknown sender", msg.To) - } - assert.Contains(t, msg.Body, triggerUser.HTMLURL()) - assert.Contains(t, msg.Body, triggerUser.Name) - // what happened - assert.Contains(t, msg.Body, "failed") - // new status of run - assert.Contains(t, msg.Body, "failure") - // prior status of this run - assert.Contains(t, msg.Body, "waiting") - assertTranslatedLocaleMailActionsNowDone(t, msg.Body) - } + assert.Fail(t, "no mail should be sent") })() - notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) - assert.True(t, mailSentToOwner) - assert.True(t, mailSentToTriggerUser) + run2.NotifyEmail = false + notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) }) - t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { - mailSentToOwner := false - mailSentToTriggerUser := false - defer MockMailSettings(func(msgs ...*Message) { - assert.LessOrEqual(t, len(msgs), 2) - for _, msg := range msgs { - switch msg.To { - case triggerUser.EmailTo(): - assert.False(t, mailSentToTriggerUser, "sent mail twice") - mailSentToTriggerUser = true - case ownerUser.EmailTo(): - assert.False(t, mailSentToOwner, "sent mail twice") - mailSentToOwner = true - default: - assert.Fail(t, "sent mail to unknown sender", msg.To) - } - assert.Contains(t, msg.Body, triggerUser.HTMLURL()) - assert.Contains(t, msg.Body, triggerUser.Name) - // what happened - assert.Contains(t, msg.Body, "recovered") - // old status of run - assert.Contains(t, msg.Body, "failure") - // new status of run - assert.Contains(t, msg.Body, "success") - // prior status of this run - assert.Contains(t, msg.Body, "running") - assertTranslatedLocaleMailActionsNowDone(t, msg.Body) - } - })() - assert.NotNil(t, setting.MailService) + for _, testCase := range []struct { + name string + triggerUser *user_model.User + owner *user_model.User + expected string + expectMail bool + }{ + { + // if the action is assigned a trigger user in a repository + // owned by a regular user, the mail is sent to the trigger user + name: "RegularTriggerUser", + triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user0", "new_trigger_user0@example.com", user_model.EmailNotificationsEnabled), + owner: getActionsNowDoneTestUser(t, "new_owner0", "new_owner0@example.com", user_model.EmailNotificationsEnabled), + expected: "trigger", + expectMail: true, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // in a repository owned by a regular user, the mail is sent to + // the user that owns the repository + name: "SystemTriggerUserAndRegularOwner", + triggerUser: actionsUser, + owner: getActionsNowDoneTestUser(t, "new_owner1", "new_owner1@example.com", user_model.EmailNotificationsEnabled), + expected: "owner", + expectMail: true, + }, + { + // if the action is assigned a trigger user with disabled notifications in a repository + // owned by a regular user, no mail is sent + name: "RegularTriggerUserNotificationsDisabled", + triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user2", "new_trigger_user2@example.com", user_model.EmailNotificationsDisabled), + owner: getActionsNowDoneTestUser(t, "new_owner2", "new_owner2@example.com", user_model.EmailNotificationsEnabled), + expectMail: false, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // owned by a regular user with disabled notifications, no mail is sent + name: "SystemTriggerUserAndRegularOwnerNotificationsDisabled", + triggerUser: actionsUser, + owner: getActionsNowDoneTestUser(t, "new_owner3", "new_owner3@example.com", user_model.EmailNotificationsDisabled), + expectMail: false, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // in a repository owned by an organization with an email contact, the mail is sent to + // this email contact + name: "SystemTriggerUserAndOrgOwner", + triggerUser: actionsUser, + owner: getActionsNowDoneTestOrg(t, "new_org1", "new_org_owner0@example.com", orgOwner), + expected: "owner", + expectMail: true, + }, + { + // if the action is assigned to a system user (e.g. ActionsUser) + // in a repository owned by an organization without an email contact, no mail is sent + name: "SystemTriggerUserAndNoMailOrgOwner", + triggerUser: actionsUser, + owner: getActionsNowDoneTestOrg(t, "new_org2", "", orgOwner), + expectMail: false, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + assignUsers(testCase.triggerUser, testCase.owner) + defer CleanUpUsers(ctx, slices.DeleteFunc([]*user_model.User{testCase.triggerUser, testCase.owner}, func(user *user_model.User) bool { + return user.IsSystem() + })) - notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) - assert.True(t, mailSentToOwner) - assert.True(t, mailSentToTriggerUser) - }) + t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { + mailSent := false + defer MockMailSettings(func(msgs ...*Message) { + assert.Len(t, msgs, 1) + msg := msgs[0] + assert.False(t, mailSent, "sent mail twice") + expectedEmail := testCase.triggerUser.Email + if testCase.expected == "owner" { // otherwise "trigger" + expectedEmail = testCase.owner.Email + } + require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender") + mailSent = true + assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL()) + assert.Contains(t, msg.Body, testCase.triggerUser.Name) + // what happened + assert.Contains(t, msg.Body, "failed") + // new status of run + assert.Contains(t, msg.Body, "failure") + // prior status of this run + assert.Contains(t, msg.Body, "waiting") + assertTranslatedLocaleMailActionsNowDone(t, msg.Body) + })() + require.NotNil(t, setting.MailService) + + notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) + assert.Equal(t, testCase.expectMail, mailSent) + }) + + t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { + mailSent := false + defer MockMailSettings(func(msgs ...*Message) { + assert.Len(t, msgs, 1) + msg := msgs[0] + assert.False(t, mailSent, "sent mail twice") + expectedEmail := testCase.triggerUser.Email + if testCase.expected == "owner" { // otherwise "trigger" + expectedEmail = testCase.owner.Email + } + require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender") + mailSent = true + assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL()) + assert.Contains(t, msg.Body, testCase.triggerUser.Name) + // what happened + assert.Contains(t, msg.Body, "recovered") + // old status of run + assert.Contains(t, msg.Body, "failure") + // new status of run + assert.Contains(t, msg.Body, "success") + // prior status of this run + assert.Contains(t, msg.Body, "running") + })() + require.NotNil(t, setting.MailService) + + notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) + assert.Equal(t, testCase.expectMail, mailSent) + }) + }) + } } diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go index 47e5d5d175..5e9cbe3e99 100644 --- a/services/mailer/main_test.go +++ b/services/mailer/main_test.go @@ -8,6 +8,7 @@ import ( "testing" "forgejo.org/models/db" + organization_model "forgejo.org/models/organization" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" @@ -51,6 +52,11 @@ func MockMailSettings(send func(msgs ...*Message)) func() { func CleanUpUsers(ctx context.Context, users []*user_model.User) { for _, u := range users { - db.DeleteByID[user_model.User](ctx, u.ID) + if u.IsOrganization() { + organization_model.DeleteOrganization(ctx, (*organization_model.Organization)(u)) + } else { + db.DeleteByID[user_model.User](ctx, u.ID) + db.DeleteByBean(ctx, &user_model.EmailAddress{UID: u.ID}) + } } } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 55adad9685..7887dacdb1 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -766,7 +766,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model issue := issues_model.Issue{ RepoID: g.repo.ID, Repo: g.repo, - Title: prTitle, + Title: util.TruncateRunes(prTitle, 255), Index: pr.Number, Content: pr.Content, MilestoneID: milestoneID, diff --git a/services/pull/check.go b/services/pull/check.go index afeb7e675e..6002e2ae26 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -28,6 +28,7 @@ import ( "forgejo.org/modules/timeutil" asymkey_service "forgejo.org/services/asymkey" notify_service "forgejo.org/services/notify" + shared_automerge "forgejo.org/services/shared/automerge" ) // prPatchCheckerQueue represents a queue to handle update pull request tests @@ -170,7 +171,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer // checkAndUpdateStatus checks if pull request is possible to leaving checking status, // and set to be either conflict or mergeable. -func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) { +func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) bool { // If status has not been changed to conflict by testPatch then we are mergeable if pr.Status == issues_model.PullRequestStatusChecking { pr.Status = issues_model.PullRequestStatusMergeable @@ -184,12 +185,15 @@ func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) { if has { log.Trace("Not updating status for %-v as it is due to be rechecked", pr) - return + return false } if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil { log.Error("Update[%-v]: %v", pr, err) + return false } + + return true } // getMergeCommit checks if a pull request has been merged @@ -339,15 +343,22 @@ func handler(items ...string) []string { } func testPR(id int64) { - pullWorkingPool.CheckIn(fmt.Sprint(id)) - defer pullWorkingPool.CheckOut(fmt.Sprint(id)) ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id)) defer finished() + if pr, updated := testPRProtected(ctx, id); pr != nil && updated { + shared_automerge.AddToQueueIfMergeable(ctx, pr) + } +} + +func testPRProtected(ctx context.Context, id int64) (*issues_model.PullRequest, bool) { + pullWorkingPool.CheckIn(fmt.Sprint(id)) + defer pullWorkingPool.CheckOut(fmt.Sprint(id)) + pr, err := issues_model.GetPullRequestByID(ctx, id) if err != nil { log.Error("Unable to GetPullRequestByID[%d] for testPR: %v", id, err) - return + return nil, false } log.Trace("Testing %-v", pr) @@ -357,12 +368,12 @@ func testPR(id int64) { if pr.HasMerged { log.Trace("%-v is already merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID) - return + return nil, false } if manuallyMerged(ctx, pr) { log.Trace("%-v is manually merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID) - return + return nil, false } if err := TestPatch(pr); err != nil { @@ -371,9 +382,10 @@ func testPR(id int64) { if err := pr.UpdateCols(ctx, "status"); err != nil { log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err) } - return + return nil, false } - checkAndUpdateStatus(ctx, pr) + + return pr, checkAndUpdateStatus(ctx, pr) } // CheckPRsForBaseBranch check all pulls with baseBrannch diff --git a/services/shared/automerge/automerge.go b/services/shared/automerge/automerge.go index 1dc309f4b3..be7b2f6eb4 100644 --- a/services/shared/automerge/automerge.go +++ b/services/shared/automerge/automerge.go @@ -21,9 +21,9 @@ import ( var PRAutoMergeQueue *queue.WorkerPoolQueue[string] func addToQueue(pr *issues_model.PullRequest, sha string) { - log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) + log.Trace("Adding pullID: %d to the automerge queue with sha %s", pr.ID, sha) if err := PRAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil { - log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err) + log.Error("Error adding pullID: %d to the automerge queue %v", pr.ID, err) } } @@ -43,32 +43,29 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m return nil } -// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) { if pull == nil || pull.HasMerged || !pull.CanAutoMerge() { return } - if err := pull.LoadBaseRepo(ctx); err != nil { - log.Error("LoadBaseRepo: %v", err) - return + commitID := pull.HeadCommitID + if commitID == "" { + commitID = getCommitIDFromRefName(ctx, pull) } - gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) - if err != nil { - log.Error("OpenRepository: %v", err) - return - } - defer gitRepo.Close() - commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) - if err != nil { - log.Error("GetRefCommitID: %v", err) + if commitID == "" { return } addToQueue(pull, commitID) } +var AddToQueueIfMergeable = func(ctx context.Context, pull *issues_model.PullRequest) { + if pull.Status == issues_model.PullRequestStatusMergeable { + StartPRCheckAndAutoMerge(ctx, pull) + } +} + func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { @@ -118,3 +115,24 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model. return pulls, nil } + +func getCommitIDFromRefName(ctx context.Context, pull *issues_model.PullRequest) string { + if err := pull.LoadBaseRepo(ctx); err != nil { + log.Error("LoadBaseRepo: %v", err) + return "" + } + + gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) + if err != nil { + log.Error("OpenRepository: %v", err) + return "" + } + defer gitRepo.Close() + commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) + if err != nil { + log.Error("GetRefCommitID: %v", err) + return "" + } + + return commitID +} diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index b3201e5d10..009efc994f 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -894,9 +894,16 @@ func (m *webhookNotifier) ActionRunNowDone(ctx context.Context, run *actions_mod Owner: run.TriggerUser, } + // The doer is the one whose perspective is used to view this ActionRun. + // In the best case we use the user that created the webhook. + // Unfortunately we don't know who that was. + // So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins. + // This is pretty close to perfect. + doer := run.Repo.Owner + payload := &api.ActionPayload{ - Run: convert.ToActionRun(ctx, run), - LastRun: convert.ToActionRun(ctx, lastRun), + Run: convert.ToActionRun(ctx, run, doer), + LastRun: convert.ToActionRun(ctx, lastRun, doer), PriorStatus: priorStatus.String(), } diff --git a/templates/repo/commit_header.tmpl b/templates/repo/commit_header.tmpl index 8a074e9545..9604daf2b0 100644 --- a/templates/repo/commit_header.tmpl +++ b/templates/repo/commit_header.tmpl @@ -14,10 +14,19 @@ {{end}} {{end}}
-
+

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

- {{if not $.PageIsWiki}} - {{end}} -
- {{end}} + {{end}} +
{{if IsMultilineCommitMessage .Commit.Message}}
{{RenderCommitBody $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}
@@ -185,9 +194,16 @@ {{end}}
{{ctx.Locale.Tr "repo.diff.commit"}} - - {{ShortSha .CommitID}} - + {{if .PageIsPullFiles}} + {{$commitShaLink := (printf "%s/commit/%s" $.RepoLink (PathEscape .CommitID))}} + + {{ShortSha .CommitID}} + + {{else}} + + {{ShortSha .CommitID}} + + {{end}}
diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl index ffa0e530e8..25402ca2f4 100644 --- a/templates/repo/commit_load_branches_and_tags.tmpl +++ b/templates/repo/commit_load_branches_and_tags.tmpl @@ -1,4 +1,4 @@ -{{if not .PageIsWiki}} +{{if not (or .PageIsWiki .PageIsPullFiles)}}
{{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}} diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl index 13d09babe1..ded7a6c5fc 100644 --- a/templates/repo/diff/new_review.tmpl +++ b/templates/repo/diff/new_review.tmpl @@ -1,17 +1,14 @@
-
- {{if $.IsShowingAllCommits}}
@@ -55,5 +52,4 @@
- {{end}}
diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index ae50ac4c46..84ba6e5358 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -127,7 +127,7 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}}