diff --git a/.deadcode-out b/.deadcode-out index 6d2c35e374..45ad117ccd 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -19,7 +19,6 @@ forgejo.org/models/auth forgejo.org/models/db TruncateBeans TruncateBeansCascade - InTransaction DumpTables GetTableNames extendBeansForCascade diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml index 5794558a85..ca259f7e7b 100644 --- a/.forgejo/workflows/build-release.yml +++ b/.forgejo/workflows/build-release.yml @@ -164,7 +164,7 @@ jobs: - name: build container & release if: ${{ secrets.TOKEN != '' }} - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.5.1 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.6.0 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" @@ -183,7 +183,7 @@ jobs: - name: build rootless container if: ${{ secrets.TOKEN != '' }} - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.5.1 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.6.0 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml index 312ccfa9db..4695076581 100644 --- a/.forgejo/workflows/publish-release.yml +++ b/.forgejo/workflows/publish-release.yml @@ -44,7 +44,7 @@ jobs: - uses: https://data.forgejo.org/actions/checkout@v6 - name: copy & sign - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.5.1 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.6.0 with: from-forgejo: ${{ vars.FORGEJO }} to-forgejo: ${{ vars.FORGEJO }} diff --git a/.golangci.yml b/.golangci.yml index 1d0dcb489f..59beb8b5fb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ linters: - govet - importas - ineffassign + - modernize - nakedret - nolintlint - revive @@ -45,6 +46,25 @@ linters: desc: use forgejo.org/modules/git instead, see https://codeberg.org/forgejo/forgejo/pulls/4941 - pkg: gopkg.in/yaml.v3 desc: use go.yaml.in/yaml instead, see https://codeberg.org/forgejo/forgejo/pulls/8956 + migration-isolation: + list-mode: lax + files: + - "**/models/forgejo_migrations/**" + deny: + - pkg: "forgejo.org/models" + desc: > + Migrations must not import application models. Application models will be the most recent schema for + Forgejo, while migrations will be operating against the database schema that existed when they were + authored. + - pkg: "forgejo.org/services" + desc: > + Migrations must not import application services. Application services will reference application + models which will use the most recent schema for Forgejo, while migrations will be operating against the + database schema that existed when they were authored. + allow: + - "forgejo.org/models/db" + - "forgejo.org/models/gitea_migrations/base" + - "forgejo.org/models/gitea_migrations/test" gocritic: disabled-checks: - ifElseChain @@ -333,21 +353,6 @@ linters: - path: services/actions/trust.go linters: - nilnil - - path: services/auth/basic.go - linters: - - nilnil - - path: services/auth/httpsign.go - linters: - - nilnil - - path: services/auth/oauth2.go - linters: - - nilnil - - path: services/auth/reverseproxy.go - linters: - - nilnil - - path: services/auth/session.go - linters: - - nilnil - path: services/contexttest/context_tests.go linters: - nilnil diff --git a/Makefile b/Makefile index 61d5b22162..d1a1e90dec 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ XGO_VERSION := go-1.21.x AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 # renovate: datasource=go -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 # renovate: datasource=go +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest diff --git a/build/lint-locale-usage/handle-tmpl.go b/build/lint-locale-usage/handle-tmpl.go index 8d03291205..7fbda8ffd5 100644 --- a/build/lint-locale-usage/handle-tmpl.go +++ b/build/lint-locale-usage/handle-tmpl.go @@ -60,9 +60,13 @@ func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.N case tmplParser.NodeField: nodeField := nodeCommand.Args[0].(*tmplParser.FieldNode) - if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") { + if len(nodeField.Ident) != 2 || nodeField.Ident[0] != "locale" { return } + resolvedPos := fset.PositionFor(token.Pos(nodeCommand.Pos), false) + if !strings.Contains(resolvedPos.Filename, "templates/mail/") { + handler.OnWarning(fset, token.Pos(nodeCommand.Pos), "encountered unexpected .locale usage") + } funcname = nodeField.Ident[1] case tmplParser.NodeVariable: diff --git a/cmd/cert.go b/cmd/cert.go index baadcbda85..516ac4ce84 100644 --- a/cmd/cert.go +++ b/cmd/cert.go @@ -150,8 +150,8 @@ func runCert(ctx context.Context, c *cli.Command) error { BasicConstraintsValid: true, } - hosts := strings.Split(c.String("host"), ",") - for _, h := range hosts { + hosts := strings.SplitSeq(c.String("host"), ",") + for h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { diff --git a/cmd/dump.go b/cmd/dump.go index b94277e529..25459c7731 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -12,6 +12,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "sync" "time" @@ -83,11 +84,9 @@ func (o outputType) Join() string { } func (o *outputType) Set(value string) error { - for _, enum := range o.Enum { - if enum == value { - o.selected = value - return nil - } + if slices.Contains(o.Enum, value) { + o.selected = value + return nil } return fmt.Errorf("allowed values are %s", o.Join()) @@ -250,8 +249,8 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) } else { for _, suffix := range outputTypeEnum.Enum { - if strings.HasSuffix(fileName, "."+suffix) { - fileName = strings.TrimSuffix(fileName, "."+suffix) + if before, ok := strings.CutSuffix(fileName, "."+suffix); ok { + fileName = before break } } @@ -330,14 +329,12 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { go dumpDatabase(ctx, archiveJobs, &wg, verbose) if len(setting.CustomConf) > 0 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { log.Info("Adding custom configuration file from %s", setting.CustomConf) if err := addFile(archiveJobs, "app.ini", setting.CustomConf, verbose); err != nil { fatal("Failed to include specified app.ini: %v", err) } - }() + }) } if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { @@ -361,15 +358,13 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") { log.Info("Skipping attachment data") } else { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error { return addObject(archiveJobs, object, path.Join("data", "attachments", objPath), verbose) }); err != nil { fatal("Failed to dump attachments: %v", err) } - }() + }) } if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") { @@ -377,15 +372,13 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { } else if !setting.Packages.Enabled { log.Info("Package registry not enabled - skipping") } else { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error { return addObject(archiveJobs, object, path.Join("data", "packages", objPath), verbose) }); err != nil { fatal("Failed to dump packages: %v", err) } - }() + }) } // Doesn't check if LogRootPath exists before processing --skip-log intentionally, @@ -399,13 +392,11 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { log.Error("Failed to check if %s exists: %v", setting.Log.RootPath, err) } if isExist { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := addRecursiveExclude(archiveJobs, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil { fatal("Failed to include log: %v", err) } - }() + }) } } diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index 8e0ef0311f..60fffe1226 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -143,8 +143,8 @@ func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error { opts.PullRequests = true opts.ReleaseAssets = true } else { - units := strings.Split(ctx.String("units"), ",") - for _, unit := range units { + units := strings.SplitSeq(ctx.String("units"), ",") + for unit := range units { switch strings.ToLower(strings.TrimSpace(unit)) { case "": continue diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index da83d4636f..b0f6bd3d44 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -457,7 +457,7 @@ INTERNAL_TOKEN = ;GLOBAL_TWO_FACTOR_REQUIREMENT = none ;; ;; Name of cookie used to store authentication information. -;COOKIE_REMEMBER_NAME = gitea_incredible +;COOKIE_REMEMBER_NAME = persistent ;; ;; Reverse proxy authentication header name of user name, email, and full name ;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER @@ -1895,7 +1895,7 @@ LEVEL = Info ;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_. ;; ;; Session cookie name -;COOKIE_NAME = i_like_gitea +;COOKIE_NAME = session ;; ;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. ;COOKIE_SECURE = diff --git a/docker/rootless/usr/local/bin/gitea b/docker/rootless/usr/local/bin/gitea index 9a9a569b12..d87ec84e86 100644 --- a/docker/rootless/usr/local/bin/gitea +++ b/docker/rootless/usr/local/bin/gitea @@ -9,7 +9,7 @@ # And place the original in /usr/lib/gitea with working files in /data/gitea GITEA="/app/gitea/gitea" WORK_DIR="/var/lib/gitea" -APP_INI="/etc/gitea/app.ini" +APP_INI="/var/lib/gitea/custom/conf/app.ini" APP_INI_SET="" for i in "$@"; do diff --git a/go.mod b/go.mod index b7c217def8..ed789efabb 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module forgejo.org go 1.25.0 -toolchain go1.26.1 +toolchain go1.26.3 require ( code.forgejo.org/f3/gof3/v3 v3.11.15 @@ -11,7 +11,7 @@ require ( code.forgejo.org/forgejo/go-rpmutils v1.0.0 code.forgejo.org/forgejo/levelqueue v1.0.0 code.forgejo.org/forgejo/reply v1.0.2 - code.forgejo.org/forgejo/runner/v12 v12.7.3 + code.forgejo.org/forgejo/runner/v12 v12.8.0 code.forgejo.org/go-chi/binding v1.0.1 code.forgejo.org/go-chi/cache v1.0.1 code.forgejo.org/go-chi/captcha v1.0.2 @@ -67,7 +67,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 github.com/inbucket/html2text v0.9.0 - github.com/jackc/pgx/v5 v5.9.1 + github.com/jackc/pgx/v5 v5.9.2 github.com/jhillyerd/enmime/v2 v2.2.0 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 @@ -75,7 +75,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.11 github.com/markbates/goth v1.82.0 github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.37 + github.com/mattn/go-sqlite3 v1.14.42 github.com/meilisearch/meilisearch-go v0.36.0 github.com/mholt/archives v0.1.5 github.com/microcosm-cc/bluemonday v1.0.27 @@ -102,13 +102,13 @@ require ( gitlab.com/gitlab-org/api/client-go v0.143.2 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.49.0 - golang.org/x/image v0.37.0 - golang.org/x/net v0.52.0 + golang.org/x/crypto v0.50.0 + golang.org/x/image v0.39.0 + golang.org/x/net v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/text v0.35.0 + golang.org/x/sys v0.43.0 + golang.org/x/text v0.36.0 google.golang.org/protobuf v1.36.11 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 @@ -175,7 +175,7 @@ require ( github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect - github.com/go-git/go-git/v5 v5.17.0 // indirect + github.com/go-git/go-git/v5 v5.18.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect @@ -257,9 +257,9 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/mod v0.33.0 // indirect + golang.org/x/mod v0.34.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -274,4 +274,4 @@ replace github.com/gliderlabs/ssh => code.forgejo.org/forgejo/ssh v0.0.0-2024121 replace git.sr.ht/~mariusor/go-xsd-duration => code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078 -replace xorm.io/xorm v1.3.9 => code.forgejo.org/xorm/xorm v1.3.9-forgejo.8 +replace xorm.io/xorm v1.3.9 => code.forgejo.org/xorm/xorm v1.3.9-forgejo.11 diff --git a/go.sum b/go.sum index 33570a84d1..219d66fdb6 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ code.forgejo.org/forgejo/levelqueue v1.0.0 h1:9krYpU6BM+j/1Ntj6m+VCAIu0UNnne1/Uf code.forgejo.org/forgejo/levelqueue v1.0.0/go.mod h1:fmG6zhVuqim2rxSFOoasgXO8V2W/k9U31VVYqLIRLhQ= code.forgejo.org/forgejo/reply v1.0.2 h1:dMhQCHV6/O3L5CLWNTol+dNzDAuyCK88z4J/lCdgFuQ= code.forgejo.org/forgejo/reply v1.0.2/go.mod h1:RyZUfzQLc+fuLIGjTSQWDAJWPiL4WtKXB/FifT5fM7U= -code.forgejo.org/forgejo/runner/v12 v12.7.3 h1:+thSawVfLeAZaWB6sYeUPvLj4lxYjCIDt/ktvkfX5Rs= -code.forgejo.org/forgejo/runner/v12 v12.7.3/go.mod h1:OO+Vy9Dww6WNV7GG/6VUWo/0WwXY+ASGlINmAfEA9Ws= +code.forgejo.org/forgejo/runner/v12 v12.8.0 h1:/MqOseYbsGaQ2qzepaZr3VyuqpESvSP/ZnC2aKfmU3g= +code.forgejo.org/forgejo/runner/v12 v12.8.0/go.mod h1:sgDAYfO4NJI1kUzGuD7klHuoFLQzWmZPw0erg7QlbJU= code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616 h1:kEZL84+02jY9RxXM4zHBWZ3Fml0B09cmP1LGkDsCfIA= code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= code.forgejo.org/go-chi/binding v1.0.1 h1:coKNI+X1NzRN7X85LlrpvBRqk0TXpJ+ja28vusQWEuY= @@ -42,8 +42,8 @@ code.forgejo.org/go-chi/captcha v1.0.2 h1:vyHDPXkpjDv8bLO9NqtWzZayzstD/WpJ5xwEkA code.forgejo.org/go-chi/captcha v1.0.2/go.mod h1:lxiPLcJ76UCZHoH31/Wbum4GUi2NgjfFZLrJkKv1lLE= code.forgejo.org/go-chi/session v1.0.3 h1:ByJ9c/UC0AU57hxiGl53TXh+NdBOBwK/bhZ9jyadEwE= code.forgejo.org/go-chi/session v1.0.3/go.mod h1:xzGtFrV/agCJoZCUhFDlqAr1he6BrAdqlaprKOB1W90= -code.forgejo.org/xorm/xorm v1.3.9-forgejo.8 h1:dsSKm2nus0NhHsqYxeuB3Gldk6TtlusD1CBGV6V1SS0= -code.forgejo.org/xorm/xorm v1.3.9-forgejo.8/go.mod h1:A7sFd3BFmRp20h6drSsCXgQRQdF8Vz8HuCSrzFS3m90= +code.forgejo.org/xorm/xorm v1.3.9-forgejo.11 h1:EXJVQ4feAFMkpFwtHAHuXkK6TLNYqabR7QTzqL8uObY= +code.forgejo.org/xorm/xorm v1.3.9-forgejo.11/go.mod h1:C18NeM/SazG/q4eqpWnoNks7GeCyRUNQIe2onniRViU= code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.superseriousbusiness.org/exif-terminator v0.11.1 h1:qnujLH4/Yk/CFtFMmtjozbdV6Ry5G3Q/E/mLlWm/gQI= @@ -284,8 +284,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= -github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= +github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= @@ -447,8 +447,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= -github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -520,8 +520,8 @@ github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkEN github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= -github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo= +github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/meilisearch/meilisearch-go v0.36.0 h1:N1etykTektXt5KPcSbhBO0d5Xx5NaKj4pJWEM7WA5dI= github.com/meilisearch/meilisearch-go v0.36.0/go.mod h1:HBfHzKMxcSbTOvqdfuRA/yf6Vk9IivcwKocWRuW7W78= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= @@ -724,8 +724,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -734,12 +734,12 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= -golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -761,8 +761,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -793,8 +793,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -851,8 +851,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -862,8 +862,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -877,8 +877,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -912,8 +912,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -995,14 +995,14 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= +modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/models/actions/pre_execution_errors.go b/models/actions/pre_execution_errors.go index 64c9b17727..000d59d71d 100644 --- a/models/actions/pre_execution_errors.go +++ b/models/actions/pre_execution_errors.go @@ -30,6 +30,7 @@ const ( ErrorCodeIncompleteWithMissingOutput ErrorCodeIncompleteWithMissingMatrixDimension ErrorCodeIncompleteWithUnknownCause + ErrorCodeUnknownJobInNeeds ) func TranslatePreExecutionError(lang translation.Locale, run *ActionRun) string { @@ -69,6 +70,8 @@ func TranslatePreExecutionError(lang translation.Locale, run *ActionRun) string return lang.TrString("actions.workflow.incomplete_with_missing_matrix_dimension", run.PreExecutionErrorDetails...) case ErrorCodeIncompleteWithUnknownCause: return lang.TrString("actions.workflow.incomplete_with_unknown_cause", run.PreExecutionErrorDetails...) + case ErrorCodeUnknownJobInNeeds: + return lang.TrString("actions.workflow.unknown_job_in_needs", run.PreExecutionErrorDetails...) } return fmt.Sprintf(" 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Rows(new(repo_model.Repository)) - if err != nil { - return nil, nil, err - } - - for rows.Next() { - var repo repo_model.Repository - err = rows.Scan(&repo) - if err != nil { - rows.Close() - return nil, nil, err - } - - repos[repo.ID] = &repo - } - _ = rows.Close() - - left -= limit - repoIDs = repoIDs[limit:] + repos, err := db.GetByIDs(ctx, "id", repoIDs, &repo_model.Repository{}) + if err != nil { + return nil, nil, err } failed := []int{} @@ -284,34 +259,9 @@ func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { } issueIDs := nl.getPendingIssueIDs() - issues := make(map[int64]*issues_model.Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(issues_model.Issue)) - if err != nil { - return nil, err - } - - for rows.Next() { - var issue issues_model.Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return nil, err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] + issues, err := db.GetByIDs(ctx, "id", issueIDs, &issues_model.Issue{}) + if err != nil { + return nil, err } failures := []int{} @@ -379,34 +329,9 @@ func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { } userIDs := nl.getUserIDs() - users := make(map[int64]*user_model.User, len(userIDs)) - left := len(userIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", userIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return nil, err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return nil, err - } - - users[user.ID] = &user - } - _ = rows.Close() - - left -= limit - userIDs = userIDs[limit:] + users, err := db.GetByIDs(ctx, "id", userIDs, &user_model.User{}) + if err != nil { + return nil, err } failures := []int{} @@ -430,34 +355,9 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { } commentIDs := nl.getPendingCommentIDs() - comments := make(map[int64]*issues_model.Comment, len(commentIDs)) - left := len(commentIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", commentIDs[:limit]). - Rows(new(issues_model.Comment)) - if err != nil { - return nil, err - } - - for rows.Next() { - var comment issues_model.Comment - err = rows.Scan(&comment) - if err != nil { - rows.Close() - return nil, err - } - - comments[comment.ID] = &comment - } - _ = rows.Close() - - left -= limit - commentIDs = commentIDs[limit:] + comments, err := db.GetByIDs(ctx, "id", commentIDs, &issues_model.Comment{}) + if err != nil { + return nil, err } failures := []int{} diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3d15c22e19..0b9522be1b 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -138,10 +138,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository return v[i].Commits > v[j].Commits }) - cnt := count - if cnt > len(v) { - cnt = len(v) - } + cnt := min(count, len(v)) return v[:cnt], nil } diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index d14838cf02..6dc143b122 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -5,6 +5,7 @@ package auth import ( "fmt" + "slices" "strings" "forgejo.org/models/perm" @@ -204,12 +205,7 @@ func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTok // ContainsCategory checks if a list of categories contains a specific category func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool { - for _, c := range categories { - if c == category { - return true - } - } - return false + return slices.Contains(categories, category) } // GetScopeLevelFromAccessMode converts permission access mode to scope level diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index fa68197cf0..9ef89cb624 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -6,6 +6,7 @@ package auth import ( "context" "crypto/sha256" + "crypto/subtle" "encoding/base32" "encoding/base64" "errors" @@ -151,9 +152,9 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1 contains := func(s string) bool { - s = strings.TrimSuffix(strings.ToLower(s), "/") + s = strings.TrimSuffix(util.ToUpperASCII(s), "/") for _, u := range app.RedirectURIs { - if strings.TrimSuffix(strings.ToLower(u), "/") == s { + if strings.TrimSuffix(util.ToUpperASCII(u), "/") == s { return true } } @@ -408,26 +409,41 @@ func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL return redirect, err } -// Invalidate deletes the auth code from the database to invalidate this code +// Invalidate deletes the auth code from the database to invalidate this code. +// It returns an error if the code was already invalidated (i.e., no rows were deleted), +// which prevents authorization code replay attacks. func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code) - return err + affected, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code) + if err != nil { + return err + } + if affected == 0 { + return fmt.Errorf("authorization code already used or does not exist") + } + return nil } -// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation. +// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE +// implementation. If a code challenge was set during authorization, a valid verifier MUST be provided. func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool { + // If no PKCE was used during authorization, no verifier is needed. + if code.CodeChallengeMethod == "" && code.CodeChallenge == "" { + return true + } + // A challenge was set but no verifier provided: reject outright, no comparison or hashing is required. + if verifier == "" { + return false + } switch code.CodeChallengeMethod { case "S256": // base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6 h := sha256.Sum256([]byte(verifier)) hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:]) - return hashedVerifier == code.CodeChallenge + return subtle.ConstantTimeCompare([]byte(hashedVerifier), []byte(code.CodeChallenge)) == 1 case "plain": - return verifier == code.CodeChallenge - case "": - return true + return subtle.ConstantTimeCompare([]byte(verifier), []byte(code.CodeChallenge)) == 1 default: - // unsupported method -> return false + // unsupported or empty method with a non-empty challenge -> reject return false } } @@ -490,22 +506,25 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi } // IncreaseCounter increases the counter and updates the grant +// IncreaseCounter atomically increments the counter only if it still matches +// the value loaded into this grant. Returns an error if the counter was already +// changed by a concurrent request (refresh token replay). func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant)) + affected, err := db.GetEngine(ctx).Where("id = ? AND counter = ?", grant.ID, grant.Counter). + Incr("counter").Update(new(OAuth2Grant)) if err != nil { return err } - updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID) - if err != nil { - return err + if affected == 0 { + return fmt.Errorf("grant counter changed unexpectedly (possible replay)") } - grant.Counter = updatedGrant.Counter + grant.Counter++ return nil } // ScopeContains returns true if the grant scope contains the specified scope func (grant *OAuth2Grant) ScopeContains(scope string) bool { - for _, currentScope := range strings.Split(grant.Scope, " ") { + for currentScope := range strings.SplitSeq(grant.Scope, " ") { if scope == currentScope { return true } diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 9c6836ed0d..aa474393c1 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -75,6 +75,13 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) { assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other")) } +func TestOAuth2Application_ContainsRedirect_Normalization(t *testing.T) { + app := &auth_model.OAuth2Application{RedirectURIs: []string{"https://website.com"}} + assert.True(t, app.ContainsRedirectURI("https://website.com")) + assert.True(t, app.ContainsRedirectURI("https://webSITE.com")) // ascii uppercase I + assert.False(t, app.ContainsRedirectURI("https://websİte.com")) // U+0130 as I, Latin Capital Letter I with Dot Above +} + func TestOAuth2Application_ValidateClientSecret(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) @@ -147,9 +154,18 @@ func TestGetOAuth2GrantByID(t *testing.T) { func TestOAuth2Grant_IncreaseCounter(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1}) + + // First increment succeeds require.NoError(t, grant.IncreaseCounter(db.DefaultContext)) assert.Equal(t, int64(2), grant.Counter) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) + + // Simulate a stale grant (counter still 1): must fail (concurrent replay) + grant.Counter = 1 + require.Error(t, grant.IncreaseCounter(db.DefaultContext), "stale counter must be rejected") + + // Counter in DB should be unchanged + unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) } func TestOAuth2Grant_ScopeContains(t *testing.T) { @@ -232,12 +248,33 @@ func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { } assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger")) - // test no code challenge + // test no PKCE at all (no challenge set, no verifier needed) + code = &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: "", + CodeChallenge: "", + } + assert.True(t, code.ValidateCodeChallenge("")) + + // test PKCE required: challenge was set but verifier is empty + code = &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: "S256", + CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", + } + assert.False(t, code.ValidateCodeChallenge(""), "PKCE required: S256 challenge set but empty verifier must be rejected") + + code = &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: "plain", + CodeChallenge: "test123", + } + assert.False(t, code.ValidateCodeChallenge(""), "PKCE required: plain challenge set but empty verifier must be rejected") + + // test challenge stored but method empty (malformed: should reject) code = &auth_model.OAuth2AuthorizationCode{ CodeChallengeMethod: "", CodeChallenge: "foierjiogerogerg", } - assert.True(t, code.ValidateCodeChallenge("")) + assert.False(t, code.ValidateCodeChallenge(""), "challenge present with empty method must be rejected") + assert.False(t, code.ValidateCodeChallenge("foierjiogerogerg"), "challenge present with empty method must be rejected even with matching verifier") } func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { @@ -262,6 +299,20 @@ func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) { unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) } +func TestOAuth2AuthorizationCode_Invalidate_DoubleUse(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) + + // First invalidation should succeed + require.NoError(t, code.Invalidate(db.DefaultContext)) + unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) + + // Second invalidation of the same code must fail (replay prevention) + err := code.Invalidate(db.DefaultContext) + require.Error(t, err) + assert.Contains(t, err.Error(), "authorization code already used") +} + func TestOAuth2AuthorizationCode_TableName(t *testing.T) { assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName()) } diff --git a/models/db/context.go b/models/db/context.go index 9be158ccca..b51861d896 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -6,6 +6,9 @@ package db import ( "context" "database/sql" + "errors" + "fmt" + "slices" "xorm.io/builder" "xorm.io/xorm" @@ -268,6 +271,91 @@ func GetByID[T any](ctx context.Context, id int64) (object *T, exist bool, err e return &bean, true, nil } +// Retrieves multiple objects with database queries similar to an xorm `.In(idField, idList)`. idField must be a unique +// field on the database table, as a map[id]obj is returned and the usage of a non-unique field would result in objects +// being overwritten in the map. +// +// The length of the IN list is constrained to DefaultMaxInSize for each database query, resulting in multiple database +// queries if the length of the idList exceeds that setting; this constraint prevents exceeding bind parameter +// limitations or query length limitations in the database engine. +func GetByIDs[Bean any, Id comparable](ctx context.Context, idField string, idList []Id, bean *Bean) (map[Id]*Bean, error) { + retval := make(map[Id]*Bean, len(idList)) + if len(idList) == 0 { + return retval, nil + } + + table, err := TableInfo(bean) + if err != nil { + return nil, fmt.Errorf("unable to fetch table info for bean %v: %w", bean, err) + } + + var structFieldName string + for _, c := range table.Columns() { + if c.Name == idField { + structFieldName = c.FieldName + break + } + } + if structFieldName == "" { + return nil, fmt.Errorf("unable to identify struct field for id field %s", idField) + } + + for idChunk := range slices.Chunk(idList, DefaultMaxInSize) { + beans := make([]*Bean, 0, len(idChunk)) + if err := GetEngine(ctx).In(idField, idChunk).Find(&beans); err != nil { + return nil, err + } + for _, bean := range beans { + retval[extractFieldValue(bean, structFieldName).(Id)] = bean + } + } + + return retval, nil +} + +// Retrieves multiple objects with database queries similar to an xorm `.In(field, valueList)`. Similar to GetByIDs, +// except that a map[Id][]*Bean is returned as the field value is not assumed to be a unique value -- if there are +// multiple rows in the table for each value, all of them are returned. +// +// The length of the IN list is constrained to DefaultMaxInSize for each database query, resulting in multiple database +// queries if the length of the idList exceeds that setting; this constraint prevents exceeding bind parameter +// limitations or query length limitations in the database engine. +func GetByFieldIn[Bean any, Id comparable](ctx context.Context, field string, valueList []Id, bean *Bean) (map[Id][]*Bean, error) { + retval := make(map[Id][]*Bean, len(valueList)) + if len(valueList) == 0 { + return retval, nil + } + + table, err := TableInfo(bean) + if err != nil { + return nil, fmt.Errorf("unable to fetch table info for bean %v: %w", bean, err) + } + + var structFieldName string + for _, c := range table.Columns() { + if c.Name == field { + structFieldName = c.FieldName + break + } + } + if structFieldName == "" { + return nil, fmt.Errorf("unable to identify struct field for field %s", field) + } + + for idChunk := range slices.Chunk(valueList, DefaultMaxInSize) { + beans := make([]*Bean, 0, len(idChunk)) + if err := GetEngine(ctx).In(field, idChunk).Find(&beans); err != nil { + return nil, err + } + for _, bean := range beans { + fieldValue := extractFieldValue(bean, structFieldName).(Id) + retval[fieldValue] = append(retval[fieldValue], bean) + } + } + + return retval, nil +} + func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) { if !cond.IsValid() { panic("cond is invalid in db.Exist(ctx, cond). This should not be possible.") @@ -416,3 +504,68 @@ func inTransaction(ctx context.Context) (*xorm.Session, bool) { return nil, false } } + +type RetryConfig struct { + ErrorIs []error + AttemptCount int +} + +var ErrNestedRetryTxFailure = errors.New("(nested)") + +type nestedRetryTxState int + +var nestedRetryTx nestedRetryTxState + +// Execute the given function in a transaction. RetryConfig will retry the function on an error, if it matches the +// ErrorIs parameter, up to the total of AttemptCount number of tries. RetryTx cannot be invoked when already within a +// transaction and will return an error immediately. +// +// ErrNestedRetryTxFailure is an error type that will occur when RetryTx is nested within each other, and indicates that +// an inner RetryTx encountered an error that matched its error list. +func RetryTx(ctx context.Context, config RetryConfig, f func(ctx context.Context) error) error { + matchError := func(err error) bool { + for _, possibleError := range config.ErrorIs { + if errors.Is(err, possibleError) { + return true + } + } + return false + } + + // Accept `ErrNestedRetryTxFailure` as error to retry on, means that a nested + // RetryTx indicated to retry the whole transaction. + config.ErrorIs = append(config.ErrorIs, ErrNestedRetryTxFailure) + + withinRetryTx, present := ctx.Value(nestedRetryTx).(bool) + if present && withinRetryTx { + // If a caller already started `RetryTx`, then we assume we don't have to actually perform retries here -- we + // can attempt the requested function once, and if an error is returned that matches the configured error list, + // we'll return that error + ErrNestedRetryTxFailure wrapping. + err := f(ctx) + if err == nil { + return nil + } else if matchError(err) { + return fmt.Errorf("nested RetryTx; internal Tx failed with error that won't be retried: %w %w", err, ErrNestedRetryTxFailure) + } + return err + } else if InTransaction(ctx) { + return errors.New("unsupported operation: attempted to use RetryTx while already within a transaction") + } else if config.AttemptCount == 0 { + return errors.New("unsupported operation: attempted to use RetryTx with 0 attempts") + } + + innerCtx := context.WithValue(ctx, nestedRetryTx, true) + var lastError error + for range config.AttemptCount { + err := WithTx(innerCtx, f) + if err == nil { + return nil + } else if !matchError(err) { + return err + } + + lastError = err + } + + return fmt.Errorf("retry tx failed after %d attempts; last error: %w", config.AttemptCount, lastError) +} diff --git a/models/db/context_test.go b/models/db/context_test.go index 525ab54d99..59ef1e7754 100644 --- a/models/db/context_test.go +++ b/models/db/context_test.go @@ -220,3 +220,103 @@ func TestAfterTx(t *testing.T) { }) } } + +func TestRetryTx(t *testing.T) { + t.Run("success", func(t *testing.T) { + err := db.RetryTx(t.Context(), db.RetryConfig{AttemptCount: 1}, func(ctx context.Context) error { + assert.True(t, db.InTransaction(ctx)) + return nil + }) + require.NoError(t, err) + }) + + t.Run("fail constantly", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{testError}, + }, func(ctx context.Context) error { + attemptCount++ + return testError + }) + require.ErrorIs(t, err, testError) + require.ErrorContains(t, err, "2 attempts") + assert.Equal(t, 2, attemptCount) + }) + + t.Run("fail w/ non retriable error", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{}, + }, func(ctx context.Context) error { + attemptCount++ + return testError + }) + require.ErrorIs(t, err, testError) + assert.Equal(t, 1, attemptCount) + }) + + t.Run("succeed on retry", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{testError}, + }, func(ctx context.Context) error { + attemptCount++ + if attemptCount == 1 { + return testError + } + return nil + }) + require.NoError(t, err) + assert.Equal(t, 2, attemptCount) + }) + + t.Run("nested", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + }, func(ctx context.Context) error { + attemptCount++ + return db.RetryTx(ctx, db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{testError}, + }, func(ctx context.Context) error { + if attemptCount == 2 { + return nil + } + return testError + }) + }) + + require.NoError(t, err) + assert.Equal(t, 2, attemptCount) + }) + + t.Run("inner RetryTx decides on error", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{}, + }, func(ctx context.Context) error { + attemptCount++ + return db.RetryTx(ctx, db.RetryConfig{ + AttemptCount: 2, + }, func(ctx context.Context) error { + if attemptCount == 2 { + return nil + } + return testError + }) + }) + + require.ErrorIs(t, err, testError) + assert.Equal(t, 1, attemptCount) + }) +} diff --git a/models/db/iterate.go b/models/db/iterate.go index d2315cb12c..c56871d503 100644 --- a/models/db/iterate.go +++ b/models/db/iterate.go @@ -80,7 +80,7 @@ func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx contex func extractFieldValue(bean any, fieldName string) any { v := reflect.ValueOf(bean) - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { v = v.Elem() } field := v.FieldByName(fieldName) diff --git a/models/db/list.go b/models/db/list.go index 057221936c..71e9a0b1d2 100644 --- a/models/db/list.go +++ b/models/db/list.go @@ -14,7 +14,7 @@ import ( const ( // DefaultMaxInSize represents default variables number on IN () in SQL - DefaultMaxInSize = 50 + DefaultMaxInSize = 500 defaultFindSliceSize = 10 ) diff --git a/models/db/name.go b/models/db/name.go index d456f49d9c..efd1c2b5f3 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -6,6 +6,7 @@ package db import ( "fmt" "regexp" + "slices" "strings" "unicode/utf8" @@ -114,10 +115,8 @@ func IsUsableName(names, patterns []string, name string) error { return ErrNameEmpty } - for i := range names { - if name == names[i] { - return ErrNameReserved{name} - } + if slices.Contains(names, name) { + return ErrNameReserved{name} } for _, pat := range patterns { diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index 8cd64177dd..7e7c58cc6c 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -46,10 +46,7 @@ func (f *file) readAt(fileMeta *DbfsMeta, offset int64, p []byte) (n int, err er blobPos := int(offset % f.blockSize) blobOffset := offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needRead := len(p) - if needRead > blobRemaining { - needRead = blobRemaining - } + needRead := min(len(p), blobRemaining) if blobOffset+int64(blobPos)+int64(needRead) > fileMeta.FileSize { needRead = int(fileMeta.FileSize - blobOffset - int64(blobPos)) } @@ -66,14 +63,8 @@ func (f *file) readAt(fileMeta *DbfsMeta, offset int64, p []byte) (n int, err er blobData = nil } - canCopy := len(blobData) - blobPos - if canCopy <= 0 { - canCopy = 0 - } - realRead := needRead - if realRead > canCopy { - realRead = canCopy - } + canCopy := max(len(blobData)-blobPos, 0) + realRead := min(needRead, canCopy) if realRead > 0 { copy(p[:realRead], fileData.BlobData[blobPos:blobPos+realRead]) } @@ -113,10 +104,7 @@ func (f *file) Write(p []byte) (n int, err error) { blobPos := int(f.offset % f.blockSize) blobOffset := f.offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needWrite := len(p) - if needWrite > blobRemaining { - needWrite = blobRemaining - } + needWrite := min(len(p), blobRemaining) buf := make([]byte, f.blockSize) readBytes, err := f.readAt(fileMeta, blobOffset, buf) if err != nil && !errors.Is(err, io.EOF) { diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index f1592d4569..c776218be0 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -81,4 +81,4 @@ act_user_id: 40 repo_id: 60 # public is_private: false - created_unix: 1577404800 # end of heatmap \ No newline at end of file + created_unix: 1577404800 # end of heatmap diff --git a/models/fixtures/public_key.yml b/models/fixtures/public_key.yml index ae620ee2d1..a1c7f4feb6 100644 --- a/models/fixtures/public_key.yml +++ b/models/fixtures/public_key.yml @@ -9,3 +9,36 @@ created_unix: 1559593109 updated_unix: 1565224552 login_source_id: 0 +- + id: 2 + owner_id: 5 + name: user5 + fingerprint: "" + content: "user5" + mode: 2 + type: 3 + created_unix: 1775805112 + updated_unix: 1775805112 + login_source_id: 0 +- + id: 3 + owner_id: 9 + name: user9@localhost + fingerprint: "SHA256:K3RfDvtQ/aYVzh6RfXGFxlffLLTRgksf9UQwTlwSM8M" + content: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDN7KuFUnlztx/UM6PUTyiBAq5SeIqr+qSVFC6JzLQAh user9@localhost" + mode: 2 + type: 1 + created_unix: 1775805112 + updated_unix: 1775805112 + login_source_id: 0 +- + id: 4 + owner_id: 9 + name: user9 + fingerprint: "" + content: "user9" + mode: 2 + type: 3 + created_unix: 1775805112 + updated_unix: 1775805112 + login_source_id: 0 diff --git a/models/forgejo_migrations/v14a_actions-approval-and-trust.go b/models/forgejo_migrations/v14a_actions-approval-and-trust.go index 9f6691c4f0..f8be109dda 100644 --- a/models/forgejo_migrations/v14a_actions-approval-and-trust.go +++ b/models/forgejo_migrations/v14a_actions-approval-and-trust.go @@ -6,7 +6,6 @@ package forgejo_migrations import ( "context" - actions_model "forgejo.org/models/actions" "forgejo.org/models/db" "forgejo.org/modules/log" "forgejo.org/modules/timeutil" @@ -59,6 +58,18 @@ type v14ActionsApprovalAndTrustTrusted struct { } func v14ActionsApprovalAndTrustPopulateTableActionUser(x *xorm.Engine) error { + type ActionUser struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(user, id)"` + RepoID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(repository, id)"` + TrustedWithPullRequests bool + LastAccess timeutil.TimeStamp `xorm:"INDEX"` + } + insertActionUser := func(ctx context.Context, user *ActionUser) error { + user.LastAccess = timeutil.TimeStampNow() + return db.Insert(ctx, user) + } + // // Users approved once were trusted before and are trusted now. // @@ -87,7 +98,7 @@ func v14ActionsApprovalAndTrustPopulateTableActionUser(x *xorm.Engine) error { if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { for _, trusted := range trustedList { log.Debug("v14a_actions-approval-and-trust: repository %d trusts user %d", trusted.RepoID, trusted.UserID) - if err := actions_model.InsertActionUser(ctx, &actions_model.ActionUser{ + if err := insertActionUser(ctx, &ActionUser{ RepoID: trusted.RepoID, UserID: trusted.UserID, TrustedWithPullRequests: true, diff --git a/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go b/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go index c639a0d2e9..8ff1b1c066 100644 --- a/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go +++ b/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go @@ -7,11 +7,8 @@ import ( "testing" "time" - actions_model "forgejo.org/models/actions" "forgejo.org/models/db" migration_tests "forgejo.org/models/gitea_migrations/test" - repo_model "forgejo.org/models/repo" - user_model "forgejo.org/models/user" "forgejo.org/modules/timeutil" webhook_module "forgejo.org/modules/webhook" @@ -20,6 +17,9 @@ import ( ) func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { + type ConcurrencyMode int + type Status int + type ActionUser struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(user, id)"` @@ -32,21 +32,18 @@ func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"index unique(repo_index) index(concurrency)"` - Repo *repo_model.Repository `xorm:"-"` - OwnerID int64 `xorm:"index"` - WorkflowID string `xorm:"index"` // the name of workflow file - Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository - TriggerUserID int64 `xorm:"index"` - TriggerUser *user_model.User `xorm:"-"` + RepoID int64 `xorm:"index unique(repo_index) index(concurrency)"` + OwnerID int64 `xorm:"index"` + WorkflowID string `xorm:"index"` // the name of workflow file + Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository + TriggerUserID int64 `xorm:"index"` ScheduleID int64 Ref string `xorm:"index"` // the commit/tag/… that caused the run - IsRefDeleted bool `xorm:"-"` CommitSHA string Event webhook_module.HookEventType // the webhook event that causes the workflow to run EventPayload string `xorm:"LONGTEXT"` TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow - Status actions_model.Status `xorm:"index"` + Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 Started timeutil.TimeStamp @@ -65,7 +62,7 @@ func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { ApprovedBy int64 `xorm:"index"` ConcurrencyGroup string `xorm:"'concurrency_group' index(concurrency)"` - ConcurrencyType actions_model.ConcurrencyMode + ConcurrencyType ConcurrencyMode PreExecutionError string `xorm:"LONGTEXT"` // used to report errors that blocked execution of a workflow } @@ -83,10 +80,10 @@ func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { require.NoError(t, v14ActionsApprovalAndTrustPopulateTableActionUser(x)) - var users []*actions_model.ActionUser + var users []*ActionUser require.NoError(t, db.GetEngine(t.Context()).Select("`repo_id`, `user_id`").OrderBy("`id`").Find(&users)) // See models/gitea_migrations/fixtures/Test_v14ActionsApprovalAndTrustPopulateTableActionUser/action_run.yml - assert.Equal(t, []*actions_model.ActionUser{ + assert.Equal(t, []*ActionUser{ { UserID: 3, RepoID: 15, diff --git a/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go b/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go index fe0a68489a..a412ceb737 100644 --- a/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go +++ b/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go @@ -10,15 +10,14 @@ package forgejo_migrations import ( "context" + "database/sql" "fmt" "strings" "forgejo.org/models/db" - "forgejo.org/models/forgefed" - user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" "forgejo.org/modules/validation" - user_service "forgejo.org/services/user" "xorm.io/xorm" ) @@ -31,6 +30,42 @@ func init() { } func changeActivityPubUsernameFormat(x *xorm.Engine) error { + 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"` + HostSchema string `xorm:"NOT NULL DEFAULT 'https'"` + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + } + type FederatedUser struct { + ID int64 `xorm:"pk autoincr"` + 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"` + InboxPath string + NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! + } + type User struct { + ID int64 `xorm:"pk autoincr"` + LowerName string `xorm:"UNIQUE NOT NULL"` + Name string `xorm:"UNIQUE NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + deleteFederatedUser := func(ctx context.Context, userID int64) error { + _, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID}) + return err + } + userLogString := func(u *User) string { + if u == nil { + return "" + } + return fmt.Sprintf("", u.ID, u.Name) + } + // Normally, the db.WithTx statement ensures that the database transaction (aka. all changes made // by this migration) will only be committed if the SQL operations inside of the iteration // (db.Iterate) don't return an error. @@ -45,9 +80,9 @@ func changeActivityPubUsernameFormat(x *xorm.Engine) error { // migrations at a later point and has been kept as-is. return db.WithTx(db.DefaultContext, func(ctx context.Context) error { // The transaction is committed only if modifying all federated users is possible. - return db.Iterate(ctx, nil, func(ctx context.Context, federatedUser *user_model.FederatedUser) error { + return db.Iterate(ctx, nil, func(ctx context.Context, federatedUser *FederatedUser) error { // localUser represents the "local" representation of an ActivityPub (federated) user - localUser := &user_model.User{} + localUser := &User{} has, err := db.GetEngine(ctx).ID(federatedUser.UserID).Get(localUser) if err != nil { log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while getting local user (ID: %d), ignoring...: %e", federatedUser.UserID, err) @@ -56,7 +91,7 @@ func changeActivityPubUsernameFormat(x *xorm.Engine) error { if !has { log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: User missing for federated user: %v", federatedUser) - err := user_model.DeleteFederatedUser(ctx, federatedUser.UserID) + err := deleteFederatedUser(ctx, federatedUser.UserID) if err != nil { log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting federated user (%s), ignoring...: %e", federatedUser, err) return nil @@ -68,24 +103,13 @@ func changeActivityPubUsernameFormat(x *xorm.Engine) error { } else { // Copied from models/forgefed/federationhost_repository.go (forgefed.GetFederationHost), // minus some validation code for FederationHost which we do not otherwise manipulate here. - federationHost := new(forgefed.FederationHost) + federationHost := new(FederationHost) has, err := db.GetEngine(ctx).ID(federatedUser.FederationHostID).Get(federationHost) if err != nil { log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while looking up federation host info (for %v), ignoring...: %e", federatedUser, err) return nil } else if !has { - log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Federation host for federated user missing, deleting: %v", federatedUser) - err := user_model.DeleteFederatedUser(ctx, federatedUser.UserID) - if err != nil { - log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting federated user (%v), ignoring...: %e", federatedUser, err) - return nil - } - - err = user_service.DeleteUser(ctx, localUser, true) - if err != nil { - log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting user (%s), ignoring...: %v", localUser.LogString(), err) - } - + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Federation host for federated user %s is missing", federatedUser) return nil } @@ -117,10 +141,10 @@ func changeActivityPubUsernameFormat(x *xorm.Engine) error { // Implicitly assumes that there won't be a lower name unique constraint violation. // Potentially a bit paranoid, but why not? - userThatShouldntExist := &user_model.User{} + userThatShouldntExist := &User{} lowernameTaken, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(newUsername)).Table("user").Get(userThatShouldntExist) if err != nil { - log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred, skipping migration of %s: %e", localUser.LogString(), err) + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred, skipping migration of %s: %e", userLogString(localUser), err) return nil } @@ -128,23 +152,23 @@ func changeActivityPubUsernameFormat(x *xorm.Engine) error { log.Warn( "Migration[v14a_ap-change-fedi-handle-structure]: New username %s for %s already taken by %s, deleting the former...", newUsername, - localUser.LogString(), - userThatShouldntExist.LogString(), + userLogString(localUser), + userLogString(userThatShouldntExist), ) - err := user_model.DeleteFederatedUser(ctx, localUser.ID) + err := deleteFederatedUser(ctx, localUser.ID) if err != nil { - log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting federated user (%s), ignoring...: %e", localUser.LogString(), err) + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting federated user (%s), ignoring...: %e", userLogString(localUser), err) } return nil } // Safe to assume that the following operations should just work now. - log.Info("Migration[v14a_ap-change-fedi-handle-structure]: Updating username of %s to %s", localUser.LogString(), newUsername) - if _, err := db.GetEngine(ctx).ID(localUser.ID).Cols("lower_name", "name").Update(&user_model.User{ + log.Info("Migration[v14a_ap-change-fedi-handle-structure]: Updating username of %s to %s", userLogString(localUser), newUsername) + if _, err := db.GetEngine(ctx).ID(localUser.ID).Cols("lower_name", "name").Update(&User{ LowerName: strings.ToLower(newUsername), Name: newUsername, }); err != nil { - log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred when updating federated user's username (%s), ignoring...: %e", localUser.LogString(), err) + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred when updating federated user's username (%s), ignoring...: %e", userLogString(localUser), err) return nil } } diff --git a/models/forgejo_migrations/v14a_migrate_task_secrets.go b/models/forgejo_migrations/v14a_migrate_task_secrets.go index a177dff92a..3484a024b2 100644 --- a/models/forgejo_migrations/v14a_migrate_task_secrets.go +++ b/models/forgejo_migrations/v14a_migrate_task_secrets.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "fmt" - admin_model "forgejo.org/models/admin" "forgejo.org/models/db" "forgejo.org/modules/json" "forgejo.org/modules/keying" @@ -17,6 +16,7 @@ import ( "forgejo.org/modules/secret" "forgejo.org/modules/setting" "forgejo.org/modules/structs" + "forgejo.org/modules/timeutil" "xorm.io/builder" "xorm.io/xorm" @@ -30,6 +30,19 @@ func init() { } func migrateTaskSecrets(x *xorm.Engine) error { + type Task struct { + ID int64 + DoerID int64 `xorm:"index"` + OwnerID int64 `xorm:"index"` + RepoID int64 `xorm:"index"` + PayloadContent string `xorm:"TEXT"` + Created timeutil.TimeStamp `xorm:"created"` + } + taskUpdateCols := func(ctx context.Context, task *Task, cols ...string) error { + _, err := db.GetEngine(ctx).ID(task.ID).Cols(cols...).Update(task) + return err + } + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { sess := db.GetEngine(ctx) @@ -39,7 +52,7 @@ func migrateTaskSecrets(x *xorm.Engine) error { messages := make([]string, 0, 100) ids := make([]int64, 0, 100) - err := db.Iterate(ctx, builder.Eq{"type": structs.TaskTypeMigrateRepo}, func(ctx context.Context, bean *admin_model.Task) error { + err := db.Iterate(ctx, builder.Eq{"type": structs.TaskTypeMigrateRepo}, func(ctx context.Context, bean *Task) error { var opts migration.MigrateOptions err := json.Unmarshal([]byte(bean.PayloadContent), &opts) if err != nil { @@ -96,7 +109,7 @@ func migrateTaskSecrets(x *xorm.Engine) error { } bean.PayloadContent = string(bs) - return bean.UpdateCols(ctx, "payload_content") + return taskUpdateCols(ctx, bean, "payload_content") }) if err == nil { @@ -106,7 +119,7 @@ func migrateTaskSecrets(x *xorm.Engine) error { log.Error("v14a_migrate_task_secrets: %s", message) } - _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&admin_model.Task{}) + _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&Task{}) } } return err diff --git a/models/forgejo_migrations/v14a_migrate_webhook_authorization.go b/models/forgejo_migrations/v14a_migrate_webhook_authorization.go index 738841eb2b..5921329b3e 100644 --- a/models/forgejo_migrations/v14a_migrate_webhook_authorization.go +++ b/models/forgejo_migrations/v14a_migrate_webhook_authorization.go @@ -8,11 +8,11 @@ import ( "fmt" "forgejo.org/models/db" - webhook_model "forgejo.org/models/webhook" "forgejo.org/modules/keying" "forgejo.org/modules/log" "forgejo.org/modules/secret" "forgejo.org/modules/setting" + "forgejo.org/modules/timeutil" "xorm.io/xorm" "xorm.io/xorm/schemas" @@ -26,6 +26,16 @@ func init() { } func migrateWebhookSecrets(x *xorm.Engine) error { + type Webhook struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook + OwnerID int64 `xorm:"INDEX"` + HeaderAuthorizationEncrypted []byte `xorm:"BLOB"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { sess := db.GetEngine(ctx) @@ -59,7 +69,7 @@ func migrateWebhookSecrets(x *xorm.Engine) error { messages := make([]string, 0, 100) ids := make([]int64, 0, 100) - err := db.Iterate(ctx, nil, func(ctx context.Context, bean *webhook_model.Webhook) error { + err := db.Iterate(ctx, nil, func(ctx context.Context, bean *Webhook) error { if len(bean.HeaderAuthorizationEncrypted) == 0 { return nil } @@ -83,7 +93,7 @@ func migrateWebhookSecrets(x *xorm.Engine) error { log.Error("migration[v14a_migrate_webhook_authorization]: %s", message) } - _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&webhook_model.Webhook{}) + _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&Webhook{}) } } return err diff --git a/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go b/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go index 0b5701c88c..9da06f4baf 100644 --- a/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go +++ b/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go @@ -7,7 +7,6 @@ import ( "testing" migration_tests "forgejo.org/models/gitea_migrations/test" - webhook_model "forgejo.org/models/webhook" "forgejo.org/modules/keying" "forgejo.org/modules/timeutil" webhook_module "forgejo.org/modules/webhook" @@ -17,6 +16,7 @@ import ( ) func Test_MigrateWebhookSecrets(t *testing.T) { + type HookContentType int type Webhook struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"INDEX"` @@ -24,7 +24,7 @@ func Test_MigrateWebhookSecrets(t *testing.T) { IsSystemWebhook bool URL string `xorm:"url TEXT"` HTTPMethod string `xorm:"http_method"` - ContentType webhook_model.HookContentType + ContentType HookContentType Secret string `xorm:"TEXT"` Events string `xorm:"TEXT"` IsActive bool `xorm:"INDEX"` @@ -45,7 +45,7 @@ func Test_MigrateWebhookSecrets(t *testing.T) { IsSystemWebhook bool URL string `xorm:"url TEXT"` HTTPMethod string `xorm:"http_method"` - ContentType webhook_model.HookContentType + ContentType HookContentType Secret string `xorm:"TEXT"` Events string `xorm:"TEXT"` IsActive bool `xorm:"INDEX"` diff --git a/models/forgejo_migrations/v14a_rework-notification.go b/models/forgejo_migrations/v14a_rework-notification.go index 77ae79d86f..04303559e8 100644 --- a/models/forgejo_migrations/v14a_rework-notification.go +++ b/models/forgejo_migrations/v14a_rework-notification.go @@ -4,7 +4,6 @@ package forgejo_migrations import ( - activities_model "forgejo.org/models/activities" "forgejo.org/modules/setting" "xorm.io/xorm" @@ -18,9 +17,10 @@ func init() { } func reworkNotification(x *xorm.Engine) error { + type NotificationStatus uint8 type Notification struct { - UserID int64 `xorm:"NOT NULL INDEX(s)"` - Status activities_model.NotificationStatus `xorm:"SMALLINT NOT NULL INDEX(s)"` + UserID int64 `xorm:"NOT NULL INDEX(s)"` + Status NotificationStatus `xorm:"SMALLINT NOT NULL INDEX(s)"` } if err := dropIndexIfExists(x, "notification", "IDX_notification_user_id"); err != nil { diff --git a/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go b/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go index 3575dad832..9f453e05f3 100644 --- a/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go +++ b/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go @@ -5,10 +5,11 @@ package forgejo_migrations import ( "context" + "fmt" "forgejo.org/models/db" - user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" "xorm.io/builder" "xorm.io/xorm" @@ -22,13 +23,45 @@ func init() { } func setProhibitLoginActivityPubUser(x *xorm.Engine) error { + type UserType int + const ( + UserTypeIndividual UserType = iota // Historic reason to make it starts at 0. + UserTypeOrganization // 1 + UserTypeUserReserved // 2 + UserTypeOrganizationReserved // 3 + UserTypeBot // 4 + UserTypeRemoteUser // 5 + UserTypeActivityPubUser // 6 + ) + type User struct { + ID int64 `xorm:"pk autoincr"` + Name string `xorm:"UNIQUE NOT NULL"` + Passwd string `xorm:"NOT NULL"` + PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'argon2'"` + Type UserType + Salt string `xorm:"VARCHAR(32)"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + ProhibitLogin bool `xorm:"NOT NULL DEFAULT false"` + } + type FederatedUser struct { + UserID int64 `xorm:"NOT NULL INDEX user_id"` + } + + userLogString := func(u *User) string { + if u == nil { + return "" + } + return fmt.Sprintf("", u.ID, u.Name) + } + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { - return db.Iterate(ctx, builder.Eq{"type": 5}, func(ctx context.Context, user *user_model.User) error { - log.Info("Checking if user %s is created from ActivityPub", user.LogString()) + return db.Iterate(ctx, builder.Eq{"type": 5}, func(ctx context.Context, user *User) error { + log.Info("Checking if user %s is created from ActivityPub", userLogString(user)) // Users created from f3 also have the RemoteUser user type. All // FederatedUser should reference exactly one User. - has, err := db.GetEngine(ctx).Table("federated_user").Get(&user_model.FederatedUser{UserID: user.ID}) + has, err := db.GetEngine(ctx).Table("federated_user").Get(&FederatedUser{UserID: user.ID}) if err != nil { return err } @@ -37,9 +70,9 @@ func setProhibitLoginActivityPubUser(x *xorm.Engine) error { return nil } - log.Info("Updating user %s", user.LogString()) - _, err = db.GetEngine(ctx).Table("user").ID(user.ID).Cols("type", "prohibit_login", "passwd", "salt", "passwd_hash_algo").Update(&user_model.User{ - Type: user_model.UserTypeActivityPubUser, + log.Info("Updating user %s", userLogString(user)) + _, err = db.GetEngine(ctx).Table("user").ID(user.ID).Cols("type", "prohibit_login", "passwd", "salt", "passwd_hash_algo").Update(&User{ + Type: UserTypeActivityPubUser, ProhibitLogin: true, Passwd: "", Salt: "", diff --git a/models/forgejo_migrations/v15c_add_mirror_remoteaddressauth.go b/models/forgejo_migrations/v15c_add_mirror_remoteaddressauth.go new file mode 100644 index 0000000000..b2f30235ad --- /dev/null +++ b/models/forgejo_migrations/v15c_add_mirror_remoteaddressauth.go @@ -0,0 +1,30 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "replace remote_address with encrypted_remote_address in table mirror", + Upgrade: addMirrorRemoteAddressAuth, + }) +} + +func addMirrorRemoteAddressAuth(x *xorm.Engine) error { + type Mirror struct { + EncryptedRemoteAddress []byte `xorm:"BLOB NULL"` + } + if _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Mirror)); err != nil { + return err + } + // No data migration is necessary or desired. `remote_address` contains sanitized URLs which don't have + // credentials, so they can't be migrated to `encrypted_remote_address`. Instead, as this data is accessed, + // `DecryptOrRecoverRemoteAddress` will recover the fully credentialed contents of the remote address from the git + // repo's `origin` remote address. + _, err := x.Exec("ALTER TABLE `mirror` DROP COLUMN `remote_address`") + return err +} diff --git a/models/forgejo_migrations/v15c_add_schedule_spec_time_zones.go b/models/forgejo_migrations/v15c_add_schedule_spec_time_zones.go new file mode 100644 index 0000000000..d72d585725 --- /dev/null +++ b/models/forgejo_migrations/v15c_add_schedule_spec_time_zones.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "forgejo.org/modules/optional" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add time zone support to action_schedule_spec", + Upgrade: addActionScheduleSpecTimeZone, + }) +} + +func addActionScheduleSpecTimeZone(x *xorm.Engine) error { + type ActionScheduleSpec struct { + TimeZone optional.Option[string] + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionScheduleSpec)) + if err != nil { + return err + } + + _, err = x.Exec("ALTER TABLE action_schedule DROP COLUMN `specs`") + return err +} diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index c1eb750230..3eaada2fdd 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -213,7 +213,7 @@ func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob { func getFilePatterns(filePatterns string) []glob.Glob { extarr := make([]glob.Glob, 0, 10) - for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") { + for expr := range strings.SplitSeq(strings.ToLower(filePatterns), ";") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { diff --git a/models/gitea_migrations/test/tests.go b/models/gitea_migrations/test/tests.go index fc54b65626..086127a2e8 100644 --- a/models/gitea_migrations/test/tests.go +++ b/models/gitea_migrations/test/tests.go @@ -265,7 +265,7 @@ func deleteDB() error { func removeAllWithRetry(dir string) error { var err error - for i := 0; i < 20; i++ { + for range 20 { err = os.RemoveAll(dir) if err == nil { break diff --git a/models/gitea_migrations/v1_11/v111.go b/models/gitea_migrations/v1_11/v111.go index fcd2ee7be3..59ca416af0 100644 --- a/models/gitea_migrations/v1_11/v111.go +++ b/models/gitea_migrations/v1_11/v111.go @@ -5,6 +5,7 @@ package v1_11 import ( "fmt" + "slices" "xorm.io/xorm" ) @@ -345,10 +346,8 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { } return AccessModeWrite <= perm.UnitsMode[UnitTypeCode], nil } - for _, id := range protectedBranch.ApprovalsWhitelistUserIDs { - if id == reviewer.ID { - return true, nil - } + if slices.Contains(protectedBranch.ApprovalsWhitelistUserIDs, reviewer.ID) { + return true, nil } // isUserInTeams diff --git a/models/gitea_migrations/v1_11/v115.go b/models/gitea_migrations/v1_11/v115.go index 65094df93d..84364e310b 100644 --- a/models/gitea_migrations/v1_11/v115.go +++ b/models/gitea_migrations/v1_11/v115.go @@ -146,7 +146,7 @@ func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) return "", fmt.Errorf("io.ReadAll: %w", err) } - newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data))))) + newAvatar := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", userID, md5.Sum(data)))) if newAvatar == oldAvatar { return newAvatar, nil } diff --git a/models/gitea_migrations/v1_20/v259.go b/models/gitea_migrations/v1_20/v259.go index 9b2b68263e..1ae8b2e30f 100644 --- a/models/gitea_migrations/v1_20/v259.go +++ b/models/gitea_migrations/v1_20/v259.go @@ -329,7 +329,7 @@ func ConvertScopedAccessTokens(x *xorm.Engine) error { for _, token := range tokens { var scopes []string allNewScopesMap := make(map[AccessTokenScope]bool) - for _, oldScope := range strings.Split(token.Scope, ",") { + for oldScope := range strings.SplitSeq(token.Scope, ",") { if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists { for _, newScope := range newScopes { allNewScopesMap[newScope] = true diff --git a/models/issues/comment.go b/models/issues/comment.go index 325fcbe30b..bfad3935fb 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "html/template" + "slices" "strconv" "unicode/utf8" @@ -198,12 +199,7 @@ func (t CommentType) HasMailReplySupport() bool { } func (t CommentType) CountedAsConversation() bool { - for _, ct := range ConversationCountedCommentType() { - if t == ct { - return true - } - } - return false + return slices.Contains(ConversationCountedCommentType(), t) } // ConversationCountedCommentType returns the comment types that are counted as a conversation @@ -619,7 +615,7 @@ func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error { if err != nil { return fmt.Errorf("FindRepoAttachmentsByUUID[uuids=%q,repoID=%d]: %w", uuids, c.Issue.RepoID, err) } - for i := 0; i < len(attachments); i++ { + for i := range attachments { attachments[i].IssueID = c.IssueID attachments[i].CommentID = c.ID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { @@ -667,21 +663,6 @@ func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error { return nil } -// LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer -func (c *Comment) LoadResolveDoer(ctx context.Context) (err error) { - if c.ResolveDoerID == 0 || c.Type != CommentTypeCode { - return nil - } - c.ResolveDoer, err = user_model.GetUserByID(ctx, c.ResolveDoerID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - c.ResolveDoer = user_model.NewGhostUser() - err = nil - } - } - return err -} - // IsResolved check if an code comment is resolved func (c *Comment) IsResolved() bool { return c.ResolveDoerID != 0 && c.Type == CommentTypeCode diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 3c87a1b41a..800d1e830e 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -133,7 +133,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } - n := 0 + readyComments := make(CommentList, 0, len(comments)) for _, comment := range comments { if re, ok := reviews[comment.ReviewID]; ok && re != nil { // If the review is pending only the author can see the comments (except if the review is set) @@ -143,17 +143,18 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } comment.Review = re } - comments[n] = comment - n++ + readyComments = append(readyComments, comment) + } - if err := comment.LoadResolveDoer(ctx); err != nil { - return nil, err - } + if err := readyComments.LoadResolveDoers(ctx); err != nil { + return nil, err + } - if err := comment.LoadReactions(ctx, issue.Repo); err != nil { - return nil, err - } + if err := readyComments.LoadReactions(ctx, issue.Repo); err != nil { + return nil, err + } + for _, comment := range readyComments { var err error if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ Ctx: ctx, @@ -165,7 +166,8 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } } - return comments[:n], nil + + return readyComments, nil } // FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number) diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 3996dcb29a..b218f11dfa 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -5,6 +5,7 @@ package issues import ( "context" + "errors" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -51,32 +52,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error { } labelIDs := comments.getLabelIDs() - commentLabels := make(map[int64]*Label, len(labelIDs)) - left := len(labelIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", labelIDs[:limit]). - Rows(new(Label)) - if err != nil { - return err - } - - for rows.Next() { - var label Label - err = rows.Scan(&label) - if err != nil { - _ = rows.Close() - return err - } - commentLabels[label.ID] = &label - } - _ = rows.Close() - left -= limit - labelIDs = labelIDs[limit:] + commentLabels, err := db.GetByIDs(ctx, "id", labelIDs, &Label{}) + if err != nil { + return err } for _, comment := range comments { @@ -101,21 +79,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { return nil } - milestones := make(map[int64]*Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestones) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] + milestones, err := db.GetByIDs(ctx, "id", milestoneIDs, &Milestone{}) + if err != nil { + return err } for _, comment := range comments { @@ -140,21 +106,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { return nil } - milestones := make(map[int64]*Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestones) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] + milestones, err := db.GetByIDs(ctx, "id", milestoneIDs, &Milestone{}) + if err != nil { + return err } for _, comment := range comments { @@ -175,34 +129,9 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { } assigneeIDs := comments.getAssigneeIDs() - assignees := make(map[int64]*user_model.User, len(assigneeIDs)) - left := len(assigneeIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", assigneeIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return err - } - - assignees[user.ID] = &user - } - _ = rows.Close() - - left -= limit - assigneeIDs = assigneeIDs[limit:] + assignees, err := db.GetByIDs(ctx, "id", assigneeIDs, &user_model.User{}) + if err != nil { + return err } for _, comment := range comments { @@ -243,34 +172,9 @@ func (comments CommentList) LoadIssues(ctx context.Context) error { } issueIDs := comments.getIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(Issue)) - if err != nil { - return err - } - - for rows.Next() { - var issue Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] + issues, err := db.GetByIDs(ctx, "id", issueIDs, &Issue{}) + if err != nil { + return err } for _, comment := range comments { @@ -295,36 +199,10 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { return nil } - e := db.GetEngine(ctx) issueIDs := comments.getDependentIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := e. - In("id", issueIDs[:limit]). - Rows(new(Issue)) - if err != nil { - return err - } - - for rows.Next() { - var issue Issue - err = rows.Scan(&issue) - if err != nil { - _ = rows.Close() - return err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] + issues, err := db.GetByIDs(ctx, "id", issueIDs, &Issue{}) + if err != nil { + return err } for _, comment := range comments { @@ -375,34 +253,10 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { return nil } - attachments := make(map[int64][]*repo_model.Attachment, len(comments)) commentsIDs := comments.getAttachmentCommentIDs() - left := len(commentsIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("comment_id", commentsIDs[:limit]). - Rows(new(repo_model.Attachment)) - if err != nil { - return err - } - - for rows.Next() { - var attachment repo_model.Attachment - err = rows.Scan(&attachment) - if err != nil { - _ = rows.Close() - return err - } - attachments[attachment.CommentID] = append(attachments[attachment.CommentID], &attachment) - } - - _ = rows.Close() - left -= limit - commentsIDs = commentsIDs[limit:] + attachments, err := db.GetByFieldIn(ctx, "comment_id", commentsIDs, &repo_model.Attachment{}) + if err != nil { + return err } for _, comment := range comments { @@ -411,6 +265,84 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { return nil } +func (comments CommentList) LoadResolveDoers(ctx context.Context) (err error) { + relevant := func(c *Comment) bool { + return c.ResolveDoerID != 0 && c.Type == CommentTypeCode + } + userIDs := make(container.Set[int64]) + for _, comment := range comments { + if relevant(comment) { + userIDs.Add(comment.ResolveDoerID) + } + } + + if len(userIDs) == 0 { + return nil + } + + userMap := make(map[int64]*user_model.User) + users, err := user_model.GetUsersByIDs(ctx, userIDs.Slice()) + if err != nil { + return err + } + for _, user := range users { + userMap[user.ID] = user + } + + for _, comment := range comments { + if !relevant(comment) { + continue + } + resolveDoer, ok := userMap[comment.ResolveDoerID] + if !ok { + comment.ResolveDoer = user_model.NewGhostUser() + } else { + comment.ResolveDoer = resolveDoer + } + } + + return nil +} + +func (comments CommentList) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { + loadIssueID := int64(0) + loadCommentIDs := make([]int64, 0, len(comments)) + + for _, comment := range comments { + if loadIssueID == 0 { + loadIssueID = comment.IssueID + } else if loadIssueID != comment.IssueID { + return errors.New("unable to load reactions from comments on different issues than each other") + } + if comment.Reactions == nil { + loadCommentIDs = append(loadCommentIDs, comment.ID) + } + } + + if loadIssueID == 0 { + return nil + } + + reactions, err := getReactionsForComments(ctx, loadIssueID, loadCommentIDs) + if err != nil { + return err + } + + allReactions := make(ReactionList, 0, len(reactions)) + for _, comment := range comments { + if comment.Reactions == nil { + comment.Reactions = reactions[comment.ID] + allReactions = append(allReactions, comment.Reactions...) + } + } + + if _, err := allReactions.LoadUsers(ctx, repo); err != nil { + return err + } + + return nil +} + func (comments CommentList) getReviewIDs() []int64 { return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { return comment.ReviewID, comment.ReviewID > 0 diff --git a/models/issues/comment_list_test.go b/models/issues/comment_list_test.go index 062a710b84..12a9144722 100644 --- a/models/issues/comment_list_test.go +++ b/models/issues/comment_list_test.go @@ -84,3 +84,111 @@ func TestCommentListLoadUser(t *testing.T) { }) } } + +func TestCommentListLoadResolveDoers(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &Issue{}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + empty := CommentList{} + require.NoError(t, empty.LoadResolveDoers(t.Context())) + + comment1, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello", + }) + require.NoError(t, err) + require.NoError(t, MarkConversation(t.Context(), comment1, doer, true)) + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) // reload after change + comment1List := CommentList{comment1} + require.NoError(t, comment1List.LoadResolveDoers(t.Context())) + require.NotNil(t, comment1.ResolveDoer) + assert.Equal(t, doer.ID, comment1.ResolveDoer.ID) + + comment2, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello again", + }) + require.NoError(t, err) + require.NoError(t, MarkConversation(t.Context(), comment2, user_model.NewGhostUser(), true)) + + // Reload for fresh objects + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) + comment2 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment2.ID}) + + comment2List := CommentList{comment1, comment2} + require.NoError(t, comment2List.LoadResolveDoers(t.Context())) + require.NotNil(t, comment1.ResolveDoer) + assert.Equal(t, doer.ID, comment1.ResolveDoer.ID) + require.NotNil(t, comment2.ResolveDoer) + assert.EqualValues(t, -1, comment2.ResolveDoer.ID) +} + +func TestCommentListLoadReactions(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &Issue{}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + empty := CommentList{} + require.NoError(t, empty.LoadReactions(t.Context(), repo)) + + comment1, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello", + }) + require.NoError(t, err) + _, err = CreateReaction(t.Context(), &ReactionOptions{ + Type: "eyes", + DoerID: doer.ID, + IssueID: issue.ID, + CommentID: comment1.ID, + }) + require.NoError(t, err) + + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) // reload after change + comment1List := CommentList{comment1} + require.NoError(t, comment1List.LoadReactions(t.Context(), repo)) + require.Len(t, comment1.Reactions, 1) + assert.Equal(t, "eyes", comment1.Reactions[0].Type) + assert.NotNil(t, comment1.Reactions[0].User) + + comment2, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello again", + }) + require.NoError(t, err) + _, err = CreateReaction(t.Context(), &ReactionOptions{ + Type: "rocket", + DoerID: doer.ID, + IssueID: issue.ID, + CommentID: comment2.ID, + }) + require.NoError(t, err) + + // Reload for fresh objects + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) + comment2 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment2.ID}) + + comment2List := CommentList{comment1, comment2} + require.NoError(t, comment2List.LoadReactions(t.Context(), repo)) + require.Len(t, comment1.Reactions, 1) + require.Len(t, comment2.Reactions, 1) + assert.Equal(t, "rocket", comment2.Reactions[0].Type) + assert.NotNil(t, comment2.Reactions[0].User) +} diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c7adf6f62e..da87b8ec2f 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -52,12 +52,29 @@ func TestFetchCodeConversations(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + _, err := issues_model.CreateReaction(t.Context(), &issues_model.ReactionOptions{ + Type: "eyes", + DoerID: 2, + IssueID: issue.ID, + CommentID: 4, + }) + require.NoError(t, err) + require.NoError(t, issues_model.MarkConversation(t.Context(), + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4}), + user, true)) + res, err := issues_model.FetchCodeConversations(db.DefaultContext, issue, user, false) require.NoError(t, err) - assert.Contains(t, res, "README.md") - assert.Contains(t, res["README.md"], int64(4)) - assert.Len(t, res["README.md"][4], 1) - assert.Equal(t, int64(4), res["README.md"][4][0][0].ID) + require.Contains(t, res, "README.md") + require.Contains(t, res["README.md"], int64(4)) + require.Len(t, res["README.md"][4], 1) + require.Len(t, res["README.md"][4][0], 1) + comment := res["README.md"][4][0][0] + assert.Equal(t, int64(4), comment.ID) + assert.NotNil(t, comment.ResolveDoer) + require.Len(t, comment.Reactions, 1) + r := comment.Reactions[0] + assert.NotNil(t, r.User) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) res, err = issues_model.FetchCodeConversations(db.DefaultContext, issue, user2, false) diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 5a02baa428..e4fd9eef2b 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -6,6 +6,7 @@ package issues import ( "context" "fmt" + "slices" "forgejo.org/models/db" project_model "forgejo.org/models/project" @@ -40,21 +41,9 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi } repoIDs := issues.getRepoIDs() - repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) - left := len(repoIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Find(&repoMaps) - if err != nil { - return nil, fmt.Errorf("find repository: %w", err) - } - left -= limit - repoIDs = repoIDs[limit:] + repoMaps, err := db.GetByIDs(ctx, "id", repoIDs, &repo_model.Repository{}) + if err != nil { + return nil, fmt.Errorf("find repository: %w", err) } for _, issue := range issues { @@ -96,21 +85,9 @@ func (issues IssueList) LoadPosters(ctx context.Context) error { } func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) { - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return nil, err - } - left -= limit - posterIDs = posterIDs[limit:] + posterMaps, err := db.GetByIDs(ctx, "id", posterIDs, &user_model.User{}) + if err != nil { + return nil, err } return posterMaps, nil } @@ -135,21 +112,15 @@ func (issues IssueList) LoadLabels(ctx context.Context) error { issueLabels := make(map[int64][]*Label, len(issues)*3) issueIDs := issues.getIssueIDs() - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + for issueIDChunk := range slices.Chunk(issueIDs, db.DefaultMaxInSize) { rows, err := db.GetEngine(ctx).Table("label"). Join("LEFT", "issue_label", "issue_label.label_id = label.id"). - In("issue_label.issue_id", issueIDs[:limit]). + In("issue_label.issue_id", issueIDChunk). Asc("label.name"). Rows(new(LabelIssue)) if err != nil { return err } - for rows.Next() { var labelIssue LabelIssue err = rows.Scan(&labelIssue) @@ -166,8 +137,6 @@ func (issues IssueList) LoadLabels(ctx context.Context) error { if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1) } - left -= limit - issueIDs = issueIDs[limit:] } for _, issue := range issues { @@ -189,21 +158,9 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { return nil } - milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestoneMaps) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] + milestoneMaps, err := db.GetByIDs(ctx, "id", milestoneIDs, &Milestone{}) + if err != nil { + return err } for _, issue := range issues { @@ -216,25 +173,19 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { func (issues IssueList) LoadProjects(ctx context.Context) error { issueIDs := issues.getIssueIDs() projectMaps := make(map[int64]*project_model.Project, len(issues)) - left := len(issueIDs) type projectWithIssueID struct { *project_model.Project `xorm:"extends"` IssueID int64 } - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - - projects := make([]*projectWithIssueID, 0, limit) + for issueIDChunk := range slices.Chunk(issueIDs, db.DefaultMaxInSize) { + projects := make([]*projectWithIssueID, 0, len(issueIDChunk)) err := db.GetEngine(ctx). Table("project"). Select("project.*, project_issue.issue_id"). Join("INNER", "project_issue", "project.id = project_issue.project_id"). - In("project_issue.issue_id", issueIDs[:limit]). + In("project_issue.issue_id", issueIDChunk). Find(&projects) if err != nil { return err @@ -242,8 +193,6 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { for _, project := range projects { projectMaps[project.IssueID] = project.Project } - left -= limit - issueIDs = issueIDs[limit:] } for _, issue := range issues { @@ -264,15 +213,10 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error { assignees := make(map[int64][]*user_model.User, len(issues)) issueIDs := issues.getIssueIDs() - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + for issueIDChunk := range slices.Chunk(issueIDs, db.DefaultMaxInSize) { rows, err := db.GetEngine(ctx).Table("issue_assignees"). Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id"). - In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()). + In("`issue_assignees`.issue_id", issueIDChunk).OrderBy(user_model.GetOrderByName()). Rows(new(AssigneeIssue)) if err != nil { return err @@ -293,8 +237,6 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error { if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1) } - left -= limit - issueIDs = issueIDs[limit:] } for _, issue := range issues { @@ -324,36 +266,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error { return nil } - pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) - left := len(issuesIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("issue_id", issuesIDs[:limit]). - Rows(new(PullRequest)) - if err != nil { - return err - } - - for rows.Next() { - var pr PullRequest - err = rows.Scan(&pr) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1) - } - return err - } - pullRequestMaps[pr.IssueID] = &pr - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] + pullRequestMaps, err := db.GetByIDs(ctx, "issue_id", issuesIDs, &PullRequest{}) + if err != nil { + return err } for _, issue := range issues { @@ -371,37 +286,10 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) { return nil } - attachments := make(map[int64][]*repo_model.Attachment, len(issues)) issuesIDs := issues.getIssueIDs() - left := len(issuesIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("issue_id", issuesIDs[:limit]). - Rows(new(repo_model.Attachment)) - if err != nil { - return err - } - - for rows.Next() { - var attachment repo_model.Attachment - err = rows.Scan(&attachment) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1) - } - return err - } - attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment) - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] + attachments, err := db.GetByFieldIn(ctx, "issue_id", issuesIDs, &repo_model.Attachment{}) + if err != nil { + return err } for _, issue := range issues { @@ -418,15 +306,10 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er comments := make(map[int64][]*Comment, len(issues)) issuesIDs := issues.getIssueIDs() - left := len(issuesIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + for issueIDChunk := range slices.Chunk(issuesIDs, db.DefaultMaxInSize) { rows, err := db.GetEngine(ctx).Table("comment"). Join("INNER", "issue", "issue.id = comment.issue_id"). - In("issue.id", issuesIDs[:limit]). + In("issue.id", issueIDChunk). Where(cond). Rows(new(Comment)) if err != nil { @@ -447,8 +330,6 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.loadComments: Close: %w", err1) } - left -= limit - issuesIDs = issuesIDs[limit:] } for _, issue := range issues { @@ -484,18 +365,12 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { } } - left := len(ids) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - + for idChunk := range slices.Chunk(ids, db.DefaultMaxInSize) { // select issue_id, sum(time) from tracked_time where issue_id in () group by issue_id rows, err := db.GetEngine(ctx).Table("tracked_time"). Where("deleted = ?", false). Select("issue_id, sum(time) as time"). - In("issue_id", ids[:limit]). + In("issue_id", idChunk). GroupBy("issue_id"). Rows(new(totalTimesByIssue)) if err != nil { @@ -516,8 +391,6 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1) } - left -= limit - ids = ids[limit:] } for _, issue := range issues { diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 2fd2641d92..03660803a4 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -94,10 +94,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error // ids in a temporary table and join from them. accum := &IssueStats{} for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk]) if err != nil { return nil, err diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index e9617548e9..0c5da6a2aa 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -5,6 +5,7 @@ package issues_test import ( "fmt" + "slices" "sort" "sync" "testing" @@ -311,7 +312,7 @@ func TestIssue_ResolveMentions(t *testing.T) { for i, user := range resolved { ids[i] = user.ID } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) assert.Equal(t, expected, ids) } @@ -338,7 +339,7 @@ func TestResourceIndex(t *testing.T) { require.NoError(t, err) var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for i := range 100 { wg.Add(1) t.Run(fmt.Sprintf("issue %d", i+1), func(t *testing.T) { t.Parallel() @@ -369,7 +370,7 @@ func TestCorrectIssueStats(t *testing.T) { issueAmount := issues_model.MaxQueryParameters + 10 var wg sync.WaitGroup - for i := 0; i < issueAmount; i++ { + for i := range issueAmount { wg.Add(1) go func(i int) { testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 22e6fcb8d4..35f69e3a0b 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -244,7 +244,7 @@ func UpdateIssueAttachments(ctx context.Context, issue *Issue, uuids []string) ( if err != nil { return fmt.Errorf("FindRepoAttachmentsByUUID[uuids=%q,repoID=%d]: %w", uuids, issue.RepoID, err) } - for i := 0; i < len(attachments); i++ { + for i := range attachments { attachments[i].IssueID = issue.ID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index ddb813cf44..38325e181f 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -63,7 +63,7 @@ func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, olderThan } // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged -func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { +func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (PullRequestList, error) { prs := make([]*PullRequest, 0, 2) sess := db.GetEngine(ctx). Join("INNER", "issue", "issue.id = pull_request.issue_id"). @@ -82,18 +82,44 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, } prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch) + // All these error cases return `false` to defer to the safer choice of not allowing write access on an error. if err != nil { + log.Error("GetUnmergedPullRequestsByHeadInfo failed: %s", err) + return false + } else if issues, err := prs.LoadIssues(ctx); err != nil { + log.Error("LoadIssues failed: %s", err) + return false + } else if err := issues.LoadPosters(ctx); err != nil { + log.Error("LoadPosters failed: %s", err) + return false + } else if err := prs.LoadHeadRepos(ctx); err != nil { + log.Error("LoadHeadRepos failed: %s", err) return false } for _, pr := range prs { if pr.AllowMaintainerEdit { + // PR Poster must have write access to the head, so that when they turned on "AllowMaintainerEdit" they + // delegated that write access to the maintainers of the PR base. If they don't currently have write + // access, they can't delegate that access. + poster := pr.Issue.Poster + posterHeadPerm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, poster) + if err != nil { + log.Error("GetUserRepoPermission failed: %s", err) + continue + } + if !posterHeadPerm.CanWrite(unit.TypeCode) { + continue + } + err = pr.LoadBaseRepo(ctx) if err != nil { + log.Error("LoadBaseRepo failed: %s", err) continue } prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user) if err != nil { + log.Error("GetUserRepoPermission failed: %s", err) continue } if prPerm.CanWrite(unit.TypeCode) { @@ -240,6 +266,25 @@ func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) { return issueList, nil } +func (prs PullRequestList) LoadHeadRepos(ctx context.Context) error { + repoIDs := []int64{} + for _, pr := range prs { + repoIDs = append(repoIDs, pr.HeadRepoID) + } + repos, err := db.GetByIDs(ctx, "id", repoIDs, &repo_model.Repository{}) + if err != nil { + return err + } + for _, pr := range prs { + repo, ok := repos[pr.HeadRepoID] + if !ok { + return fmt.Errorf("unable to find repo %d", pr.HeadRepoID) + } + pr.HeadRepo = repo + } + return nil +} + // GetIssueIDs returns all issue ids func (prs PullRequestList) GetIssueIDs() []int64 { return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) { diff --git a/models/issues/reaction.go b/models/issues/reaction.go index 522040c022..21975c6b00 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "slices" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -176,6 +177,34 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList return reactions, count, err } +func getReactionsForComments(ctx context.Context, issueID int64, commentIDs []int64) (map[int64]ReactionList, error) { + reactions := make(map[int64]ReactionList, len(commentIDs)) + + for commentIDChunk := range slices.Chunk(commentIDs, db.DefaultMaxInSize) { + rows, err := db.GetEngine(ctx). + Where(builder.Eq{"issue_id": issueID}). + In("reaction.`type`", setting.UI.Reactions). + In("comment_id", commentIDChunk). + Rows(&Reaction{}) + if err != nil { + return nil, err + } + + for rows.Next() { + var reaction Reaction + err = rows.Scan(&reaction) + if err != nil { + _ = rows.Close() + return nil, err + } + reactions[reaction.CommentID] = append(reactions[reaction.CommentID], &reaction) + } + + _ = rows.Close() + } + return reactions, nil +} + func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) { reaction := &Reaction{ Type: opts.Type, diff --git a/models/issues/review.go b/models/issues/review.go index 5370117a81..6ff36615db 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -457,6 +457,11 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { return nil, nil, err } + // delete previous review requests from the same user + reviewCond := builder.Eq{"reviewer_id": doer.ID, "issue_id": issue.ID} + if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { + return nil, nil, err + } } review.Official = official @@ -511,10 +516,14 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi // GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) { + return GetReviewByIssueIDUserIDAndTypes(ctx, issueID, userID, []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest}) +} + +func GetReviewByIssueIDUserIDAndTypes(ctx context.Context, issueID, userID int64, types []ReviewType) (*Review, error) { review := new(Review) has, err := db.GetEngine(ctx).Where( - builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). + builder.In("type", types). And(builder.Eq{"issue_id": issueID, "reviewer_id": userID, "original_author_id": 0})). Desc("id"). Get(review) @@ -707,12 +716,12 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user } defer committer.Close() - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + review, err := GetReviewByIssueIDUserIDAndTypes(ctx, issue.ID, reviewer.ID, []ReviewType{ReviewTypeRequest}) if err != nil && !IsErrReviewNotExist(err) { return nil, err } - if review == nil || review.Type != ReviewTypeRequest { + if review == nil { return nil, nil } diff --git a/models/issues/review_list.go b/models/issues/review_list.go index 04c08bc5c4..878ceac9ce 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -20,7 +20,7 @@ type ReviewList []*Review // LoadReviewers loads reviewers func (reviews ReviewList) LoadReviewers(ctx context.Context) error { reviewerIDs := make([]int64, len(reviews)) - for i := 0; i < len(reviews); i++ { + for i := range reviews { reviewerIDs[i] = reviews[i].ReviewerID } reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs) diff --git a/models/issues/review_test.go b/models/issues/review_test.go index bdeaae5ea3..e035be617b 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -321,6 +321,82 @@ func TestAddReviewRequest(t *testing.T) { assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) } +func TestSubmitPendingReviewDeletesReviewRequest(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pull.LoadIssue(db.DefaultContext)) + issue := pull.Issue + require.NoError(t, issue.LoadRepo(db.DefaultContext)) + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + reviewRequest, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeRequest, + }) + require.NoError(t, err) + + // creating a pending review should NOT remove review requests + reviewPending, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypePending, + }) + require.NoError(t, err) + unittest.AssertExistsIf(t, true, &issues_model.Review{ID: reviewRequest.ID}) + // submitting a pending review to finish it SHOULD remove review requests + _, _, err = issues_model.SubmitReview( + db.DefaultContext, + reviewer, + issue, + issues_model.ReviewTypeReject, + "test content", + reviewPending.CommitID, + false, + []string{}, + ) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &issues_model.Review{ID: reviewRequest.ID}) +} + +// this test is for handling a state correctly that should never exist, but is representable and was +// achievable thanks to #12243 +func TestReviewRequestDeletesReviewRequestsBeforeRejectedReviews(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + sess := db.GetEngine(db.DefaultContext) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pull.LoadIssue(db.DefaultContext)) + issue := pull.Issue + require.NoError(t, issue.LoadRepo(db.DefaultContext)) + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + // this one will end up being a ReviewTypeRequest. We are initially creating it as + // ReviewTypeReject to avoid it being deleted on making the actual rejected review + reviewRequest, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeReject, + }) + require.NoError(t, err) + // this review is an actual rejected review that somehow managed to be saved without deleting + // reviewRequest. This is a state that is representable and is/was achievable thanks to #12243 + _, err = issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeReject, + }) + require.NoError(t, err) + reviewRequest.Type = issues_model.ReviewTypeRequest + _, err = sess.ID(reviewRequest.ID).Cols("type").Update(reviewRequest) + require.NoError(t, err) + + _, err = issues_model.RemoveReviewRequest(db.DefaultContext, issue, reviewer, doer) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &issues_model.Review{ID: reviewRequest.ID}) +} + func TestAddTeamReviewRequest(t *testing.T) { defer unittest.OverrideFixtures("models/fixtures/TestAddTeamReviewRequest")() require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 2f050759d2..54173681bd 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -350,10 +350,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed // we get the statistics in smaller chunks and get accumulates var accum int64 for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk]) if err != nil { return 0, err diff --git a/models/organization/team.go b/models/organization/team.go index 209471e013..b7b93821ad 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -161,10 +161,16 @@ func (t *Team) LoadRepositories(ctx context.Context) (err error) { return err } -// LoadMembers returns paginated members in team of organization. +// LoadMembers loads the members of the team in t.Members. func (t *Team) LoadMembers(ctx context.Context) (err error) { + return t.LoadPaginatedMembers(ctx, db.ListOptionsAll) +} + +// LoadPaginatedMembers loads paginated members of the team in t.Members. +func (t *Team) LoadPaginatedMembers(ctx context.Context, listOptions db.ListOptions) (err error) { t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{ - TeamID: t.ID, + ListOptions: listOptions, + TeamID: t.ID, }) return err } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 873f7bf9b6..545ad63eb4 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -5,11 +5,13 @@ package packages import ( "context" + "errors" "strconv" "strings" "forgejo.org/models/db" "forgejo.org/modules/optional" + "forgejo.org/modules/setting" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" @@ -155,6 +157,25 @@ func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error }) } +func (pv *PackageVersion) LockForUpdate(ctx context.Context) error { + if !db.InTransaction(ctx) { + return errors.New("invalid state for PackageVersion.LockForUpdate: database is not in a transaction") + } else if setting.Database.Type.IsSQLite3() { + // SQLite both doesn't support "SELECT ... FOR UPDATE", and it's irrelevant for SQLite as the entire database is + // locked for write when a write transaction is open. + return nil + } + + pvfu := PackageVersion{} + has, err := db.GetEngine(ctx).ID(pv.ID).ForUpdate().Get(&pvfu) + if err != nil { + return err + } else if !has { + return ErrPackageNotExist + } + return nil +} + // SearchValue describes a value to search // If ExactMatch is true, the field must match the value otherwise a LIKE search is performed. type SearchValue struct { diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 22639d1e42..fd1b93c867 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -6,6 +6,7 @@ package access import ( "context" "fmt" + "strings" actions_model "forgejo.org/models/actions" "forgejo.org/models/db" @@ -115,7 +116,8 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool { } func (p *Permission) LogString() string { - format := "") + return fmt.Sprintf(format.String(), args...) } func GetActionRepoPermission(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask) (Permission, error) { diff --git a/models/project/column_test.go b/models/project/column_test.go index aef7a6f9d4..2f4cc79367 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -164,7 +164,7 @@ func Test_NewColumn(t *testing.T) { require.NoError(t, err) assert.Len(t, columns, 3) - for i := 0; i < maxProjectColumns-3; i++ { + for i := range maxProjectColumns - 3 { err := NewColumn(db.DefaultContext, &Column{ Title: fmt.Sprintf("column-%d", i+4), ProjectID: project1.ID, diff --git a/models/pull/review_state.go b/models/pull/review_state.go index 2702d5d5a1..3fc3ab65c2 100644 --- a/models/pull/review_state.go +++ b/models/pull/review_state.go @@ -6,6 +6,7 @@ package pull import ( "context" "fmt" + "maps" "forgejo.org/models/db" "forgejo.org/modules/log" @@ -100,9 +101,7 @@ func mergeFiles(oldFiles, newFiles map[string]ViewedState) map[string]ViewedStat return oldFiles } - for file, viewed := range newFiles { - oldFiles[file] = viewed - } + maps.Copy(oldFiles, newFiles) return oldFiles } diff --git a/models/repo.go b/models/repo.go index 1b9cc8fa60..6a4da96b95 100644 --- a/models/repo.go +++ b/models/repo.go @@ -14,10 +14,8 @@ import ( asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/db" issues_model "forgejo.org/models/issues" - access_model "forgejo.org/models/perm/access" repo_model "forgejo.org/models/repo" "forgejo.org/models/unit" - user_model "forgejo.org/models/user" "forgejo.org/modules/log" "forgejo.org/services/stats" @@ -277,7 +275,7 @@ func DoctorUserStarNum(ctx context.Context) (err error) { } // DeleteDeployKey delete deploy keys -func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { +func DeleteDeployKey(ctx context.Context, id, repoID int64) error { key, err := asymkey_model.GetDeployKeyByID(ctx, id) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { @@ -286,21 +284,10 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error return fmt.Errorf("GetDeployKeyByID: %w", err) } - // Check if user has access to delete this key. - if !doer.IsAdmin { - repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) - if err != nil { - return fmt.Errorf("GetRepositoryByID: %w", err) - } - has, err := access_model.IsUserRepoAdmin(ctx, repo, doer) - if err != nil { - return fmt.Errorf("GetUserRepoPermission: %w", err) - } else if !has { - return asymkey_model.ErrKeyAccessDenied{ - UserID: doer.ID, - KeyID: key.ID, - Note: "deploy", - } + if key.RepoID != repoID { + return asymkey_model.ErrKeyAccessDenied{ + KeyID: key.ID, + Note: "deploy", } } diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 1fe9afd8e9..c64f9dc734 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -6,10 +6,14 @@ package repo import ( "context" + "errors" + "net/url" "time" "forgejo.org/models/db" + "forgejo.org/modules/keying" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" ) @@ -31,7 +35,9 @@ type Mirror struct { LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"` LFSEndpoint string `xorm:"lfs_endpoint TEXT"` - RemoteAddress string `xorm:"VARCHAR(2048)"` + // Encrypted remote address w/ credentials; can be NULL if a mirror has not performed a sync since this field was + // introduced, in which case the remote address exists only in the repo's configured git remote on disk. + EncryptedRemoteAddress []byte `xorm:"BLOB NULL"` } func init() { @@ -73,6 +79,71 @@ func (m *Mirror) ScheduleNextUpdate() { } } +// InsertMirror inserts a mirror to database. RemoteAddress must be provided so that it can be encrypted and stored +// during the insert process. +func (m *Mirror) InsertWithAddress(ctx context.Context, addr string) error { + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err := db.GetEngine(ctx).Insert(m); err != nil { + return err + } + return m.UpdateRemoteAddress(ctx, addr) + }) +} + +// Stores a credential-free version of the address in `RemoteAddress`, encrypts the original into `RemoteAddressAuth`, +// and stores both in the database. The ID of the mirror must be known, so this must be done after the mirror is +// inserted. +func (m *Mirror) UpdateRemoteAddress(ctx context.Context, addr string) error { + if m.ID == 0 { + return errors.New("must persist mirror to database before using UpdateRemoteAddress") + } + + m.EncryptedRemoteAddress = keying.PullMirror.Encrypt( + []byte(addr), + keying.ColumnAndID("remote_address_auth", m.ID), + ) + _, err := db.GetEngine(ctx).ID(m.ID).Cols("encrypted_remote_address").Update(m) + return err +} + +// Retrieves the encrypted remote address and decrypts it. Note that this field is expected to be absent for mirrors +// created before the introduction of EncryptedRemoteAddress, in which case credentials are not known to Forgejo +// directly (but may be on-disk in the repository's config file) and None will be returned. +func (m *Mirror) DecryptRemoteAddress() (optional.Option[string], error) { + if m.EncryptedRemoteAddress == nil { + return optional.None[string](), nil + } + + contents, err := keying.PullMirror.Decrypt(m.EncryptedRemoteAddress, keying.ColumnAndID("remote_address_auth", m.ID)) + if err != nil { + return optional.None[string](), err + } + return optional.Some(string(contents)), nil +} + +// Retrieves the remote address but sanitizes it of sensitive credentials. May be absent for mirrors created before the +// introduction of EncryptedRemoteAddress. +func (m *Mirror) SanitizedRemoteAddress() (optional.Option[string], error) { + maybeAddr, err := m.DecryptRemoteAddress() + if err != nil { + return optional.None[string](), err + } else if has, addr := maybeAddr.Get(); has { + parsedURL, err := url.Parse(addr) + if err != nil { + return optional.None[string](), err + } + + // Remove the password if present. Retain the username for consistency with `AddAuthCredentialHelperForRemote` + // which retains the username for the `git clone` command line, which ends up as the remote URL in the mirror's + // git config. + if parsedURL.User != nil { + parsedURL.User = url.User(parsedURL.User.Username()) + } + return optional.Some(parsedURL.String()), nil + } + return optional.None[string](), nil +} + // GetMirrorByRepoID returns mirror information of a repository. func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) { m := &Mirror{RepoID: repoID} @@ -115,9 +186,3 @@ func MirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any) er } return sess.Iterate(new(Mirror), f) } - -// InsertMirror inserts a mirror to database -func InsertMirror(ctx context.Context, mirror *Mirror) error { - _, err := db.GetEngine(ctx).Insert(mirror) - return err -} diff --git a/models/repo/release.go b/models/repo/release.go index edd628fa0f..8aa447bda8 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -608,6 +608,7 @@ func InsertReleases(ctx context.Context, rels ...*Release) error { if len(rel.Attachments) > 0 { for i := range rel.Attachments { rel.Attachments[i].ReleaseID = rel.ID + rel.Attachments[i].RepoID = rel.RepoID } if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { diff --git a/models/repo/release_test.go b/models/repo/release_test.go index 69f9333589..940de757c7 100644 --- a/models/repo/release_test.go +++ b/models/repo/release_test.go @@ -20,11 +20,14 @@ func TestMigrate_InsertReleases(t *testing.T) { UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", } r := &Release{ + RepoID: 1001, Attachments: []*Attachment{a}, } err := InsertReleases(db.DefaultContext, r) require.NoError(t, err) + + assert.EqualValues(t, 1001, unittest.AssertExistsAndLoadBean(t, &Attachment{UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12"}).RepoID) } func TestReleaseLoadRepo(t *testing.T) { diff --git a/models/repo/repo.go b/models/repo/repo.go index cdb30aa1a9..0c45f483e4 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "html/template" + "maps" "net" "net/url" "path/filepath" @@ -543,9 +544,7 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { if len(repo.DocumentRenderingMetas) == 0 { metas := map[string]string{} - for k, v := range repo.ComposeMetas(ctx) { - metas[k] = v - } + maps.Copy(metas, repo.ComposeMetas(ctx)) metas["mode"] = "document" repo.DocumentRenderingMetas = metas } @@ -786,8 +785,8 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo // getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url func getRepositoryURLPathSegments(repoURL string) []string { - if strings.HasPrefix(repoURL, setting.AppURL) { - return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") + if after, ok := strings.CutPrefix(repoURL, setting.AppURL); ok { + return strings.Split(after, "/") } sshURLVariants := [4]string{ @@ -798,8 +797,8 @@ func getRepositoryURLPathSegments(repoURL string) []string { } for _, sshURL := range sshURLVariants { - if strings.HasPrefix(repoURL, sshURL) { - return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") + if after, ok := strings.CutPrefix(repoURL, sshURL); ok { + return strings.Split(after, "/") } } diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 732d7b627c..a0eb0ce7c2 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -361,7 +361,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { } // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { accessCond := builder.NewCond() if !opts.Collaborate.ValueOrZeroValue() { accessCond = builder.Eq{"owner_id": opts.OwnerID} @@ -401,7 +401,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { if opts.Keyword != "" { // separate keyword subQueryCond := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { if opts.TopicOnly { subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) } else { @@ -416,7 +416,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { keywordCond := builder.In("id", subQuery) if !opts.TopicOnly { likes := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) // If the string looks like "org/repo", match against that pattern too diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index aa6f2fa0ae..3db6dc95e8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -237,10 +237,8 @@ func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { } func (cfg *ActionsConfig) DisableWorkflow(file string) { - for _, workflow := range cfg.DisabledWorkflows { - if file == workflow { - return - } + if slices.Contains(cfg.DisabledWorkflows, file) { + return } cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) diff --git a/models/repo/upload.go b/models/repo/upload.go index a213cb1986..67b5409650 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -117,7 +117,7 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) { defer committer.Close() ids := make([]int64, len(uploads)) - for i := 0; i < len(uploads); i++ { + for i := range uploads { ids[i] = uploads[i].ID } if err = db.DeleteByIDs[Upload](ctx, ids...); err != nil { diff --git a/models/secret/secret.go b/models/secret/secret.go index 911d94d78a..758f346f22 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -20,7 +20,7 @@ import ( var ( namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") - forbiddenPrefixPattern = regexp.MustCompile("(?i)^FORGEJO_|GITEA_|GITHUB_") + forbiddenPrefixPattern = regexp.MustCompile("(?i)^(FORGEJO_|GITEA_|GITHUB_|[0-9])") ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name") ) diff --git a/models/secret/secret_test.go b/models/secret/secret_test.go index 5c54b25967..e323986084 100644 --- a/models/secret/secret_test.go +++ b/models/secret/secret_test.go @@ -257,12 +257,18 @@ func TestSecretValidateName(t *testing.T) { valid bool }{ {"FORGEJO_", false}, + {"PRE_FORGEJO_", true}, + {"PRE_FORGEJO_SUF", true}, {"FORGEJO_123", false}, {"FORGEJO_ABC", false}, {"GITEA_", false}, + {"PRE_GITEA_", true}, + {"PRE_GITEA_SUF", true}, {"GITEA_123", false}, {"GITEA_ABC", false}, {"GITHUB_", false}, + {"PRE_GITHUB_", true}, + {"PRE_GITHUB_SUF", true}, {"GITHUB_123", false}, {"GITHUB_ABC", false}, {"123_TEST", false}, diff --git a/models/unit/unit.go b/models/unit/unit.go index 434e5f0acc..2a31c804aa 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -248,22 +248,12 @@ func LoadUnitConfig() error { // UnitGlobalDisabled checks if unit type is global disabled func (u Type) UnitGlobalDisabled() bool { - for _, ud := range DisabledRepoUnitsGet() { - if u == ud { - return true - } - } - return false + return slices.Contains(DisabledRepoUnitsGet(), u) } // CanBeDefault checks if the unit type can be a default repo unit func (u *Type) CanBeDefault() bool { - for _, nadU := range NotAllowedDefaultRepoUnits { - if *u == nadU { - return false - } - } - return true + return !slices.Contains(NotAllowedDefaultRepoUnits, *u) } // Unit is a section of one repository diff --git a/models/unittest/fixture_loader.go b/models/unittest/fixture_loader.go index 5aea06550c..3cf2efdced 100644 --- a/models/unittest/fixture_loader.go +++ b/models/unittest/fixture_loader.go @@ -151,8 +151,8 @@ func (l *loader) buildFixtureFile(fixturePath string) (*fixtureFile, error) { switch v := value.(type) { case string: // Try to decode hex. - if strings.HasPrefix(v, "0x") { - value, err = hex.DecodeString(strings.TrimPrefix(v, "0x")) + if after, ok := strings.CutPrefix(v, "0x"); ok { + value, err = hex.DecodeString(after) if err != nil { return nil, err } diff --git a/models/unittest/mock_http.go b/models/unittest/mock_http.go index b8413104b3..5e420533d8 100644 --- a/models/unittest/mock_http.go +++ b/models/unittest/mock_http.go @@ -102,13 +102,13 @@ func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveM // parse back the fixture file into a series of HTTP headers followed by response body lines := strings.Split(stringFixture, "\n") for idx, line := range lines { - colonIndex := strings.Index(line, ": ") - if colonIndex != -1 { + before, after, ok := strings.Cut(line, ": ") + if ok { // Because we modified the body with ReplaceAll() above, we need to // remove Content-Length. w.Write() should add it back. - header := line[0:colonIndex] + header := before if !strings.EqualFold(header, "Content-Length") { - w.Header().Set(line[0:colonIndex], line[colonIndex+2:]) + w.Header().Set(before, after) } } else { // we reached the end of the headers (empty line), so what follows is the body diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index 141fc66b99..939891283d 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -9,7 +9,7 @@ import ( ) func fieldByName(v reflect.Value, field string) reflect.Value { - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { v = v.Elem() } f := v.FieldByName(field) diff --git a/models/user/avatar.go b/models/user/avatar.go index cc1b1b7b9d..726d67f5e0 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -108,7 +108,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool { if !u.UseCustomAvatar || len(u.Avatar) == 0 { return true } - avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data))))) + avatarID := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", u.ID, md5.Sum(data)))) return u.Avatar != avatarID } diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 85f5b16c65..35b33933c2 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -5,6 +5,7 @@ package user_test import ( "fmt" + "slices" "testing" "forgejo.org/models/db" @@ -77,12 +78,7 @@ func TestListEmails(t *testing.T) { assert.Greater(t, count, int64(5)) contains := func(match func(s *user_model.SearchEmailResult) bool) bool { - for _, v := range emails { - if match(v) { - return true - } - } - return false + return slices.ContainsFunc(emails, match) } assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 18 })) diff --git a/models/user/moderation.go b/models/user/moderation.go index 7bc857489a..765414acc0 100644 --- a/models/user/moderation.go +++ b/models/user/moderation.go @@ -87,7 +87,7 @@ func newUserData(user *User) UserData { // (e.g. FieldName -> field_name) corresponding to UserData struct fields. var userDataColumnNames = sync.OnceValue(func() []string { mapper := new(names.GonicMapper) - udType := reflect.TypeOf(UserData{}) + udType := reflect.TypeFor[UserData]() columnNames := make([]string, 0, udType.NumField()) for i := 0; i < udType.NumField(); i++ { columnNames = append(columnNames, mapper.Obj2Table(udType.Field(i).Name)) diff --git a/models/user/user.go b/models/user/user.go index 7e2101d7cc..2c20cd977d 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1243,8 +1243,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } // Finally, if email address is the protected email address: - if strings.HasSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) { - username := strings.TrimSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) + if before, ok := strings.CutSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)); ok { + username := before user := &User{} has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user) if err != nil { diff --git a/models/user/user_test.go b/models/user/user_test.go index d1af3a750f..6da645d672 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -273,9 +273,9 @@ func TestHashPasswordDeterministic(t *testing.T) { b := make([]byte, 16) u := &user_model.User{} algos := hash.RecommendedHashAlgorithms - for j := 0; j < len(algos); j++ { + for j := range algos { u.PasswdHashAlgo = algos[j] - for i := 0; i < 50; i++ { + for range 50 { // generate a random password rand.Read(b) pass := string(b) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index b23f3fd348..196a5313bc 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -429,7 +429,7 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error { if len(ws) == 0 { return nil } - for i := 0; i < len(ws); i++ { + for i := range ws { ws[i].Type = strings.TrimSpace(ws[i].Type) } return db.Insert(ctx, ws) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 99c9446805..ed54ccc98b 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "io" + "slices" "strings" actions_model "forgejo.org/models/actions" @@ -609,11 +610,8 @@ func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobpars matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matched = true } if matched { break @@ -658,11 +656,8 @@ func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt * matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matched = true } if matched { break diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index fdbc4ff291..744a431ea8 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -101,7 +101,7 @@ func Generate(n int) (string, error) { buffer := make([]byte, n) max := big.NewInt(int64(len(validChars))) for { - for j := 0; j < n; j++ { + for j := range n { rnd, err := rand.Int(rand.Reader, max) if err != nil { return "", err diff --git a/modules/auth/password/password_test.go b/modules/auth/password/password_test.go index 1fe3fb5ce1..8f5d64514c 100644 --- a/modules/auth/password/password_test.go +++ b/modules/auth/password/password_test.go @@ -51,7 +51,7 @@ func TestComplexity_Generate(t *testing.T) { test := func(t *testing.T, modes []string) { testComplextity(modes) - for i := 0; i < maxCount; i++ { + for range maxCount { pwd, err := Generate(pwdLen) require.NoError(t, err) assert.Len(t, pwd, pwdLen) diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go index 10693ec663..f3277ff616 100644 --- a/modules/auth/password/pwn/pwn.go +++ b/modules/auth/password/pwn/pwn.go @@ -101,7 +101,7 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { } defer resp.Body.Close() - for _, pair := range strings.Split(string(body), "\n") { + for pair := range strings.SplitSeq(string(body), "\n") { parts := strings.Split(pair, ":") if len(parts) != 2 { continue diff --git a/modules/avatar/identicon/block.go b/modules/avatar/identicon/block.go index cb1803a231..fc8ce90212 100644 --- a/modules/avatar/identicon/block.go +++ b/modules/avatar/identicon/block.go @@ -24,8 +24,8 @@ func drawBlock(img *image.Paletted, x, y, size, angle int, points []int) { rotate(points, m, m, angle) } - for i := 0; i < size; i++ { - for j := 0; j < size; j++ { + for i := range size { + for j := range size { if pointInPolygon(i, j, points) { img.SetColorIndex(x+i, y+j, 1) } diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 13e8ec88e6..19f87da85a 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -134,7 +134,7 @@ func drawBlocks(p *image.Paletted, size int, c, b1, b2 blockFunc, b1Angle, b2Ang // then we make it left-right mirror, so we didn't draw 3/6/9 before for x := 0; x < size/2; x++ { - for y := 0; y < size; y++ { + for y := range size { p.SetColorIndex(size-x, y, p.ColorIndexAt(x, y)) } } diff --git a/modules/charset/charset.go b/modules/charset/charset.go index cb03deb966..d4121fb27f 100644 --- a/modules/charset/charset.go +++ b/modules/charset/charset.go @@ -164,7 +164,7 @@ func DetectEncoding(content []byte) (string, error) { } times := 1024 / len(content) detectContent = make([]byte, 0, times*len(content)) - for i := 0; i < times; i++ { + for range times { detectContent = append(detectContent, content...) } } else { diff --git a/modules/charset/charset_test.go b/modules/charset/charset_test.go index 358220494b..c29987beb6 100644 --- a/modules/charset/charset_test.go +++ b/modules/charset/charset_test.go @@ -243,7 +243,7 @@ func stringMustEndWith(t *testing.T, expected, value string) { func TestToUTF8WithFallbackReader(t *testing.T) { resetDefaultCharsetsOrder() - for testLen := 0; testLen < 2048; testLen++ { + for testLen := range 2048 { pattern := " test { () }\n" input := "" for len(input) < testLen { diff --git a/modules/forgefed/actor.go b/modules/forgefed/actor.go index 5383d5adaf..1f6e1f1fdf 100644 --- a/modules/forgefed/actor.go +++ b/modules/forgefed/actor.go @@ -6,6 +6,7 @@ package forgefed import ( "fmt" "net/url" + "slices" "strconv" "strings" @@ -107,12 +108,7 @@ func newActorID(uri string) (ActorID, error) { } func containsEmptyString(ar []string) bool { - for _, elem := range ar { - if elem == "" { - return true - } - } - return false + return slices.Contains(ar, "") } func removeEmptyStrings(ls []string) []string { diff --git a/modules/forgefed/repository.go b/modules/forgefed/repository.go index 63680ccd35..1e85d1e64c 100644 --- a/modules/forgefed/repository.go +++ b/modules/forgefed/repository.go @@ -88,7 +88,7 @@ func ToRepository(it ap.Item) (*Repository, error) { return (*Repository)(unsafe.Pointer(&i)), nil default: // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes - typ := reflect.TypeOf(new(Repository)) + typ := reflect.TypeFor[*Repository]() if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Repository); ok { return i, nil } diff --git a/modules/git/command.go b/modules/git/command.go index bf1d624dbf..4ab081418b 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "net/url" "os" "os/exec" "runtime/trace" @@ -446,6 +447,54 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS return stdoutBuf.Bytes(), stderr, nil } +// If `remoteURL` is a URL with a password in it, add parameters to the git command that will read that password from a +// credential store file, and return the URL that should be used in the command instead of the original, and a cleanup +// function to call to remove the credential file. If `remoteURL` doesn't have a password, then it is returned as-is. +// This function must be invoked on the the git command before the git sub-command -- eg. before the `clone` or `fetch` +// parameter is added to the command's args. +func (c *Command) AddAuthCredentialHelperForRemote(remoteURL string) (commandURL string, cleanup func(), err error) { + parsedFromURL, _ := url.Parse(remoteURL) + + // If the clone URL has credentials, build a credential file for usage by git-credential-store + // to prevent credential leak in the process list. + // https://git-scm.com/docs/git-credential-store#_storage_format + // credential.helper adjustment must be set before the git subcommand + if strings.Contains(remoteURL, "://") && strings.Contains(remoteURL, "@") && parsedFromURL != nil { + credentialsFile, err := os.CreateTemp("", "forgejo-clone-credentials-") + if err != nil { + return "", nil, err + } + credentialsPath := credentialsFile.Name() + + cleanup := func() { + _ = credentialsFile.Close() + if err := util.Remove(credentialsPath); err != nil { + log.Warn("Unable to remove temporary file %q: %v", credentialsPath, err) + } + } + _, err = credentialsFile.Write([]byte(parsedFromURL.String())) + if err != nil { + cleanup() + return "", nil, err + } + err = credentialsFile.Close() + if err != nil { + cleanup() + return "", nil, err + } + + c.AddArguments("-c").AddDynamicArguments("credential.helper=store --file=" + credentialsPath) + + // remove the password from the URL argument + parsedFromURL.User = url.User(parsedFromURL.User.Username()) + commandURL = parsedFromURL.String() + + return commandURL, cleanup, nil + } + + return remoteURL, func() {}, nil +} + // AllowLFSFiltersArgs return globalCommandArgs with lfs filter, it should only be used for tests func AllowLFSFiltersArgs() TrustedCmdArgs { // Now here we should explicitly allow lfs filters to run diff --git a/modules/git/commit.go b/modules/git/commit.go index 4fb13ecd4f..36ba8ef8ca 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -269,8 +269,8 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits var keywords, authors, committers []string var after, before string - fields := strings.Fields(searchString) - for _, k := range fields { + fields := strings.FieldsSeq(searchString) + for k := range fields { switch { case strings.HasPrefix(k, "author:"): authors = append(authors, strings.TrimPrefix(k, "author:")) diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index 6511a1689a..62f58f8767 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "io" + "maps" "path" "sort" @@ -45,9 +46,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath return nil, nil, err } - for pth, found := range commits { - revs[pth] = found - } + maps.Copy(revs, commits) } } else { sort.Strings(entryPaths) diff --git a/modules/git/fetch_test.go b/modules/git/fetch_test.go index 95a1fa387d..b7ead10d4a 100644 --- a/modules/git/fetch_test.go +++ b/modules/git/fetch_test.go @@ -26,7 +26,7 @@ func TestFetch(t *testing.T) { fetchedCommitID, err := repo.Fetch(otherRepoPath, "refs/heads/master") require.NoError(t, err) - assert.Equal(t, "95d3505f2db273e40be79f84416051ae85e9ea0d", fetchedCommitID) + assert.Equal(t, "5684d0c8cfdfb17fcd59101826efc9ff54b80df4", fetchedCommitID) c, err := repo.getCommit(MustIDFromString(fetchedCommitID)) require.NoError(t, err) diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 2f5ec08991..87c1c9a4ff 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -75,9 +75,9 @@ func (f Format) Parser(r io.Reader) *Parser { // hexEscaped produces hex-escaped characters from a string. For example, "\n\0" // would turn into "%0a%00". func (f Format) hexEscaped(delim []byte) string { - escaped := "" - for i := 0; i < len(delim); i++ { - escaped += "%" + hex.EncodeToString([]byte{delim[i]}) + var escaped strings.Builder + for i := range delim { + escaped.WriteString("%" + hex.EncodeToString([]byte{delim[i]})) } - return escaped + return escaped.String() } diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 83ddb766af..5666a425f4 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -65,7 +65,7 @@ func TestGrepSearch(t *testing.T) { return } - res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{MatchesPerFile: 1}) + res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{RefName: "95d3505f2db273e40be79f84416051ae85e9ea0d", MatchesPerFile: 1}) require.NoError(t, err) assert.Equal(t, []*GrepResult{ { diff --git a/modules/git/hook.go b/modules/git/hook.go index bef4d024c8..3b650fe9db 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "forgejo.org/modules/log" @@ -27,12 +28,7 @@ var ErrNotValidHook = errors.New("not a valid Git hook") // IsValidHookName returns true if given name is a valid Git hook. func IsValidHookName(name string) bool { - for _, hn := range hookNames { - if hn == name { - return true - } - } - return false + return slices.Contains(hookNames, name) } // Hook represents a Git hook. diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 1d7e74a0d7..9b49a18aaa 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -21,7 +21,7 @@ type Cache interface { } func getCacheKey(repoPath, commitID, entryPath string) string { - hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) + hashBytes := sha256.Sum256(fmt.Appendf(nil, "%s:%s:%s", repoPath, commitID, entryPath)) return fmt.Sprintf("last_commit:%x", hashBytes) } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 50786e7a42..800e83c4a4 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -346,10 +346,7 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st results := make([]string, len(paths)) remaining := len(paths) - nextRestart := (len(paths) * 3) / 4 - if nextRestart > 70 { - nextRestart = 70 - } + nextRestart := min((len(paths)*3)/4, 70) lastEmptyParent := head.ID.String() commitSinceLastEmptyParent := uint64(0) commitSinceNextRestart := uint64(0) diff --git a/modules/git/notes.go b/modules/git/notes.go index a52314bdd7..1bc68b6366 100644 --- a/modules/git/notes.go +++ b/modules/git/notes.go @@ -8,6 +8,7 @@ import ( "context" "io" "os" + "strings" "forgejo.org/modules/log" ) @@ -33,7 +34,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string) (*Note, err return nil, err } - path := "" + var path strings.Builder tree := ¬es.Tree log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID) @@ -43,12 +44,12 @@ func GetNote(ctx context.Context, repo *Repository, commitID string) (*Note, err for len(commitID) > 2 { entry, err = tree.GetTreeEntryByPath(commitID) if err == nil { - path += commitID + path.WriteString(commitID) break } if IsErrNotExist(err) { tree, err = tree.SubTree(commitID[0:2]) - path += commitID[0:2] + "/" + path.WriteString(commitID[0:2] + "/") commitID = commitID[2:] } if err != nil { @@ -80,9 +81,9 @@ func GetNote(ctx context.Context, repo *Repository, commitID string) (*Note, err _ = dataRc.Close() closed = true - lastCommit, err := repo.getCommitByPathWithID(notes.ID, path) + lastCommit, err := repo.getCommitByPathWithID(notes.ID, path.String()) if err != nil { - log.Error("Unable to get the commit for the path %q. Error: %v", path, err) + log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err) return nil, err } diff --git a/modules/git/parse.go b/modules/git/parse.go index c7b84d7198..d2d70d4cfa 100644 --- a/modules/git/parse.go +++ b/modules/git/parse.go @@ -33,16 +33,16 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { posEnd += pos } line := data[pos:posEnd] - posTab := bytes.IndexByte(line, '\t') - if posTab == -1 { + before, after, ok := bytes.Cut(line, []byte{'\t'}) + if !ok { return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) } entry := new(TreeEntry) entry.ptree = ptree - entryAttrs := line[:posTab] - entryName := line[posTab+1:] + entryAttrs := before + entryName := after entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type diff --git a/modules/git/pushoptions/pushoptions.go b/modules/git/pushoptions/pushoptions.go index 3fa2e01c44..14e2c5d283 100644 --- a/modules/git/pushoptions/pushoptions.go +++ b/modules/git/pushoptions/pushoptions.go @@ -52,7 +52,7 @@ func NewFromMap(o *map[string]string) Interface { func (o *gitPushOptions) ReadEnv() Interface { if pushCount, err := strconv.Atoi(os.Getenv(EnvCount)); err == nil { - for idx := 0; idx < pushCount; idx++ { + for idx := range pushCount { _ = o.Parse(os.Getenv(fmt.Sprintf(EnvFormat, idx))) } } diff --git a/modules/git/ref.go b/modules/git/ref.go index 1475d4dc5a..fdccd2b2e2 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -105,8 +105,8 @@ func (ref RefName) IsFor() bool { } func (ref RefName) nameWithoutPrefix(prefix string) string { - if strings.HasPrefix(string(ref), prefix) { - return strings.TrimPrefix(string(ref), prefix) + if after, ok := strings.CutPrefix(string(ref), prefix); ok { + return after } return "" } diff --git a/modules/git/repo.go b/modules/git/repo.go index 21845d9b55..8f9b95f1f2 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -18,7 +18,6 @@ import ( "strings" "time" - "forgejo.org/modules/log" "forgejo.org/modules/proxy" "forgejo.org/modules/setting" "forgejo.org/modules/util" @@ -46,9 +45,9 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro return commits, nil } - parts := bytes.Split(logs, []byte{'\n'}) + parts := bytes.SplitSeq(logs, []byte{'\n'}) - for _, commitID := range parts { + for commitID := range parts { commit, err := repo.GetCommit(string(commitID)) if err != nil { return nil, err @@ -141,44 +140,13 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op envs = proxy.EnvWithProxy(parsedFromURL) } - fromURL := from - sanitizedFrom := from + sanitizedFrom := util.SanitizeCredentialURLs(from) - // If the clone URL has credentials, build a credential file for usage by git-credential-store - // to prevent credential leak in the process list. - // https://git-scm.com/docs/git-credential-store#_storage_format - // credential.helper adjustment must be set before the git subcommand - if strings.Contains(from, "://") && strings.Contains(from, "@") { - sanitizedFrom = util.SanitizeCredentialURLs(from) - if parsedFromURL != nil { - credentialsFile, err := os.CreateTemp("", "forgejo-clone-credentials-") - if err != nil { - return err - } - credentialsPath := credentialsFile.Name() - - defer func() { - _ = credentialsFile.Close() - if err := util.Remove(credentialsPath); err != nil { - log.Warn("Unable to remove temporary file %q: %v", credentialsPath, err) - } - }() - _, err = credentialsFile.Write([]byte(parsedFromURL.String())) - if err != nil { - return err - } - err = credentialsFile.Close() - if err != nil { - return err - } - - cmd.AddArguments("-c").AddDynamicArguments("credential.helper=store --file=" + credentialsPath) - - // remove the password from the URL argument - parsedFromURL.User = url.User(parsedFromURL.User.Username()) - fromURL = parsedFromURL.String() - } + fromURL, cleanup, err := cmd.AddAuthCredentialHelperForRemote(from) + if err != nil { + return err } + defer cleanup() cmd.AddArguments("clone") diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 2b07513162..56a86bde14 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -96,8 +96,8 @@ func (ca GitAttribute) String() string { // sometimes used within gitlab-language: https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type func (ca GitAttribute) Prefix() string { s := ca.String() - if i := strings.IndexByte(s, '?'); i >= 0 { - return s[:i] + if before, _, ok := strings.Cut(s, "?"); ok { + return before } return s } diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index f58757a9a2..7fc5c573dd 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -95,7 +95,7 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go index ee4beb2f87..2832c4f572 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/repo_language_stats.go @@ -168,11 +168,21 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) || - (!isFalse(isVendored) && analyze.IsVendor(f.Name())) || - enry.IsDotFile(f.Name()) || - enry.IsConfiguration(f.Name()) || - (!isFalse(isDocumentation) && enry.IsDocumentation(f.Name())) { + // Don't skip this file if it is explicitly set to be detectable. + // Skip this file if one of the following conditions holds: + // 1. Explicitly set to not be detectable. + // 2. Explicitly set that it is vendored. + // 3. Explicitly set that it is documentation. + // 4. Is not explicitly set to not be vendored and is by heuristic considered to be vendored. + // 5. It is considered to be a dot file. + // 6. It is considered to be a configuration file. + // 7. Is not explicitly set to not be documentation and is by heuristic considered to be documentation. + if !isTrue(isDetectable) && + (isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) || + (!isFalse(isVendored) && analyze.IsVendor(f.Name())) || + enry.IsDotFile(f.Name()) || + enry.IsConfiguration(f.Name()) || + (!isFalse(isDocumentation) && enry.IsDocumentation(f.Name()))) { continue } diff --git a/modules/git/repo_language_stats_test.go b/modules/git/repo_language_stats_test.go index e3d8ba1f69..2bc57b56c5 100644 --- a/modules/git/repo_language_stats_test.go +++ b/modules/git/repo_language_stats_test.go @@ -34,6 +34,16 @@ func TestRepository_GetLanguageStats(t *testing.T) { "Python": 67, "Java": 112, }, stats) + + stats, err = gitRepo.GetLanguageStats("5684d0c8cfdfb17fcd59101826efc9ff54b80df4") + require.NoError(t, err) + + assert.Equal(t, map[string]int64{ + "Cobra": 67, + "Python": 67, + "Markdown": 15, + "Java": 112, + }, stats) } func TestMergeLanguageStats(t *testing.T) { diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index f7f04e1f10..bd851a3be3 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -42,8 +42,8 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { return "", err } - tagRefs := strings.Split(stdout, "\n") - for _, tagRef := range tagRefs { + tagRefs := strings.SplitSeq(stdout, "\n") + for tagRef := range tagRefs { if len(strings.TrimSpace(tagRef)) > 0 { fields := strings.Fields(tagRef) if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) { @@ -65,7 +65,7 @@ func (repo *Repository) GetTagID(name string) (string, error) { return "", err } // Make sure exact match is used: "v1" != "release/v1" - for _, line := range strings.Split(stdout, "\n") { + for line := range strings.SplitSeq(stdout, "\n") { fields := strings.Fields(line) if len(fields) == 2 && fields[1] == "refs/tags/"+name { return fields[0], nil diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.idx b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.idx deleted file mode 100644 index 186136cb12..0000000000 Binary files a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.idx and /dev/null differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.pack b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.pack deleted file mode 100644 index 046061c688..0000000000 Binary files a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.pack and /dev/null differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.rev b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.rev deleted file mode 100644 index 7d8c6f3562..0000000000 Binary files a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.rev and /dev/null differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.idx b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.idx new file mode 100644 index 0000000000..80e1ee36b5 Binary files /dev/null and b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.idx differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.pack b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.pack new file mode 100644 index 0000000000..94b6852051 Binary files /dev/null and b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.pack differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.rev b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.rev new file mode 100644 index 0000000000..c2e2e3aeae Binary files /dev/null and b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.rev differ diff --git a/modules/git/tests/repos/language_stats_repo/packed-refs b/modules/git/tests/repos/language_stats_repo/packed-refs index 63e01583a4..b046027fce 100644 --- a/modules/git/tests/repos/language_stats_repo/packed-refs +++ b/modules/git/tests/repos/language_stats_repo/packed-refs @@ -1,2 +1,2 @@ # pack-refs with: peeled fully-peeled sorted -95d3505f2db273e40be79f84416051ae85e9ea0d refs/heads/master +5684d0c8cfdfb17fcd59101826efc9ff54b80df4 refs/heads/master diff --git a/modules/git/tree.go b/modules/git/tree.go index f6201f6cc9..9a91787c9e 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -170,7 +170,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 8b6c4c467c..5e3bb8ac21 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -171,7 +171,7 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) { } entry := te entryLink := "" - for i := 0; i < 999; i++ { + for range 999 { if entry.IsLink() { next, link, err := entry.FollowLink() entryLink = link diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index aa092cc56b..6277154acd 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -20,7 +20,7 @@ func TestSubTree_Issue29101(t *testing.T) { require.NoError(t, err) // old code could produce a different error if called multiple times - for i := 0; i < 10; i++ { + for range 10 { _, err = commit.SubTree("file1.txt") require.Error(t, err) assert.True(t, IsErrNotExist(err)) diff --git a/modules/hostmatcher/hostmatcher.go b/modules/hostmatcher/hostmatcher.go index 1069310316..15c6371422 100644 --- a/modules/hostmatcher/hostmatcher.go +++ b/modules/hostmatcher/hostmatcher.go @@ -6,6 +6,7 @@ package hostmatcher import ( "net" "path/filepath" + "slices" "strings" ) @@ -38,7 +39,7 @@ func isBuiltin(s string) bool { // ParseHostMatchList parses the host list HostMatchList func ParseHostMatchList(settingKeyHint, hostList string) *HostMatchList { hl := &HostMatchList{SettingKeyHint: settingKeyHint, SettingValue: hostList} - for _, s := range strings.Split(hostList, ",") { + for s := range strings.SplitSeq(hostList, ",") { s = strings.ToLower(strings.TrimSpace(s)) if s == "" { continue @@ -61,7 +62,7 @@ func ParseSimpleMatchList(settingKeyHint, matchList string) *HostMatchList { SettingKeyHint: settingKeyHint, SettingValue: matchList, } - for _, s := range strings.Split(matchList, ",") { + for s := range strings.SplitSeq(matchList, ",") { s = strings.ToLower(strings.TrimSpace(s)) if s == "" { continue @@ -98,10 +99,8 @@ func (hl *HostMatchList) checkPattern(host string) bool { } func (hl *HostMatchList) checkIP(ip net.IP) bool { - for _, pattern := range hl.patterns { - if pattern == "*" { - return true - } + if slices.Contains(hl.patterns, "*") { + return true } for _, builtin := range hl.builtins { switch builtin { diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 7978fc38a1..311f7215b2 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -59,7 +59,7 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { ifNoneMatch := req.Header.Get("If-None-Match") if len(ifNoneMatch) > 0 { - for _, item := range strings.Split(ifNoneMatch, ",") { + for item := range strings.SplitSeq(ifNoneMatch, ",") { item = strings.TrimPrefix(strings.TrimSpace(item), "W/") // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives if item == etag { return true diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index d385ac21c9..4c71437fc5 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "net/url" "path" @@ -86,9 +87,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { } if opts.AdditionalHeaders != nil { - for k, v := range opts.AdditionalHeaders { - header[k] = v - } + maps.Copy(header, opts.AdditionalHeaders) } } diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index 14a43cf3be..8ec3c1181f 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -129,8 +129,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio changes.Updates = append(changes.Updates, updates...) return nil } - lines := strings.Split(stdout, "\n") - for _, line := range lines { + lines := strings.SplitSeq(stdout, "\n") + for line := range lines { line = strings.TrimSpace(line) if len(line) == 0 { continue diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 08c1b21c26..09dfe10e08 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -8,6 +8,7 @@ import ( "fmt" "net/url" "regexp" + "slices" "strconv" "strings" @@ -447,12 +448,7 @@ func (o *valuedOption) IsChecked() bool { case api.IssueFormFieldTypeDropdown: checks := strings.Split(o.field.Get(fmt.Sprintf("form-field-%s", o.field.ID)), ",") idx := strconv.Itoa(o.index) - for _, v := range checks { - if v == idx { - return true - } - } - return false + return slices.Contains(checks, idx) case api.IssueFormFieldTypeCheckboxes: return o.field.Get(fmt.Sprintf("form-field-%s-%d", o.field.ID, o.index)) == "on" } diff --git a/modules/keying/keying.go b/modules/keying/keying.go index 751fd77521..14fbaaca98 100644 --- a/modules/keying/keying.go +++ b/modules/keying/keying.go @@ -41,6 +41,8 @@ var ( MigrateTask = deriveKey("migrate_repo_task") // Used for the `webhook` table. Webhook = deriveKey("webhook") + // Used for the `mirror` table. + PullMirror = deriveKey("pullmirror") ) var ( diff --git a/modules/label/parser.go b/modules/label/parser.go index 12fc176967..b27b2c9ee6 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -72,7 +72,7 @@ func parseYamlFormat(fileName string, data []byte) ([]*Label, error) { func parseLegacyFormat(fileName string, data []byte) ([]*Label, error) { lines := strings.Split(string(data), "\n") list := make([]*Label, 0, len(lines)) - for i := 0; i < len(lines); i++ { + for i := range lines { line := strings.TrimSpace(lines[i]) if len(line) == 0 { continue @@ -108,7 +108,7 @@ func LoadTemplateDescription(fileName string) (string, error) { return "", err } - for i := 0; i < len(list); i++ { + for i := range list { if i > 0 { buf.WriteString(", ") } diff --git a/modules/log/event_format.go b/modules/log/event_format.go index 6835a4ca5b..70df2cbce2 100644 --- a/modules/log/event_format.go +++ b/modules/log/event_format.go @@ -208,7 +208,7 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms } } if hasColorValue { - msg = []byte(fmt.Sprintf(msgFormat, msgArgs...)) + msg = fmt.Appendf(nil, msgFormat, msgArgs...) } } // try to reuse the pre-formatted simple text message @@ -227,8 +227,8 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms buf = append(buf, msg...) if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level { - lines := bytes.Split([]byte(event.Stacktrace), []byte("\n")) - for _, line := range lines { + lines := bytes.SplitSeq([]byte(event.Stacktrace), []byte("\n")) + for line := range lines { buf = append(buf, "\n\t"...) buf = append(buf, line...) } diff --git a/modules/log/event_writer_conn_test.go b/modules/log/event_writer_conn_test.go index 0cf447149a..6d528a68d1 100644 --- a/modules/log/event_writer_conn_test.go +++ b/modules/log/event_writer_conn_test.go @@ -63,11 +63,9 @@ func TestConnLogger(t *testing.T) { } expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.Filename, event.Line, event.Caller, strings.ToUpper(event.Level.String())[0], event.MsgSimpleText) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { listenReadAndClose(t, l, expected) - }() + }) logger.SendLogEvent(&event) wg.Wait() diff --git a/modules/log/flags.go b/modules/log/flags.go index 1e4fe830c1..c428d58a1d 100644 --- a/modules/log/flags.go +++ b/modules/log/flags.go @@ -124,7 +124,7 @@ func FlagsFromString(from string, def ...uint32) Flags { return Flags{defined: true, flags: def[0]} } flags := uint32(0) - for _, flag := range strings.Split(strings.ToLower(from), ",") { + for flag := range strings.SplitSeq(strings.ToLower(from), ",") { flags |= flagFromString[strings.TrimSpace(flag)] } return Flags{defined: true, flags: flags} diff --git a/modules/log/level_test.go b/modules/log/level_test.go index e6cacc723b..73e2355960 100644 --- a/modules/log/level_test.go +++ b/modules/log/level_test.go @@ -33,11 +33,11 @@ func TestLevelMarshalUnmarshalJSON(t *testing.T) { require.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) - err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 2)), &testLevel) + err = json.Unmarshal(fmt.Appendf(nil, `{"level":%d}`, 2), &testLevel) require.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) - err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 10012)), &testLevel) + err = json.Unmarshal(fmt.Appendf(nil, `{"level":%d}`, 10012), &testLevel) require.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) @@ -52,5 +52,5 @@ func TestLevelMarshalUnmarshalJSON(t *testing.T) { } func makeTestLevelBytes(level string) []byte { - return []byte(fmt.Sprintf(`{"level":"%s"}`, level)) + return fmt.Appendf(nil, `{"level":"%s"}`, level) } diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index dab6057cf4..22dcf93d75 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -80,8 +80,8 @@ func newFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca filePath := node.Data[m[6]:m[7]] hash := node.Data[m[8]:m[9]] urlFullSource := urlFull - if strings.HasSuffix(filePath, "?display=source") { - filePath = strings.TrimSuffix(filePath, "?display=source") + if before, ok := strings.CutSuffix(filePath, "?display=source"); ok { + filePath = before } else if Type(filePath) != "" { urlFullSource = node.Data[m[0]:m[6]] + filePath + "?display=source#" + hash } diff --git a/modules/markup/html.go b/modules/markup/html.go index d60021bfbb..77b5dc8029 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "strings" "sync" @@ -124,13 +125,7 @@ func CustomLinkURLSchemes(schemes []string) { if !validScheme.MatchString(s) { continue } - without := false - for _, sna := range xurls.SchemesNoAuthority { - if s == sna { - without = true - break - } - } + without := slices.Contains(xurls.SchemesNoAuthority, s) if without { s += ":" } else { @@ -675,9 +670,9 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { // It makes page handling terrible, but we prefer GitHub syntax // And fall back to MediaWiki only when it is obvious from the look // Of text and link contents - sl := strings.Split(content, "|") - for _, v := range sl { - if equalPos := strings.IndexByte(v, '='); equalPos == -1 { + sl := strings.SplitSeq(content, "|") + for v := range sl { + if found := strings.Contains(v, "="); !found { // There is no equal in this argument; this is a mandatory arg if props["name"] == "" { if IsLinkStr(v) { @@ -1148,7 +1143,7 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { } // Ensure that every group (m[0]...m[9]) has a match - for i := 0; i < 10; i++ { + for i := range 10 { if m[i] == -1 { return } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 2b19e0f1c9..9a112109dd 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -182,10 +182,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } buf, _ = ExtractMetadataBytes(buf, rc) - metaLength := bufWithMetadataLength - len(buf) - if metaLength < 0 { - metaLength = 0 - } + metaLength := max(bufWithMetadataLength-len(buf), 0) rc.metaLength = metaLength pc.Set(markdownutil.RenderConfigKey, rc) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 82c2c7fe8c..61ded3cedc 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -319,7 +319,7 @@ func TestTotal_RenderWiki(t *testing.T) { answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw")) - for i := 0; i < len(sameCases); i++ { + for i := range sameCases { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -363,7 +363,7 @@ func TestTotal_RenderString(t *testing.T) { answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master")) - for i := 0; i < len(sameCases); i++ { + for i := range sameCases { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index 84817ef1e4..d27318c623 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -24,7 +24,7 @@ func (r *BlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) { l := n.Lines().Len() - for i := 0; i < l; i++ { + for i := range l { line := n.Lines().At(i) _, _ = w.Write(util.EscapeHTML(line.Value(source))) } diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index aaf116ff20..9345dd528a 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -63,7 +63,7 @@ func TestExtractMetadata(t *testing.T) { func TestExtractMetadataBytes(t *testing.T) { t.Run("ValidFrontAndBody", func(t *testing.T) { var meta IssueTemplate - body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) + body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta) require.NoError(t, err) assert.Equal(t, bodyTest, string(body)) assert.Equal(t, metaTest, meta) @@ -72,19 +72,19 @@ func TestExtractMetadataBytes(t *testing.T) { t.Run("NoFirstSeparator", func(t *testing.T) { var meta IssueTemplate - _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta) + _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta) require.Error(t, err) }) t.Run("NoLastSeparator", func(t *testing.T) { var meta IssueTemplate - _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta) + _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta) require.Error(t, err) }) t.Run("NoBody", func(t *testing.T) { var meta IssueTemplate - body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) + body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, sepTest), &meta) require.NoError(t, err) assert.Empty(t, string(body)) assert.Equal(t, metaTest, meta) diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go index dbfab3e9dc..53add219f5 100644 --- a/modules/markup/markdown/toc.go +++ b/modules/markup/markdown/toc.go @@ -44,7 +44,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str } li := ast.NewListItem(currentLevel * 2) a := ast.NewLink() - a.Destination = []byte(fmt.Sprintf("#%s", url.QueryEscape(header.ID))) + a.Destination = fmt.Appendf(nil, "#%s", url.QueryEscape(header.ID)) a.AppendChild(a, ast.NewString([]byte(header.Text))) li.AppendChild(li, a) ul.AppendChild(ul, li) diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go index eedaf58556..16779d5099 100644 --- a/modules/markup/markdown/transform_heading.go +++ b/modules/markup/markdown/transform_heading.go @@ -17,7 +17,7 @@ import ( func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) { for _, attr := range v.Attributes() { if _, ok := attr.Value.([]byte); !ok { - v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) + v.SetAttribute(attr.Name, fmt.Appendf(nil, "%v", attr.Value)) } } txt := mdutil.Text(v, reader.Source()) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index b1c3d35e73..0a66caf1d5 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -319,23 +319,19 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr _ = pw2.Close() }() - wg.Add(1) - go func() { + wg.Go(func() { err = donotpanic.SafeFuncWithError(func() error { return SanitizeReader(pr2, renderer.Name(), output) }) _ = pr2.Close() - wg.Done() - }() + }) } else { pw2 = nopCloser{output} } - wg.Add(1) - go func() { + wg.Go(func() { err = donotpanic.SafeFuncWithError(func() error { return postProcessOrCopy(ctx, renderer, pr, pw2) }) _ = pr.Close() _ = pw2.Close() - wg.Done() - }() + }) if err1 := renderer.Render(ctx, input, pw); err1 != nil { return err1 diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index ed163d30ac..2f83d2ee7b 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -58,7 +58,7 @@ type PackageMetadata struct { Time map[string]time.Time `json:"time,omitempty"` Homepage string `json:"homepage,omitempty"` Keywords []string `json:"keywords,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` Author User `json:"author"` ReadmeFilename string `json:"readmeFilename,omitempty"` Users map[string]bool `json:"users,omitempty"` @@ -75,7 +75,7 @@ type PackageMetadataVersion struct { Author User `json:"author"` Homepage string `json:"homepage,omitempty"` License string `json:"license,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` Keywords []string `json:"keywords,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` BundleDependencies []string `json:"bundleDependencies,omitempty"` diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index 6bb77f302b..0e5bf19ce7 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -22,5 +22,5 @@ type Metadata struct { OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"` Bin map[string]string `json:"bin,omitempty"` Readme string `json:"readme,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` } diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 992ade7e8f..dd9fac96c6 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -142,8 +142,8 @@ func ParseDebugHeaderID(r io.ReadSeeker) (string, error) { if _, err := r.Read(b); err != nil { return "", err } - if i := bytes.IndexByte(b, 0); i != -1 { - buf.Write(b[:i]) + if before, _, ok := bytes.Cut(b, []byte{0}); ok { + buf.Write(before) return buf.String(), nil } buf.Write(b) diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go index 191efc7c0e..7d498c66b8 100644 --- a/modules/packages/rubygems/marshal.go +++ b/modules/packages/rubygems/marshal.go @@ -91,7 +91,7 @@ func (e *MarshalEncoder) marshal(v any) error { val := reflect.ValueOf(v) typ := reflect.TypeOf(v) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { val = val.Elem() typ = typ.Elem() } @@ -250,7 +250,7 @@ func (e *MarshalEncoder) marshalArray(arr reflect.Value) error { return err } - for i := 0; i < length; i++ { + for i := range length { if err := e.marshal(arr.Index(i).Interface()); err != nil { return err } diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go index 34fc4f1784..094fa0c7a4 100644 --- a/modules/packages/swift/metadata.go +++ b/modules/packages/swift/metadata.go @@ -47,7 +47,7 @@ type Metadata struct { Keywords []string `json:"keywords,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` License string `json:"license,omitempty"` - Author Person `json:"author,omitempty"` + Author Person `json:"author"` Manifests map[string]*Manifest `json:"manifests,omitempty"` } diff --git a/modules/private/serv.go b/modules/private/serv.go index fb8496930e..ac5be3b767 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "net/url" + "strings" asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/perm" @@ -47,17 +48,18 @@ type ServCommandResults struct { // ServCommand preps for a serv call func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", + var reqURL strings.Builder + reqURL.WriteString(setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", keyID, url.PathEscape(ownerName), url.PathEscape(repoName), mode, - ) + )) for _, verb := range verbs { if verb != "" { - reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) + fmt.Fprintf(&reqURL, "&verb=%s", url.QueryEscape(verb)) } } - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequest(ctx, reqURL.String(), "GET") return requestJSONResp(req, &ServCommandResults{}) } diff --git a/modules/public/public.go b/modules/public/public.go index a7db5b62e9..52cb8757a0 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -45,7 +45,7 @@ func FileHandlerFunc() http.HandlerFunc { func parseAcceptEncoding(val string) container.Set[string] { parts := strings.Split(val, ";") types := make(container.Set[string]) - for _, v := range strings.Split(parts[0], ",") { + for v := range strings.SplitSeq(parts[0], ",") { types.Add(strings.TrimSpace(v)) } return types diff --git a/modules/queue/base_levelqueue_common.go b/modules/queue/base_levelqueue_common.go index 8b4f35c47d..c57bf8597b 100644 --- a/modules/queue/base_levelqueue_common.go +++ b/modules/queue/base_levelqueue_common.go @@ -83,7 +83,7 @@ func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) { } conn = cfg.ConnStr } - for i := 0; i < 10; i++ { + for range 10 { if db, err = nosql.GetManager().GetLevelDB(conn); err == nil { break } diff --git a/modules/queue/base_redis.go b/modules/queue/base_redis.go index ec3c6dc16d..8b20e0b443 100644 --- a/modules/queue/base_redis.go +++ b/modules/queue/base_redis.go @@ -49,7 +49,7 @@ func newBaseRedisGeneric(cfg *BaseConfig, unique bool, client nosql.RedisClient) } var err error - for i := 0; i < 10; i++ { + for range 10 { err = client.Ping(graceful.GetManager().ShutdownContext()).Err() if err == nil { break diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go index caa930158c..758faf1459 100644 --- a/modules/queue/base_test.go +++ b/modules/queue/base_test.go @@ -88,7 +88,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) // test blocking push if queue is full for i := 0; i < cfg.Length; i++ { - err = q.PushItem(ctx, []byte(fmt.Sprintf("item-%d", i))) + err = q.PushItem(ctx, fmt.Appendf(nil, "item-%d", i)) require.NoError(t, err) } ctxTimed, cancel = context.WithTimeout(ctx, 10*time.Millisecond) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index 8f1a93f273..9c655b7fdc 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -5,6 +5,7 @@ package queue import ( "context" + "maps" "sync" "time" @@ -68,9 +69,7 @@ func (m *Manager) ManagedQueues() map[int64]ManagedWorkerPoolQueue { defer m.mu.Unlock() queues := make(map[int64]ManagedWorkerPoolQueue, len(m.Queues)) - for k, v := range m.Queues { - queues[k] = v - } + maps.Copy(queues, m.Queues) return queues } diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go index 2d1228db2c..87f01755aa 100644 --- a/modules/queue/workergroup.go +++ b/modules/queue/workergroup.go @@ -142,11 +142,7 @@ func (q *WorkerPoolQueue[T]) basePushForShutdown(items ...T) bool { // doStartNewWorker starts a new worker for the queue, the worker reads from worker's channel and handles the items. func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { - wp.wg.Add(1) - - go func() { - defer wp.wg.Done() - + wp.wg.Go(func() { log.Debug("Queue %q starts new worker", q.GetName()) defer log.Debug("Queue %q stops idle worker", q.GetName()) @@ -187,7 +183,7 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { q.workerNumMu.Unlock() } } - }() + }) } // doFlush flushes the queue: it tries to read all items from the queue and handles them. diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go index 8d907ed8cd..da857b9405 100644 --- a/modules/queue/workerqueue_test.go +++ b/modules/queue/workerqueue_test.go @@ -78,17 +78,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) { runCount := 2 // we can run these tests even hundreds times to see its stability t.Run("1/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1}) } }) t.Run("3/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1}) } }) t.Run("4/5", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5}) } }) @@ -97,17 +97,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) { func TestWorkerPoolQueuePersistence(t *testing.T) { runCount := 2 // we can run these tests even hundreds times to see its stability t.Run("1/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1, Length: 100}) } }) t.Run("3/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1, Length: 100}) } }) t.Run("4/5", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5, Length: 100}) } }) @@ -142,7 +142,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett q, _ := newWorkerPoolQueueForTest("pr_patch_checker_test", queueSetting, testHandler, true) stop := runWorkerPoolQueue(q) - for i := 0; i < testCount; i++ { + for i := range testCount { _ = q.Push("task-" + strconv.Itoa(i)) } close(startWhenAllReady) @@ -187,7 +187,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) { q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 1, Length: 100}, handler, false) stop := runWorkerPoolQueue(q) - for i := 0; i < 5; i++ { + for i := range 5 { require.NoError(t, q.Push(i)) } @@ -203,7 +203,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) { q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 3, Length: 100}, handler, false) stop = runWorkerPoolQueue(q) - for i := 0; i < 15; i++ { + for i := range 15 { require.NoError(t, q.Push(i)) } @@ -264,12 +264,12 @@ func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) { stop := runWorkerPoolQueue(q) const workloadSize = 12 - for i := 0; i < workloadSize; i++ { + for i := range workloadSize { require.NoError(t, q.Push(i)) } workerIDs := make(map[string]struct{}) - for i := 0; i < workloadSize; i++ { + for i := range workloadSize { c := <-chGoroutineIDs workerIDs[c] = struct{}{} t.Logf("%d workers: overall=%d current=%d", i, len(workerIDs), q.GetWorkerNumber()) diff --git a/modules/repository/init.go b/modules/repository/init.go index 7b1442be93..66a65599a8 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -152,7 +152,7 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg } labels := make([]*issues_model.Label, len(list)) - for i := 0; i < len(list); i++ { + for i := range list { labels[i] = &issues_model.Label{ Name: list[i].Name, Exclusive: list[i].Exclusive, diff --git a/modules/setting/config.go b/modules/setting/config.go index 6299640e61..90f3d12d11 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -4,6 +4,7 @@ package setting import ( + "strings" "sync" "forgejo.org/modules/log" @@ -23,11 +24,11 @@ type OpenWithEditorApp struct { type OpenWithEditorAppsType []OpenWithEditorApp func (t OpenWithEditorAppsType) ToTextareaString() string { - ret := "" + var ret strings.Builder for _, app := range t { - ret += app.DisplayName + " = " + app.OpenURL + "\n" + ret.WriteString(app.DisplayName + " = " + app.OpenURL + "\n") } - return ret + return ret.String() } func DefaultOpenWithEditorApps() OpenWithEditorAppsType { diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index 458dbb51bb..68a7c94db2 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -51,10 +51,10 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) { for _, unescapeIdx := range escapeStringIndices { preceding := encoded[last:unescapeIdx[0]] if !inKey { - if splitter := strings.Index(preceding, "__"); splitter > -1 { - section += preceding[:splitter] + if before, after, ok := strings.Cut(preceding, "__"); ok { + section += before inKey = true - key += preceding[splitter+2:] + key += after } else { section += preceding } @@ -77,9 +77,9 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) { } remaining := encoded[last:] if !inKey { - if splitter := strings.Index(remaining, "__"); splitter > -1 { - section += remaining[:splitter] - key += remaining[splitter+2:] + if before, after, ok := strings.Cut(remaining, "__"); ok { + section += before + key += after } else { section += remaining } @@ -113,25 +113,24 @@ func decodeEnvironmentKey(prefixRegexp *regexp.Regexp, suffixFile, envKey string func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { prefixRegexp := regexp.MustCompile(EnvConfigKeyPrefixGitea) for _, kv := range envs { - idx := strings.IndexByte(kv, '=') - if idx < 0 { + before, after, ok0 := strings.Cut(kv, "=") + if !ok0 { continue } // parse the environment variable to config section name and key name - envKey := kv[:idx] - envValue := kv[idx+1:] + envKey := before + keyValue := after ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(prefixRegexp, EnvConfigKeySuffixFile, envKey) if !ok { continue } // use environment value as config value, or read the file content as value if the key indicates a file - keyValue := envValue if useFileValue { - fileContent, err := os.ReadFile(envValue) + fileContent, err := os.ReadFile(keyValue) if err != nil { - log.Error("Error reading file for %s : %v", envKey, envValue, err) + log.Error("Error reading file for %s : %v", envKey, keyValue, err) continue } if bytes.HasSuffix(fileContent, []byte("\r\n")) { diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index b112a50cfa..948dae0bea 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -108,7 +108,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing func IndexerGlobFromString(globstr string) []Glob { extarr := make([]Glob, 0, 10) - for _, expr := range strings.Split(strings.ToLower(globstr), ",") { + for expr := range strings.SplitSeq(strings.ToLower(globstr), ",") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { diff --git a/modules/setting/log.go b/modules/setting/log.go index ecc591fd35..7799f8187b 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -269,8 +269,8 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger } var eventWriters []log.EventWriter - modes := strings.Split(modeVal, ",") - for _, modeName := range modes { + modes := strings.SplitSeq(modeVal, ",") + for modeName := range modes { modeName = strings.TrimSpace(modeName) if modeName == "" { continue diff --git a/modules/setting/markup.go b/modules/setting/markup.go index 4ab9e7b2d1..0ece86dfd1 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -85,8 +85,8 @@ func loadMarkupFrom(rootCfg ConfigProvider) { func newMarkupSanitizer(name string, sec ConfigSection) { rule, ok := createMarkupSanitizerRule(name, sec) if ok { - if strings.HasPrefix(name, "sanitizer.") { - names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2) + if after, ok0 := strings.CutPrefix(name, "sanitizer."); ok0 { + names := strings.SplitN(after, ".", 2) name = names[0] } for _, renderer := range ExternalMarkupRenderers { diff --git a/modules/setting/mirror.go b/modules/setting/mirror.go index 58c57c5c95..083c67db45 100644 --- a/modules/setting/mirror.go +++ b/modules/setting/mirror.go @@ -48,11 +48,7 @@ func loadMirrorFrom(rootCfg ConfigProvider) { Mirror.MinInterval = 1 * time.Minute } if Mirror.DefaultInterval < Mirror.MinInterval { - if time.Hour*8 < Mirror.MinInterval { - Mirror.DefaultInterval = Mirror.MinInterval - } else { - Mirror.DefaultInterval = time.Hour * 8 - } + Mirror.DefaultInterval = max(time.Hour*8, Mirror.MinInterval) log.Warn("Mirror.DefaultInterval is less than Mirror.MinInterval, set to %s", Mirror.DefaultInterval.String()) } } diff --git a/modules/setting/storage.go b/modules/setting/storage.go index e458300727..93958219a8 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "strings" ) @@ -27,12 +28,7 @@ var storageTypes = []StorageType{ // IsValidStorageType returns true if the given storage type is valid func IsValidStorageType(storageType StorageType) bool { - for _, t := range storageTypes { - if t == storageType { - return true - } - } - return false + return slices.Contains(storageTypes, storageType) } // MinioStorageConfig represents the configuration for a minio storage diff --git a/modules/structs/action.go b/modules/structs/action.go index a39ae11d65..cb6d76f3e3 100644 --- a/modules/structs/action.go +++ b/modules/structs/action.go @@ -70,13 +70,13 @@ type ActionRun struct { // the current status of this run Status string `json:"status"` // when the action run was started - Started time.Time `json:"started,omitempty"` + Started time.Time `json:"started"` // when the action run was stopped - Stopped time.Time `json:"stopped,omitempty"` + Stopped time.Time `json:"stopped"` // when the action run was created - Created time.Time `json:"created,omitempty"` + Created time.Time `json:"created"` // when the action run was last updated - Updated time.Time `json:"updated,omitempty"` + Updated time.Time `json:"updated"` // how long the action run ran for Duration time.Duration `json:"duration,omitempty"` // the url of this action run diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 6208c28be1..37c71f5736 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -204,7 +204,7 @@ func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error { if err != nil { return err } - for _, v := range strings.Split(str, ",") { + for v := range strings.SplitSeq(str, ",") { if v = strings.TrimSpace(v); v == "" { continue } diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 059f19c2bb..3fa43ce0cb 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -118,7 +118,7 @@ type Repository struct { // enum: ["sha1", "sha256"] ObjectFormatName string `json:"object_format_name"` // swagger:strfmt date-time - MirrorUpdated time.Time `json:"mirror_updated,omitempty"` + MirrorUpdated time.Time `json:"mirror_updated"` RepoTransfer *RepoTransfer `json:"repo_transfer"` Topics []string `json:"topics"` } diff --git a/modules/structs/user.go b/modules/structs/user.go index 49e4c495cf..e0767071d0 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -34,9 +34,9 @@ type User struct { // Is the user an administrator IsAdmin bool `json:"is_admin"` // swagger:strfmt date-time - LastLogin time.Time `json:"last_login,omitempty"` + LastLogin time.Time `json:"last_login"` // swagger:strfmt date-time - Created time.Time `json:"created,omitempty"` + Created time.Time `json:"created"` // Is user restricted Restricted bool `json:"restricted"` // Is user active diff --git a/modules/structs/user_gpgkey.go b/modules/structs/user_gpgkey.go index ff9b0aea1d..deae70de33 100644 --- a/modules/structs/user_gpgkey.go +++ b/modules/structs/user_gpgkey.go @@ -21,9 +21,9 @@ type GPGKey struct { CanCertify bool `json:"can_certify"` Verified bool `json:"verified"` // swagger:strfmt date-time - Created time.Time `json:"created_at,omitempty"` + Created time.Time `json:"created_at"` // swagger:strfmt date-time - Expires time.Time `json:"expires_at,omitempty"` + Expires time.Time `json:"expires_at"` } // GPGKeyEmail an email attached to a GPGKey diff --git a/modules/structs/user_key.go b/modules/structs/user_key.go index 08eed59a89..b92552b200 100644 --- a/modules/structs/user_key.go +++ b/modules/structs/user_key.go @@ -15,7 +15,7 @@ type PublicKey struct { Title string `json:"title,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` // swagger:strfmt date-time - Created time.Time `json:"created_at,omitempty"` + Created time.Time `json:"created_at"` Owner *User `json:"user,omitempty"` ReadOnly bool `json:"read_only,omitempty"` KeyType string `json:"key_type,omitempty"` diff --git a/modules/templates/eval/eval_test.go b/modules/templates/eval/eval_test.go index 3e68203638..6b13d14007 100644 --- a/modules/templates/eval/eval_test.go +++ b/modules/templates/eval/eval_test.go @@ -13,7 +13,7 @@ import ( ) func tokens(s string) (a []any) { - for _, v := range strings.Fields(s) { + for v := range strings.FieldsSeq(s) { a = append(a, v) } return a diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index d60397df08..4290e1c29f 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -248,7 +248,7 @@ func extractErrorLine(code []byte, lineNum, posNum int, target string) string { b := bufio.NewReader(bytes.NewReader(code)) var line []byte var err error - for i := 0; i < lineNum; i++ { + for i := range lineNum { if line, err = b.ReadBytes('\n'); err != nil { if i == lineNum-1 && errors.Is(err, io.EOF) { err = nil diff --git a/modules/templates/scopedtmpl/scopedtmpl.go b/modules/templates/scopedtmpl/scopedtmpl.go index 41a8ca86e9..d9866b3513 100644 --- a/modules/templates/scopedtmpl/scopedtmpl.go +++ b/modules/templates/scopedtmpl/scopedtmpl.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "io" + "maps" "reflect" "sync" texttemplate "text/template" @@ -40,9 +41,7 @@ func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) { panic("cannot add new functions to frozen template set") } t.all.Funcs(funcMap) - for k, v := range funcMap { - t.parseFuncs[k] = v - } + maps.Copy(t.parseFuncs, funcMap) } func (t *ScopedTemplate) New(name string) *template.Template { @@ -159,9 +158,7 @@ func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateS textTmplPtr.muFuncs.Lock() ts.execFuncs = map[string]reflect.Value{} - for k, v := range textTmplPtr.execFuncs { - ts.execFuncs[k] = v - } + maps.Copy(ts.execFuncs, textTmplPtr.execFuncs) textTmplPtr.muFuncs.Unlock() var collectTemplates func(nodes []parse.Node) @@ -220,9 +217,7 @@ func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecuto tmpl := texttemplate.New("") tmplPtr := ptr[textTemplate](tmpl) tmplPtr.execFuncs = map[string]reflect.Value{} - for k, v := range ts.execFuncs { - tmplPtr.execFuncs[k] = v - } + maps.Copy(tmplPtr.execFuncs, ts.execFuncs) if funcMap != nil { tmpl.Funcs(funcMap) } diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go index 60f918be47..8e1ef71f5d 100644 --- a/modules/templates/util_misc.go +++ b/modules/templates/util_misc.go @@ -17,12 +17,11 @@ import ( asymkey_model "forgejo.org/models/asymkey" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" - "forgejo.org/modules/git" - giturl "forgejo.org/modules/git/url" "forgejo.org/modules/json" "forgejo.org/modules/log" "forgejo.org/modules/repository" "forgejo.org/modules/svg" + mirror_service "forgejo.org/services/mirror" "github.com/editorconfig/editorconfig-core-go/v2" ) @@ -164,17 +163,11 @@ type remoteAddress struct { Password string } -func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress { +func mirrorRemoteAddress(ctx context.Context, mirror *repo_model.Mirror) remoteAddress { ret := remoteAddress{} - remoteURL, err := git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) + u, err := mirror_service.DecryptOrRecoverRemoteAddress(ctx, mirror) if err != nil { - log.Error("GetRemoteURL %v", err) - return ret - } - - u, err := giturl.Parse(remoteURL) - if err != nil { - log.Error("giturl.Parse %v", err) + log.Error("DecryptOrRecoverRemoteAddress %v", err) return ret } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 02851ed75d..e1ad83b88d 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -246,7 +246,8 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n } func RenderLabels(ctx *Context, labels []*issues_model.Label, repoLink string, isPull bool) template.HTML { - htmlCode := `` + var htmlCode strings.Builder + htmlCode.WriteString(``) for _, label := range labels { // Protect against nil value in labels - shouldn't happen but would cause a panic if so if label == nil { @@ -257,11 +258,11 @@ func RenderLabels(ctx *Context, labels []*issues_model.Label, repoLink string, i if isPull { issuesOrPull = "pulls" } - htmlCode += fmt.Sprintf("%s ", + fmt.Fprintf(&htmlCode, "%s ", repoLink, issuesOrPull, label.ID, RenderLabel(ctx, label)) } - htmlCode += "" - return template.HTML(htmlCode) + htmlCode.WriteString("") + return template.HTML(htmlCode.String()) } func RenderUser(ctx context.Context, user user_model.User) template.HTML { diff --git a/modules/test/logchecker.go b/modules/test/logchecker.go index 8e8fc32216..af82ff0461 100644 --- a/modules/test/logchecker.go +++ b/modules/test/logchecker.go @@ -53,11 +53,11 @@ func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) { } } -var checkerIndex int64 +var checkerIndex atomic.Int64 func NewLogChecker(namePrefix string, level log.Level) (logChecker *LogChecker, cancel func()) { logger := log.GetManager().GetLogger(namePrefix) - newCheckerIndex := atomic.AddInt64(&checkerIndex, 1) + newCheckerIndex := checkerIndex.Add(1) writerName := namePrefix + "-" + fmt.Sprint(newCheckerIndex) lc := &LogChecker{} diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index 6ced5f6780..54f0462703 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -501,7 +501,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { // Printf takes a format and args and prints the string to os.Stdout func Printf(format string, args ...any) { if log.CanColorStdout { - for i := 0; i < len(args); i++ { + for i := range args { args[i] = log.NewColoredValue(args[i]) } } diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index 89361088a4..d0f5841411 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -119,7 +119,7 @@ func (l *locale) LookupNewStyleMessage(trKey string) string { return "" } -func (l *locale) LookupPluralByCount(trKey string, count any) string { +func (l *locale) LookupPluralByCount(trKey string, count any, isDefaultLang bool) string { n, err := util.ToInt64(count) if err != nil { log.Error("Invalid plural count '%s'", count) @@ -127,10 +127,10 @@ func (l *locale) LookupPluralByCount(trKey string, count any) string { } pluralForm := l.pluralRule(n) - return l.LookupPluralByForm(trKey, pluralForm) + return l.LookupPluralByForm(trKey, pluralForm, isDefaultLang) } -func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex) string { +func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex, isDefaultLang bool) string { suffix := "" switch pluralForm { case PluralFormZero: @@ -155,7 +155,14 @@ func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex) st return result } - log.Error("Missing translation for plural form %s", suffix) + // Severify depends on the lang. A missing string in default lang will affect + // all translations, while community translations may just be incomplete + logFunc := log.Debug + if isDefaultLang { + logFunc = log.Error + } + + logFunc("Missing translation for key `%[1]s`, plural form `%[2]s`", trKey, suffix) return "" } @@ -266,11 +273,11 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { } func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { - message := l.LookupPluralByCount(trKey, count) + message := l.LookupPluralByCount(trKey, count, false) if message == "" { if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { - message = defaultLang.LookupPluralByCount(trKey, count) + message = defaultLang.LookupPluralByCount(trKey, count, true) } if message == "" { message = trKey @@ -294,7 +301,7 @@ func (l *locale) TrPluralStringAllForms(trKey string) ([]string, []string) { allPresent := true for i, form := range l.usedPluralForms { - result[i] = l.LookupPluralByForm(trKey, form) + result[i] = l.LookupPluralByForm(trKey, form, false) if result[i] == "" { allPresent = false } @@ -304,7 +311,7 @@ func (l *locale) TrPluralStringAllForms(trKey string) ([]string, []string) { if hasDefaultLang { fallback = make([]string, len(defaultLang.usedPluralForms)) for i, form := range defaultLang.usedPluralForms { - fallback[i] = defaultLang.LookupPluralByForm(trKey, form) + fallback[i] = defaultLang.LookupPluralByForm(trKey, form, true) } } else { log.Error("Plural set for '%s' is incomplete and no fallback language is set.", trKey) diff --git a/modules/updatechecker/update_checker.go b/modules/updatechecker/update_checker.go index b0932ba663..8b524b6519 100644 --- a/modules/updatechecker/update_checker.go +++ b/modules/updatechecker/update_checker.go @@ -60,9 +60,9 @@ func getVersionDNS(domainEndpoint string) (version string, err error) { } for _, record := range records { - if strings.HasPrefix(record, "forgejo_versions=") { + if after, ok := strings.CutPrefix(record, "forgejo_versions="); ok { // Get all supported versions, separated by a comma. - supportedVersions := strings.Split(strings.TrimPrefix(record, "forgejo_versions="), ",") + supportedVersions := strings.Split(after, ",") // For now always return the latest supported version. return supportedVersions[len(supportedVersions)-1], nil } diff --git a/modules/util/remove.go b/modules/util/remove.go index 2a65a6b0aa..e2cffc92c9 100644 --- a/modules/util/remove.go +++ b/modules/util/remove.go @@ -12,7 +12,7 @@ import ( // Remove removes the named file or (empty) directory with at most 5 attempts. func Remove(name string) error { var err error - for i := 0; i < 5; i++ { + for range 5 { err = os.Remove(name) if err == nil { break @@ -35,7 +35,7 @@ func Remove(name string) error { // RemoveAll removes the named file or (empty) directory with at most 5 attempts. func RemoveAll(name string) error { var err error - for i := 0; i < 5; i++ { + for range 5 { err = os.RemoveAll(name) if err == nil { break @@ -58,7 +58,7 @@ func RemoveAll(name string) error { // Rename renames (moves) oldpath to newpath with at most 5 attempts. func Rename(oldpath, newpath string) error { var err error - for i := 0; i < 5; i++ { + for i := range 5 { err = os.Rename(oldpath, newpath) if err == nil { break diff --git a/modules/util/rotatingfilewriter/writer_test.go b/modules/util/rotatingfilewriter/writer_test.go index 5b3b351667..c3664d8c4f 100644 --- a/modules/util/rotatingfilewriter/writer_test.go +++ b/modules/util/rotatingfilewriter/writer_test.go @@ -24,7 +24,7 @@ func TestCompressOldFile(t *testing.T) { ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660) require.NoError(t, err) - for i := 0; i < 999; i++ { + for range 999 { f.WriteString("This is a test file\n") ng.WriteString("This is a test file\n") } diff --git a/modules/util/timer_test.go b/modules/util/timer_test.go index 602800c248..1f9a4ac586 100644 --- a/modules/util/timer_test.go +++ b/modules/util/timer_test.go @@ -12,19 +12,19 @@ import ( ) func TestDebounce(t *testing.T) { - var c int64 + var c atomic.Int64 d := Debounce(50 * time.Millisecond) - d(func() { atomic.AddInt64(&c, 1) }) - assert.EqualValues(t, 0, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) + d(func() { c.Add(1) }) + assert.EqualValues(t, 0, c.Load()) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 1, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - assert.EqualValues(t, 1, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) + assert.EqualValues(t, 1, c.Load()) + d(func() { c.Add(1) }) + assert.EqualValues(t, 1, c.Load()) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 2, atomic.LoadInt64(&c)) + assert.EqualValues(t, 2, c.Load()) } diff --git a/modules/util/truncate.go b/modules/util/truncate.go index 7207a89177..35836f745c 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -47,7 +47,7 @@ func SplitTrimSpace(input, sep string) []string { input = strings.ReplaceAll(input, "\r\n", "\n") var stringList []string - for _, s := range strings.Split(input, sep) { + for s := range strings.SplitSeq(input, sep) { // trim leading and trailing space stringList = append(stringList, strings.TrimSpace(s)) } diff --git a/modules/util/util_test.go b/modules/util/util_test.go index a85113b2f4..24fca75e7b 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -243,7 +243,7 @@ func TestGeneratingEd25519Keypair(t *testing.T) { // And another 32 bytes are required, which is included as random value // in the OpenSSH format. b := make([]byte, 64) - for i := 0; i < 64; i++ { + for i := range 64 { b[i] = byte(i) } rand.Reader = bytes.NewReader(b) diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 463e7e8f7a..23d0622de4 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -266,17 +266,17 @@ func addEmailBindingRules() { } func portOnly(hostport string) string { - colon := strings.IndexByte(hostport, ':') - if colon == -1 { + _, after, ok := strings.Cut(hostport, ":") + if !ok { return "" } - if i := strings.Index(hostport, "]:"); i != -1 { - return hostport[i+len("]:"):] + if _, after, ok := strings.Cut(hostport, "]:"); ok { + return after } if strings.Contains(hostport, "]") { return "" } - return hostport[colon+len(":"):] + return after } func validPort(p string) bool { diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 848fb70af5..ce451b8ff4 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -7,6 +7,7 @@ import ( "net" "net/url" "regexp" + "slices" "strings" "forgejo.org/modules/setting" @@ -40,12 +41,7 @@ func IsValidSiteURL(uri string) bool { return false } - for _, scheme := range setting.Service.ValidSiteURLSchemes { - if scheme == u.Scheme { - return true - } - } - return false + return slices.Contains(setting.Service.ValidSiteURLSchemes, u.Scheme) } // IsAPIURL checks if URL is current Gitea instance API URL diff --git a/modules/validation/validatable.go b/modules/validation/validatable.go index 1b0d4aa382..1751e727f3 100644 --- a/modules/validation/validatable.go +++ b/modules/validation/validatable.go @@ -6,6 +6,7 @@ package validation import ( "fmt" "reflect" + "slices" "strings" "unicode/utf8" @@ -87,10 +88,8 @@ func ValidateMaxLen(value string, maxLen int, name string) []string { } func ValidateOneOf(value any, allowed []any, name string) []string { - for _, allowedElem := range allowed { - if value == allowedElem { - return []string{} - } + if slices.Contains(allowed, value) { + return []string{} } return []string{fmt.Sprintf("Field %s contains the value %v, which is not in allowed subset %v", name, value, allowed)} } diff --git a/modules/web/handler.go b/modules/web/handler.go index 4a7f28b1fa..e3f0b029fd 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -17,7 +17,7 @@ import ( var responseStatusProviders = map[reflect.Type]func(req *http.Request) types.ResponseStatusProvider{} func RegisterResponseStatusProvider[T any](fn func(req *http.Request) types.ResponseStatusProvider) { - responseStatusProviders[reflect.TypeOf((*T)(nil)).Elem()] = fn + responseStatusProviders[reflect.TypeFor[T]()] = fn } // responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written @@ -49,9 +49,9 @@ func (r *responseWriter) WriteHeader(statusCode int) { } var ( - httpReqType = reflect.TypeOf((*http.Request)(nil)) - respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() - cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() + httpReqType = reflect.TypeFor[*http.Request]() + respWriterType = reflect.TypeFor[http.ResponseWriter]() + cancelFuncType = reflect.TypeFor[goctx.CancelFunc]() ) // preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 123eb29015..06bf55b571 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -30,7 +30,7 @@ func AssignForm(form any, data map[string]any) { typ := reflect.TypeOf(form) val := reflect.ValueOf(form) - for typ.Kind() == reflect.Ptr { + for typ.Kind() == reflect.Pointer { typ = typ.Elem() val = val.Elem() } @@ -51,7 +51,7 @@ func AssignForm(form any, data map[string]any) { } func getRuleBody(field reflect.StructField, prefix string) string { - for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + for rule := range strings.SplitSeq(field.Tag.Get("binding"), ";") { if strings.HasPrefix(rule, prefix) { return rule[len(prefix) : len(rule)-1] } @@ -99,7 +99,7 @@ func Validate(errs binding.Errors, data map[string]any, f any, l translation.Loc typ := reflect.TypeOf(f) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { typ = typ.Elem() } diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 4603e64052..c8bb8276c5 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -5,6 +5,7 @@ package middleware import ( "context" + "maps" "time" "forgejo.org/modules/setting" @@ -22,9 +23,7 @@ func (ds ContextData) GetData() ContextData { } func (ds ContextData) MergeFrom(other ContextData) ContextData { - for k, v := range other { - ds[k] = v - } + maps.Copy(ds, other) return ds } diff --git a/modules/web/route.go b/modules/web/route.go index ceb97ba333..dc83178f74 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -107,8 +107,8 @@ func (r *Route) Methods(methods, pattern string, h ...any) { middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { - methods := strings.Split(methods, ",") - for _, method := range methods { + methods := strings.SplitSeq(methods, ",") + for method := range methods { r.R.With(middlewares...).Method(strings.TrimSpace(method), fullPattern, handlerFunc) } } else { diff --git a/options/locale/locale_ca.ini b/options/locale/locale_ca.ini index e6f3cac5e2..5877fb4754 100644 --- a/options/locale/locale_ca.ini +++ b/options/locale/locale_ca.ini @@ -1,6 +1,6 @@ [common] home = Inici -dashboard = Panell de control +dashboard = Tauler de control explore = Explorar help = Ajuda logo = Logotip @@ -246,25 +246,25 @@ admin_password = Contrasenya err_empty_admin_password = La contrasenya de l'administrador no por ser buida. ssh_port = Por del servidor SSH disable_gravatar = Deshabilitar Gravatar -disable_registration = Deshabilitar l'auto-registre +disable_registration = Deshabilita l'auto-registre openid_signin = Habilita l'inici de sessió amb OpenID enable_captcha = Habilita el CAPTCHA al registre -default_keep_email_private = Amaga les direccions de correu per defecte +default_keep_email_private = Amaga les adreces de correu, per defecte app_slogan = Eslogan de la instància app_slogan_helper = Escriu l'eslogan de la teva instància aquí. Deixa buit per deshabilitar. -repo_path = Ruta de l'arrel del repositori +repo_path = Camí arrel del repositori log_root_path_helper = Els arxius dels registres es s'escriuran en aquest directori. optional_title = Configuracions opcionals host = Amfitrió lfs_path = Ruta arreal de Git LFS -run_user = Executar com a usuari +run_user = Executa com a usuari domain_helper = Domini o adreça de l'hosta per al servidor. http_port = Port d'escolta HTTP app_url_helper = Adreces base per a clonació HTTP(S) i notificacions per correu. log_root_path = Ruta dels registres smtp_from_invalid = L'adreça d'"Enviar correu com a" és invalida smtp_from_helper = L'adreça de correu que Forgejo utilitzarà. Entri el correu en pla o en format "Nom" . -register_confirm = Requereix confirmació de correu per a registrar-se +register_confirm = Requereix una confirmació de correu per a registrar-se disable_gravatar.description = Deshabilitar l'ús de Gravatar o d'altres serveis d'avatars de tercers. S'utilitzaran imatges per defecte per als avatars dels uauris fins que pujin el seu propi a la instància. federated_avatar_lookup.description = Cerca d'avatars amb Libravatar. allow_only_external_registration = Permet el registre només amb serveis externs @@ -272,8 +272,8 @@ allow_only_external_registration.description = Els usuaris nomes podràn crear n enable_captcha.description = Requereix als usuaris passar el CAPTCHA per a poder-se crear comptes. require_sign_in_view = Requereix inciar sessió per a veure el contingut de la instància default_keep_email_private.description = Habilita l'ocultament de les direccions de correu per a nous usuaris per defecte, amb tal que aquesta informació no sigui filtrada immediatament despres de registrar-se. -default_allow_create_organization = Per defecte permet crear organitzacions -default_enable_timetracking = Per defecta habilita el seguiment de temps +default_allow_create_organization = Permet crear organitzacions, per defecte +default_enable_timetracking = Habilita el seguiment de temps, per defecte default_enable_timetracking.description = Per defecte activa el de seguiment de temps als nous repositoris. admin_name = Nom d'usuari de l'administrador install_btn_confirm = Instaŀlar Forgejo @@ -287,7 +287,7 @@ env_config_keys = configuració de l'entorn db_type = Tipus de base de dades lfs_path_helper = Els arxius seguits per Git LFS es desaran en aquest directory. Deixa buit per deshabilitar. http_port_helper = Numero de port que utilitzarà el servidor web de Forgejo. -repo_path_helper = Els repositoris Git remotes es desaran en aquest diectori. +repo_path_helper = Els repositoris Git remots es desaran en aquest directori. run_user_helper = El nom d'usuari del sistema operatiu sota el que Forgejo s'executa. Notis que aquest usuari ha de tenir accés a la ruta arrel del repositori. ssh_port_helper = Numero del port que utilitzarà el servidor SSH. Deixa buit per deshablitar el servidor SSH. require_sign_in_view.description = Limita l'accès al contingut per als usuaris connectats. Els visitatnts només podran veure les pàgines d'autenticació. @@ -948,7 +948,7 @@ settings.basic_settings = Configuració bàsica settings.event_issues = Modificació settings.web_hook_name_msteams = Microsoft Teams settings.tracker_issue_style.numeric = Numèric -settings.allow_only_contributors_to_track_time = Permet només als contribuidors fer un seguiment del temps +settings.allow_only_contributors_to_track_time = Fes que només els contribuïdors puguin fer un seguiment de temps settings.admin_stats_indexer = Indexador d'estadístiques del codi settings.admin_code_indexer = Indexador del codi issue_labels = Etiquetes @@ -2189,8 +2189,8 @@ settings.use_external_wiki = Usar una wiki externa settings.external_wiki_url = URL de la wiki externa settings.external_wiki_url_error = L'URL de la wiki externa no és vàlida. settings.external_wiki_url_desc = Els visitants es redirigiran a l'URL de la wiki externa quan facin clic a la pestanya wiki. -settings.enable_timetracker = Activar el seguiment de temps -settings.pulls_desc = Activar les «pull requests» del repositori +settings.enable_timetracker = Habilita el seguiment de temps +settings.pulls_desc = Activa les «pull requests» del repositori settings.pulls.enable_autodetect_manual_merge = Activar la detecció automàtica de les fusions manuals (Nota: En alguns casos, la detecció es pot equivocar) settings.projects_desc = Activar els projectes del repositori settings.actions_desc = Activar la «pipeline» CI/CD integrada amb Forgejo Actions @@ -2610,6 +2610,11 @@ settings.web_hook_name_wechatwork = WeCom (Wechat Work) settings.lfs_pointers.found = S'ha trobat %d punters de blob - %d associats, %d no associats (%d no són a l'emmagatzematge) settings.lfs_pointers.associateAccessible = Associar %d OIDs accessibles +settings.matrix.access_token_helper = Es recomana muntar un compte de Matrix dedicat per això. El testimoni d'accés el podeu trobar al client web Element (en una pestanya privada/d'incògnit) > User menu (cantó superior esquerre) > All settings > Help & About > Advanced > Access Token (sota de l'URL del homeserver). Tanqueu la pestanya privada/d'incògnit (tancar la sessió invalidaria el testimoni). +settings.matrix.room_id_helper = L'ID de sala el podeu trobar al client web Element > Room Settings > Advanced > Internal room ID. Per exemple: %s. + +diff.parent = pare + [user] unblock = Desbloquejar followers_one = %d seguidor @@ -2853,6 +2858,344 @@ dashboard.system_status = Estat del sistema dashboard.operation_name = Nom d'operació dashboard.clean_unbind_oauth = Neteja les connexions OAuth desenllaçades +notices = Notificacions del sistema +dashboard.operation_switch = Intercanvia +dashboard.operation_run = Executar +dashboard.clean_unbind_oauth_success = S'han eliminat totes les connexions OAuth desenllaçades. +dashboard.task.started = Tasca iniciada: %[1]s +dashboard.task.process = Tasca: %[1]s +dashboard.task.cancelled = Tasca: %[1]s cancel·lada: %[3]s +dashboard.task.error = Error en la tasca: %[1]s: %[3]s +dashboard.task.finished = Tasca: %[1] iniciada per %[2s] ha finalitzat +dashboard.task.unknown = Tasca desconeguda: %[1]s +dashboard.cron.started = Cron iniciat: %[1]s +dashboard.cron.process = Cron: %[1]s +dashboard.cron.cancelled = Cron: %[1]s cancel·lat: %[3]s +dashboard.cron.error = Error amb Cron: %s: %[3]s +dashboard.sync_repo_branches = La sincronització ha ignorat branques de les dades Git a la base de dades +dashboard.check_repo_stats = Comprovar totes les estadístiques del repositori +dashboard.deleted_branches_cleanup = Netejar branques eliminades +dashboard.update_migration_poster_id = Actualitza les ID de l'autor de migració +dashboard.git_gc_repos = Recull la brossa de tots els repositoris +dashboard.resync_all_sshkeys = Actualitzar el fitxer ".ssh/authorized_keys" amb les claus SSH de Forgejo. +dashboard.resync_all_sshprincipals = Actualitzar el fitxer ".ssh/authorized_principals" amb els principals SSH de Forgejo. +dashboard.resync_all_hooks = Resincronitzar els ganxos Git de tots els repositoris (pre-receive, update, post-receive, proc-receive, …) +dashboard.reinit_missing_repos = Reinicialitza tots els repositoris Git que falten i que tenen registres +dashboard.sync_external_users = Sincronitzar dades d'usuari externes +dashboard.cleanup_hook_task_table = Netejar la taula hook_task +dashboard.cleanup_packages = Netejar paquets caducats +dashboard.cleanup_actions = Netejar registres i artefactes d'accions caducats +dashboard.server_uptime = Temps de funcionament del servidor +dashboard.current_goroutine = Goroutines actuals +dashboard.current_memory_usage = Ús de memòria actual +dashboard.total_memory_allocated = Total de memòria adjudicada +dashboard.memory_obtained = Memòria rebuda +dashboard.pointer_lookup_times = Temps de consulta dels punters +dashboard.memory_free_times = Alliberaments de memòria +dashboard.current_heap_usage = Ús del heap actual +dashboard.heap_memory_obtained = Memòria de heap rebuda +dashboard.heap_memory_idle = Memòria del heap en repòs +dashboard.heap_memory_in_use = Memòria del heap en ús +dashboard.heap_memory_released = Memòria del heap alliberada +dashboard.heap_objects = Objectes del heap +dashboard.bootstrap_stack_usage = Ús de l'arrancada de la pila (stack) +dashboard.stack_memory_obtained = Memòria de la pila (stack) rebuda +dashboard.mspan_structures_usage = Ús de les estructures MSpan +dashboard.mspan_structures_obtained = Estructures MSpan rebudes +dashboard.mcache_structures_usage = Ús de les estructures MCache +dashboard.mcache_structures_obtained = Estructures MCache rebudes +dashboard.gc_metadata_obtained = Metadades del GC rebudes +dashboard.next_gc_recycle = Proper cicle de GC +dashboard.last_gc_time = Temps des del darrer GC +dashboard.total_gc_pause = Pausa total de GC +dashboard.last_gc_pause = Darrera pausa de GC +dashboard.delete_old_actions = Eliminar totes les activitats antigues de la base de dades +dashboard.delete_old_actions.started = Eliminar totes les activitats antigues des que s'ha iniciat la base de dades. +dashboard.delete_old_system_notices = Eliminar totes les notificacions de sistema antigues de la base de dades +dashboard.gc_lfs = Recull la brossa d'objectes meta LFS +dashboard.stop_zombie_tasks = Aturar tasques d'accions zombi +dashboard.stop_endless_tasks = Aturar tasques d'accions infinites +dashboard.cancel_abandoned_jobs = Cancel·lar feines d'accions abandonades +dashboard.sync_branch.started = S'ha iniciat la sincronització de branca +dashboard.sync_tag.started = S'ha iniciat la sincronització d'etiquetes +dashboard.rebuild_issue_indexer = Reconstruir l'indexador d'incidències +users.user_manage_panel = Gestionar comptes d'usuari +users.new_account = Crear compte d'usuari +users.restricted = Restringit +users.reserved = Reservat +users.bot = Bot +users.remote = Remot +users.2fa = A2F +users.repos = Repos +users.created = Creats +users.last_login = Darrer inici de sessió +users.never_login = Mai ha iniciat sessió +users.send_register_notify = Notificar sobre el registre per correu +users.new_success = S'ha creat l'usuari "%s". +users.edit = Editar +users.auth_source = Font d'autenticació +users.local = Local +users.auth_login_name = Nom d'autenticació de sessió +users.password_helper = Deixeu la contrasenya buida si no la voleu canviar. +users.update_profile_success = S'ha actualitzat el compte d'usuari. +users.edit_account = Editar compte d'usuari +users.max_repo_creation = Nombre màxim de repositoris +users.max_repo_creation_desc = (Introduïu -1 per usar el límit global predeterminat.) +users.is_activated = Compte actiu +users.activated.description = Completar la verificació per correu. L'usuari d'un compte inactiu no podrà iniciar-hi sessió fins que n'hagi verificat el correu. +users.prohibit_login = Compte suspès +users.block.description = Fa que l'usuari no pugui interactuar amb aquest servei a través del seu compte i en prohibeix l'inici de sessió. +users.is_admin = Compte d'administrador +users.admin.description = Dona accés complet a totes les funcions d'administració a través de la interfície web i l'API. +users.is_restricted = Compte restringit +users.restricted.description = Només permet interactuar amb repositoris i organitzacions quan l'usuari n'és col·laborador. Això no permet l'accés als repositoris públics d'aquesta instància. +users.allow_git_hook = Pot crear ganxos Git +users.allow_git_hook_tooltip = Els ganxos Git s'executen com a l'usuari del sistema operatiu on corre Forgejo, i tenen el mateix nivell d'accés a l'amfitrió. Per tant, els usuaris amb aquest privilegi especial poden accedir i modificar tots els repositoris de Forgejo, així com la base de dades que fa servir Forgejo. Això també els permet obtenir privilegis d'administrador a Forgejo. +users.allow_import_local = Pot importar repositoris locals +users.local_import.description = Permet la importació de repositoris des del sistema de fitxers local del servidor. Això pot ser un problema de seguretat. +users.allow_create_organization = Pot crear organitzacions +users.organization_creation.description = Permet crear noves organitzacions. +users.update_profile = Actualitzar compte d'usuari +users.delete_account = Eliminar compte d'usuari +users.cannot_delete_self = No podeu eliminar-vos vós mateix +users.still_own_repo = Aquest usuari encara té un o més repositoris. Elimineu-los o transferiu-los abans. +users.still_has_org = Aquest usuari és membre d'una organització. Elimineu-lo de totes les organitzacions abans. +users.purge = Purgar usuari +users.purge_help = Elimina forçosament l'usuari i tots els seus repositoris, organitzacions i paquets. També s'eliminaran tots els seus comentaris i incidències. +users.still_own_packages = Aquest usuari encara té un o més paquets. Elimineu-los abans. +users.deletion_success = S'ha eliminat l'usuari. +users.reset_2fa = Restableix l'A2F +users.list_status_filter.menu_text = Filtrar +users.list_status_filter.reset = Restaurar +users.list_status_filter.is_active = Actiu +users.list_status_filter.not_active = Inactiu +users.list_status_filter.not_admin = No administrador +users.list_status_filter.is_restricted = Restringit +users.list_status_filter.not_restricted = No restringit +users.list_status_filter.is_prohibit_login = Inici de sessió prohibit +users.list_status_filter.not_prohibit_login = Inici de sessió permès +users.list_status_filter.is_2fa_enabled = A2F activada +users.list_status_filter.not_2fa_enabled = A2F desactivada +users.details = Detalls d'usuari +emails.email_manage_panel = Gestionar correus d'usuari +emails.primary = Principal +emails.filter_sort.email_reverse = Correu (invers) +emails.filter_sort.name_reverse = Nom d'usuari (invers) +emails.updated = Correu actualitzat +emails.not_updated = No s'ha pogut actualitzar l'adreça de correu: %v +emails.duplicate_active = Aquesta adreça de correu ja pertany a un altre usuari. +emails.change_email_header = Actualitzar propietats del correu +emails.change_email_text = Esteu segur que voleu modificar aquesta adreça de correu? +emails.delete = Eliminar adreça de correu +emails.delete_desc = Esteu segur que voleu eliminar aquesta adreça de correu? +emails.deletion_success = S'ha eliminat l'adreça de correu. +emails.delete_primary_email_error = No podeu eliminar l'adreça de correu principal. +orgs.org_manage_panel = Gestionar organitzacions +repos.repo_manage_panel = Gestionar repositoris +repos.unadopted = Repositoris no-adoptats +repos.unadopted.no_more = No s'han trobat repositoris no-adoptats. +repos.owner = Propietari +repos.lfs_size = Mida LFS +packages.package_manage_panel = Gestionar paquets +packages.total_size = Mida total: %s +packages.unreferenced_size = Mida sense referenciar: %s +packages.cleanup = Netejar les dades caducades +packages.cleanup.success = S'han netejat les dades caducades satisfactòriament +packages.owner = Propietari +packages.creator = Creador +packages.published = Publicat +defaulthooks = Webhooks predeterminats +defaulthooks.desc = Els webhooks fan una petició automàtica HTTP POST a un servidor quan s'activen alguns esdeveniments de Forgejo. Els webhooks definits aquí són predeterminats i es copiaran a tots els nous repositoris. Llegiu-ne més en la guia de webhooks. +defaulthooks.add_webhook = Afegir webhook per defecte +defaulthooks.update_webhook = Actualitzar webhook per defecte +systemhooks = Webhooks del sistema +systemhooks.desc = Els webhooks fan una petició automàtica HTTP POST a un servidor quan s'activen alguns esdeveniments de Forgejo. Els webhooks definits aquí s'aplicaran a tots els repositoris del sistema, així que considereu l'impacte que poden tenir en el rendiment. Llegiu-ne més en la guia de webhooks. +systemhooks.add_webhook = Afegir webhook de sistema +systemhooks.update_webhook = Actualitzar webhook de sistema +auths.auth_manage_panel = Gestionar fonts d'autenticació +auths.new = Afegir font d'autenticació +auths.syncenabled = Activar la sincronització d'usuaris +auths.updated = Actualitzat +auths.auth_type = Tipus d'autenticació +auths.auth_name = Nom d'autenticació +auths.security_protocol = Protocol de seguretat +auths.bind_dn = DN bind +auths.bind_password = Contrasenya bind +auths.user_base = Base de cerca d'usuari +auths.user_dn = DN d'usuari +auths.attribute_username = Atribut de nom d'usuari +auths.attribute_username_placeholder = Deixeu-ho buit per usar el nom d'usuari de Forgejo. +auths.attribute_name = Atribut de nom +auths.attribute_surname = Atribut de cognom +auths.attribute_mail = Atribut de correu electrònic +auths.attribute_ssh_public_key = Atribut de clau pública SSH +auths.attribute_avatar = Atribut d'avatar +auths.attributes_in_bind = Recupera els atributs del context bind DN +auths.default_domain_name = Nom de domini per defecte usat per l'adreça de correu +auths.allow_deactivate_all = Permet que un resultat de cerca buit desactivi tots els usuaris +auths.use_paged_search = Usar cerca en pàgines +auths.search_page_size = Mida de pàgina +auths.filter = Filtre d'usuari +auths.admin_filter = Filtre d'administrador +auths.restricted_filter = Filtre restringit +auths.restricted_filter_helper = Deixeu-ho buit per no marcar cap usuari com a restringit. Useu un asterisc ("*") per marcar tots els usuaris que no coincideixin amb el Filtre d'administrador com a restringits. +auths.verify_group_membership = Verificar l'adhesió de grup a LDAP (deixeu el filtre buit per ignorar-ho) +auths.group_search_base = DN base de cerca de grup +auths.group_attribute_list_users = Atribut de grup amb la llista d'usuaris +auths.user_attribute_in_group = Atribut d'usuari llistat en el grup +auths.map_group_to_team = Assigna els grups LDAP a equips d'organització (deixeu-ho buit per saltar-ho) +auths.map_group_to_team_removal = Elimina els usuaris dels equips sincronitzats si no pertanyen al grup LDAP corresponent +auths.enable_ldap_groups = Activar grups LDAP +auths.smtp_auth = Tipus d'autenticació SMTP +auths.smtphost = Amfitrió SMTP +auths.smtpport = Port SMTP +auths.allowed_domains = Dominis permesos +auths.allowed_domains_helper = Deixeu-ho buit per permetre tots els dominis. Separeu múltiples dominis amb una coma (","). +auths.skip_tls_verify = Salta't la verificació TLS +auths.force_smtps = Força l'SMTPS +auths.force_smtps_helper = L'SMTPS sempre usa el port 465. Indiqueu això per forçar l'SMTPS en un altre port. (Sinó s'usarà l'STARTTLS en altres ports, sempre que l'amfitrió ho suporti.) +auths.helo_hostname = Hostname HELO +auths.helo_hostname_helper = Hostname enviat amb HELO. Deixeu-ho en blanc per enviar el hostname actual. +auths.disable_helo = Deshabilita HELO +auths.pam_service_name = Nom de servei PAM +auths.pam_email_domain = Domini de correu PAM (opcional) +auths.oauth2_provider = Proveïdor OAuth2 +auths.oauth2_icon_url = URL d'icona +auths.oauth2_clientID = ID de client (clau) +auths.oauth2_clientSecret = Secret de client +auths.openIdConnectAutoDiscoveryURL = URL d'OpenID Connect Auto Discovery +auths.oauth2_use_custom_url = Usar URL personalitzades enlloc d'URL predeterminades +auths.oauth2_tokenURL = URL de testimoni (token) +auths.oauth2_authURL = URL d'autorització +auths.oauth2_profileURL = URL de perfil +auths.oauth2_emailURL = URL de correu +auths.skip_local_two_fa = Salta l'A2F local +auths.skip_local_two_fa_helper = No marcar aquesta opció farà que els usuaris que tenen l'A2F definida igualment hauran de passar l'A2F per iniciar sessió +auths.oauth2_tenant = Tenant +auths.oauth2_scopes = Àmbits addicionals +auths.oauth2_required_claim_value_helper = Assigneu aquest valor per restringir l'inici de sessió des d'aquesta font als usuaris que declarin aquest nom i valor +auths.oauth2_map_group_to_team_removal = Elimina els usuaris dels equips sincronitzats si no pertanyen al grup corresponent. +auths.tips = Consells +auths.tips.gmail_settings = Configuracions de Gmail: +auths.tips.oauth2.general = Autenticació OAuth2 +auths.tips.oauth2.general.tip = Quan registreu una nova autenticació OAuth2, l'URL de crida/redirecció serà: +auths.tip.oauth2_provider = Proveïdor OAuth2 +auths.tip.bitbucket = Registreu un nou consumidor OAuth2 a %s i afegiu els permisos "Compte" - "Lectura" +auths.tip.nextcloud = Registreu un nou consumidor OAuth en la vostra instància amb el menú "Configuració -> Seguretat -> Client OAuth 2.0" +auths.tip.dropbox = Crea una nova aplicació a %s +auths.tip.facebook = Registreu una nova aplicació a %s i afegiu el producte "Facebook Login" +auths.tip.github = Registra una aplicació OAuth nova a %s +auths.tip.gitlab_new = Registra una aplicació nova a %s +auths.tip.google_plus = Obté les credencials del client OAuth2 amb la consola del Google API a %s +auths.tip.openid_connect = Usa l'URL d'OpenID Connect Discovery (/.well-known/openid-configuration) per a especificar els endpoints +auths.tip.mastodon = Introduïu l'URL a una instància diferent de Mastodon amb la que voleu autenticar-vos (o bé useu la instància per defecte) +auths.edit = Editar font d'autenticació +auths.activated = S'ha activat aquesta font d'autenticació +auths.new_success = S'ha afegit l'autenticació "%s". +auths.update_success = S'ha actualitzat la font d'autenticació. +auths.update = Actualitzar font d'autenticació +auths.delete = Eliminar font d'autenticació +auths.delete_auth_title = Eliminar font d'autenticació +auths.delete_auth_desc = Eliminar una font d'autenticació fa que els usuaris que en fan ús no puguin iniciar sessió. Continuar? +auths.still_in_used = Aquesta font d'autenticació encara s'utilitza. Convertiu o elimineu els usuaris que en fan ús abans. +auths.deletion_success = S'ha eliminat la font d'autenticació. +auths.login_source_exist = La font d'autenticació "%s" ja existeix. +auths.unable_to_initialize_openid = No s'ha pogut inicialitzar el proveïdor d'OpenID Connect: %s +auths.invalid_openIdConnectAutoDiscoveryURL = URL d'Auto Discovery invàlida (ha de ser una URL vàlida començant amb http:// o https://) +config.server_config = Configuració de servidor +config.app_name = Títol de la instància +config.app_slogan = Eslògan de la instància +config.app_ver = Versió de Forgejo +config.app_url = URL base +config.custom_conf = Camí al fitxer de configuració +config.custom_file_root_path = Camí arrel dels fitxers personalitzada +config.domain = Domini del servidor +config.offline_mode = Mode local +config.disable_router_log = Desactiva els registres de l'encaminador +config.run_user = Executa com a usuari +config.run_mode = Mode d'execució +config.git_version = Versió de Git +config.app_data_path = Camí a les dades d'aplicació +config.repo_root_path = Camí arrel del repositori +config.log_file_root_path = Camí de registres +config.script_type = Tipus d'script +config.reverse_auth_user = Usuari d'autenticació al servidor intermediari revers +config.ssh_config = Configuració SSH +config.ssh_start_builtin_server = Usa el servidor integrat +config.ssh_domain = Domini del servidor SSH +config.ssh_listen_port = Port d'escolta +config.ssh_root_path = Camí arrel +config.ssh_key_test_path = Camí al test de clau +config.ssh_keygen_path = Camí keygen ("ssh-keygen") +config.ssh_minimum_key_size_check = Comprovació mínima de la mida de clau +config.ssh_minimum_key_sizes = Mides mínimes de clau +config.lfs_config = Configuració LFS +config.lfs_content_path = Camí de contingut LFS +config.lfs_http_auth_expiry = Temps de caducitat d'autenticació HTTP de l'LFS +config.db_config = Configuració de la base de dades +config.service_config = Configuració del servei +config.register_email_confirm = Requereix una confirmació de correu per a registrar-se +config.disable_register = Deshabilita l'auto-registre +config.allow_only_internal_registration = Permet el registre només a través de Forgejo +config.allow_only_external_registration = Permet el registre només a través de serveis externs +config.enable_openid_signup = Habilita l'auto-registre amb OpenID +config.enable_openid_signin = Habilita l'inici de sessió amb OpenID +config.show_registration_button = Mostra el botó de registre +config.require_sign_in_view = Requereix iniciar sessió per veure el contingut +config.mail_notify = Habilita les notificacions per correu +config.enable_captcha = Habilita el CAPTCHA +config.active_code_lives = Temps de caducitat del codi d'activació +config.reset_password_code_lives = Temps de caducitat del codi de recuperació +config.default_keep_email_private = Amaga les adreces de correu, per defecte +config.default_allow_create_organization = Permet crear organitzacions, per defecte +config.enable_timetracking = Habilita el seguiment de temps +config.default_enable_timetracking = Habilita el seguiment de temps, per defecte +config.allow_dots_in_usernames = Permet que el usuaris utilitzin punts en el nom d'usuari. Això no afecta als comptes que ja existeixen. +config.default_allow_only_contributors_to_track_time = Fes que només els contribuïdors puguin fer un seguiment de temps +config.no_reply_address = Domini del correu ocult +config.default_visibility_organization = Visibilitat per defecte de les noves organitzacions +config.default_enable_dependencies = Habilita les dependències d'incidències, per defecte +config.webhook_config = Configuració dels Webhooks +config.queue_length = Longitud de la cua +config.deliver_timeout = Temps d'espera d'entrega +config.skip_tls_verify = Salta't la verificació TLS +config.mailer_config = Configuració del remitent +config.mailer_enable_helo = Habilita HELO +config.mailer_protocol = Protocol +config.mailer_smtp_addr = Amfitrió SMTP +config.mailer_smtp_port = Port SMTP +config.mailer_user = Usuari +config.mailer_use_sendmail = Usa Sendmail +config.mailer_sendmail_path = Camí de Sendmail +config.mailer_sendmail_args = Arguments extra per Sendmail +config.mailer_sendmail_timeout = Temps d'espera per Sendmail +config.test_email_placeholder = Correu electrònic (ex. test@example.com) +config.send_test_mail = Envia un correu de prova +config.send_test_mail_submit = Envia +config.test_mail_failed = No s'ha pogut enviar el correu de prova a "%s": %v +config.test_mail_sent = S'ha enviat un correu de prova a "%s". +config.cache_config = +config.cache_adapter = Adaptador de la memòria cau +config.cache_interval = Interval de la memòria cau +config.cache_conn = Connexió de la memòria cau +config.cache_item_ttl = TTL dels ítems de la memòria cau +config.cache_test = Prova la memòria cau +config.cache_test_failed = No s'ha pogut examinar la memòria cau: %v. +config.cache_test_slow = La memòria cau s'ha provat correctament, però la resposta és lenta: %s. +config.cache_test_succeeded = La memòria cau s'ha provat correctament, s'ha rebut una resposta en %s. +config.session_config = Configuració de la sessió +config.session_provider = Proveïdor de la sessió +config.provider_config = Configuració del proveïdor +config.cookie_name = Nom de la galeta +config.gc_interval_time = Interval de temps del GC +config.session_life_time = Temps de vida de la sessió +config.https_only = Només HTTPS +config.picture_config = Configuració d'imatge i avatar +config.disable_gravatar = Deshabilita Gravatar +config.enable_federated_avatar = Habilita els avatars federats + +dashboard.update_checker = Comprovador d'actualització + [actions] variables = Variables variables.management = Gestionar variables diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index f1893024ce..6904af13d0 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -609,7 +609,7 @@ organization_leave_success=Úspěšně jste opustili organizaci %s. invalid_ssh_key=Nepodařilo se ověřit váš klíč SSH: %s invalid_gpg_key=Nepodařilo se ověřit váš klíč GPG: %s -invalid_ssh_principal=Neplatný SSH Principal certifikát: %s +invalid_ssh_principal=Neplatný principal: %s must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč. unable_verify_ssh_key=Nepodařilo se ověřit klíč SSH, zkontrolujte, zda neobsahuje chyby. auth_failed=Ověření selhalo: %v @@ -805,7 +805,7 @@ key_content_gpg_placeholder=Začíná s „-----BEGIN PGP PUBLIC KEY BLOCK----- add_new_principal=Přidat principal ssh_key_been_used=Tento klíč SSH byl na server již přidán. ssh_key_name_used=U vašeho účtu již existuje klíč SSH se stejným názvem. -ssh_principal_been_used=Tento SSH Principal certifikát již byl přidán na server. +ssh_principal_been_used=Tento principal již byl přidán na server. gpg_key_id_used=Veřejný klíč GPG se stejným ID již existuje. gpg_no_key_email_found=Tento klíč GPG neodpovídá žádné aktivované e-mailové adrese spojené s vaším účtem. Může být stále přidán, pokud podepíšete zadaný token. gpg_key_matched_identities=Odpovídající identity: @@ -847,7 +847,7 @@ gpg_key_deletion_desc=Odstraněním klíče GPG zneplatníte ověření revizí, ssh_principal_deletion_desc=Odstranění SSH Principal certifikátu zruší jeho přístup k vašemu účtu. Pokračovat? ssh_key_deletion_success=Klíč SSH byl odstraněn. gpg_key_deletion_success=Klíč GPG byl odstraněn. -ssh_principal_deletion_success=SSH Principal certifikát byl odstraněn. +ssh_principal_deletion_success=Principal byl odstraněn. added_on=Přidáno %s valid_until_date=Platné do %s valid_forever=Platné navždy @@ -857,7 +857,7 @@ can_read_info=Čtení can_write_info=Zápis key_state_desc=Tento klíč byl použit během posledních 7 dní token_state_desc=Tento token byl použit během posledních 7 dní -principal_state_desc=Tento SSH Principal certifikát byl použit během posledních 7 dní +principal_state_desc=Tento principal certifikát byl použit během posledních 7 dní show_openid=Zobrazit na profilu hide_openid=Odstranit z profilu ssh_disabled=SSH je zakázáno @@ -2381,7 +2381,7 @@ settings.matrix.room_id=ID místnosti settings.matrix.message_type=Typ zprávy settings.archive.button=Archivovat repozitář settings.archive.header=Archivovat tento repozitář -settings.archive.text = Archivováním repozitáře jej celý převedete do stavu pouze pro čtení. Bude skryt z nástěnky. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat problémy a žádosti o sloučení. +settings.archive.text = Archivováním repozitáře jej celý převedete do stavu pouze pro čtení. Bude skryt z nástěnky. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat problémy a žádosti o sloučení. Je doporučena dokumentace důvodu archivace pro budoucí vývojáře, kteří chtějí vytvořit fork repozitáře. settings.archive.success=Repozitář byl úspěšně archivován. settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů. settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c34f152934..541aa82833 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -80,7 +80,7 @@ rerun_all=Alle Jobs neu starten save=Speichern add=Hinzufügen add_all=Alle hinzufügen -remove=Löschen +remove=Entfernen remove_all=Alle entfernen remove_label_str=Element „%s“ entfernen edit=Bearbeiten @@ -100,7 +100,7 @@ copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden write=Verfassen preview=Vorschau -loading=Laden … +loading=Wird geladen … error=Fehler error404=Die Seite, die du versuchst aufzurufen, existiert nicht oder wurde entfernt, oder du bist nicht berechtigt, diese anzusehen. @@ -202,7 +202,7 @@ table_modal.label.columns = Spalten link_modal.header = Einen Link hinzufügen link_modal.url = URL link_modal.description = Beschreibung -link_modal.paste_reminder = Hinweis: Wenn du einen URL in der Zwischenablage hast, kannst du durch Einfügen im Editor direkt einen Link erstellen. +link_modal.paste_reminder = Hinweis: Wenn du eine URL in der Zwischenablage hast, kannst du durch Einfügen im Editor direkt einen Link erstellen. [filter] string.asc=A–Z @@ -257,7 +257,7 @@ err_admin_name_is_invalid=Administratornutzername ist ungültig general_title=Allgemeine Einstellungen app_name=Instanztitel -app_name_helper=Hier Ihren Instanznamen eingeben. Er wird auf jeder Seite angezeigt. +app_name_helper=Gib hier den Instanznamen ein. Er wird auf jeder Seite angezeigt. repo_path=Repository-Verzeichnis repo_path_helper=Remote-Git-Repositorys werden in diesem Verzeichnis gespeichert. lfs_path=Git-LFS-Wurzelpfad @@ -287,7 +287,7 @@ register_confirm=E-Mail-Bestätigung benötigt zum Registrieren mail_notify=E-Mail-Benachrichtigungen aktivieren server_service_title=Sonstige Server- und Drittserviceeinstellungen offline_mode=Offline-Modus aktivieren -offline_mode.description=Drittanbieter-CDNs deaktivieren und alle Ressourcen lokal bereitstellen. +offline_mode.description=Content Delivery Networks von Drittanbietern deaktivieren und alle Ressourcen lokal bereitstellen. disable_gravatar=Gravatar deaktivieren disable_gravatar.description=Gravatar und andere Drittanbieter-Avatar-Quellen deaktivieren. Ein Standardavatar wird verwendet, bis der Nutzer einen eigenen Avatar auf deren Instanz hochlädt. federated_avatar_lookup=Föderierte Profilbilder einschalten @@ -332,7 +332,7 @@ no_reply_address=Versteckte E-Mail-Domain no_reply_address_helper=Domain-Name für Benutzer mit einer versteckten Emailadresse. Zum Beispiel wird der Benutzername „Joe“ in Git als „joe@noreply.example.org“ protokolliert, wenn die versteckte E-Mail-Domain „noreply.example.org“ festgelegt ist. password_algorithm=Passwort-Hashing-Algorithmus invalid_password_algorithm=Ungültiger Passwort-Hash-Algorithmus -password_algorithm_helper=Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein. +password_algorithm_helper=Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der Argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein. enable_update_checker=Aktualisierungsprüfung aktivieren env_config_keys=Umgebungskonfiguration env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: @@ -385,10 +385,10 @@ remember_me=Dieses Gerät speichern forgot_password_title=Passwort vergessen forgot_password=Passwort vergessen? sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen! -confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an %s gesendet. Um den Registrierungsprozess abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von: %s. Falls die E-Mail inkorrekt sein sollte, kannst du dich einloggen und anfragen, eine weitere Bestätigungs-E-Mail an eine andere Adresse zu senden. +confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an %s gesendet. Um den Registrierung abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von: %s. Falls die E-Mail inkorrekt sein sollte, kannst du dich einloggen und anfragen, eine weitere Bestätigungs-E-Mail an eine andere Adresse zu senden. must_change_password=Aktualisiere dein Passwort allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen) -reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an %s gesendet. Um den Kontowiederherstellungsprozess abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von %s. +reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an %s gesendet. Um den Kontowiederherstellung abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von %s. active_your_account=Aktiviere dein Konto account_activated=Konto wurde aktiviert prohibit_login=Das Konto ist gesperrt @@ -448,7 +448,7 @@ back_to_sign_in = Zurück zur Anmeldung sign_in_openid = Mit OpenID fortfahren hint_login = Hast du bereits ein Konto? Jetzt anmelden! hint_register = Brauchst du ein Konto? Jetzt registrieren. -unauthorized_credentials = Die Zugangsdaten sind inkorrekt oder abgelaufen. Versuchen es erneut oder siehe %s für mehr Informationen +unauthorized_credentials = Die Zugangsdaten sind inkorrekt oder abgelaufen. Versuche es erneut oder siehe %s für mehr Informationen use_onetime_code = Einen Einmal-Code benutzen [mail] @@ -634,7 +634,7 @@ Biography = Biografie Website = Webseite Location = Ort To = Branchname -AccessToken = Zugangstoken +AccessToken = Zugriffstoken username_claiming_cooldown = Der Benutzername kann nicht beansprucht werden, weil seine Schutzzeit noch nicht vorbei ist. Er kann am %[1]s beansprucht werden. email_domain_is_not_allowed = Die Domain der E-Mail-Adresse des Benutzers %s steht in Konflikt mit EMAIL_DOMAIN_ALLOWLIST oder EMAIL_DOMAIN_BLOCKLIST. Bitte stelle sicher, dass du die E-Mail-Adresse richtig gesetzt hast. @@ -664,10 +664,10 @@ form.name_pattern_not_allowed=Das Muster „%s“ ist nicht in einem Benutzernam form.name_chars_not_allowed=Benutzername „%s“ enthält ungültige Zeichen. block_user = Benutzer blockieren block_user.detail = Bitte beachte, dass die Blockierung eines Benutzers auch andere Auswirkungen hat, so wie: -block_user.detail_2 = Dieser Benutzer wird nicht mehr nicht mit deinen Repositorys oder von dir erstellten Issues und Kommentaren interagieren können. +block_user.detail_2 = Dieser Benutzer wird nicht mehr mit deinen Repositorys oder von dir erstellten Issues und Kommentaren interagieren können. block_user.detail_1 = Ihr werdet euch nicht mehr gegenseitig folgen und es auch nicht mehr können. block = Blockieren -follow_blocked_user = Du kannst diesen Benutzer nicht folgen, weil du ihn blockiert hast, oder er dich blockiert hat. +follow_blocked_user = Du kannst diesem Benutzer nicht folgen, weil du ihn blockiert hast oder er dich blockiert hat. block_user.detail_3 = Ihr werdet nicht mehr in der Lage sein, euch gegenseitig als Repository-Mitarbeiter hinzuzufügen. unblock = Nicht mehr blockieren followers_one = %d Follower @@ -741,7 +741,7 @@ comment_type_group_issue_ref=Issue-Referenz saved_successfully=Die Einstellungen wurden erfolgreich gespeichert. privacy=Datenschutz keep_activity_private=Aktivität auf der Profilseite ausblenden -lookup_avatar_by_mail=Profilbild anhand der E-Mail-Addresse suchen +lookup_avatar_by_mail=Profilbild anhand der E-Mail-Adresse suchen enable_custom_avatar=Benutzerdefiniertes Profilbild verwenden choose_new_avatar=Neues Profilbild auswählen update_avatar=Profilbild aktualisieren @@ -763,7 +763,7 @@ manage_emails=E-Mail-Adressen verwalten manage_themes=Standard-Theme manage_openid=OpenID-Adressen email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet. -theme_desc=Dieses Thema wird für die Weboberfläche verwendet, wenn du angemeldet bist. +theme_desc=Dieses Theme wird für die Weboberfläche verwendet, wenn du angemeldet bist. primary=Primär activated=Aktiviert requires_activation=Erfordert Aktivierung @@ -773,7 +773,7 @@ activations_pending=Aktivierung ausstehend can_not_add_email_activations_pending=Es gibt eine ausstehende Aktivierung, versuche es in ein paar Minuten erneut, wenn du eine neue E-Mail hinzufügen möchtest. delete_email=Löschen email_deletion=E-Mail-Adresse entfernen -email_deletion_desc=Diese E-Mail-Adresse und die damit verbundenen Informationen werden von deinem Konto entfernt. Git-Commits von dieser E-Mail-Addresse bleiben unverändert. Fortfahren? +email_deletion_desc=Diese E-Mail-Adresse und die damit verbundenen Informationen werden von deinem Konto entfernt. Git-Commits von dieser E-Mail-Adresse bleiben unverändert. Fortfahren? email_deletion_success=Die E-Mail-Adresse wurde entfernt. theme_update_success=Deine Theme-Auswahl wurde gespeichert. theme_update_error=Das ausgewählte Theme existiert nicht. @@ -808,7 +808,7 @@ ssh_key_been_used=Dieser SSH-Schlüssel wird auf diesem Server bereits verwendet ssh_key_name_used=Ein gleichnamiger SSH-Key existiert bereits in deinem Account. ssh_principal_been_used=Diese Identität ist bereits auf dem Server vorhanden. gpg_key_id_used=Ein öffentlicher GPG-Schlüssel mit der gleichen ID existiert bereits. -gpg_no_key_email_found=Dieser GPG-Schlüssel entspricht keiner mit deinem Konto verbundenen aktivierten E-Mail-Addresse. Er kann trotzdem hinzugefügt werden, wenn du den gegebenen Token signierst. +gpg_no_key_email_found=Dieser GPG-Schlüssel entspricht keiner mit deinem Konto verbundenen aktivierten E-Mail-Adresse. Er kann trotzdem hinzugefügt werden, wenn du den gegebenen Token signierst. gpg_key_matched_identities=Passende Identitäten: gpg_key_matched_identities_long=Die eingebetteten Identitäten in diesem Schlüssel stimmen mit den folgenden aktivierten E-Mail-Adressen für diesen Benutzer überein. Commits, die mit diesen E-Mail-Addressen committed wurden, können mit diesem Schlüssel verifiziert werden. gpg_key_verified=Verifizierter Schlüssel @@ -825,9 +825,9 @@ ssh_key_verified=Verifizierter Schlüssel ssh_key_verified_long=Der Schlüssel wurde mit einem Token verifiziert. Er kann verwendet werden, um Commits zu verifizieren, die mit irgendeiner für diesen Nutzer aktivierten E-Mail-Adresse und irgendeiner Identität dieses Schlüssels übereinstimmen. ssh_key_verify=Verifizieren ssh_invalid_token_signature=Der gegebene SSH-Schlüssel, Signatur oder Token stimmen nicht überein oder der Token ist veraltet. -ssh_token_required=Sie müssen eine Signatur für das Token unten angeben +ssh_token_required=Du musst eine Signatur für das folgende Token angeben ssh_token=Token -ssh_token_help=Sie können eine Signatur wie folgt generieren: +ssh_token_help=Du kannst eine Signatur wie folgt generieren: ssh_token_signature=SSH-Textsignatur (armored signature) key_signature_ssh_placeholder=Beginnt mit „-----BEGIN SSH SIGNATURE-----“ verify_ssh_key_success=SSH-Schlüssel „%s“ wurde verifiziert. @@ -864,7 +864,7 @@ hide_openid=Nicht im Profil anzeigen ssh_disabled=SSH ist deaktiviert ssh_signonly=SSH ist derzeit deaktiviert, sodass diese Schlüssel nur zur Commit-Signaturverifizierung verwendet werden. ssh_externally_managed=Dieser SSH-Schlüssel wird extern für diesen Benutzer verwaltet -manage_access_token=Zugriffstokens +manage_access_token=Zugriffstoken generate_new_token=Neuen Token erzeugen tokens_desc=Diese Tokens gewähren vollen Zugriff auf dein Konto via Forgejo-API. token_name=Token-Name @@ -917,7 +917,7 @@ revoke_oauth2_grant=Autorisierung widerrufen revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher? revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen. -twofa_desc=Um dein Konto gegen Passwortdiebstahl zu schützen, kannst du eine Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter („TOTP“) zu erhalten. +twofa_desc=Um dein Konto gegen Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter („TOTP“) zu erhalten. twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung eingeschaltet. twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet. twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren @@ -935,7 +935,7 @@ passcode_invalid=Die PIN ist falsch. Probiere es erneut. twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen einmalig verwendbaren Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird. twofa_failed_get_secret=Fehler beim Abrufen des Geheimnisses. -webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard „WebAuthn“ unterstützen. +webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard „WebAuthn“ unterstützen. webauthn_register_key=Sicherheitsschlüssel hinzufügen webauthn_nickname=Nickname webauthn_delete_key=Sicherheitsschlüssel entfernen @@ -974,7 +974,7 @@ visibility.limited_tooltip=Nur für angemeldete Benutzer sichtbar visibility.private=Privat visibility.private_tooltip=Sichtbar nur für Mitglieder von Organisationen, denen du beigetreten bist user_block_success = Dieser Benutzer wurde erfolgreich blockiert. -twofa_recovery_tip = Falls du dein Gerät verlierst, wirst du in der Lage sein, einen einmalig verwendbaren Wiederherstellungsschlüssel zu verwenden, um den auf dein Konto wiederherzustellen. +twofa_recovery_tip = Falls du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel benutzen, um den Zugriff auf dein Konto wiederherzustellen. webauthn_alternative_tip = Du möchtest vielleicht eine zusätzliche Authentifizierungsmethode einrichten. blocked_users_none = Keine Benutzer blockiert. webauthn_key_loss_warning = Falls du deine Sicherheitsschlüssel verlierst, wirst du Zugang zu deinem Konto verlieren. @@ -990,7 +990,7 @@ additional_repo_units_hint_description = Einen „Mehr aktivieren“-Hinweis fü pronouns = Pronomen pronouns_unspecified = Nicht spezifiziert language.title = Standardsprache -keep_activity_private.description = Deine öffentliche Aktivität wird nur für dich selbst und die Instanzadminstratoren sichtbar sein. +keep_activity_private.description = Deine öffentliche Aktivität wird nur für dich selbst und die Instanzadministratoren sichtbar sein. language.localization_project = Hilf uns, Forgejo in deine Sprache zu übersetzen! Mehr erfahren. language.description = Diese Sprache wird in deinem Konto gespeichert und standardmäßig nach dem Anmelden benutzt. user_block_yourself = Du kannst dich nicht selbst blockieren. @@ -1019,10 +1019,10 @@ quota.sizes.assets.attachments.all = Anhänge quota.sizes.assets.packages.all = Pakete quota.sizes.wiki = Wiki regenerate_token_success = Der Token wurde regeneriert. Anwendungen, die ihn benutzen, haben nicht länger Zugriff auf dein Konto und müssen mit dem neuen Token aktualisiert werden. -access_token_regeneration = Zugangstoken regenerieren -access_token_regeneration_desc = Einen Token zu regenerieren, wird den Zugriff auf dein Konto von Anwendungen, die ihn nutzen, zurückziehen. Dies kann nicht rückgängig gemacht werden. Fortsetzen? -regenerate_token = Regenerieren -ssh_token_help_ssh_agent = , oder, falls Sie einen SSH-Agenten benutzen (mit der Variable SSH_AUTH_SOCK gesetzt): +access_token_regeneration = Zugriffstoken neu generieren +access_token_regeneration_desc = Wenn du einen Token neu generierst, verlieren Anwendungen, die ihn nutzen, den Zugriff auf dein Konto. Dies kann nicht rückgängig gemacht werden. Fortfahren? +regenerate_token = Neu generieren +ssh_token_help_ssh_agent = , oder, falls du einen SSH-Agenten benutzt (mit der Variable SSH_AUTH_SOCK gesetzt): [repo] owner=Besitzer @@ -1082,7 +1082,7 @@ mirror_address_desc=Gib alle erforderlichen Anmeldedaten im Abschnitt „Authent mirror_address_url_invalid=Die angegebene URL ist ungültig. Achte darauf, dass alle Komponenten der URL korrekt escapt wurden. mirror_address_protocol_invalid=Die angegebene URL ist ungültig. Nur Orte mit „http(s)://“ oder „git://“ können fürs Spiegeln benutzt werden. mirror_lfs=Großdatei-Speicher (LFS) -mirror_lfs_desc=Spiegeln von LFS-Dateien aktivieren. +mirror_lfs_desc=Spiegeln von LFS-Daten aktivieren. mirror_lfs_endpoint=LFS-Endpunkt mirror_lfs_endpoint_desc=Sync wird versuchen, die Klon-URL zu verwenden, um den LFS-Server zu bestimmen. Du kannst auch einen eigenen Endpunkt angeben, wenn die LFS-Dateien woanders gespeichert werden. mirror_last_synced=Zuletzt synchronisiert @@ -1141,7 +1141,7 @@ template.invalid=Es muss ein Vorlagen-Repository ausgewählt werden archive.title=Dieses Repository ist archiviert. Du kannst Dateien ansehen und es klonen, kannst aber seinen Status nicht verändern, zum Beispiel nichts pushen, keine Issues eröffnen und keine Pull-Requests oder Kommentare erstellen. archive.title_date=Dieses Repository wurde am %s archiviert. Du kannst Dateien ansehen und es klonen, kannst aber seinen Status nicht verändern, zum Beispiel nichts pushen, und keine Issues eröffnen oder Pull-Requests oder Kommentare erstellen. form.reach_limit_of_creation_1=Du hast bereits dein Limit von %d Repository erreicht. -form.reach_limit_of_creation_n=Du hast bereits dein Limit von %d Repositorys erreicht. +form.reach_limit_of_creation_n=Der Besitzer hat bereits das Limit von %d Repositorys erreicht. form.name_reserved=Der Repository-Name „%s“ ist reserviert. form.name_pattern_not_allowed=Das Muster „%s“ ist in Repository-Namen nicht erlaubt. @@ -1236,12 +1236,12 @@ commit=Commit release=Release releases=Releases tag=Tag -released_this=hat releast +released_this=hat es veröffentlicht file.title=%s an %s file_raw=Originalformat file_history=Verlauf file_view_source=Quelltext anzeigen -file_view_rendered=Ansicht rendern +file_view_rendered=Gerendert anzeigen file_view_raw=Originalformat anzeigen file_permalink=Permalink file_too_large=Die Datei ist zu groß zum Anzeigen. @@ -1624,7 +1624,7 @@ issues.unlock_comment=hat diese Diskussion %s entsperrt issues.lock_confirm=Sperren issues.unlock_confirm=Entsperren issues.lock.notice_1=- Andere Nutzer können keine neuen Kommentare beisteuern. -issues.lock.notice_2=– Du und andere Mitarbeiter mit Zugriff auf dieses Repository können weiterhin für andere sichtbare Kommentare hinterlassen. +issues.lock.notice_2=- Du und andere Mitarbeiter mit Zugriff auf dieses Repository können weiterhin für andere sichtbare Kommentare hinterlassen. issues.lock.notice_3=- Du kannst die Diskussion jederzeit wieder entsperren. issues.unlock.notice_1=- Jeder wird wieder in der Lage sein, zu diesem Issue zu kommentieren. issues.unlock.notice_2=- Du kannst den Issue jederzeit wieder sperren. @@ -1745,7 +1745,7 @@ pulls.compare_changes=Neuer Pull-Request pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben pulls.allow_edits_from_maintainers_desc=Nutzer mit Schreibzugriff auf den Basisbranch können auch auf diesen Branch pushen pulls.allow_edits_from_maintainers_err=Aktualisieren fehlgeschlagen -pulls.compare_changes_desc=Wähle den Zielbranch, in das zusammengeführt werden soll, und den Quellbranch, von dem gepullt werden soll, aus. +pulls.compare_changes_desc=Wähle den Zielbranch, in den zusammengeführt werden soll, und den Quellbranch, von dem gepullt werden soll, aus. pulls.has_viewed_file=Gesehen pulls.has_changed_since_last_review=Seit deiner letzten Sichtung geändert pulls.viewed_files_label=%[1]d / %[2]d Dateien betrachtet @@ -2087,7 +2087,7 @@ settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern settings.releases_desc=Repository-Releases aktivieren settings.packages_desc=Repository-Paket-Registry aktivieren settings.projects_desc=Repository-Projekte aktivieren -settings.actions_desc=Aktiviere integrierte CI/CD-Pipelines mit Forgejo-Actions +settings.actions_desc=Integrierte CI/CD-Pipelines mit Forgejo-Actions aktivieren settings.admin_settings=Administratoreinstellungen settings.admin_enable_health_check=Repository-Health-Checks aktivieren (git fsck) settings.admin_code_indexer=Code-Indexer @@ -2192,7 +2192,7 @@ settings.githook_edit_desc=Wenn ein Hook nicht aktiv ist, wird der Standardinhal settings.githook_name=Hook-Name settings.githook_content=Hook-Inhalt settings.update_githook=Hook aktualisieren -settings.add_webhook_desc=Forgejo sendet eine POST-Anfrage mit festgelegtem Content-Type an die Ziel-URL. Mehr Informationen findest du in der Anleitung zu Webhooks (Englisch). +settings.add_webhook_desc=Forgejo sendet eine POST-Anfrage mit festgelegtem Content-Type an die Ziel-URL. Mehr dazu in der Anleitung zu Webhooks (auf Englisch). settings.payload_url=Ziel-URL settings.http_method=HTTP-Methode settings.content_type=POST-Content-Type @@ -2352,7 +2352,7 @@ settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, settings.block_rejected_reviews=Zusammenführung bei abgelehnten Sichtungen blockieren settings.block_rejected_reviews_desc=Zusammenführen ist nicht möglich, wenn Änderungen durch offizielle Prüfer angefragt werden, auch wenn genügend Genehmigungen existieren. settings.block_on_official_review_requests=Merge bei offiziellen Sichtungsanfragen blockieren -settings.block_on_official_review_requests_desc=Merge ist nicht möglich, wenn offizielle Sichtungsanfrangen vorliegen, selbst wenn genügend Genehmigungen existieren. +settings.block_on_official_review_requests_desc=Merge ist nicht möglich, wenn offizielle Sichtungsanfragen vorliegen, selbst wenn genügend Genehmigungen existieren. settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist settings.block_outdated_branch_desc=Merge ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist. settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: @@ -2373,7 +2373,7 @@ settings.tags.protection.allowed.teams=Erlaubte Teams settings.tags.protection.allowed.noone=Niemand settings.tags.protection.create=Regel hinzufügen settings.tags.protection.none=Es gibt keine geschützten Tags. -settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im Guide für geschützte Tags (Englisch). +settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu in der Anleitung für geschützte Tags (auf Englisch). settings.bot_token=Bot-Token settings.chat_id=Chat-ID settings.thread_id=Thread-ID @@ -2382,12 +2382,12 @@ settings.matrix.room_id=Raum-ID settings.matrix.message_type=Nachrichtentyp settings.archive.button=Repo archivieren settings.archive.header=Dieses Repo archivieren -settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird von der Übersichtsseite versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. +settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird von der Übersichtsseite versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. Es wird empfohlen, den Archivierungsgrund zu dokumentieren, um zukünftigen Entwicklern, die planen, das Repository zu forken, zu helfen. settings.archive.success=Das Repo wurde erfolgreich archiviert. settings.archive.error=Beim Versuch, das Repository zu archivieren, ist ein Fehler aufgetreten. Weitere Details finden sich im Log. settings.archive.error_ismirror=Du kannst kein gespiegeltes Repo archivieren. -settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar in archivierten Repos. -settings.archive.tagsettings_unavailable=Tag-Einstellungen sind nicht verfügbar in archivierten Repos. +settings.archive.branchsettings_unavailable=In archivierten Repos sind Branch-Einstellungen nicht verfügbar. +settings.archive.tagsettings_unavailable=In archivierten Repos sind Tag-Einstellungen nicht verfügbar. settings.unarchive.button=Archivierung zurücksetzen settings.unarchive.header=Archivierung dieses Repositorys zurücksetzen settings.unarchive.text=Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen. @@ -2555,7 +2555,7 @@ branch.included=Enthalten branch.create_new_branch=Branch aus Branch erstellen: branch.confirm_create_branch=Branch erstellen branch.warning_rename_default_branch=Du benennst den Standard-Branch um. -branch.rename_branch_to=Umbenennung des Zweigs "%s". +branch.rename_branch_to=Branch "%s" wird umbenannt. branch.create_branch_operation=Branch erstellen branch.new_branch=Neue Branch erstellen branch.new_branch_from=Neuen Branch von „%s“ erstellen @@ -2592,7 +2592,7 @@ settings.add_collaborator_blocked_them = Der Mitarbeiter konnte nicht hinzugefü settings.wiki_rename_branch_main = Den Wiki-Branch-Namen normalisieren settings.enter_repo_name = Gib den Besitzer- und den Repository-Namen genau wie angezeigt ein: settings.wiki_branch_rename_success = Der Branch-Name des Repository-Wikis wurde erfolgreich normalisiert. -settings.archive.mirrors_unavailable = Spiegel sind nicht verfügbar in archivierten Repos. +settings.archive.mirrors_unavailable = In archivierten Repos sind Spiegel nicht verfügbar. pulls.blocked_by_user = Du kannst keinen Pull-Request in diesem Repository erstellen, weil du vom Repository-Besitzer blockiert wurdest. settings.add_collaborator_blocked_our = Der Mitarbeiter konnte nicht hinzugefügt werden, weil der Repository-Besitzer ihn blockiert hat. issues.blocked_by_user = Du kannst keine Issues in diesem Repository erstellen, weil du vom Repository-Besitzer blockiert wurdest. @@ -2628,7 +2628,7 @@ settings.units.overview = Übersicht settings.wiki_rename_branch_main_notices_1 = Diese Aktion KANN NICHT rückgängig gemacht werden. settings.wiki_rename_branch_main_notices_2 = Dies wird den internen Branch des Repository-Wikis von %s permanent umbenennen. Existierende Checkouts müssen aktualisiert werden. settings.wiki_branch_rename_failure = Der Branch-Name des Repository-Wiki konnte nicht normalisiert werden. -settings.confirm_wiki_branch_rename = Den Wiki-Branch umbenenennen +settings.confirm_wiki_branch_rename = Den Wiki-Branch umbenennen activity.navbar.contributors = Mitwirkende contributors.contribution_type.deletions = Löschungen contributors.contribution_type.additions = Einfügungen @@ -2665,7 +2665,7 @@ settings.add_webhook.invalid_path = Der Pfad darf kein „.“ oder „..“ enh settings.sourcehut_builds.manifest_path = Build-Manifest-Pfad settings.sourcehut_builds.visibility = Job-Sichtbarkeit settings.sourcehut_builds.secrets = Geheimnisse -settings.sourcehut_builds.secrets_helper = Dem Job zugriff auf die Build-Geheimnisse geben (benötigt die SECRETS:RO-Berechtigung) +settings.sourcehut_builds.secrets_helper = Dem Job Zugriff auf die Build-Geheimnisse geben (benötigt die SECRETS:RO-Berechtigung) settings.web_hook_name_sourcehut_builds = SourceHut-Builds settings.graphql_url = GraphQL-URL settings.matrix.room_id_helper = Die Raum-ID kann über den Element-Webclient ermittelt werden: Raumeinstellungen > erweitert > interne Raum-ID. Beispielsweise %s. @@ -2688,8 +2688,8 @@ project = Projekte comments.edit.already_changed = Die Änderungen an diesem Kommentar können nicht gespeichert werden. Es scheint, als seien die Inhalte bereits durch einen anderen Benutzer verändert worden. Bitte die Seite neu laden und das Bearbeiten erneut versuchen, um dessen Änderungen nicht zu überschreiben issues.edit.already_changed = Die Änderungen an diesem Issue können nicht gespeichert werden. Es scheint, als seien die Inhalte bereits durch einen anderen Benutzer verändert worden. Bitte die Seite neu laden und das Bearbeiten erneut versuchen, um deren Änderungen nicht zu überschreiben pulls.edit.already_changed = Die Änderungen an diesem Pull-Request können nicht gespeichert werden. Es scheint, als seien die Inhalte bereits durch einen anderen Benutzer verändert worden. Bitte die Seite neu laden und das Bearbeiten erneut versuchen, um dessen Änderungen nicht zu überschreiben -subscribe.pull.guest.tooltip = Anmelden, um diesen Pull-Request zu abbonieren. -subscribe.issue.guest.tooltip = Anmelden, um dieses Issue zu abbonieren. +subscribe.pull.guest.tooltip = Anmelden, um diesen Pull-Request zu abonnieren. +subscribe.issue.guest.tooltip = Anmelden, um dieses Issue zu abonnieren. issues.author.tooltip.pr = Dieser Benutzer ist der Autor dieses Pull-Requests. issues.author.tooltip.issue = Dieser Benutzer ist der Autor dieses Issues. activity.commit = Commit-Aktivität @@ -2702,8 +2702,8 @@ release.add_external_asset = Externes Asset hinzufügen release.invalid_external_url = Ungültige externe URL: „%s“ activity.published_prerelease_label = Pre-Release activity.published_tag_label = Tag -settings.pull_mirror_sync_quota_exceeded = Quota überschritten, Änderungen werden nicht gepullt. -settings.transfer_quota_exceeded = Der neue Eigentümer (%s) hat die Quota überschritten. Das Repository wurde nicht übertragen. +settings.pull_mirror_sync_quota_exceeded = Kontingent überschritten, Änderungen werden nicht gepullt. +settings.transfer_quota_exceeded = Der neue Eigentümer (%s) hat das Kontingent überschritten. Das Repository wurde nicht übertragen. no_eol.text = Kein EOL no_eol.tooltip = Diese Datei enthält am Ende kein Zeilenende-Zeichen (EOL). pulls.cmd_instruction_merge_warning = Achtung: Die Einstellung „Autoerkennung von manuellen Zusammenführungen“ ist für dieses Repository nicht aktiviert. Du musst hinterher diesen Pull-Request als manuell zusammengeführt markieren. @@ -2941,7 +2941,7 @@ dashboard.cron.error=Fehler in Cron: %s: %[3]s dashboard.cron.finished=Cron: %[1]s ist beendet dashboard.delete_inactive_accounts=Alle nicht aktivierten Konten löschen dashboard.delete_inactive_accounts.started=Löschen aller nicht aktivierten Account-Aufgabe gestartet. -dashboard.delete_repo_archives=Lösche alle Repository-Archive (ZIP, TAR.GZ, etc.) +dashboard.delete_repo_archives=Lösche alle Repository-Archive (ZIP, TAR.GZ usw.) dashboard.delete_repo_archives.started=Löschen aller Repository-Archive gestartet. dashboard.delete_missing_repos=Alle Repository-Datensätze mit verloren gegangenen Git-Dateien löschen dashboard.delete_missing_repos.started=Alle Repositorys löschen, die den Git-Dateien-Task nicht gestartet haben. @@ -3184,8 +3184,8 @@ auths.oauth2_required_claim_value=Benötigter Claim-Wert auths.oauth2_required_claim_value_helper=Setze diesen Wert, damit Nutzer aus dieser Quelle sich nur anmelden dürfen, wenn sie einen Claim mit diesem Namen und Wert besitzen auths.oauth2_group_claim_name=Claim-Name, der Gruppennamen für diese Quelle angibt (optional). auths.oauth2_admin_group=Gruppen-Claim-Wert für Administratoren (optional – erfordert Claim-Namen oben). -auths.oauth2_restricted_group=Gruppen-Claim-Wert für eingeschränkte User. (Optional – erfordert Claim-Namen oben) -auths.oauth2_map_group_to_team=Gruppen aus OAuth-Claims den Organisationsteams zuordnen (optional – oben muss der Name des Claims angegeben werden). +auths.oauth2_restricted_group=Gruppen-Claim-Wert für eingeschränkte Benutzer (optional – erfordert Claim-Namen oben). +auths.oauth2_map_group_to_team=Gruppen aus OAuth-Claims den Organisationsteams zuordnen (optional – erfordert Claim-Namen oben). auths.oauth2_map_group_to_team_removal=Benutzer aus synchronisierten Teams entfernen, wenn der Benutzer nicht zur entsprechenden Gruppe gehört. auths.tips=Tipps auths.tips.oauth2.general=OAuth2-Authentifizierung @@ -3202,13 +3202,13 @@ auths.tip.twitter=Gehe auf %s, erstelle eine Anwendung und stelle sicher, dass d auths.tip.discord=Registriere unter %s eine neue Anwendung auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem Abschnitt „Yandex.Passport API“: „Zugriff auf E-Mail-Adresse“, „Zugriff auf Benutzeravatar“ und „Zugriff auf Benutzername, Vor- und Nachname, Geschlecht“` -auths.tip.mastodon=Gib eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige) -auths.edit=Authentifikationsquelle bearbeiten -auths.activated=Diese Authentifikationsquelle ist aktiviert +auths.tip.mastodon=Gib eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder verwende die Standard-URL) +auths.edit=Authentifizierungsquelle bearbeiten +auths.activated=Diese Authentifizierungsquelle ist aktiviert auths.new_success=Die Authentifizierung „%s“ wurde hinzugefügt. auths.update_success=Diese Authentifizierungsquelle wurde aktualisiert. auths.update=Authentifizierungsquelle aktualisieren -auths.delete=Authentifikationsquelle löschen +auths.delete=Authentifizierungsquelle löschen auths.delete_auth_title=Authentifizierungsquelle löschen auths.delete_auth_desc=Das Löschen einer Authentifizierungsquelle verhindert, dass Benutzer sich darüber anmelden können. Fortfahren? auths.still_in_used=Diese Authentifizierungsquelle wird noch verwendet. Bearbeite oder lösche zuerst alle Benutzer, die diese Authentifizierungsquelle benutzen. @@ -3228,7 +3228,7 @@ config.domain=Server-Domain config.offline_mode=Lokaler Modus config.disable_router_log=Router-Log deaktivieren config.run_user=Ausführen als -config.run_mode=Laufzeit-Modus +config.run_mode=Betriebsmodus config.git_version=Git-Version config.app_data_path=App-Datenpfad config.repo_root_path=Repository-Wurzelpfad @@ -3264,7 +3264,7 @@ config.db_ssl_mode=SSL config.db_path=Verzeichnis config.service_config=Service-Konfiguration -config.register_email_confirm=E-Mail-Bestätigung benötigt zum Registrieren +config.register_email_confirm=E-Mail-Bestätigung zur Registrierung erforderlich config.disable_register=Selbstregistrierung deaktivieren config.allow_only_internal_registration=Registrierung nur über Forgejo selbst erlauben config.allow_only_external_registration=Registrierung nur über externe Dienste erlauben @@ -3321,14 +3321,14 @@ config.cache_item_ttl=Cache-Item-TTL config.session_config=Session-Konfiguration config.session_provider=Session-Anbieter -config.provider_config=Provider-Einstellungen +config.provider_config=Anbieter-Einstellungen config.cookie_name=Cookie-Name config.gc_interval_time=GC-Intervall config.session_life_time=Session-Lebensdauer config.https_only=Nur HTTPS config.cookie_life_time=Cookie-Lebensdauer -config.picture_config=Bild-und-Profilbild-Konfiguration +config.picture_config=Bild- und Avatar-Konfiguration config.picture_service=Bilderdienst config.disable_gravatar=Gravatar deaktivieren config.enable_federated_avatar=Föderierte Profilbilder einschalten @@ -3383,7 +3383,7 @@ monitor.queue.exemplar=Beispieltyp monitor.queue.numberworkers=Anzahl der Worker monitor.queue.activeworkers=Aktive Worker monitor.queue.maxnumberworkers=Maximale Anzahl der Worker -monitor.queue.numberinqueue=Nummer in der Warteschlange +monitor.queue.numberinqueue=Anzahl in der Warteschlange monitor.queue.review_add=Worker hinzufügen/prüfen monitor.queue.settings.title=Pool-Einstellungen monitor.queue.settings.desc=Pools wachsen dynamisch basierend auf der Blockierung der Arbeitswarteschlange. @@ -3411,7 +3411,7 @@ notices.op=Aktion notices.delete_success=Diese Systemmeldung wurde gelöscht. self_check.database_fix_mysql = Für MySQL-/MariaDB-Benutzer: Du kannst den Befehl „forgejo doctor convert“ verwenden, um die Collation-Probleme zu lösen, oder du kannst das Problem mit „ALTER … COLLATE …“-SQLs manuell lösen. dashboard.sync_tag.started = Tag-Synchronisierung gestartet -self_check.database_collation_case_insensitive = Datenbank benutzt eine Collation %s, welcher der Groß-/Kleinschreibung egal ist. Obwohl Forgejo damit arbeiten könnte, könnte es ein paar seltene Fälle geben, bei denen es nicht wie erwartet funktioniert. +self_check.database_collation_case_insensitive = Die Datenbank verwendet die Collation %s, die nicht zwischen Groß- und Kleinschreibung unterscheidet. Forgejo kann damit arbeiten, es gibt aber seltene Fälle, in denen es nicht wie erwartet funktioniert. self_check = Selbstprüfung dashboard.sync_repo_tags = Tags aus Git-Daten zu Datenbank synchronisieren emails.change_email_text = Bist du dir sicher, dass du diese E-Mail-Addresse aktualisieren möchtest? @@ -3427,7 +3427,7 @@ auths.tip.gitlab_new = Registriere eine neue Anwendung auf %s auths.default_domain_name = Standarddomainname, der für die E-Mail-Adresse benutzt wird config.app_slogan = Instanz-Slogan config.cache_test_failed = Konnte den Cache nicht untersuchen: %v. -config.cache_test_succeeded = Cache-Test erfolgreich, eine Antwort erhalten in %s. +config.cache_test_succeeded = Cache-Test erfolgreich, Antwort in %s erhalten. config.cache_test = Cache testen config.cache_test_slow = Cache-Test erfolgreich, aber die Antwort ist langsam: %s. users.block.description = Diesem Benutzer verbieten, durch sein Konto mit diesem Dienst zu interagieren, und ihn am Einloggen hindern. @@ -3567,7 +3567,7 @@ workflow.dispatch.trigger_found = Dieser Workflow hat einen workflow_dispatch workflow.dispatch.success = Ausführung des Workflows wurde erfolgreich angefragt. runs.expire_log_message = Logs wurden gelöscht, weil sie zu alt waren. runs.no_workflows.help_write_access = Keine Ahnung, wie man mit Forgejo Actions anfangen soll? Schau im Schnellstart in der Benutzerdokumentation vorbei, um deinen ersten Workflow zu schreiben, dann setze einen Forgejo-Runner auf, um deine Jobs auszuführen. -runs.no_workflows.help_no_write_access = Um über Forgejo Actions zu lernen, siehe die Dokumentation. +runs.no_workflows.help_no_write_access = Um mehr über Forgejo Actions zu erfahren, siehe die Dokumentation. variables.not_found = Die Variable wurde nicht gefunden. [projects] @@ -3602,7 +3602,7 @@ branch_kind = Branches suchen … commit_kind = Commits suchen … runner_kind = Runner suchen … no_results = Keine passenden Ergebnisse gefunden. -code_search_unavailable = Die Code-Suche ist momentan nicht verfügbar. Bitte kontaktiere den Webseitenadministrator. +code_search_unavailable = Die Code-Suche ist momentan nicht verfügbar. Bitte kontaktiere den Instanz-Administrator. keyword_search_unavailable = Die Suche mittels Schlüsselwort ist momentan nicht verfügbar. Bitte kontaktiere den Webseitenadministrator. exact = Exakt exact_tooltip = Nur Ergebnisse einbinden, die auf den exakten Suchbegriff passen diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6ec0ba978c..2df0b3a728 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2569,7 +2569,7 @@ settings.matrix.access_token_helper = It is recommended to setup a dedicated Mat settings.matrix.room_id_helper = The Room ID can be retrieved from the Element web client > Room Settings > Advanced > Internal room ID. Example: %s. settings.archive.button = Archive repo settings.archive.header = Archive this repo -settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests. +settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests. Documenting the archival reason is recommended to guide future developers who plan to fork the repository. settings.archive.success = The repo was successfully archived. settings.archive.error = An error occurred while trying to archive the repo. See the log for more details. settings.archive.error_ismirror = You cannot archive a mirrored repo. diff --git a/options/locale/locale_eo.ini b/options/locale/locale_eo.ini index 249597b718..d610b7b1d9 100644 --- a/options/locale/locale_eo.ini +++ b/options/locale/locale_eo.ini @@ -772,7 +772,7 @@ enable_custom_avatar = Uzi propran profilbildon change_password = Ŝanĝi pasvorton keep_pronouns_private = Montri pronomojn nur al la aŭtentikigitaj uzantoj keep_pronouns_private.description = Tio maskos viajn pronomojn kontraŭ neaŭtentikigitaj vizitantoj. -add_new_principal = +add_new_principal = gpg_token_required = Vi devas disponigi signaturon por la malsupran ĵetono gpg_token = Ĵetono gpg_token_help = Vi povas generi signaturon uzante: @@ -927,6 +927,10 @@ size_format = %[1]s: %[2]s; %[3]s: %[4]s template = Ŝablono template_select = Elektu ŝablonon +editor.name_your_file = Nomu vian dosieron… +editor.or = aŭ +editor.add_tmpl.filename = dosiernomo + [org] code = Fontkodo settings = Agordoj diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index a7aa0f17ae..264a010805 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -32,7 +32,7 @@ password=Contraseña access_token=Token de acceso re_type=Confirmar contraseña captcha=CAPTCHA -twofa=Autenticación de dos factores +twofa=Autenticación de doble factor twofa_scratch=Código de respaldo passcode=Código de acceso @@ -80,9 +80,9 @@ rerun_all=Volver a ejecutar todos los trabajos save=Guardar add=Añadir add_all=Añadir todos -remove=Eliminar -remove_all=Eliminar todos -remove_label_str=`Eliminar elemento "%s"` +remove=Retirar +remove_all=Retirar todos +remove_label_str=`Retirar elemento "%s"` edit=Editar enabled=Activo @@ -103,7 +103,7 @@ preview=Vista previa loading=Cargando… error=Error -error404=La página a la que está intentando acceder no existe,ha sido eliminada o no está autorizado a verla. +error404=La página a la que está intentando acceder no existe,ha sido retirada o no está autorizado a verla. go_back=Volver never=Nunca @@ -147,7 +147,7 @@ filter.private = Privado toggle_menu = Alternar menú invalid_data = Datos inválidos: %v confirm_delete_artifact = ¿Estás seguro de que deseas eliminar el artefacto "%s"? -more_items = Mas cosas +more_items = Mas elementos copy_generic = Copiar al portapapeles filter.not_fork = No hay bifurcaciones filter.is_fork = Bifurcaciones @@ -202,7 +202,7 @@ table_modal.placeholder.content = Contenido link_modal.header = Añadir enlace link_modal.description = Descripción link_modal.paste_reminder = Pista: Con una URL en tu portapapeles, puedes pegar directamente en el editor para crear un enlace. -link_modal.url = URL +link_modal.url = Url [filter] string.asc=A - Z @@ -384,8 +384,8 @@ manual_activation_only=Póngase en contacto con el administrador del sitio para remember_me=Recordar este dispositivo forgot_password_title=Contraseña olvidada forgot_password=¿Has olvidado tu contraseña? -sign_up_successful=La cuenta se ha creado correctamente. ¡Le damos la bienvenida! -confirmation_mail_sent_prompt=Se ha enviado un nuevo correo de confirmación a %s. Para completar el proceso de registro, revisa tu bandeja de entrada y sigue el enlace proporcionado dentro de los próximos %s. Si la dirección no es correcto, puedes iniciar sesión y solicitar otro correo de confirmación para ser enviado a una dirección diferente. +sign_up_successful=La cuenta se ha creado correctamente. ¡Bienvenido! +confirmation_mail_sent_prompt=Se ha enviado un nuevo correo electrónico de confirmación a %s. Para completar el registro, comprueba tu bandeja de entrada y sigue el enlace que aparece en el correo en un plazo de: %s. Si la dirección de correo electrónico no es correcta, puedes iniciar sesión y solicitar que se envíe otro correo electrónico de confirmación a otra dirección. must_change_password=Actualizar su contraseña allow_password_change=Obligar al usuario a cambiar la contraseña (recomendado) reset_password_mail_sent_prompt=Se ha enviado un correo de confirmación a %s. Para completar el proceso de recuperación de la cuenta, consulta tu bandeja de entrada y sigue el enlace proporcionado dentro de los próximos %s. @@ -442,7 +442,7 @@ password_pwned_err=No se pudo completar la solicitud a HaveIBeenPwned change_unconfirmed_email = Si has proporcionado una dirección de correo electrónico errónea durante el registro, la puedes cambiar debajo y se enviará una confirmación a la nueva dirección. change_unconfirmed_email_error = No es posible cambiar la dirección de correo electrónico: %v change_unconfirmed_email_summary = Cambia la dirección de correo electrónico a quien se envía el correo de activación. -last_admin = No puedes eliminar al último admin (administrador). Debe haber, al menos, un admin. +last_admin = No puedes retirar el último admin (administrador). Debe haber, al menos, un admin. sign_up_button = Regístrate ahora. hint_login = ¿Ya tienes cuenta? ¡Ingresa ahora! hint_register = ¿Necesitas una cuenta? Regístrate ahora. @@ -516,7 +516,7 @@ admin.new_user.subject = Se acaba de registrar el nuevo usuario %s admin.new_user.user_info = Información del usuario admin.new_user.text = Por favor, pulsa aquí para gestionar este usuario desde el panel de administración. account_security_caution.text_1 = Si fuiste tú, puedes ignorar este correo. -removed_security_key.subject = Se ha eliminado una clave de seguridad +removed_security_key.subject = Se ha retirado una clave de seguridad removed_security_key.no_2fa = Ya no hay otros métodos 2FA configurados, lo que significa que ya no es necesario iniciar sesión en tu cuenta con 2FA. password_change.subject = Tu contraseña ha sido modificada password_change.text_1 = La contraseña de tu cuenta acaba de ser modificada. @@ -527,7 +527,7 @@ totp_disabled.no_2fa = Ya no hay otros métodos 2FA configurados, lo que signifi account_security_caution.text_2 = Si no fuiste tú, tu cuenta está comprometida. Ponte en contacto con los administradores de este sitio. totp_enrolled.subject = Has activado TOTP como método 2FA totp_enrolled.text_1.no_webauthn = Acabas de activar TOTP para tu cuenta. Esto significa que para todos los futuros inicios de sesión en tu cuenta, debes utilizar TOTP como método 2FA. -removed_security_key.text_1 = La clave de seguridad "%[1]s" acaba de ser eliminada de tu cuenta. +removed_security_key.text_1 = La clave de seguridad "%[1]s" acaba de ser retirada de tu cuenta. primary_mail_change.text_1 = El correo principal de su cuenta acaba de cambiar a %[1]s. Esto significa que esta dirección de correo electrónico ya no recibirá notificaciones por correo electrónico de su cuenta. totp_enrolled.text_1.has_webauthn = Acabas de activar TOTP para tu cuenta. Esto significa que para todos los futuros inicios de sesión en tu cuenta, podrás utilizar TOTP como método 2FA o bien utilizar cualquiera de tus claves de seguridad. @@ -603,7 +603,7 @@ enterred_invalid_owner_name=El nuevo nombre de usuario no es válido. enterred_invalid_password=La contraseña que ha introducido es incorrecta. user_not_exist=Este usuario no existe. team_not_exist=Este equipo no existe. -last_org_owner=No puedes eliminar al último usuario del equipo de "propietarios". Todas las organizaciones deben tener al menos un propietario. +last_org_owner=No puedes retirar el último usuario del equipo de "propietarios". Todas las organizaciones deben tener al menos un propietario. cannot_add_org_to_team=Una organización no puede ser añadida como miembro de un equipo. duplicate_invite_to_team=El usuario ya fue invitado como miembro del equipo. organization_leave_success=Ha abandonado correctamente la organización %s. @@ -622,7 +622,7 @@ org_still_own_repo=Esta organización todavía posee uno o más repositorios, el org_still_own_packages=Esta organización todavía posee uno o más paquetes, elimínalos primero. target_branch_not_exist=La rama de destino no existe. -admin_cannot_delete_self = No puedes eliminarte a ti mismo cuando eres un admin (administrador). Por favor, elimina primero tus privilegios de administrador. +admin_cannot_delete_self = No puedes eliminarte a ti mismo cuando eres un admin (administrador). Por favor, primero retire los privilegios de administrador. username_error_no_dots = ` solo puede contener carácteres alfanuméricos ("0-9","a-z","A-Z"), guiones ("-"), y guiones bajos ("_"). No puede empezar o terminar con carácteres no alfanuméricos y también están prohibidos los carácteres no alfanuméricos consecutivos.` unsupported_login_type = No se admite el tipo de inicio de sesión para eliminar la cuenta. required_prefix = La entrada debe empezar por "%s" @@ -738,7 +738,7 @@ comment_type_group_review_request=Revisión solicitada comment_type_group_pull_request_push=Confirmaciones añadidas comment_type_group_project=Proyecto comment_type_group_issue_ref=Referencia del incidente -saved_successfully=Su configuración se ha guardado correctamente. +saved_successfully=Tu configuración se guardó correctamente. privacy=Privacidad keep_activity_private=Ocultar actividad de la página de perfil lookup_avatar_by_mail=Buscar avatar por dirección de correo electrónico @@ -772,8 +772,8 @@ activate_email=Enviar activación activations_pending=Activaciones pendientes can_not_add_email_activations_pending=Hay una activación pendiente, inténtelo de nuevo en unos minutos si desea agregar un nuevo correo electrónico. delete_email=Eliminar -email_deletion=Eliminar dirección de correo electrónico -email_deletion_desc=Esta dirección de correo electrónico y la información relacionada se eliminará de su cuenta. Las confirmaciones de Git hechas por esta dirección de correo electrónico permanecerán intactas. ¿Desea Continuar? +email_deletion=Retirar dirección de correo-e +email_deletion_desc=Esta dirección de correo electrónico y la información relacionada se retirará de su cuenta. Las confirmaciones de Git hechas por esta dirección de correo electrónico permanecerán intactas. ¿Desea Continuar? email_deletion_success=La dirección de correo electrónico ha sido eliminada. theme_update_success=Su tema fue actualizado. theme_update_error=El tema seleccionado no existe. @@ -840,8 +840,8 @@ add_key_success=La clave SSH "%s" ha sido añadida. add_gpg_key_success=La clave GPG "%s" ha sido añadida. add_principal_success=El certificado SSH principal "%s" ha sido añadido. delete_key=Eliminar -ssh_key_deletion=Eliminar clave SSH -gpg_key_deletion=Eliminar clave GPG +ssh_key_deletion=Retirar clave SSH +gpg_key_deletion=Retirar clave GPG ssh_principal_deletion=Eliminar principal de certificado SSH ssh_key_deletion_desc=Eliminando una clave SSH se revoca su acceso a su cuenta. ¿Continuar? gpg_key_deletion_desc=Eliminando una clave GPG se des-verifican los commits firmados con ella. ¿Continuar? @@ -2383,7 +2383,7 @@ settings.matrix.room_id=ID de sala settings.matrix.message_type=Tipo de mensaje settings.archive.button=Archivar repositorio settings.archive.header=Archivar este repositorio -settings.archive.text=Archivar el repositorio lo hará de sólo lectura. Se ocultará del tablero. Nadie (¡ni siquiera tú!) será capaz de hacer nuevas confirmaciones, o abrir nuevas incidencias o solicitudes de incorporación de cambios. +settings.archive.text=Archivar el repositorio lo hará de sólo lectura. Se ocultará del tablero. Nadie (¡ni siquiera tú!) será capaz de hacer nuevas confirmaciones, o abrir nuevas incidencias o solicitudes de incorporación de cambios. Se recomienda documentar el motivo del archivado para orientar a futuros desarrolladores que planeen bifurcar el repositorio. settings.archive.success=El repositorio ha sido archivado exitosamente. settings.archive.error=Ha ocurrido un error al intentar archivar el repositorio. Vea el registro para más detalles. settings.archive.error_ismirror=No puede archivar un repositorio replicado. @@ -2769,6 +2769,8 @@ settings.matrix.access_token_helper = Se recomienda configurar una cuenta de Mat settings.archive.mirrors_unavailable = Los espejos no están disponibles en repos archivados. release.summary_card_alt = Tarjeta de resumen de una release titulada "%s" en el repositorio %s +settings.matrix.room_id_helper = El ID de Sala puede ser obtenido desde el cliente web Element > Ajustes de Sala > Avanzado > ID de sala interna. Ejemplo: %s. + [graphs] component_loading = Cargando %s… component_loading_failed = No se pudo cargar %s @@ -2815,7 +2817,7 @@ settings.permission=Permisos settings.repoadminchangeteam=El administrador del repositorio puede añadir y eliminar el acceso a equipos settings.visibility=Visibilidad settings.visibility.public=Público -settings.visibility.limited=Limitado (visible solo para usuarios autenticados) +settings.visibility.limited=Limitado (visible solo para usuarios accedidos) settings.visibility.limited_shortname=Limitado settings.visibility.private=Privado (Visible sólo para miembros de la organización) settings.visibility.private_shortname=Privado @@ -2994,7 +2996,7 @@ dashboard.update_checker=Buscador de actualizaciones dashboard.delete_old_system_notices=Borrar todos los avisos antiguos del sistema de la base de datos dashboard.gc_lfs=Recoger basura meta-objetos LFS dashboard.stop_zombie_tasks=Detener tareas zombie -dashboard.stop_endless_tasks=Detener tareas interminables +dashboard.stop_endless_tasks=Detiene tareas de acciones interminables dashboard.cancel_abandoned_jobs=Cancelar trabajos abandonados dashboard.start_schedule_tasks=Iniciar tareas programadas dashboard.sync_branch.started=Inició la sincronización de ramas @@ -3124,21 +3126,21 @@ auths.host=Servidor auths.port=Puerto auths.bind_dn=Bind DN auths.bind_password=Contraseña Bind -auths.user_base=Base de búsqueda de usuarios +auths.user_base=Base de búsqueda del usuario auths.user_dn=DN de Usuario auths.attribute_username=Atributo nombre de usuario auths.attribute_username_placeholder=Dejar vacío para usar el nombre de usuario introducido en Forgejo. auths.attribute_name=Atributo nombre auths.attribute_surname=Atributo apellido -auths.attribute_mail=Atributo correo electrónico -auths.attribute_ssh_public_key=Atributo Clave Pública SSH -auths.attribute_avatar=Atributo del avatar -auths.attributes_in_bind=Obtener atributos en el contexto de Bind DN +auths.attribute_mail=Atributo correo-e +auths.attribute_ssh_public_key=Atributo clave pública SSH +auths.attribute_avatar=Atributo avatar +auths.attributes_in_bind=Obtiene atributos en el contexto de bind DN auths.allow_deactivate_all=Permitir un resultado de búsqueda vacío para desactivar todos los usuarios auths.use_paged_search=Usar búsqueda paginada auths.search_page_size=Tamaño de página auths.filter=Filtro de usuario -auths.admin_filter=Filtro de aministrador +auths.admin_filter=Filtro de administrador auths.restricted_filter=Filtro restringido auths.restricted_filter_helper=Dejar en blanco para no establecer ningún usuario como restringido. Utilice un asterisco ('*') para establecer todos los usuarios que no coincidan con el filtro de administración como restringido. auths.verify_group_membership=Verificar pertenencia al grupo en LDAP (dejar el filtro vacío para saltar) @@ -3152,15 +3154,15 @@ auths.ms_ad_sa=Atributos de búsqueda de MS AD auths.smtp_auth=Tipo de autenticación SMTP auths.smtphost=Servidor SMTP auths.smtpport=Puerto SMTP -auths.allowed_domains=Dominios Permitidos -auths.allowed_domains_helper=Dejar vacío para permitir todos los dominios. Separa múltiples dominios con una coma (','). +auths.allowed_domains=Dominios permitidos +auths.allowed_domains_helper=Dejar vacío para permitir todos los dominios. Separa múltiples dominios con una coma (","). auths.skip_tls_verify=Omitir la verificación TLS auths.force_smtps=Forzar SMTPS auths.force_smtps_helper=SMTPS se utiliza siempre en el puerto 465. Establezca esto para forzar SMTPS en otros puertos. (De lo contrario, STARTTLS se utilizará en otros puertos si es soportado por el host.) auths.helo_hostname=Nombre de anfitrión HELO auths.helo_hostname_helper=Nombre de anfitrión enviado con HELO. Déjelo vacío para enviar el nombre de anfitrión actual. auths.disable_helo=Desactivar HELO -auths.pam_service_name=Nombre del Servicio PAM +auths.pam_service_name=Nombre del servicio PAM auths.pam_email_domain=Dominio de correo de PAM (opcional) auths.oauth2_provider=Proveedor OAuth2 auths.oauth2_icon_url=URL de icono @@ -3217,23 +3219,23 @@ auths.unable_to_initialize_openid=No se puede inicializar el proveedor de OpenID auths.invalid_openIdConnectAutoDiscoveryURL=URL de auto descubrimiento no válida (esta debe ser una URL válida comenzando con http:// o https://) config.server_config=Configuración del servidor -config.app_name=Título del sitio +config.app_name=Título de la instancia config.app_ver=Versión de Forgejo -config.app_url=URL base de Forgejo +config.app_url=URL base config.custom_conf=Ruta del fichero de configuración -config.custom_file_root_path=Ruta raíz de los archivos personalizada -config.domain=Dominio del Servidor -config.offline_mode=Modo offline -config.disable_router_log=Deshabilitar Log del Router -config.run_user=Ejecutar como usuario +config.custom_file_root_path=Ruta raíz del archivo personalizado +config.domain=Dominio del servidor +config.offline_mode=Modo local +config.disable_router_log=Inhabilitar bitácora de enrutado +config.run_user=Usuario a ejecutar como config.run_mode=Modo de ejecución config.git_version=Versión de Git -config.app_data_path=Ruta de datos de Forgejo -config.repo_root_path=Ruta del Repositorio +config.app_data_path=Ruta de datos de App +config.repo_root_path=Ruta del repositorio raíz config.lfs_root_path=Ruta raíz de LFS -config.log_file_root_path=Ruta de ficheros de registro -config.script_type=Tipo de Script -config.reverse_auth_user=Autenticación Inversa de Usuario +config.log_file_root_path=Ruta bitácora +config.script_type=Tipo de guion +config.reverse_auth_user=Autenticación inversa de proxy del usuario config.ssh_config=Configuración SSH config.ssh_enabled=Habilitado @@ -3243,16 +3245,16 @@ config.ssh_port=Puerto config.ssh_listen_port=Puerto de escucha config.ssh_root_path=Ruta raíz config.ssh_key_test_path=Ruta de la clave de prueba -config.ssh_keygen_path=Ruta del generador de claves ('ssh-keygen') -config.ssh_minimum_key_size_check=Tamaño mínimo de la clave de verificación +config.ssh_keygen_path=Ruta del generador de claves ("ssh-keygen") +config.ssh_minimum_key_size_check=Comprobante de tamaño de clave mínimo config.ssh_minimum_key_sizes=Tamaños de clave mínimos config.lfs_config=Configuración LFS config.lfs_enabled=Habilitado config.lfs_content_path=Ruta de contenido LFS -config.lfs_http_auth_expiry=Caducidad de la autentificación HTTP LFS +config.lfs_http_auth_expiry=Caducidad de la autenticación HTTP LFS -config.db_config=Configuración de la Base de Datos +config.db_config=Configuración de base de datos config.db_type=Tipo config.db_host=Host config.db_name=Nombre @@ -3262,99 +3264,99 @@ config.db_ssl_mode=SSL config.db_path=Ruta config.service_config=Configuración del servicio -config.register_email_confirm=Requerir confirmación de correo electrónico para registrarse -config.disable_register=Deshabilitar auto-registro -config.allow_only_internal_registration=Permitir el registro solo desde Forgejo -config.allow_only_external_registration=Permitir el registro únicamente a través de servicios externos -config.enable_openid_signup=Habilitar el auto-registro con OpenID -config.enable_openid_signin=Habilitar el inicio de sesión con OpenID -config.show_registration_button=Mostrar Botón de Registro -config.require_sign_in_view=Requerir inicio de sesión obligatorio para ver páginas -config.mail_notify=Habilitar las notificaciones por correo electrónico +config.register_email_confirm=Requerir confirmación de correo-e para registrarse +config.disable_register=Inhabilitar auto-registro +config.allow_only_internal_registration=Concede registro solo por medio de Forgejo +config.allow_only_external_registration=Concede registro solo a través de servicios externos +config.enable_openid_signup=Habilitar auto-registro con OpenID +config.enable_openid_signin=Habilitar inicio de sesión con OpenID +config.show_registration_button=Mostrar botón de registro +config.require_sign_in_view=Requiere inicio de sesión para ver contenido +config.mail_notify=Habilitar notificaciones por correo-e config.enable_captcha=Activar CAPTCHA -config.active_code_lives=Habilitar Vida del Código -config.reset_password_code_lives=Caducidad del código de recuperación de cuenta -config.default_keep_email_private=Ocultar direcciones de correo electrónico por defecto -config.default_allow_create_organization=Permitir la creación de organizaciones por defecto +config.active_code_lives=Código de activación del tiempo de vida +config.reset_password_code_lives=Código de recuperación del tiempo de vida +config.default_keep_email_private=Ocultar direcciones de correo-e por defecto +config.default_allow_create_organization=Concede la creación de organizaciones por defecto config.enable_timetracking=Habilitar seguimiento de tiempo config.default_enable_timetracking=Habilitar seguimiento de tiempo por defecto config.allow_dots_in_usernames = Permite utilizar puntos en los nombres de usuario. No tiene efecto sobre cuentas existentes. config.default_allow_only_contributors_to_track_time=Deje que solo los colaboradores hagan un seguimiento del tiempo -config.no_reply_address=Dominio de correos electrónicos ocultos -config.default_visibility_organization=Visibilidad por defecto para nuevas organizaciones +config.no_reply_address=Dominio de correo-e ocultos +config.default_visibility_organization=Visibilidad por defecto para organizaciones nuevas config.default_enable_dependencies=Habilitar dependencias de incidencias por defecto config.webhook_config=Configuración de Webhooks -config.queue_length=Tamaño de Cola de Envío -config.deliver_timeout=Timeout de Entrega -config.skip_tls_verify=Saltar verificación TLS +config.queue_length=Longitud de cola +config.deliver_timeout=Vencimiento de entrega +config.skip_tls_verify=Omitir verificación TLS -config.mailer_config=Configuración del servidor de correo +config.mailer_config=Configuración del cartero config.mailer_enabled=Activado config.mailer_enable_helo=Habilitar HELO config.mailer_name=Nombre config.mailer_protocol=Protocolo -config.mailer_smtp_addr=Dirección SMTP +config.mailer_smtp_addr=Hospedaje SMTP config.mailer_smtp_port=Puerto SMTP config.mailer_user=Usuario config.mailer_use_sendmail=Usar Sendmail config.mailer_sendmail_path=Ruta de Sendmail config.mailer_sendmail_args=Argumentos adicionales por Sendmail -config.mailer_sendmail_timeout=Tiempo de espera de Sendmail +config.mailer_sendmail_timeout=Vencimiento de Sendmail config.mailer_use_dummy=Dummy config.test_email_placeholder=Correo electrónico (ej. test@ejemplo.com) -config.send_test_mail=Enviar prueba de correo +config.send_test_mail=Enviar prueba de correo-e config.send_test_mail_submit=Enviar -config.test_mail_failed=Fallo al enviar un correo electrónico de prueba a "%s": %v -config.test_mail_sent=Se ha enviado un correo electrónico de prueba a "%s". +config.test_mail_failed=Fallo al enviar una prueba de correo-e a «%s»: %v +config.test_mail_sent=Se ha enviado un correo-e de prueba a «%s». config.oauth_config=Configuración OAuth config.oauth_enabled=Activado -config.cache_config=Configuración de la Caché -config.cache_adapter=Adaptador de la Caché -config.cache_interval=Intervalo de la Caché -config.cache_conn=Conexión de la Caché -config.cache_item_ttl=Período de vida para elementos de caché +config.cache_config=Configuración de caché +config.cache_adapter=Adaptador de caché +config.cache_interval=Intervalo de caché +config.cache_conn=Conexión de caché +config.cache_item_ttl=TTL de elemento caché -config.session_config=Configuración de la Sesión -config.session_provider=Proveedor de la Sesión -config.provider_config=Configuración del Proveedor -config.cookie_name=Nombre de la Cookie -config.gc_interval_time=Intervalo de tiempo del GC -config.session_life_time=Tiempo de Vida de la Sesión -config.https_only=Sólo HTTPS +config.session_config=Configuración de sesión +config.session_provider=Proveedor de sesión +config.provider_config=Configuración de proveedor +config.cookie_name=Nombre de cookie +config.gc_interval_time=Tiempo de intervalo GC +config.session_life_time=Tiempo de vida de sesión +config.https_only=Solo HTTPS config.cookie_life_time=Tiempo de Vida de la Cookie config.picture_config=Configuración de imagen y avatar config.picture_service=Servicio de Imágen config.disable_gravatar=Desactivar Gravatar -config.enable_federated_avatar=Habilitar Avatares Federados +config.enable_federated_avatar=Habilitar avatares federados config.git_config=Configuración de Git -config.git_disable_diff_highlight=Desactivar resaltado de sintaxis del Diff -config.git_max_diff_lines=Líneas de Diff máximas (por un solo archivo) -config.git_max_diff_line_characters=Carácteres de Diff máximos (para una sola línea) -config.git_max_diff_files=Máximo de archivos de Diff (que se mostrarán) +config.git_disable_diff_highlight=Inhabilitar resaltado de diff de sintaxis +config.git_max_diff_lines=Líneas de diff máximas por archivo +config.git_max_diff_line_characters=Caracteres de diff máx por línea +config.git_max_diff_files=Diff de archivos máxima mostrada config.git_gc_args=Argumentos de GC -config.git_migrate_timeout=Tiempo de espera de migración -config.git_mirror_timeout=Tiempo de espera de actualización de réplicas -config.git_clone_timeout=Tiempo de espera de operación de clones -config.git_pull_timeout=Tiempo de espera de operación de pull -config.git_gc_timeout=Tiempo de espera de operación de GC +config.git_migrate_timeout=Vencimiento de migración +config.git_mirror_timeout=Vencimiento de actualización de réplica +config.git_clone_timeout=Vencimiento de operación de clonado +config.git_pull_timeout=Vencimiento de operación de pull +config.git_gc_timeout=Vencimiento de operación de GC -config.log_config=Configuración del Log +config.log_config=Configuración de bitácora config.logger_name_fmt=Registro: %s config.disabled_logger=Desactivado config.access_log_mode=Modo de registro del Acceso -config.access_log_template=Plantilla de registro de acceso +config.access_log_template=Plantilla de bitácora de acceso config.xorm_log_sql=Registrar SQL config.set_setting_failed=Error al configurar %s monitor.stats=Estadísticas -monitor.cron=Tareas de Cron +monitor.cron=Tareas de cron monitor.name=Nombre monitor.schedule=Agenda monitor.next=Siguiente @@ -3380,9 +3382,9 @@ monitor.queue.type=Tipo monitor.queue.exemplar=Ejemplo monitor.queue.numberworkers=Número de trabajadores monitor.queue.activeworkers=Trabajadores activos -monitor.queue.maxnumberworkers=Número máximo de trabajadores +monitor.queue.maxnumberworkers=Nº máx de trabajadores monitor.queue.numberinqueue=Número en cola -monitor.queue.review_add=Revisar / Añadir Trabajadores +monitor.queue.review_add=Revisar / Añadir trabajadores monitor.queue.settings.title=Ajustes del grupo monitor.queue.settings.desc=Los grupos de trabajadores crecen dinámicamente en respuesta al bloqueo de cola de sus trabajadores. monitor.queue.settings.maxnumberworkers=Número máximo de trabajadores @@ -3393,10 +3395,10 @@ monitor.queue.settings.changed=Ajustes actualizados monitor.queue.settings.remove_all_items=Eliminar todo monitor.queue.settings.remove_all_items_done=Todos los elementos en la cola han sido eliminados. -notices.system_notice_list=Notificaciones del Sistema -notices.view_detail_header=Ver detalles de notificación +notices.system_notice_list=Notificaciones del sistema +notices.view_detail_header=Detalles de notificación notices.operations=Operaciones -notices.select_all=Sleccionar todo +notices.select_all=Seleccionar todo notices.deselect_all=Deseleccionar todo notices.inverse_selection=Selección inversa notices.delete_selected=Eliminar seleccionado @@ -3440,6 +3442,8 @@ self_check.database_inconsistent_collation_columns = La base de datos está usan self_check.database_fix_mysql = Para usuarios de MySQL/MariaDB, podrían usar el comando "forgejo doctor convert" para arreglar los problemas de intercalación, o también podrían arreglarlos utilizando consultas SQL manuales, como "ALTER ... COLLATE ...". +config.cache_test_failed = Incorrecto al probar la caché: %v. + [action] create_repo=creó el repositorio %s rename_repo=repositorio renombrado de %[1]s a %[3]s @@ -3533,8 +3537,8 @@ runs.no_runs=El flujo de trabajo no tiene ejecuciones todavía. workflow.disable=Desactivar flujo de trabajo workflow.disable_success=Flujo de trabajo "%s" desactivado exitosamente. -workflow.enable=Activar flujo de trabajo -workflow.enable_success=Flujo de trabajo '%s' habilitado con éxito. +workflow.enable=Habilitar flujo +workflow.enable_success=Flujo «%s» habilitado correctamente. workflow.disabled=El flujo de trabajo está deshabilitado. need_approval_desc=Necesita aprobación para ejecutar flujos de trabajo para el pull request del fork. @@ -3571,7 +3575,7 @@ runs.no_workflows.help_no_write_access = Para aprender sobre Forgejo Actions, v type-1.display_name=Proyecto individual type-2.display_name=Proyecto de repositorio type-3.display_name=Proyecto de organización -deleted.display_name = Proyecto borrado +deleted.display_name = Proyecto eliminado [git.filemode] changed_filemode=%[1]s → %[2]s diff --git a/options/locale/locale_et.ini b/options/locale/locale_et.ini index e86a0669dd..092cb47ecf 100644 --- a/options/locale/locale_et.ini +++ b/options/locale/locale_et.ini @@ -142,6 +142,8 @@ captcha = Robotilõks unpin = Lõpeta esiletõstmine powered_by = Siin on kasutusel %s +collaborative = Koostöö + [search] search = Otsi… fuzzy = Hägus @@ -189,7 +191,7 @@ buttons.heading.tooltip = Lisa pealkiri buttons.italic.tooltip = Lisa kaldkirjas tekst (Ctrl+I / ⌘I) buttons.quote.tooltip = Tsiteeri teksti buttons.code.tooltip = Lisa kood -buttons.link.tooltip = Lisa link +buttons.link.tooltip = Lisa link (Ctrl+K / ⌘K) buttons.list.ordered.tooltip = Lisa nummerdatud nimekiri buttons.list.unordered.tooltip = Lisa nimekiri buttons.list.task.tooltip = Lisa ülesannete nimekiri diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 64639e80a1..3472ab5c1e 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -7,7 +7,7 @@ sign_in=ورود sign_in_or=یا sign_out=خروج sign_up=ثبت نام -link_account=پیوند به حساب +link_account=پیوند به حساب‌کاربری register=ثبت نام version=نسخه powered_by=قدرت از %s diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 763e036069..ff0b5597e5 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -2084,7 +2084,7 @@ settings.transfer_desc = Siirrä tämä tietovarasto käyttäjälle tai organisa settings.add_collaborator = Lisää avustaja settings.mirror_settings.push_mirror.none = Työntöpeilejä ei ole määritetty settings.collaborator_deletion_desc = Avustajan poistaminen peruuttaa hänen pääsynsä tähän tietovarastoon. Jatketaanko? -settings.archive.text = Tietovaraston arkistointi asettaa sen pelkkään lukutilaan. Se piilotetaan kojelaudalta. Kukaan ei voi tehdä (et edes sinä) uusia kommitteja, tai avata ongelmia tai vetopyyntöjä. +settings.archive.text = Tietovaraston arkistointi asettaa sen pelkkään lukutilaan. Se piilotetaan kojelaudalta. Kukaan ei voi tehdä (et edes sinä) uusia kommitteja, tai avata ongelmia tai vetopyyntöjä. Arkistoinnin syy on suositeltavaa kertoa, sillä kyseinen tieto auttaa tietovaraston haarauttamista harkitsevia kehittäjiä. settings.mirror_settings.docs = Määritä tietovarastosi synkronoimaan kommitit, tagit ja haarat automaattisesti toisen tietovaraston kanssa. settings.add_collaborator_duplicate = Avustaja on jo lisätty tähän tietovarastoon. settings.add_collaborator_blocked_them = Avustajaa ei voi lisätä, koska hän on estänyt tietovaraston omistajan. @@ -2612,7 +2612,7 @@ repo_updated=Päivitetty %s members=Jäsenet teams=Tiimit lower_members=jäsentä -lower_repositories=tietovarastot +lower_repositories=tietovarastoa create_new_team=Uusi tiimi create_team=Luo tiimi org_desc=Kuvaus diff --git a/options/locale/locale_fil.ini b/options/locale/locale_fil.ini index 0181f32452..ba2da26e59 100644 --- a/options/locale/locale_fil.ini +++ b/options/locale/locale_fil.ini @@ -832,7 +832,7 @@ generate_token_success = Nag-generate na ang iyong bagong token. Kopyahin ito ng delete_token = Burahin access_token_deletion = Burahin ang access token access_token_deletion_desc = Ang pagbura ng isang token ay babawiin ang pag-access sa iyong account para sa mga application gamit ito. Ang gawaing ito ay hindi pwedeng baguhin. Magpatuloy? -repo_and_org_access = Access sa Repositoryo at Organisasyon +repo_and_org_access = Access sa repositoryo at organisasyon permissions_public_only = Publiko lamang permissions_access_all = Lahat (publiko, pribado, at limitado) select_permissions = Pumili ng mga pahintulot @@ -2478,7 +2478,7 @@ settings.chat_id = ID ng chat settings.matrix.message_type = Uri ng mensahe settings.tags.protection.allowed.noone = Walang sinuman settings.archive.header = I-archive ang repo na ito -settings.archive.text = Ang pag-archive ng repo ay gagawin itong buo na read-only. Itatago ito sa dashboard. Walang tao (kahit ikaw rin!) ay makakagawa ng bagong commit, o magbukas ng mga isyu o hiling sa paghila. +settings.archive.text = Gagawing read-only ang buong repositoryo ang pag-archive. Itatago ito sa dashboard. Hindi magagawa ng anumang tao (kahit ikaw rin!) ang paggawa ng mga bagong commit at makapagbukas ng mga isyu at hiling sa paghila. Inirerekomenda ang pagdokumento ng dahilan ng pagka-archive para bigyan ng tagubilin ang mga developer na nagpaplanong i-fork ang repositoryo sa lalong madaling panahon. settings.archive.error = May naganap na error habang sinusubukang i-archive ang repo. Tignan ang log para sa mga detalye. settings.archive.error_ismirror = Hindi ka makaka-archive ng naka-mirror na repo. settings.unarchive.button = I-unarchive ang repo diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index d48753763a..1d856d1502 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -873,7 +873,7 @@ delete_token=Supprimer access_token_deletion=Supprimer le jeton d'accès access_token_deletion_desc=Supprimer un jeton révoquera l'accès à votre compte pour toutes les applications l'utilisant. Cette action est irréversible. Continuer ? delete_token_success=Ce jeton a été supprimé. Les applications l'utilisant n'ont plus accès à votre compte. -repo_and_org_access=Accès aux Organisations et Dépôts +repo_and_org_access=Accès aux dépôts et organisations permissions_public_only=Publique uniquement permissions_access_all=Tout (public, privé et limité) select_permissions=Sélectionner les autorisations diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 27e61dd0ac..f3fed02c5b 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -96,7 +96,7 @@ copy_error = Theip ar an gcóipeáil copy_type_unsupported = Ní féidir an cineál comhaid seo a chóipeáil write = Scríobh preview = Réamhamharc -loading = Á lódáil... +loading = Á lódáil… error = Earráid error404 = Níl an leathanach atá tú ag iarraidh a bhaint amach ann, nó baineadh éníl údarú agat é a fheiceáil. go_back = Ar ais @@ -190,7 +190,7 @@ buttons.bold.tooltip = Cuir téacs trom leis (Ctrl+B / ⌘B) buttons.italic.tooltip = Cuir téacs iodálach leis (Ctrl+I / ⌘I) buttons.quote.tooltip = Téacs luaigh buttons.code.tooltip = Cuir cód leis -buttons.link.tooltip = Cuir nasc leis +buttons.link.tooltip = Cuir nasc leis (Ctrl+K / ⌘K) buttons.list.unordered.tooltip = Cuir liosta piléar leis buttons.list.ordered.tooltip = Cuir liosta uimhrithe buttons.list.task.tooltip = Cuir liosta tascanna leis @@ -261,7 +261,7 @@ repo_path = Cosán Fréimhe an Stór repo_path_helper = Sábhálfar stórais iargúlta Git chuig an eolaire seo. lfs_path = Cosán Fréamh Git LFS lfs_path_helper = Stórálfar comhaid a rianóidh Git LFS san eolaire seo. Fág folamh le díchumasú. -domain = Fearann ​​Freastalaí +domain = Fearann freastalaí domain_helper = Seoladh fearainn nó óstach don fhreastalaí. ssh_port = Port Freastalaí SSH app_url_helper = Seoladh bonn le haghaidh URLanna clóin HTTP(S) agus fógraí ríomhphoist. @@ -305,12 +305,51 @@ invalid_log_root_path = Tá an cosán logála neamhbhailí:%v no_reply_address = Fearann Ríomhphoist Folaite password_algorithm = Algartam Hais Pasfhocal invalid_password_algorithm = Algartam hais pasfhocail neamhbhailí -password_algorithm_helper = Socraigh an algartam hashing pasfhocal. Tá riachtanais agus neart éagsúla ag halgartaim. Tá an algartam argon2 sách slán ach úsáideann sé go leor cuimhne agus d'fhéadfadh sé a bheith míchuí do chórais bheaga. +password_algorithm_helper = Socraigh an algartam haiseála pasfhocail. Bíonn riachtanais agus láidreachtaí difriúla ag halgartaim. Tá an algartam argon2 sách slán ach úsáideann sé go leor cuimhne agus d'fhéadfadh sé a bheith mí-oiriúnach do chórais bheaga. enable_update_checker = Cumasaigh Seiceoir Nuashonraithe env_config_keys = Cumraíocht Comhshaoil env_config_keys_prompt = Cuirfear na hathróga comhshaoil seo a leanas i bhfeidhm ar do chomhad cumraíochta freisin: docker_helper = Má ritheann tú Forgejo taobh istigh de Docker, léigh an doiciméadú le do thoil sula n-athraíonn tú aon socruithe. +require_db_desc = Éilíonn Forgejo MySQL, PostgreSQL, SQLite3 nó TiDB (prótacal MySQL). +sqlite_helper = Cosán comhaid don bhunachar sonraí SQLite3.
Cuir isteach cosán absalóideach má ritheann tú Forgejo mar sheirbhís. +reinstall_error = Tá tú ag iarraidh suiteáil i mbunachar sonraí Forgejo atá ann cheana féin +reinstall_confirm_message = Is féidir go leor fadhbanna a chruthú má athshuiteáiltear é le bunachar sonraí Forgejo atá ann cheana féin. I bhformhór na gcásanna, ba chóir duit d'"app.ini" atá ann cheana a úsáid chun Forgejo a rith. Má tá a fhios agat cad atá á dhéanamh agat, deimhnigh an méid seo a leanas: +reinstall_confirm_check_3 = Deimhníonn tú go bhfuil tú cinnte go hiomlán go bhfuil an Forgejo seo ag rith leis an suíomh app.ini ceart agus go bhfuil tú cinnte go gcaithfidh tú athshuiteáil a dhéanamh. Deimhníonn tú go n-aithníonn tú na rioscaí thuas. +app_name = Teideal an sampla +app_name_helper = Cuir isteach ainm d’eiseamláire anseo. Taispeánfar é ar gach leathanach. +app_slogan = Slogan samplach +app_slogan_helper = Cuir isteach mana do shampla anseo. Fág folamh chun é a dhíchumasú. +run_user = Úsáideoir le rith mar +run_user_helper = Ainm úsáideora an chórais oibriúcháin a ritheann Forgejo mar úsáid. Tabhair faoi deara go gcaithfidh rochtain a bheith ag an úsáideoir seo ar chonair fréimhe an stórais. +ssh_port_helper = Uimhir an phoirt a úsáidfidh an freastalaí SSH. Fág folamh chun an freastalaí SSH a dhíchumasú. +http_port = Port éisteachta HTTP +http_port_helper = Uimhir an phoirt a úsáidfidh freastalaí gréasáin Forgejo. +app_url = Bun-URL +smtp_from_helper = Seoladh ríomhphoist a úsáidfidh Forgejo. Cuir isteach seoladh ríomhphoist simplí nó bain úsáid as an bhformáid "Ainm" . +offline_mode.description = Díchumasaigh líonraí seachadta ábhair tríú páirtí agus freastalaigh na hacmhainní go léir go háitiúil. +disable_gravatar.description = Díchumasaigh úsáid Gravatar nó foinsí avatar tríú páirtí eile. Úsáidfear íomhánna réamhshocraithe d’avatars úsáideoirí mura n-uaslódálann siad a n-avatar féin chuig an gcás. +federated_avatar_lookup.description = Cuardaigh avatars ag baint úsáide as Libravatar. +disable_registration.description = Ní bheidh ach riarthóirí samplaí in ann cuntais úsáideora nua a chruthú. Moltar go mór clárú a choinneáil díchumasaithe mura bhfuil sé ar intinn agat sampla poiblí a óstáil do gach duine agus má tá tú réidh le déileáil le líon mór cuntas turscair. +allow_only_external_registration = Ceadaigh clárú trí sheirbhísí seachtracha amháin +allow_only_external_registration.description = Ní bheidh úsáideoirí in ann cuntais nua a chruthú ach trí sheirbhísí seachtracha cumraithe a úsáid. +openid_signin.description = Ceadaigh d’úsáideoirí síniú isteach trí OpenID. +openid_signup.description = Ceadaigh d’úsáideoirí cuntais a chruthú trí OpenID má tá féinchlárú cumasaithe. +enable_captcha.description = Éiligh ar úsáideoirí CAPTCHA a úsáid chun cuntais a chruthú. +require_sign_in_view = Éilítear síniú isteach chun ábhar an sampla a fheiceáil +require_sign_in_view.description = Teorainn a chur le rochtain ar ábhar d'úsáideoirí atá sínithe isteach. Ní bheidh aíonna in ann cuairt a thabhairt ach ar na leathanaigh fíordheimhnithe. +default_keep_email_private.description = Cumasaigh seoladh ríomhphoist a cheilt d’úsáideoirí nua de réir réamhshocraithe ionas nach sceithfear an fhaisnéis seo díreach tar éis clárúcháin. +default_allow_create_organization.description = Ceadaigh d’úsáideoirí nua eagraíochtaí a chruthú de réir réamhshocraithe. Nuair a bhíonn an rogha seo díchumasaithe, beidh ar riarthóir cead a thabhairt d’úsáideoirí nua eagraíochtaí a chruthú. +default_enable_timetracking.description = Ceadaigh úsáid ghné rianaithe ama do stórtha nua de réir réamhshocraithe. +admin_setting.description = Is rogha é cuntas riarthóra a chruthú. Beidh an chéad úsáideoir cláraithe ina riarthóir go huathoibríoch. +config_location_hint = Sábhálfar na roghanna cumraíochta seo i: +install_btn_confirm = Suiteáil Forgejo +test_git_failed = Níorbh fhéidir an t-ordú "git" a thástáil: %v +sqlite3_not_available = Ní thacaíonn an leagan seo de Forgejo le SQLite3. Íoslódáil an leagan dénártha oifigiúil ó %s (ní an leagan "gobuild"). +run_user_not_match = Ní hé an t-ainm úsáideora "úsáideoir le rith mar" an t-ainm úsáideora reatha: %s -> %s +enable_update_checker_helper_forgejo = Déanfaidh sé seiceáil tréimhsiúil le haghaidh leaganacha nua de Forgejo trí thaifead DNS TXT a sheiceáil ag release.forgejo.org. +no_reply_address_helper = Ainm fearainn d'úsáideoirí a bhfuil seoladh ríomhphoist i bhfolach acu. Mar shampla, logálfar an t-ainm úsáideora "joe" i Git mar "joe@noreply.example.org" má shocraítear an fearann ríomhphoist i bhfolach go "noreply.example.org". + [home] uname_holder = Ainm Úsáideora nó Seoladh Ríomhphoist switch_dashboard_context = Athraigh Comhthéacs an Deais @@ -329,6 +368,8 @@ show_only_private = Ag taispeáint príobháideach amháin show_only_public = Ag taispeáint poiblí amháin issues.in_your_repos = I do stórais +my_orgs = Eagraíochtaí + [explore] repos = Stórais users = Úsáideoirí @@ -342,7 +383,7 @@ relevant_repositories = Níl ach stórtha ábhartha á dtaispeáint, Sínigh isteach anois! +hint_register = An bhfuil cuntas uait? Cláraigh anois. +sign_up_button = Cláraigh anois. +confirmation_mail_sent_prompt = Tá ríomhphost deimhnithe nua seolta chuig %s. Chun an próiseas clárúcháin a chríochnú, seiceáil do bhosca isteach agus lean an nasc a cuireadh ar fáil laistigh den chéad %s eile. Mura bhfuil an ríomhphost ceart, is féidir leat logáil isteach agus ríomhphost deimhnithe eile a iarraidh chuig seoladh difriúil. +reset_password_mail_sent_prompt = Tá ríomhphost deimhnithe seolta chuig %s. Chun an próiseas aisghabhála cuntais a chríochnú, seiceáil do bhosca isteach agus lean an nasc a chuirtear ar fáil laistigh den chéad %s eile. +prohibit_login = Tá an cuntas ar fionraí +prohibit_login_desc = Tá do chuntas curtha ar fionraí ó idirghníomhú leis an gcás. Téigh i dteagmháil le riarthóir an chás chun rochtain a fháil ar ais. +change_unconfirmed_email_summary = Athraigh an seoladh ríomhphoist a seoltar ríomhphost gníomhachtaithe chuige. +change_unconfirmed_email = Má thug tú an seoladh ríomhphoist mícheart le linn clárúcháin, is féidir leat é a athrú thíos, agus seolfar dearbhú chuig an seoladh nua ina ionad. +change_unconfirmed_email_error = Ní féidir an seoladh ríomhphoist a athrú: %v +send_reset_mail = Seol ríomhphost aisghabhála +non_local_account = Ní féidir le húsáideoirí nach áitiúla iad a bpasfhocal a nuashonrú tríd an gcomhéadan gréasáin Forgejo. +unauthorized_credentials = Tá na dintiúir mícheart nó imithe in éag. Déan iarracht d’ordú a athdhéanamh nó féach %s le haghaidh tuilleadh eolais +use_onetime_code = Úsáid cód aonuaire +oauth_signin_tab = Nasc le cuntas atá ann cheana féin +authorize_application_description = Má dheonaíonn tú rochtain, beidh sé in ann rochtain a fháil ar fhaisnéis do chuntais go léir agus scríobh chuici, lena n-áirítear stórtha príobháideacha agus eagraíochtaí. +sign_in_openid = Lean ar aghaidh le OpenID + [mail] view_it_on = Féach air ar %s reply = nó freagra a thabhairt ar an r-phost seo go díreach @@ -440,6 +499,33 @@ team_invite.text_1 = Tá cuireadh tugtha ag %[1]s duit chun dul le foireann %[2] team_invite.text_2 = Cliceáil ar an nasc seo a leanas le do thoil chun dul isteach san fhoireann: team_invite.text_3 = Nóta: Bhí an cuireadh seo beartaithe do %[1]s. Mura raibh tú ag súil leis an gcuireadh seo, is féidir leat neamhaird a dhéanamh den ríomhphost seo. +link_not_working_do_paste = Nach bhfuil an nasc ag obair? Bain triail as é a chóipeáil agus a ghreamú i mbarra URL do bhrabhsálaí. +admin.new_user.subject = Úsáideoir nua %s díreach cláraithe +admin.new_user.user_info = Faisnéis úsáideora +admin.new_user.text = Le do thoil, cliceáil anseo chun an t-úsáideoir seo a bhainistiú ón bpainéal riaracháin. +register_notify.text_2 = Is féidir leat síniú isteach i do chuntas ag baint úsáide as d'ainm úsáideora: %s +register_notify.text_3 = Más duine eile a chruthaigh an cuntas seo duit, beidh ort do phasfhocal a shocrú ar dtús. +reset_password.text = Más tusa a bhí ann, cliceáil an nasc seo a leanas le do chuntas a aisghabháil laistigh de %s: +password_change.subject = Tá d’fhocal faire athraithe +password_change.text_1 = Athraíodh pasfhocal do chuntais díreach anois. +primary_mail_change.subject = Tá do phríomhphost athraithe +primary_mail_change.text_1 = Athraíodh príomhsheoladh ríomhphoist do chuntais go %[1]s díreach anois. Ciallaíonn sé seo nach bhfaighidh an seoladh ríomhphoist seo fógraí ríomhphoist a thuilleadh chun do chuntas. +totp_disabled.subject = Tá TOTP díchumasaithe +totp_disabled.text_1 = Díchumasaíodh pasfhocal aonuaire bunaithe ar am (TOTP) ar do chuntas díreach anois. +totp_disabled.no_2fa = Níl aon mhodhanna 2FA eile cumraithe a thuilleadh, rud a chiallaíonn nach gá logáil isteach i do chuntas le 2FA a thuilleadh. +removed_security_key.subject = Baineadh eochair slándála +removed_security_key.text_1 = Baineadh an eochair slándála "%[1]s" as do chuntas díreach anois. +removed_security_key.no_2fa = Níl aon mhodhanna 2FA eile cumraithe a thuilleadh, rud a chiallaíonn nach gá logáil isteach i do chuntas le 2FA a thuilleadh. +account_security_caution.text_1 = Más tusa a bhí ann, is féidir leat neamhaird a dhéanamh den ríomhphost seo go sábháilte. +account_security_caution.text_2 = Mura tusa a bhí ann, tá do chuntas i mbaol. Téigh i dteagmháil le riarthóirí an tsuímh seo, le do thoil. +totp_enrolled.subject = Tá TOTP gníomhachtaithe agat mar mhodh 2FA +totp_enrolled.text_1.no_webauthn = Tá TOTP cumasaithe agat chun do chuntas díreach anois. Ciallaíonn sé seo go gcaithfidh tú TOTP a úsáid mar mhodh 2FA i ngach logáil isteach chuig do chuntas amach anseo. +totp_enrolled.text_1.has_webauthn = Tá TOTP cumasaithe agat chun do chuntas díreach anois. Ciallaíonn sé seo, i gcás gach logáil isteach chuig do chuntas amach anseo, gur féidir leat TOTP a úsáid mar mhodh 2FA nó aon cheann de do chuid eochracha slándála a úsáid. +repo.transfer.subject_to = Tá %s ag iarraidh stórlann "%s" a aistriú go %s +repo.transfer.subject_to_you = Tá %s ag iarraidh stórlann "%s" a aistriú chugat +repo.collaborator.added.subject = Chuir %s le %s thú mar chomhoibrí +repo.collaborator.added.text = Cuireadh leis an stór thú mar chomhoibrí: + [modal] yes = Tá no = Níl @@ -467,7 +553,7 @@ require_error = ` ní féidir a bheith folamh.` git_ref_name_error = ` caithfidh gur ainm tagartha Git dea-chruthaithe é.` size_error = ` ní mór méid %s.` min_size_error = ` ní mór go mbeadh carachtar %s ar a laghad ann.` -max_size_error = caithfidh %s carachtar ar a mhéad a bheith ann. +max_size_error = `ní mór %s carachtar ar a mhéad a bheith ann.` email_error = `ní seoladh ríomhphoist bailí é.` url_error = `ní URL bailí é "%s".` include_error = ` ní mór fotheaghrán a bheith ann "%s".` @@ -519,6 +605,29 @@ auth_failed = Theip ar fhíordheimhniú:%v target_branch_not_exist = Níl spriocbhrainse ann. admin_cannot_delete_self = Ní féidir leat tú féin a scriosadh nuair is riarachán tú. Bain do phribhléidí riaracháin ar dtús. +FullName = Ainm iomlán +Description = Cur síos +Pronouns = Forainmneacha +Biography = Beathaisnéis +Website = Suíomh Gréasáin +Location = Suíomh +To = Ainm na brainse +AccessToken = Comhartha rochtana +alpha_dash_error = `Níor cheart ach carachtair alfa-uimhriúla, fleascáin ("-") agus fo-líne ("_") a bheith sa.` +alpha_dash_dot_error = `Níor cheart ach carachtair alfa-uimhriúla, fleasc ("-"), fo-líne ("_") agus ponc (".") a bheith sa.` +username_error = `Ní féidir ach carachtair alfa-uimhriúla ("0-9", "a-z", "A-Z"), fleasc ("-"), fo-líne ("_") agus ponc (".") a bheith ann. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.` +username_error_no_dots = `Ní féidir ach carachtair alfa-uimhriúla ("0-9", "a-z", "A-Z"), fleasc ("-") agus fo-líne ("_") a bheith ann. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.` +username_claiming_cooldown = Ní féidir an t-ainm úsáideora a éileamh, mar níl a thréimhse fuaraithe thart fós. Is féidir é a éileamh ar %[1]s. +email_domain_is_not_allowed = Tá coimhlint idir fearann seoladh ríomhphoist an úsáideora %s agus EMAIL_DOMAIN_ALLOWLIST nó EMAIL_DOMAIN_BLOCKLIST. Cinntigh go bhfuil an seoladh ríomhphoist socraithe i gceart agat. +last_org_owner = Ní féidir leat an t-úsáideoir deireanach a bhaint den fhoireann "úinéirí". Ní mór úinéir amháin ar a laghad a bheith ann d'eagraíocht. +unable_verify_ssh_key = Ní féidir an eochair SSH a fhíorú, déan seiceáil dhúbailte uirthi le haghaidh earráidí. +still_own_repo = Tá stór amháin nó níos mó i do chuntas, scrios nó aistrigh iad ar dtús. +still_has_org = Is ball d'eagraíocht amháin nó níos mó do chuntas, fág iad ar dtús. +still_own_packages = Tá pacáiste amháin nó níos mó i do chuntas, scrios iad ar dtús. +org_still_own_repo = Tá stór amháin nó níos mó fós faoi úinéireacht na heagraíochta seo, scrios nó aistrigh iad ar dtús. +org_still_own_packages = Tá pacáiste amháin nó níos mó fós ag an eagraíocht seo, scrios iad ar dtús. +required_prefix = Ní mór an ionchur a thosú le "%s" + [user] change_avatar = Athraigh do abhatár… joined_on = Cláraigh ar %s @@ -539,6 +648,29 @@ disabled_public_activity = Dhíchumasaigh an t-úsáideoir seo infheictheacht ph form.name_reserved = Tá an t-ainm úsáideora "%s" in áirithe. form.name_pattern_not_allowed = Ní cheadaítear an patrún "%s" in ainm úsáideora. +followers.title.one = Leantóir +followers.title.few = Leantóirí +following.title.one = Ag leanúint +following.title.few = Ag leanúint +followers_one = %d leantóir +followers_few = %d leantóirí +following_one = %d ag leanúint +following_few = %d ag leanúint +block_user = Úsáideoir blocáilte +block_user.detail = Tabhair faoi deara go bhfuil éifeachtaí eile ag baint le húsáideoir a bhlocáil, amhail: +block_user.detail_1 = Scoirfidh sibh de bheith ag leanúint a chéile agus ní bheidh sibh in ann leanúint a chéile. +block_user.detail_2 = Ní bheidh an t-úsáideoir seo in ann idirghníomhú leis na stórtha atá i do sheilbh, ná leis na saincheisteanna agus na tuairimí atá cruthaithe agat. +block_user.detail_3 = Ní bheidh sibh in ann a chéile a chur leis mar chomhoibritheoirí stóir. +follow_blocked_user = Ní féidir leat an t-úsáideoir seo a leanúint mar gur chuir tú bac air nó mar gur chuir an t-úsáideoir seo bac ort. +block = Bloc +unblock = Díbhlocáil +public_activity.visibility_hint.self_public = Tá do ghníomhaíocht le feiceáil ag gach duine, seachas idirghníomhaíochtaí i spásanna príobháideacha. Cumraigh. +public_activity.visibility_hint.admin_public = Tá an ghníomhaíocht seo le feiceáil ag gach duine, ach mar riarthóir is féidir leat idirghníomhaíochtaí i spásanna príobháideacha a fheiceáil freisin. +public_activity.visibility_hint.self_private = Ní féidir ach leatsa agus le riarthóirí an cháis do ghníomhaíocht a fheiceáil. Cumraigh. +public_activity.visibility_hint.admin_private = Tá an ghníomhaíocht seo le feiceáil agat mar is riarthóir thú, ach ba mhaith leis an úsáideoir go bhfanfadh sí príobháideach. +public_activity.visibility_hint.self_private_profile = Ní féidir ach leatsa agus le riarthóirí an cháis do ghníomhaíocht a fheiceáil mar go bhfuil do phróifíl príobháideach. Cumraigh. +form.name_chars_not_allowed = Tá carachtair neamhbhailí san ainm úsáideora "%s". + [settings] profile = Próifíl account = Cuntas @@ -565,13 +697,13 @@ update_language_success = Tá an teanga nuashonraithe. update_profile_success = Nuashonraíodh do phróifíl. change_username = Tá d'ainm úsáideora athraithe. change_username_prompt = Nóta: Athraíonn athrú d'ainm úsáideora URL do chuntais freisin. -change_username_redirect_prompt = Athreoróidh an sean-ainm úsáideora go dtí go n-éilíonn duine é +change_username_redirect_prompt = Déanfar an seanainm úsáideora a atreorú go dtí go n-éileoidh duine éigin é. continue = Lean ar aghaidh cancel = Cealaigh language = Teanga ui = Téama hidden_comment_types = Cineálacha tráchtaireachta ceilte -hidden_comment_types.ref_tooltip = Tuairimí ina dtagraíodh an tsaincheist seo ó shaincheiste/coiste eile... +hidden_comment_types.ref_tooltip = Tráchtanna inar tagraíodh don cheist seo ó cheist/tiomantas/… eile hidden_comment_types.issue_ref_tooltip = Tuairimí ina n-athraíonn an t-úsáideoir an brainse/clib a bhaineann leis an tsaincheist comment_type_group_reference = Tagairt comment_type_group_label = Lipéad @@ -603,7 +735,7 @@ new_password = Pasfhocal Nua retype_new_password = Deimhnigh Pasfhocal Nua password_incorrect = Tá an pasfhocal reatha mícheart. manage_emails = Bainistigh Seoltaí Ríomhphoist -email_desc = Úsáidfear do phríomhsheoladh ríomhphoist le haghaidh fógraí, aisghabháil pasfhocal agus, ar choinníoll nach bhfuil sé i bhfolach, oibríochtaí Git bunaithe ar an ngréas +email_desc = Úsáidfear do phríomhsheoladh ríomhphoist le haghaidh fógraí, aisghabháil pasfhocail agus, ar choinníoll nach bhfuil sé i bhfolach, oibríochtaí Git gréasánbhunaithe. primary = Príomhúil activated = Gníomhachtaithe requires_activation = Éilíonn gníomhachtú @@ -613,7 +745,7 @@ activations_pending = Gníomhartha ar Feitheamh can_not_add_email_activations_pending = Tá gníomhachtú ar feitheamh, déan iarracht arís i gceann cúpla nóiméad más mian leat ríomhphost nua a chur leis. delete_email = Bain email_deletion = Bain Seoladh R-phoist -email_deletion_desc = Bainfear an seoladh ríomhphoist agus an fhaisnéis ghaolmhar as do chuntas. Ní bheidh na tiomáintí Git a bhaineann leis an seoladh ríomhphoist seo athraithe. Lean ar aghaidh? +email_deletion_desc = Bainfear an seoladh ríomhphoist seo agus faisnéis ghaolmhar as do chuntas. Fanfaidh na gealltanais Git ón seoladh ríomhphoist seo gan athrú. Ar mhaith leat leanúint ar aghaidh? email_deletion_success = Tá an seoladh ríomhphoist bainte. theme_update_success = Nuashonraíodh do théama. theme_update_error = Níl an téama roghnaithe ann. @@ -695,12 +827,12 @@ generate_new_token = Gin Comhartha Nua token_name = Ainm Comhartha generate_token = Gin Comhartha generate_token_success = Gintear do chomhartha nua. Cóipeáil é anois mar ní thaispeánfar é arís. -generate_token_name_duplicate = Úsáideadh %s mar ainm feidhmchláir cheana féin. Úsáid ceann nua le do thoil. +generate_token_name_duplicate = Tá %s in úsáid mar ainm feidhmchláir cheana féin. Bain úsáid as ceann nua le do thoil. delete_token = Scrios access_token_deletion = Scrios Comhartha Rochtana access_token_deletion_desc = Cúlghairfear rochtain ar do chuntas le haghaidh feidhmchláir a úsáideann é a scriosadh comhartha. Ní féidir é seo a chur ar ais. Lean ar aghaidh? delete_token_success = Tá an comhartha scriosta. Níl rochtain ag iarratais a úsáideann é ar do chuntas a thuilleadh. -repo_and_org_access = Rochtain Stórála agus Eagraíochta +repo_and_org_access = Rochtain ar stórtha agus ar eagraíochta permissions_public_only = Poiblí amháin permissions_access_all = Gach (poiblí, príobháideach agus teoranta) select_permissions = Roghnaigh ceadanna @@ -753,7 +885,7 @@ then_enter_passcode = Agus cuir isteach an paschód a léirítear san fheidhmchl passcode_invalid = Tá an pascód mícheart. Bain triail as arís. twofa_enrolled = Tá do chuntas cláraithe go rathúil. Stóráil d'eochair aisghabhála aonúsáide (%s) in áit shábháilte, mar ní thaispeánfar é arís. twofa_failed_get_secret = Theip ar rún a fháil. -webauthn_desc = Is feistí crua-earraí iad eochracha slándála ina bhfuil eochracha cripte Is féidir iad a úsáid le haghaidh fíordheimhniú dhá fhachtóir. Caithfidh eochracha slándála tacú le caigh deán Fíordheimhnithe WebAuthn +webauthn_desc = Is gléasanna crua-earraí iad eochracha slándála ina bhfuil eochracha cripteagrafacha. Is féidir iad a úsáid le haghaidh fíordheimhniú dhá fhachtóir. Ní mór d'eochracha slándála tacú leis an gcaighdeán WebAuthn Authenticator. webauthn_register_key = Cuir Eochair Slándála webauthn_nickname = Leasainm webauthn_delete_key = Bain Eochair Slándála @@ -784,6 +916,89 @@ visibility.limited = Teoranta visibility.private = Príobháideach visibility.private_tooltip = Ní fheictear ach do bhaill d'eagraíochtaí a chuaigh tú isteach +orgs = Eagraíochtaí +blocked_users = Úsáideoirí blocáilte +storage_overview = Forbhreathnú ar stóráil +quota = Cuóta +biography_placeholder = Inis beagán do dhaoine eile fút féin! (Tacaítear le Markdown) +profile_desc = Fút féin +password_username_disabled = Ní cheadaítear d’úsáideoirí nach úsáideoirí áitiúla iad a n-ainm úsáideora a athrú. Téigh i dteagmháil le riarthóir do shuíomh le haghaidh tuilleadh sonraí. +pronouns = Forainmneacha +pronouns_unspecified = Gan sonrú +update_theme = Athraigh téama +update_language = Athraigh teanga +change_username_redirect_prompt.with_cooldown.one = Beidh an seanainm úsáideora ar fáil do gach duine tar éis tréimhse fuaraithe %[1]d lá. Is féidir leat an seanainm úsáideora a éileamh ar ais fós le linn na tréimhse fuaraithe. +change_username_redirect_prompt.with_cooldown.few = Beidh an seanainm úsáideora ar fáil do gach duine tar éis tréimhse fuaraithe %[1]d lá. Is féidir leat an seanainm úsáideora a éileamh ar ais fós le linn na tréimhse fuaraithe. +language.title = Teanga réamhshocraithe +language.description = Sábhálfar an teanga seo chuig do chuntas agus úsáidfear í mar an teanga réamhshocraithe tar éis duit logáil isteach. +language.localization_project = Cabhraigh linn Forgejo a aistriú go do theanga féin! Foghlaim tuilleadh. +hints = Leideanna +additional_repo_units_hint = Moltar aonaid stórtha breise a chumasú +additional_repo_units_hint_description = Taispeáin leid "Cumasaigh níos mó" do stórtha nach bhfuil na haonaid uile atá ar fáil cumasaithe iontu. +update_hints = Leideanna nuashonraithe +update_hints_success = Tá na leideanna nuashonraithe. +hidden_comment_types_description = Ní thaispeánfar cineálacha tráchta atá seiceáilte anseo laistigh de leathanaigh eagráin. Má sheiceálann tú "Lipéad", mar shampla, baintear na tráchtanna uile a dúirt " chuir