forked from mirrors/forgejo
Compare commits
49 commits
forgejo
...
bp-v13.0/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621106123a | ||
|
|
f1a497d3c1 | ||
|
|
8ac5410a62 | ||
|
|
cb0845cd3e | ||
|
|
a50968d0de | ||
|
|
3bc1ae21ac | ||
|
|
b8448e7cde | ||
|
|
fc14793f7d |
||
|
|
fa1a2ba669 | ||
|
|
afbf1efe02 |
||
|
|
449b5bf10e |
||
|
|
8885844e72 |
||
|
|
a2068a47ce | ||
|
|
2c26525a9a | ||
|
|
6e26d31473 | ||
|
|
6e60d538bd | ||
|
|
2022feee7d | ||
|
|
494d5625e2 | ||
|
|
c7f2d7394b | ||
|
|
76afa21433 | ||
|
|
d7e08cfb8c | ||
|
|
714b88f8b2 | ||
|
|
d59c49ec52 | ||
|
|
48914b9465 | ||
|
|
f7603e7356 | ||
|
|
0fda75e08e | ||
|
|
08f37b5771 | ||
|
|
b1b99d5c70 | ||
|
|
5a622f7640 | ||
|
|
4dbd9c7261 | ||
|
|
9ff712ee6a | ||
|
|
01f0dbde9e | ||
|
|
83e7ff3ba2 | ||
|
|
72a38d19ac | ||
|
|
72faa337d0 | ||
|
|
65189bdb3e | ||
|
|
d6d4033a19 | ||
|
|
80fcbf165c | ||
|
|
2d3d5048d6 | ||
|
|
029a493779 | ||
|
|
877726a2bf | ||
|
|
6755ff1631 | ||
|
|
72abb7079b | ||
|
|
7a3fcbabf2 | ||
|
|
a674198198 | ||
|
|
d0b820039d | ||
|
|
d452207e50 | ||
|
|
010366d641 | ||
|
|
391f9a2f2c |
152 changed files with 8444 additions and 644 deletions
|
|
@ -233,6 +233,7 @@ jobs:
|
|||
options: --tmpfs /bitnami/minio/data
|
||||
ldap:
|
||||
image: data.forgejo.org/oci/forgejo-test-openldap:1
|
||||
options: --memory 500M
|
||||
pgsql:
|
||||
image: data.forgejo.org/oci/bitnami/postgresql:16
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.22 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.22 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -41,7 +41,7 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che
|
|||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.8.0 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.3.1 # 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.31.0 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasource=go
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
|
||||
|
|
@ -514,7 +514,7 @@ lint-disposable-emails-fix:
|
|||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
$(GO) run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
###
|
||||
# Development and testing targets
|
||||
|
|
|
|||
55
assets/go-licenses.json
generated
55
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,8 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
golog "log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -15,11 +17,15 @@ import (
|
|||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/migrations"
|
||||
migrate_base "forgejo.org/models/migrations/base"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/storage"
|
||||
"forgejo.org/services/doctor"
|
||||
|
||||
exif_terminator "code.superseriousbusiness.org/exif-terminator"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
|
|
@ -34,6 +40,7 @@ func cmdDoctor() *cli.Command {
|
|||
cmdDoctorCheck(),
|
||||
cmdRecreateTable(),
|
||||
cmdDoctorConvert(),
|
||||
cmdAvatarStripExif(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +105,14 @@ You should back-up your database before doing this and ensure that your database
|
|||
}
|
||||
}
|
||||
|
||||
func cmdAvatarStripExif() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "avatar-strip-exif",
|
||||
Usage: "Strip EXIF metadata from all images in the avatar storage",
|
||||
Action: runAvatarStripExif,
|
||||
}
|
||||
}
|
||||
|
||||
func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error {
|
||||
stdCtx, cancel := installSignals(stdCtx)
|
||||
defer cancel()
|
||||
|
|
@ -230,3 +245,78 @@ func runDoctorCheck(stdCtx context.Context, ctx *cli.Command) error {
|
|||
}
|
||||
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
|
||||
}
|
||||
|
||||
func runAvatarStripExif(ctx context.Context, c *cli.Command) error {
|
||||
ctx, cancel := installSignals(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type HasCustomAvatarRelativePath interface {
|
||||
CustomAvatarRelativePath() string
|
||||
}
|
||||
|
||||
doExifStrip := func(obj HasCustomAvatarRelativePath, name string, target_storage storage.ObjectStorage) error {
|
||||
if obj.CustomAvatarRelativePath() == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Stripping avatar for %s...", name)
|
||||
|
||||
avatarFile, err := target_storage.Open(obj.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("storage.Avatars.Open: %w", err)
|
||||
}
|
||||
_, imgType, err := image.DecodeConfig(avatarFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("image.DecodeConfig: %w", err)
|
||||
}
|
||||
|
||||
// reset io.Reader for exif termination scan
|
||||
_, err = avatarFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("avatarFile.Seek: %w", err)
|
||||
}
|
||||
|
||||
cleanedData, err := exif_terminator.Terminate(avatarFile, imgType)
|
||||
if err != nil && strings.Contains(err.Error(), "cannot be processed") {
|
||||
// expected error for an image type that isn't supported by exif_terminator
|
||||
log.Info("... image type %s is not supported by exif_terminator, skipping.", imgType)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error cleaning exif data: %w", err)
|
||||
}
|
||||
|
||||
if err := storage.SaveFrom(target_storage, obj.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
_, err := io.Copy(w, cleanedData)
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to create dir %s: %w", obj.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
|
||||
log.Info("... completed %s.", name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error {
|
||||
return doExifStrip(user, fmt.Sprintf("user %s", user.Name), storage.Avatars)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error {
|
||||
return doExifStrip(repo, fmt.Sprintf("repo %s", repo.Name), storage.RepoAvatars)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
16
go.mod
16
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module forgejo.org
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.24.7
|
||||
toolchain go1.25.3
|
||||
|
||||
require (
|
||||
code.forgejo.org/f3/gof3/v3 v3.11.0
|
||||
|
|
@ -17,6 +17,8 @@ require (
|
|||
code.forgejo.org/go-chi/session v1.0.2
|
||||
code.gitea.io/actions-proto-go v0.4.0
|
||||
code.gitea.io/sdk/gitea v0.21.0
|
||||
code.superseriousbusiness.org/exif-terminator v0.11.0
|
||||
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||
connectrpc.com/connect v1.18.1
|
||||
github.com/42wim/httpsig v1.2.3
|
||||
|
|
@ -34,6 +36,7 @@ require (
|
|||
github.com/djherbis/buffer v1.2.0
|
||||
github.com/djherbis/nio/v3 v3.0.1
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
|
|
@ -116,6 +119,7 @@ require (
|
|||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||
|
|
@ -160,6 +164,10 @@ require (
|
|||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
|
|
@ -167,6 +175,7 @@ require (
|
|||
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/go-errors/errors v1.1.1 // indirect
|
||||
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.6.2 // indirect
|
||||
|
|
@ -176,8 +185,10 @@ require (
|
|||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-webauthn/x v0.1.25 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
|
|
@ -249,6 +260,7 @@ require (
|
|||
golang.org/x/tools v0.36.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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
|
|
|||
38
go.sum
38
go.sum
|
|
@ -46,6 +46,12 @@ code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zC
|
|||
code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||
code.superseriousbusiness.org/exif-terminator v0.11.0 h1:Hof0MCcsa+1fS17gf86fTTZ8AQnMY9h9kzcc+2C6mVg=
|
||||
code.superseriousbusiness.org/exif-terminator v0.11.0/go.mod h1:9sutT1axa/kSdlPLlRFjCNKmyo/KNx8eX3XZvWBlAEY=
|
||||
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0 h1:r9uq8StaSHYKJ8DklR9Xy+E9c40G1Z8yj5TRGi8L6+4=
|
||||
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0/go.mod h1:IK1OlR6APjVB3E9tuYGvf0qXMrwP+TrzcHS5rf4wffQ=
|
||||
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 h1:I512jiIeXDC4//2BeSPrRM2ZS4wpBKUaPeTPxakMNGA=
|
||||
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0/go.mod h1:SNHomXNW88o1pFfLHpD4KsCZLfcr4z5dm+xcX5SV10A=
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
|
||||
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
|
||||
|
|
@ -209,6 +215,22 @@ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
|
|||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b h1:NgNuLvW/gAFKU30ULWW0gtkCt56JfB7FrZ2zyo0wT8I=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4=
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
|
||||
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c=
|
||||
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
|
||||
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 h1:XVUp6qW3BIkmM3/1EkrHpa6bL56APOynfXcZEmIgOhs=
|
||||
|
|
@ -258,6 +280,10 @@ github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6n
|
|||
github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
|
||||
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
|
||||
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
|
|
@ -291,6 +317,8 @@ github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx
|
|||
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
|
||||
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
|
||||
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
|
|
@ -308,6 +336,9 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
|
|||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
|
@ -404,6 +435,7 @@ github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+
|
|||
github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8=
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
|
|
@ -722,7 +754,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
|
@ -921,8 +957,10 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
|
|||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ type ActionRun struct {
|
|||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
NotifyEmail bool
|
||||
|
||||
PreExecutionError string `xorm:"LONGTEXT"` // used to report errors that blocked execution of a workflow
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
// ActionTask represents a distribution of job
|
||||
type ActionTask struct {
|
||||
ID int64
|
||||
JobID int64
|
||||
JobID int64 `xorm:"index"`
|
||||
Job *ActionRunJob `xorm:"-"`
|
||||
Steps []*ActionTaskStep `xorm:"-"`
|
||||
Attempt int64
|
||||
|
|
|
|||
|
|
@ -5,39 +5,82 @@ package db
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Iterate iterate all the Bean object
|
||||
// Iterate iterate all the Bean object. The table being iterated must have a single-column primary key.
|
||||
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
|
||||
var start int
|
||||
var dummy Bean
|
||||
batchSize := setting.Database.IterateBufferSize
|
||||
sess := GetEngine(ctx)
|
||||
|
||||
table, err := TableInfo(&dummy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch table info for bean %v: %w", dummy, err)
|
||||
}
|
||||
if len(table.PrimaryKeys) != 1 {
|
||||
return fmt.Errorf("iterate only supported on a table with 1 primary key field, but table %s had %d", table.Name, len(table.PrimaryKeys))
|
||||
}
|
||||
|
||||
pkDbName := table.PrimaryKeys[0]
|
||||
var pkStructFieldName string
|
||||
|
||||
for _, c := range table.Columns() {
|
||||
if c.Name == pkDbName {
|
||||
pkStructFieldName = c.FieldName
|
||||
break
|
||||
}
|
||||
}
|
||||
if pkStructFieldName == "" {
|
||||
return fmt.Errorf("iterate unable to identify struct field for primary key %s", pkDbName)
|
||||
}
|
||||
|
||||
var lastPK any
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
beans := make([]*Bean, 0, batchSize)
|
||||
|
||||
sess := GetEngine(ctx)
|
||||
sess = sess.OrderBy(pkDbName)
|
||||
if cond != nil {
|
||||
sess = sess.Where(cond)
|
||||
}
|
||||
if err := sess.Limit(batchSize, start).Find(&beans); err != nil {
|
||||
if lastPK != nil {
|
||||
sess = sess.Where(builder.Gt{pkDbName: lastPK})
|
||||
}
|
||||
|
||||
if err := sess.Limit(batchSize).Find(&beans); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(beans) == 0 {
|
||||
return nil
|
||||
}
|
||||
start += len(beans)
|
||||
|
||||
for _, bean := range beans {
|
||||
if err := f(ctx, bean); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
lastBean := beans[len(beans)-1]
|
||||
lastPK = extractFieldValue(lastBean, pkStructFieldName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractFieldValue(bean any, fieldName string) any {
|
||||
v := reflect.ValueOf(bean)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
field := v.FieldByName(fieldName)
|
||||
return field.Interface()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,42 +5,113 @@ package db_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestIterate(t *testing.T) {
|
||||
db.SetLogSQL(t.Context(), true)
|
||||
defer test.MockVariableValue(&setting.Database.IterateBufferSize, 50)()
|
||||
|
||||
t.Run("No Modifications", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
xe, err := unittest.GetXORMEngine()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, xe.Sync(&repo_model.RepoUnit{}))
|
||||
|
||||
cnt, err := db.GetEngine(db.DefaultContext).Count(&repo_model.RepoUnit{})
|
||||
require.NoError(t, err)
|
||||
// Fetch all the repo unit IDs...
|
||||
var remainingRepoIDs []int64
|
||||
db.GetEngine(t.Context()).Table(&repo_model.RepoUnit{}).Cols("id").Find(&remainingRepoIDs)
|
||||
|
||||
var repoUnitCnt int
|
||||
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repo *repo_model.RepoUnit) error {
|
||||
repoUnitCnt++
|
||||
// Ensure that every repo unit ID is found when doing iterate:
|
||||
err = db.Iterate(t.Context(), nil, func(ctx context.Context, repo *repo_model.RepoUnit) error {
|
||||
remainingRepoIDs = slices.DeleteFunc(remainingRepoIDs, func(n int64) bool {
|
||||
return repo.ID == n
|
||||
})
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, cnt, repoUnitCnt)
|
||||
assert.Empty(t, remainingRepoIDs)
|
||||
})
|
||||
|
||||
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error {
|
||||
has, err := db.ExistByID[repo_model.RepoUnit](ctx, repoUnit.ID)
|
||||
t.Run("Concurrent Delete", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
xe, err := unittest.GetXORMEngine()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, xe.Sync(&repo_model.RepoUnit{}))
|
||||
|
||||
// Fetch all the repo unit IDs...
|
||||
var remainingRepoIDs []int64
|
||||
db.GetEngine(t.Context()).Table(&repo_model.RepoUnit{}).Cols("id").Find(&remainingRepoIDs)
|
||||
|
||||
// Ensure that every repo unit ID is found, even if someone else performs a DELETE on the table while we're
|
||||
// iterating. In real-world usage the deleted record may or may not be returned, but the important
|
||||
// subject-under-test is that no *other* record is skipped.
|
||||
didDelete := false
|
||||
err = db.Iterate(t.Context(), nil, func(ctx context.Context, repo *repo_model.RepoUnit) error {
|
||||
// While on page 2 (assuming ID ordering, 50 record buffer size)...
|
||||
if repo.ID == 51 {
|
||||
// Delete a record that would have been on page 1.
|
||||
affected, err := db.GetEngine(t.Context()).ID(25).Delete(&repo_model.RepoUnit{})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if affected != 1 {
|
||||
return fmt.Errorf("expected to delete 1 record, but affected %d records", affected)
|
||||
}
|
||||
if !has {
|
||||
return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID}
|
||||
didDelete = true
|
||||
}
|
||||
remainingRepoIDs = slices.DeleteFunc(remainingRepoIDs, func(n int64) bool {
|
||||
return repo.ID == n
|
||||
})
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, didDelete, "didDelete")
|
||||
assert.Empty(t, remainingRepoIDs)
|
||||
})
|
||||
|
||||
t.Run("Verify cond applied", func(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
xe, err := unittest.GetXORMEngine()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, xe.Sync(&repo_model.RepoUnit{}))
|
||||
|
||||
// Fetch all the repo unit IDs...
|
||||
var remainingRepoIDs []int64
|
||||
db.GetEngine(t.Context()).Table(&repo_model.RepoUnit{}).Cols("id").Find(&remainingRepoIDs)
|
||||
|
||||
// Remove those that we're not expecting to find based upon `Iterate`'s condition. We'll trim the front few
|
||||
// records and last few records, which will confirm that cond is applied on all pages.
|
||||
remainingRepoIDs = slices.DeleteFunc(remainingRepoIDs, func(n int64) bool {
|
||||
return n <= 15 || n > 1000
|
||||
})
|
||||
err = db.Iterate(t.Context(), builder.Gt{"id": 15}.And(builder.Lt{"id": 1000}), func(ctx context.Context, repo *repo_model.RepoUnit) error {
|
||||
removedRecord := false
|
||||
// Remove the record from remainingRepoIDs, but track to make sure we did actually remove a record
|
||||
remainingRepoIDs = slices.DeleteFunc(remainingRepoIDs, func(n int64) bool {
|
||||
if repo.ID == n {
|
||||
removedRecord = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if !removedRecord {
|
||||
return fmt.Errorf("unable to find record in remainingRepoIDs for repo %d, indicating a cond application failure", repo.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, remainingRepoIDs)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
24
models/fixtures/TestParseCommitWithSSHSignature/gpg_key.yml
Normal file
24
models/fixtures/TestParseCommitWithSSHSignature/gpg_key.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-
|
||||
id: 7
|
||||
owner_id: 2
|
||||
key_id: C21381CD63F609EE
|
||||
primary_key_id:
|
||||
content: xsBNBGjmzbQBCADPn1Cl1LibN3K7+9A+If/RjDdTfFVbGk7A8pEDpkgzkmbIcMsi+WvPypbnoTJWyc4E5rsj7MYU6NKK3HADFX4rI6h/732xLmK/R+ekeT8wv+XKO1ncTtm/YITC5qRl84xeYfg4DuGGikhCc8xj8ZKWrx5r1ekISidyvNDJrrcHFToqlqGX74w9mRSSQJm1RayX+5nDph7qOpV8ZCpZT2LS00BYkYO62OHl2Ecm+qWiLyyA4iDR4DVSR9qyLzlXdEI2qQXf28zLV4YPaHckNWdRnaqlr/Mlbd+rbZt/49IBDgXPc+EGo6OXYmZls7pyPSCyirz/ObIyM/9t0JIh8rFrABEBAAE=
|
||||
verified: true
|
||||
can_sign: true
|
||||
can_encrypt_comms: true
|
||||
can_encrypt_storage: true
|
||||
can_certify: true
|
||||
emails: "[{\"ID\":8,\"UID\":1,\"Email\":\"user2+signingkey@example.com\",\"LowerEmail\":\"user2+signingkey@example.com\",\"IsActivated\":true,\"IsPrimary\":false}]"
|
||||
|
||||
-
|
||||
id: 8
|
||||
owner_id: 2
|
||||
key_id: 9836974DF1195913
|
||||
primary_key_id: C21381CD63F609EE
|
||||
content: zsBNBGjmzbQBCADUa4mDg7owJwoBGi85flwoNw/QOvZew2LSZh7++dt+ClKcxryDpkAF4jmPsmKZEjhYfe4QE53E6jlYXRhprISONuR7gYPvvgCrvmEQKoh6o/tOlv0I4Ngix3SkE44u/CpFEVOrmFHBUvjgRHreOhwL3eyUao3PVte4kyAmvzZuGCmFFr7e0D48EGVbSLTGGj/GtHArnWPbs96S6qjXU/4qSvHsmNbGousgbovxXF/AxqIzyjvXheNEEQzUPjainDlOi3Z9BPJXr7T3ytt3slp48teKEXAC/uGVvFICwGN3WRCG8XsVwIQLUE23xclVdVqdzkO4Jr1D+Gtg95fYC2PvABEBAAE=
|
||||
verified: false
|
||||
can_sign: true
|
||||
can_encrypt_comms: true
|
||||
can_encrypt_storage: true
|
||||
can_certify: true
|
||||
|
|
@ -119,6 +119,9 @@ var migrations = []*Migration{
|
|||
NewMigration("Migrate `data` column of `secret` table to store keying material", MigrateActionSecretsToKeying),
|
||||
// v39 -> v40
|
||||
NewMigration("Add index for release sha1", AddIndexForReleaseSha1),
|
||||
// NOTE: v42 -> v43 -- effectively backported into Forgejo v13 as part of backporting
|
||||
// https://codeberg.org/forgejo/forgejo/pulls/9530, but the migration was omitted to avoid future upgrade conflicts.
|
||||
// The migration will effectively occur automatically via table `Sync` on DB engine initialization.
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
@ -171,19 +174,29 @@ func Migrate(x *xorm.Engine) error {
|
|||
return fmt.Errorf("sync: %w", err)
|
||||
}
|
||||
|
||||
currentVersion := &ForgejoVersion{ID: 1}
|
||||
has, err := x.Get(currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get: %w", err)
|
||||
} else if !has {
|
||||
// If the version record does not exist we think
|
||||
// it is a fresh installation and we can skip all migrations.
|
||||
currentVersion.ID = 0
|
||||
currentVersion.Version = ExpectedVersion()
|
||||
|
||||
if _, err = x.InsertOne(currentVersion); err != nil {
|
||||
var versionRecords []*ForgejoVersion
|
||||
if err := x.Find(&versionRecords); err != nil {
|
||||
return fmt.Errorf("find: %w", err)
|
||||
}
|
||||
if len(versionRecords) == 0 {
|
||||
// If the version record does not exist we think it is a fresh installation and we can skip all migrations;
|
||||
// engine init calls `SyncAllTables` which will create the fresh database.
|
||||
upToDate := &ForgejoVersion{ID: 1, Version: ExpectedVersion()}
|
||||
if _, err := x.InsertOne(upToDate); err != nil {
|
||||
return fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
// continue with the migration routine, but nothing will be applied; this allows transition into the newer
|
||||
// forgejo_migration library and for it to be configured and populated.
|
||||
versionRecords = []*ForgejoVersion{upToDate}
|
||||
} else if len(versionRecords) > 1 {
|
||||
return fmt.Errorf(
|
||||
"corrupt migrations: Forgejo database has unexpected records in the table `forgejo_version`; a single record is expected w/ ID=1, but %d records were found",
|
||||
len(versionRecords))
|
||||
}
|
||||
currentVersion := versionRecords[0]
|
||||
if currentVersion.ID != 1 {
|
||||
return fmt.Errorf(
|
||||
"corrupt migrations: Forgejo database has corrupted records in the table `forgejo_version`; a single record with ID=1 is expected, but a record with ID=%d was found instead", currentVersion.ID)
|
||||
}
|
||||
|
||||
v := currentVersion.Version
|
||||
|
|
@ -202,7 +215,7 @@ func Migrate(x *xorm.Engine) error {
|
|||
|
||||
// Some migration tasks depend on the git command
|
||||
if git.DefaultContext == nil {
|
||||
if err = git.InitSimple(context.Background()); err != nil {
|
||||
if err := git.InitSimple(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -212,11 +225,11 @@ func Migrate(x *xorm.Engine) error {
|
|||
log.Info("Migration[%d]: %s", v+int64(i), m.description)
|
||||
// Reset the mapper between each migration - migrations are not supposed to depend on each other
|
||||
x.SetMapper(names.GonicMapper{})
|
||||
if err = m.migrate(x); err != nil {
|
||||
if err := m.migrate(x); err != nil {
|
||||
return fmt.Errorf("migration[%d]: %s failed: %w", v+int64(i), m.description, err)
|
||||
}
|
||||
currentVersion.Version = v + int64(i) + 1
|
||||
if _, err = x.ID(1).Update(currentVersion); err != nil {
|
||||
if _, err := x.ID(1).Update(currentVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
migration_tests "forgejo.org/models/migrations/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -37,3 +38,38 @@ func TestEnsureUpToDate(t *testing.T) {
|
|||
err = EnsureUpToDate(x)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMigrateFreshDB(t *testing.T) {
|
||||
x, deferable := migration_tests.PrepareTestEnv(t, 0, new(ForgejoVersion))
|
||||
defer deferable()
|
||||
require.NotNil(t, x)
|
||||
|
||||
err := Migrate(x)
|
||||
require.NoError(t, err)
|
||||
|
||||
var versionRecords []*ForgejoVersion
|
||||
err = x.Find(&versionRecords)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versionRecords, 1)
|
||||
v := versionRecords[0]
|
||||
assert.EqualValues(t, 1, v.ID)
|
||||
assert.EqualValues(t, 40, v.Version)
|
||||
}
|
||||
|
||||
func TestMigrateFailWithCorruption(t *testing.T) {
|
||||
x, deferable := migration_tests.PrepareTestEnv(t, 0, new(ForgejoVersion))
|
||||
defer deferable()
|
||||
require.NotNil(t, x)
|
||||
|
||||
// ID != 1
|
||||
_, err := x.InsertOne(&ForgejoVersion{ID: 100, Version: 100})
|
||||
require.NoError(t, err)
|
||||
err = Migrate(x)
|
||||
require.ErrorContains(t, err, "corrupted records in the table `forgejo_version`")
|
||||
|
||||
// Two versions...
|
||||
_, err = x.InsertOne(&ForgejoVersion{ID: 1, Version: 1000})
|
||||
require.NoError(t, err)
|
||||
err = Migrate(x)
|
||||
require.ErrorContains(t, err, "unexpected records in the table `forgejo_version`")
|
||||
}
|
||||
|
|
|
|||
14
models/migrations/main_test.go
Normal file
14
models/migrations/main_test.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
migration_tests "forgejo.org/models/migrations/test"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
migration_tests.MainTest(m)
|
||||
}
|
||||
|
|
@ -449,20 +449,31 @@ func Migrate(x *xorm.Engine) error {
|
|||
}
|
||||
|
||||
var previousVersion int64
|
||||
currentVersion := &Version{ID: 1}
|
||||
has, err := x.Get(currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get: %w", err)
|
||||
} else if !has {
|
||||
// If the version record does not exist, it is a fresh installation, and we can skip all migrations.
|
||||
// XORM model framework will create all tables when initializing.
|
||||
currentVersion.ID = 0
|
||||
currentVersion.Version = maxDBVer
|
||||
if _, err = x.InsertOne(currentVersion); err != nil {
|
||||
var versionRecords []*Version
|
||||
if err := x.Find(&versionRecords); err != nil {
|
||||
return fmt.Errorf("find: %w", err)
|
||||
}
|
||||
if len(versionRecords) == 0 {
|
||||
// If the version record does not exist we think it is a fresh installation and we can skip all migrations;
|
||||
// engine init calls `SyncAllTables` which will create the fresh database.
|
||||
upToDate := &Version{ID: 1, Version: maxDBVer}
|
||||
if _, err := x.InsertOne(upToDate); err != nil {
|
||||
return fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
// continue with the migration routine, but nothing will be applied; this allows transition into the newer
|
||||
// forgejo library and for it to be configured and populated.
|
||||
versionRecords = []*Version{upToDate}
|
||||
} else if len(versionRecords) > 1 {
|
||||
return fmt.Errorf(
|
||||
"corrupt migrations: Forgejo database has unexpected records in the table `version`; a single record is expected w/ ID=1, but %d records were found",
|
||||
len(versionRecords))
|
||||
} else {
|
||||
previousVersion = currentVersion.Version
|
||||
previousVersion = versionRecords[0].Version
|
||||
}
|
||||
currentVersion := versionRecords[0]
|
||||
if currentVersion.ID != 1 {
|
||||
return fmt.Errorf(
|
||||
"corrupt migrations: Forgejo database has corrupted records in the table `version`; a single record with ID=1 is expected, but a record with ID=%d was found instead", currentVersion.ID)
|
||||
}
|
||||
|
||||
curDBVer := currentVersion.Version
|
||||
|
|
@ -486,7 +497,7 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
|
|||
|
||||
// Some migration tasks depend on the git command
|
||||
if git.DefaultContext == nil {
|
||||
if err = git.InitSimple(context.Background()); err != nil {
|
||||
if err := git.InitSimple(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -500,11 +511,11 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
|
|||
log.Info("Migration[%d]: %s", m.idNumber, m.description)
|
||||
// Reset the mapper between each migration - migrations are not supposed to depend on each other
|
||||
x.SetMapper(names.GonicMapper{})
|
||||
if err = m.Migrate(x); err != nil {
|
||||
if err := m.Migrate(x); err != nil {
|
||||
return fmt.Errorf("migration[%d]: %s failed: %w", m.idNumber, m.description, err)
|
||||
}
|
||||
currentVersion.Version = migrationIDNumberToDBVersion(m.idNumber)
|
||||
if _, err = x.ID(1).Update(currentVersion); err != nil {
|
||||
if _, err := x.ID(1).Update(currentVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ package migrations
|
|||
import (
|
||||
"testing"
|
||||
|
||||
migration_tests "forgejo.org/models/migrations/test"
|
||||
"forgejo.org/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrations(t *testing.T) {
|
||||
|
|
@ -25,3 +27,38 @@ func TestMigrations(t *testing.T) {
|
|||
assert.Equal(t, []*migration{{idNumber: 71}}, getPendingMigrations(71, preparedMigrations))
|
||||
assert.Equal(t, []*migration{}, getPendingMigrations(72, preparedMigrations))
|
||||
}
|
||||
|
||||
func TestMigrateFreshDB(t *testing.T) {
|
||||
x, deferable := migration_tests.PrepareTestEnv(t, 0, new(Version))
|
||||
defer deferable()
|
||||
require.NotNil(t, x)
|
||||
|
||||
err := Migrate(x)
|
||||
require.NoError(t, err)
|
||||
|
||||
var versionRecords []*Version
|
||||
err = x.Find(&versionRecords)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versionRecords, 1)
|
||||
v := versionRecords[0]
|
||||
assert.EqualValues(t, 1, v.ID)
|
||||
assert.EqualValues(t, 305, v.Version)
|
||||
}
|
||||
|
||||
func TestMigrateFailWithCorruption(t *testing.T) {
|
||||
x, deferable := migration_tests.PrepareTestEnv(t, 0, new(Version))
|
||||
defer deferable()
|
||||
require.NotNil(t, x)
|
||||
|
||||
// ID != 1
|
||||
_, err := x.InsertOne(&Version{ID: 100, Version: 100})
|
||||
require.NoError(t, err)
|
||||
err = Migrate(x)
|
||||
require.ErrorContains(t, err, "corrupted records in the table `version`")
|
||||
|
||||
// Two versions...
|
||||
_, err = x.InsertOne(&Version{ID: 1, Version: 1000})
|
||||
require.NoError(t, err)
|
||||
err = Migrate(x)
|
||||
require.ErrorContains(t, err, "unexpected records in the table `version`")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ func GetAllAdmins(ctx context.Context) ([]*User, error) {
|
|||
|
||||
// MustHaveTwoFactor returns true if the user is a individual and requires 2fa
|
||||
func (u *User) MustHaveTwoFactor() bool {
|
||||
if !u.IsIndividual() || setting.GlobalTwoFactorRequirement.IsNone() {
|
||||
if u.IsActions() || !u.IsIndividual() || setting.GlobalTwoFactorRequirement.IsNone() {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -1194,7 +1194,9 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) []
|
|||
return newCommits
|
||||
}
|
||||
|
||||
// GetUserByEmail returns the user object by given e-mail if exists.
|
||||
// GetUserByEmail returns the user associated with the email, if it exists
|
||||
// and is activated. If the email is a no-reply address, then the user
|
||||
// associated with that no-reply address is returned.
|
||||
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
if len(email) == 0 {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
|
|
@ -1227,6 +1229,26 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
|||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
|
||||
// GetUserByEmailSimple returns the user associated with the email, if it exists.
|
||||
//
|
||||
// NOTE: You likely should use `GetUserByEmail`, which handles the no-reply
|
||||
// address and only uses activated emails to get the user.
|
||||
func GetUserByEmailSimple(ctx context.Context, email string) (*User, error) {
|
||||
if len(email) == 0 {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
|
||||
emailAddress := &EmailAddress{}
|
||||
has, err := db.GetEngine(ctx).Where("lower_email = ?", strings.ToLower(email)).Get(emailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrUserNotExist{Name: email}
|
||||
}
|
||||
|
||||
return GetUserByID(ctx, emailAddress.UID)
|
||||
}
|
||||
|
||||
// GetUser checks if a user already exists
|
||||
func GetUser(ctx context.Context, user *User) (bool, error) {
|
||||
return db.GetEngine(ctx).Get(user)
|
||||
|
|
|
|||
|
|
@ -645,6 +645,7 @@ func TestMustHaveTwoFactor(t *testing.T) {
|
|||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
|
||||
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
ghostUser := user_model.NewGhostUser()
|
||||
actionsUser := user_model.NewActionsUser()
|
||||
|
||||
t.Run("NoneTwoFactorRequirement", func(t *testing.T) {
|
||||
// this should be the default, so don't have to set the variable
|
||||
|
|
@ -653,6 +654,7 @@ func TestMustHaveTwoFactor(t *testing.T) {
|
|||
assert.False(t, restrictedUser.MustHaveTwoFactor())
|
||||
assert.False(t, org.MustHaveTwoFactor())
|
||||
assert.False(t, ghostUser.MustHaveTwoFactor())
|
||||
assert.False(t, actionsUser.MustHaveTwoFactor())
|
||||
})
|
||||
|
||||
t.Run("AllTwoFactorRequirement", func(t *testing.T) {
|
||||
|
|
@ -663,6 +665,7 @@ func TestMustHaveTwoFactor(t *testing.T) {
|
|||
assert.True(t, restrictedUser.MustHaveTwoFactor())
|
||||
assert.False(t, org.MustHaveTwoFactor())
|
||||
assert.True(t, ghostUser.MustHaveTwoFactor())
|
||||
assert.False(t, actionsUser.MustHaveTwoFactor())
|
||||
})
|
||||
|
||||
t.Run("AdminTwoFactorRequirement", func(t *testing.T) {
|
||||
|
|
@ -673,6 +676,7 @@ func TestMustHaveTwoFactor(t *testing.T) {
|
|||
assert.False(t, restrictedUser.MustHaveTwoFactor())
|
||||
assert.False(t, org.MustHaveTwoFactor())
|
||||
assert.False(t, ghostUser.MustHaveTwoFactor())
|
||||
assert.False(t, actionsUser.MustHaveTwoFactor())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -696,6 +700,7 @@ func TestIsAccessAllowed(t *testing.T) {
|
|||
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
prohibitLoginUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 37})
|
||||
ghostUser := user_model.NewGhostUser()
|
||||
actionsUser := user_model.NewActionsUser()
|
||||
|
||||
// users with enabled WebAuthn
|
||||
normalWebAuthnUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32})
|
||||
|
|
@ -711,6 +716,7 @@ func TestIsAccessAllowed(t *testing.T) {
|
|||
runTest(t, restrictedUser, false, true)
|
||||
runTest(t, prohibitLoginUser, false, false)
|
||||
runTest(t, ghostUser, false, false)
|
||||
runTest(t, actionsUser, false, true)
|
||||
})
|
||||
|
||||
t.Run("enabled 2fa", func(t *testing.T) {
|
||||
|
|
@ -736,6 +742,7 @@ func TestIsAccessAllowed(t *testing.T) {
|
|||
runTest(t, restrictedUser, false, false)
|
||||
runTest(t, prohibitLoginUser, false, false)
|
||||
runTest(t, ghostUser, false, false)
|
||||
runTest(t, actionsUser, false, true)
|
||||
})
|
||||
|
||||
t.Run("enabled 2fa", func(t *testing.T) {
|
||||
|
|
@ -761,6 +768,7 @@ func TestIsAccessAllowed(t *testing.T) {
|
|||
runTest(t, restrictedUser, false, true)
|
||||
runTest(t, prohibitLoginUser, false, false)
|
||||
runTest(t, ghostUser, false, false)
|
||||
runTest(t, actionsUser, false, true)
|
||||
})
|
||||
|
||||
t.Run("enabled 2fa", func(t *testing.T) {
|
||||
|
|
@ -999,6 +1007,7 @@ func TestPronounsPrivacy(t *testing.T) {
|
|||
|
||||
func TestGetUserByEmail(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "noreply.example.org")()
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
u, err := user_model.GetUserByEmail(t.Context(), "user2@example.com")
|
||||
|
|
@ -1017,4 +1026,33 @@ func TestGetUserByEmail(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, u.ID)
|
||||
})
|
||||
|
||||
t.Run("No-reply", func(t *testing.T) {
|
||||
u, err := user_model.GetUserByEmail(t.Context(), "user1@noreply.example.org")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, u.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUserByEmailSimple(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "noreply.example.org")()
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
u, err := user_model.GetUserByEmailSimple(t.Context(), "user2@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 2, u.ID)
|
||||
})
|
||||
|
||||
t.Run("Not activated", func(t *testing.T) {
|
||||
u, err := user_model.GetUserByEmailSimple(t.Context(), "user11@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 11, u.ID)
|
||||
})
|
||||
|
||||
t.Run("No-reply", func(t *testing.T) {
|
||||
u, err := user_model.GetUserByEmailSimple(t.Context(), "user1@noreply.example.org")
|
||||
require.ErrorIs(t, err, user_model.ErrUserNotExist{Name: "user1@noreply.example.org"})
|
||||
assert.Nil(t, u)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type DetectedWorkflow struct {
|
|||
EntryName string
|
||||
TriggerEvent *jobparser.Event
|
||||
Content []byte
|
||||
EventDetectionError error
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -128,6 +129,7 @@ func DetectWorkflows(
|
|||
Name: triggedEvent.Event(),
|
||||
},
|
||||
Content: content,
|
||||
EventDetectionError: err,
|
||||
}
|
||||
workflows = append(workflows, dwf)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
|
||||
_ "image/gif" // for processing gif images
|
||||
_ "image/jpeg" // for processing jpeg images
|
||||
|
|
@ -17,6 +18,7 @@ import (
|
|||
"forgejo.org/modules/avatar/identicon"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
exif_terminator "code.superseriousbusiness.org/exif-terminator"
|
||||
"golang.org/x/image/draw"
|
||||
|
||||
_ "golang.org/x/image/webp" // for processing webp images
|
||||
|
|
@ -66,15 +68,29 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) {
|
|||
return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight)
|
||||
}
|
||||
|
||||
var cleanedBytes []byte
|
||||
if imgType != "gif" { // "gif" is the only imgType supported above, but not supported by exif_terminator
|
||||
cleanedData, err := exif_terminator.Terminate(bytes.NewReader(data), imgType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error cleaning exif data: %w", err)
|
||||
}
|
||||
cleanedBytes, err = io.ReadAll(cleanedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading cleaned data: %w", err)
|
||||
}
|
||||
} else { // gif
|
||||
cleanedBytes = data
|
||||
}
|
||||
|
||||
// If the origin is small enough, just use it, then APNG could be supported,
|
||||
// otherwise, if the image is processed later, APNG loses animation.
|
||||
// And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails.
|
||||
// So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error.
|
||||
if len(data) < int(maxOriginSize) {
|
||||
return data, nil
|
||||
return cleanedBytes, nil
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
img, _, err := image.Decode(bytes.NewReader(cleanedBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("image.Decode: %w", err)
|
||||
}
|
||||
|
|
@ -94,7 +110,7 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) {
|
|||
|
||||
// usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller
|
||||
if len(data) <= len(resized) {
|
||||
return data, nil
|
||||
return cleanedBytes, nil
|
||||
}
|
||||
|
||||
return resized, nil
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
|
||||
jpegstructure "code.superseriousbusiness.org/go-jpeg-image-structure/v2"
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -30,8 +33,8 @@ func Test_RandomImage(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_ProcessAvatarPNG(t *testing.T) {
|
||||
setting.Avatar.MaxWidth = 4096
|
||||
setting.Avatar.MaxHeight = 4096
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
|
||||
|
||||
data, err := os.ReadFile("testdata/avatar.png")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -41,8 +44,8 @@ func Test_ProcessAvatarPNG(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_ProcessAvatarJPEG(t *testing.T) {
|
||||
setting.Avatar.MaxWidth = 4096
|
||||
setting.Avatar.MaxHeight = 4096
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
|
||||
|
||||
data, err := os.ReadFile("testdata/avatar.jpeg")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -51,17 +54,28 @@ func Test_ProcessAvatarJPEG(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ProcessAvatarGIF(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
|
||||
|
||||
data, err := os.ReadFile("testdata/avatar.gif")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = processAvatarImage(data, 262144)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ProcessAvatarInvalidData(t *testing.T) {
|
||||
setting.Avatar.MaxWidth = 5
|
||||
setting.Avatar.MaxHeight = 5
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 5)()
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 5)()
|
||||
|
||||
_, err := processAvatarImage([]byte{}, 12800)
|
||||
assert.EqualError(t, err, "image.DecodeConfig: image: unknown format")
|
||||
}
|
||||
|
||||
func Test_ProcessAvatarInvalidImageSize(t *testing.T) {
|
||||
setting.Avatar.MaxWidth = 5
|
||||
setting.Avatar.MaxHeight = 5
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 5)()
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 5)()
|
||||
|
||||
data, err := os.ReadFile("testdata/avatar.png")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -71,8 +85,8 @@ func Test_ProcessAvatarInvalidImageSize(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_ProcessAvatarImage(t *testing.T) {
|
||||
setting.Avatar.MaxWidth = 4096
|
||||
setting.Avatar.MaxHeight = 4096
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
|
||||
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
|
||||
scaledSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor
|
||||
|
||||
newImgData := func(size int, optHeight ...int) []byte {
|
||||
|
|
@ -135,3 +149,40 @@ func Test_ProcessAvatarImage(t *testing.T) {
|
|||
_, err = processAvatarImage(origin, 262144)
|
||||
require.ErrorContains(t, err, "image width is too large: 10 > 5")
|
||||
}
|
||||
|
||||
func safeExifJpeg(t *testing.T, jpeg []byte) {
|
||||
t.Helper()
|
||||
|
||||
parser := jpegstructure.NewJpegMediaParser()
|
||||
mediaContext, err := parser.ParseBytes(jpeg)
|
||||
require.NoError(t, err)
|
||||
|
||||
sl := mediaContext.(*jpegstructure.SegmentList)
|
||||
|
||||
rootIfd, _, err := sl.Exif()
|
||||
require.NoError(t, err)
|
||||
err = rootIfd.EnumerateTagsRecursively(func(ifd *exif.Ifd, ite *exif.IfdTagEntry) error {
|
||||
assert.Equal(t, "Orientation", ite.TagName(), "only Orientation EXIF tag expected")
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ProcessAvatarExif(t *testing.T) {
|
||||
t.Run("greater than max origin size", func(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/exif.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
processedData, err := processAvatarImage(data, 12800)
|
||||
require.NoError(t, err)
|
||||
safeExifJpeg(t, processedData)
|
||||
})
|
||||
t.Run("smaller than max origin size", func(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/exif.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
processedData, err := processAvatarImage(data, 128000)
|
||||
require.NoError(t, err)
|
||||
safeExifJpeg(t, processedData)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
BIN
modules/avatar/testdata/avatar.gif
vendored
Normal file
BIN
modules/avatar/testdata/avatar.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
BIN
modules/avatar/testdata/exif.jpg
vendored
Normal file
BIN
modules/avatar/testdata/exif.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -7,6 +7,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -17,7 +19,7 @@ var (
|
|||
)
|
||||
|
||||
// LineBlame returns the latest commit at the given line
|
||||
func (repo *Repository) LineBlame(revision, file string, line uint64) (*Commit, error) {
|
||||
func (repo *Repository) LineBlame(revision, file string, line uint64) (*Commit, uint64, error) {
|
||||
res, _, gitErr := NewCommand(repo.Ctx, "blame").
|
||||
AddOptionFormat("-L %d,%d", line, line).
|
||||
AddOptionValues("-p", revision).
|
||||
|
|
@ -26,24 +28,40 @@ func (repo *Repository) LineBlame(revision, file string, line uint64) (*Commit,
|
|||
stdErr := gitErr.Stderr()
|
||||
|
||||
if stdErr == fmt.Sprintf("fatal: no such path %s in %s\n", file, revision) {
|
||||
return nil, ErrBlameFileDoesNotExist
|
||||
return nil, 0, ErrBlameFileDoesNotExist
|
||||
}
|
||||
if notEnoughLinesRe.MatchString(stdErr) {
|
||||
return nil, ErrBlameFileNotEnoughLines
|
||||
return nil, 0, ErrBlameFileNotEnoughLines
|
||||
}
|
||||
|
||||
return nil, gitErr
|
||||
return nil, 0, gitErr
|
||||
}
|
||||
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
objectIDLen := objectFormat.FullLength()
|
||||
if len(res) < objectIDLen {
|
||||
return nil, fmt.Errorf("output of blame is invalid, cannot contain commit ID: %s", res)
|
||||
|
||||
if len(res) < objectIDLen+1 {
|
||||
return nil, 0, fmt.Errorf("output of blame is invalid, cannot contain commit ID: %s", res)
|
||||
}
|
||||
|
||||
return repo.GetCommit(res[:objectIDLen])
|
||||
commit, err := repo.GetCommit(res[:objectIDLen])
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("GetCommit: %w", err)
|
||||
}
|
||||
|
||||
endIdxOriginalLineNo := strings.IndexRune(res[objectIDLen+1:], ' ')
|
||||
if endIdxOriginalLineNo == -1 {
|
||||
return nil, 0, fmt.Errorf("output of blame is invalid, cannot contain original line number: %s", res)
|
||||
}
|
||||
|
||||
originalLineNo, err := strconv.ParseUint(res[objectIDLen+1:objectIDLen+1+endIdxOriginalLineNo], 10, 64)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("strconv.ParseUint: %w", err)
|
||||
}
|
||||
|
||||
return commit, originalLineNo, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
|
|
@ -17,17 +20,20 @@ func TestLineBlame(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.LineBlame("HEAD", "foo/link_short", 1)
|
||||
commit, lineno, err := repo.LineBlame("HEAD", "foo/link_short", 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "37991dec2c8e592043f47155ce4808d4580f9123", commit.ID.String())
|
||||
assert.EqualValues(t, 1, lineno)
|
||||
|
||||
commit, err = repo.LineBlame("HEAD", "foo/link_short", 512)
|
||||
commit, lineno, err = repo.LineBlame("HEAD", "foo/link_short", 512)
|
||||
require.ErrorIs(t, err, ErrBlameFileNotEnoughLines)
|
||||
assert.Nil(t, commit)
|
||||
assert.Zero(t, lineno)
|
||||
|
||||
commit, err = repo.LineBlame("HEAD", "non-existent/path", 512)
|
||||
commit, lineno, err = repo.LineBlame("HEAD", "non-existent/path", 512)
|
||||
require.ErrorIs(t, err, ErrBlameFileDoesNotExist)
|
||||
assert.Nil(t, commit)
|
||||
assert.Zero(t, lineno)
|
||||
})
|
||||
|
||||
t.Run("SHA256", func(t *testing.T) {
|
||||
|
|
@ -37,16 +43,68 @@ func TestLineBlame(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.LineBlame("HEAD", "foo/link_short", 1)
|
||||
commit, lineno, err := repo.LineBlame("HEAD", "foo/link_short", 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "6aae864a3d1d0d6a5be0cc64028c1e7021e2632b031fd8eb82afc5a283d1c3d1", commit.ID.String())
|
||||
assert.EqualValues(t, 1, lineno)
|
||||
|
||||
commit, err = repo.LineBlame("HEAD", "foo/link_short", 512)
|
||||
commit, lineno, err = repo.LineBlame("HEAD", "foo/link_short", 512)
|
||||
require.ErrorIs(t, err, ErrBlameFileNotEnoughLines)
|
||||
assert.Nil(t, commit)
|
||||
assert.Zero(t, lineno)
|
||||
|
||||
commit, err = repo.LineBlame("HEAD", "non-existent/path", 512)
|
||||
commit, lineno, err = repo.LineBlame("HEAD", "non-existent/path", 512)
|
||||
require.ErrorIs(t, err, ErrBlameFileDoesNotExist)
|
||||
assert.Nil(t, commit)
|
||||
assert.Zero(t, lineno)
|
||||
})
|
||||
|
||||
t.Run("Moved line", func(t *testing.T) {
|
||||
test := func(t *testing.T, objectFormatName string) {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
require.NoError(t, InitRepository(t.Context(), tmpDir, false, objectFormatName))
|
||||
|
||||
gitRepo, err := OpenRepository(t.Context(), tmpDir)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, "ANSWER"), []byte("abba\n"), 0o666))
|
||||
require.NoError(t, AddChanges(tmpDir, true))
|
||||
require.NoError(t, CommitChanges(tmpDir, CommitChangesOptions{Message: "Favourite singer of everyone who follows a automata course"}))
|
||||
|
||||
firstCommit, err := gitRepo.GetRefCommitID("HEAD")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, "ANSWER"), append(bytes.Repeat([]byte("baba\n"), 9), []byte("abba\n")...), 0o666))
|
||||
require.NoError(t, AddChanges(tmpDir, true))
|
||||
require.NoError(t, CommitChanges(tmpDir, CommitChangesOptions{Message: "Now there's several of them"}))
|
||||
|
||||
secondCommit, err := gitRepo.GetRefCommitID("HEAD")
|
||||
require.NoError(t, err)
|
||||
|
||||
commit, lineno, err := gitRepo.LineBlame("HEAD", "ANSWER", 10)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, firstCommit, commit.ID.String())
|
||||
assert.EqualValues(t, 1, lineno)
|
||||
|
||||
for i := range uint64(9) {
|
||||
commit, lineno, err = gitRepo.LineBlame("HEAD", "ANSWER", i+1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, secondCommit, commit.ID.String())
|
||||
assert.Equal(t, i+1, lineno)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SHA1", func(t *testing.T) {
|
||||
test(t, Sha1ObjectFormat.Name())
|
||||
})
|
||||
|
||||
t.Run("SHA256", func(t *testing.T) {
|
||||
skipIfSHA256NotSupported(t)
|
||||
|
||||
test(t, Sha256ObjectFormat.Name())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ func TestParseGitURLs(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git",
|
||||
kase: "git@[fe80::14fc:cec5:c174:d88%2510]:go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "[fe80:14fc:cec5:c174:d88%10]",
|
||||
Host: "[fe80::14fc:cec5:c174:d88%10]",
|
||||
Path: "go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 1,
|
||||
|
|
@ -132,11 +132,11 @@ func TestParseGitURLs(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git",
|
||||
kase: "https://[fe80::14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git",
|
||||
expected: &GitURL{
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "[fe80:14fc:cec5:c174:d88%10]:20",
|
||||
Host: "[fe80::14fc:cec5:c174:d88%10]:20",
|
||||
Path: "/go-gitea/gitea.git",
|
||||
},
|
||||
extraMark: 0,
|
||||
|
|
|
|||
|
|
@ -55,10 +55,13 @@ var (
|
|||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||
|
||||
// anyHashPattern splits url containing SHA into parts
|
||||
anyHashPattern = regexp.MustCompile(`https?://(?:(?:\S+/){3,4}(?:commit|tree|blob)/)([0-9a-f]{7,64})(/[-+~_%.a-zA-Z0-9/]+)?(\?[-+~_%\.a-zA-Z0-9=&]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
anyHashPattern = regexp.MustCompile(`https?://[^\s/]+/(\S+/(?:commit|tree|blob))/([0-9a-f]{7,64})(/[-+~_%.a-zA-Z0-9/]+)?(\?[-+~_%\.a-zA-Z0-9=&]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
|
||||
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
|
||||
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
comparePattern = regexp.MustCompile(`https?://[^\s/]+/(?:\S+/)?([^\s/]+/[^\s/]+)/compare/([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(\?[-+~_%\.a-zA-Z0-9=&/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
|
||||
// pullReviewCommitPattern matches "https://domain.tld/<subpath...>/<owner>/<repo>/pulls/<id>/commits/<sha>"
|
||||
pullReviewCommitPattern = regexp.MustCompile(`https?://[^\s/]+/(?:\S+/)?([^\s/]+/[^\s/]+)/pulls/(\d+)/commits/([0-9a-f]{7,64})(#[-+~_%.a-zA-Z0-9]+)?`)
|
||||
|
||||
validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
|
||||
|
||||
|
|
@ -147,6 +150,7 @@ func (p *postProcessError) Error() string {
|
|||
type processor func(ctx *RenderContext, node *html.Node)
|
||||
|
||||
var defaultProcessors = []processor{
|
||||
pullReviewCommitPatternProcessor,
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
filePreviewPatternProcessor,
|
||||
|
|
@ -177,6 +181,7 @@ func PostProcess(
|
|||
}
|
||||
|
||||
var commitMessageProcessors = []processor{
|
||||
pullReviewCommitPatternProcessor,
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
|
|
@ -209,6 +214,7 @@ func RenderCommitMessage(
|
|||
}
|
||||
|
||||
var commitMessageSubjectProcessors = []processor{
|
||||
pullReviewCommitPatternProcessor,
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
|
|
@ -796,6 +802,64 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
|||
}
|
||||
}
|
||||
|
||||
// pullReviewCommitPatternProcessor creates links to pull review commits.
|
||||
func pullReviewCommitPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := pullReviewCommitPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
repoSlug := node.Data[m[2]:m[3]]
|
||||
id := node.Data[m[4]:m[5]]
|
||||
sha := base.ShortSha(node.Data[m[6]:m[7]])
|
||||
|
||||
// Create an `<a>` node with a text of
|
||||
// `!123 (commit <code>abcdef1234</code>)`
|
||||
aNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.A.String(),
|
||||
Attr: []html.Attribute{{Key: "href", Val: urlFull}, {Key: "class", Val: "commit"}},
|
||||
}
|
||||
|
||||
text := "!" + id + " (commit "
|
||||
|
||||
baseURLEnd := strings.Index(urlFull, repoSlug) + len(repoSlug)
|
||||
if len(ctx.Links.Base) > 0 && !strings.HasPrefix(ctx.Links.Base, urlFull[:baseURLEnd]) {
|
||||
text = repoSlug + "@" + text
|
||||
}
|
||||
|
||||
aNode.AppendChild(&html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: text,
|
||||
})
|
||||
|
||||
textNode := &html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: sha,
|
||||
}
|
||||
|
||||
codeNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Code.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}},
|
||||
}
|
||||
|
||||
codeNode.AppendChild(textNode)
|
||||
aNode.AppendChild(codeNode)
|
||||
|
||||
aNode.AppendChild(&html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: ")",
|
||||
})
|
||||
|
||||
replaceContent(node, m[0], m[1], aNode)
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
return
|
||||
|
|
@ -952,7 +1016,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
}
|
||||
}
|
||||
|
||||
// fullHashPatternProcessor renders SHA containing URLs
|
||||
// fullHashPatternProcessor renders URLs that contain a SHA
|
||||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
return
|
||||
|
|
@ -966,37 +1030,103 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
text := base.ShortSha(node.Data[m[2]:m[3]])
|
||||
|
||||
// 3rd capture group matches a optional path
|
||||
subpath := ""
|
||||
if m[5] > 0 {
|
||||
subpath = node.Data[m[4]:m[5]]
|
||||
// In most cases, the URL will look like this:
|
||||
// `https://domain.tld/<owner>/<repo>/<path>/<sha>`.
|
||||
// The amount of components in `<path>` is variable, but that alone is doable with regexp.
|
||||
//
|
||||
// However, Forgejo also allows being hosted on a sub path, i.e.
|
||||
// `https://domain.tld/<sub>/<owner>/<repo>/<path>/<sha>`.
|
||||
// And this sub path can also have any amount of components. But fishing out a section
|
||||
// between two variable length matches is not something regular grammars are capable of.
|
||||
//
|
||||
// Instead, the regexp extracts the entire path section before the SHA
|
||||
// (i.e. `<sub>/<owner>/<repo>/<path>`), and we find the components we need by counting.
|
||||
// `<sub>` is unknown, but the possible values for `<path>` are defined by us
|
||||
// (see `router/web/web.go`). So we count from the back.
|
||||
subPath := node.Data[m[2]:m[3]]
|
||||
|
||||
components := strings.Split(subPath, "/")
|
||||
componentCount := len(components)
|
||||
|
||||
// In most cases, the `<owner>` component is right at the start of the path.
|
||||
ownerIndex := 0
|
||||
|
||||
// But if there are more than three components, this could be `<sub>` or an app route
|
||||
// with two components. Or both.
|
||||
if componentCount > 3 {
|
||||
// As mentioned, we count from the back. We decrement for the `<repo>` component, and the one
|
||||
// component from the app route that's guaranteed to be there.
|
||||
// We also adjust this to be an array index, so we subtract one more.
|
||||
ownerIndex = componentCount - 3
|
||||
|
||||
// We then check for known app routes that use two components.
|
||||
// Currently, this checks for:
|
||||
// - `src/commit`
|
||||
// - `commits/commit`
|
||||
//
|
||||
// This does have one scenario where we cannot figure things out reliably:
|
||||
// If there is a sub path, and the repository is named like one of the known app routes
|
||||
// (e.g. `src`), we cannot distinguish between the repo and the app route.
|
||||
// We assume that naming a repository like that is uncommon, and prioritize the case where its
|
||||
// part of the app route.
|
||||
if components[componentCount-1] == "commit" &&
|
||||
(components[componentCount-2] == "src" || components[componentCount-2] == "commits") {
|
||||
ownerIndex--
|
||||
}
|
||||
}
|
||||
|
||||
repoSlug := components[ownerIndex] + "/" + components[ownerIndex+1]
|
||||
|
||||
text := base.ShortSha(node.Data[m[4]:m[5]])
|
||||
|
||||
// We need to figure out the base of the provided URL, which is up to and including the
|
||||
// `<owner>/<repo>` slug.
|
||||
// With that we can determine if it matches the current repo, or if the slug should be shown.
|
||||
baseURLEnd := strings.Index(urlFull, repoSlug) + len(repoSlug)
|
||||
if len(ctx.Links.Base) > 0 && !strings.HasPrefix(ctx.Links.Base, urlFull[:baseURLEnd]) {
|
||||
text = repoSlug + "@" + text
|
||||
}
|
||||
|
||||
// 3rd capture group matches an optional file path after the SHA
|
||||
filePath := ""
|
||||
if m[7] > 0 {
|
||||
filePath = node.Data[m[6]:m[7]]
|
||||
}
|
||||
|
||||
// 5th capture group matches a optional url hash
|
||||
hash := ""
|
||||
if m[9] > 0 {
|
||||
hash = node.Data[m[8]:m[9]][1:]
|
||||
if m[11] > 0 {
|
||||
hash = node.Data[m[10]:m[11]][1:]
|
||||
|
||||
// Truncate long diff IDs
|
||||
if len(hash) > 15 && strings.HasPrefix(hash, "diff-") {
|
||||
hash = hash[:15]
|
||||
}
|
||||
}
|
||||
|
||||
start := m[0]
|
||||
end := m[1]
|
||||
|
||||
// If url ends in '.', it's very likely that it is not part of the
|
||||
// actual url but used to finish a sentence.
|
||||
// If the URL ends in '.', it's very likely that it is not part of the
|
||||
// actual URL but used to finish a sentence.
|
||||
if strings.HasSuffix(urlFull, ".") {
|
||||
end--
|
||||
urlFull = urlFull[:len(urlFull)-1]
|
||||
if hash != "" {
|
||||
hash = hash[:len(hash)-1]
|
||||
} else if subpath != "" {
|
||||
subpath = subpath[:len(subpath)-1]
|
||||
} else if filePath != "" {
|
||||
filePath = filePath[:len(filePath)-1]
|
||||
}
|
||||
}
|
||||
|
||||
if subpath != "" {
|
||||
text += subpath
|
||||
if filePath != "" {
|
||||
decoded, err := url.QueryUnescape(filePath)
|
||||
if err != nil {
|
||||
text += decoded
|
||||
} else {
|
||||
text += filePath
|
||||
}
|
||||
}
|
||||
|
||||
if hash != "" {
|
||||
|
|
@ -1019,41 +1149,71 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
return
|
||||
}
|
||||
|
||||
// Ensure that every group (m[0]...m[7]) has a match
|
||||
for i := 0; i < 8; i++ {
|
||||
// Ensure that every group (m[0]...m[9]) has a match
|
||||
for i := 0; i < 10; i++ {
|
||||
if m[i] == -1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
text1 := base.ShortSha(node.Data[m[2]:m[3]])
|
||||
textDots := base.ShortSha(node.Data[m[4]:m[5]])
|
||||
text2 := base.ShortSha(node.Data[m[6]:m[7]])
|
||||
repoSlug := node.Data[m[2]:m[3]]
|
||||
text1 := base.ShortSha(node.Data[m[4]:m[5]])
|
||||
textDots := base.ShortSha(node.Data[m[6]:m[7]])
|
||||
text2 := base.ShortSha(node.Data[m[8]:m[9]])
|
||||
|
||||
query := ""
|
||||
if m[11] > 0 {
|
||||
query = node.Data[m[10]:m[11]][1:]
|
||||
}
|
||||
|
||||
hash := ""
|
||||
if m[9] > 0 {
|
||||
hash = node.Data[m[8]:m[9]][1:]
|
||||
if m[13] > 0 {
|
||||
hash = node.Data[m[12]:m[13]][1:]
|
||||
}
|
||||
|
||||
start := m[0]
|
||||
end := m[1]
|
||||
|
||||
// If url ends in '.', it's very likely that it is not part of the
|
||||
// actual url but used to finish a sentence.
|
||||
// If the URL ends in '.', it's very likely that it is not part of the
|
||||
// actual URL but used to finish a sentence.
|
||||
if strings.HasSuffix(urlFull, ".") {
|
||||
end--
|
||||
urlFull = urlFull[:len(urlFull)-1]
|
||||
if hash != "" {
|
||||
hash = hash[:len(hash)-1]
|
||||
} else if query != "" {
|
||||
query = query[:len(query)-1]
|
||||
} else if text2 != "" {
|
||||
text2 = text2[:len(text2)-1]
|
||||
}
|
||||
}
|
||||
|
||||
text := text1 + textDots + text2
|
||||
|
||||
baseURLEnd := strings.Index(urlFull, repoSlug) + len(repoSlug)
|
||||
if len(ctx.Links.Base) > 0 && !strings.HasPrefix(ctx.Links.Base, urlFull[:baseURLEnd]) {
|
||||
text = repoSlug + "@" + text
|
||||
}
|
||||
|
||||
extra := ""
|
||||
if query != "" {
|
||||
query, err := url.ParseQuery(query)
|
||||
if err == nil && query.Has("files") {
|
||||
extra = query.Get("files")
|
||||
}
|
||||
}
|
||||
|
||||
if hash != "" {
|
||||
text += " (" + hash + ")"
|
||||
if extra != "" {
|
||||
extra += "#"
|
||||
}
|
||||
|
||||
extra += hash
|
||||
}
|
||||
|
||||
if extra != "" {
|
||||
text += " (" + extra + ")"
|
||||
}
|
||||
replaceContent(node, start, end, createCodeLink(urlFull, text, "compare"))
|
||||
node = node.NextSibling.NextSibling
|
||||
|
|
@ -1094,7 +1254,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
// Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
|
||||
before := node.Data[:(preview.start - offset)]
|
||||
after := node.Data[(preview.end - offset):]
|
||||
afterNode := &html.Node{
|
||||
afterTextNode := &html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: after,
|
||||
}
|
||||
|
|
@ -1103,22 +1263,20 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
case "div", "li", "td", "th", "details":
|
||||
nextSibling := node.NextSibling
|
||||
node.Parent.InsertBefore(previewNode, nextSibling)
|
||||
node.Parent.InsertBefore(afterNode, nextSibling)
|
||||
node.Parent.InsertBefore(afterTextNode, nextSibling)
|
||||
case "p", "span", "em", "strong":
|
||||
nextSibling := node.Parent.NextSibling
|
||||
node.Parent.Parent.InsertBefore(previewNode, nextSibling)
|
||||
afterPNode := &html.Node{
|
||||
nextParentSibling := node.Parent.NextSibling
|
||||
node.Parent.Parent.InsertBefore(previewNode, nextParentSibling)
|
||||
afterNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: node.Parent.Data,
|
||||
Attr: node.Parent.Attr,
|
||||
}
|
||||
afterPNode.AppendChild(afterNode)
|
||||
node.Parent.Parent.InsertBefore(afterPNode, nextSibling)
|
||||
siblingNode := node.NextSibling
|
||||
if siblingNode != nil {
|
||||
node.NextSibling = nil
|
||||
siblingNode.PrevSibling = nil
|
||||
afterPNode.AppendChild(siblingNode)
|
||||
afterNode.AppendChild(afterTextNode)
|
||||
node.Parent.Parent.InsertBefore(afterNode, nextParentSibling)
|
||||
for sibling := node.NextSibling; sibling != nil; sibling = node.NextSibling {
|
||||
sibling.Parent.RemoveChild(sibling)
|
||||
afterNode.AppendChild(sibling)
|
||||
}
|
||||
default:
|
||||
matched = false
|
||||
|
|
@ -1126,7 +1284,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
if matched {
|
||||
offset = preview.end
|
||||
node.Data = before
|
||||
node = afterNode
|
||||
node = afterTextNode
|
||||
}
|
||||
}
|
||||
node = node.NextSibling
|
||||
|
|
|
|||
|
|
@ -303,12 +303,12 @@ func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *Rend
|
|||
func TestRender_AutoLink(t *testing.T) {
|
||||
setting.AppURL = TestAppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
test := func(input, expected, base string) {
|
||||
var buffer strings.Builder
|
||||
err := PostProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
Base: base,
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(input), &buffer)
|
||||
|
|
@ -319,7 +319,7 @@ func TestRender_AutoLink(t *testing.T) {
|
|||
err = PostProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
Base: base,
|
||||
},
|
||||
Metas: localMetas,
|
||||
IsWiki: true,
|
||||
|
|
@ -328,19 +328,87 @@ func TestRender_AutoLink(t *testing.T) {
|
|||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||
}
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
// render valid issue URLs
|
||||
test(util.URLJoin(TestRepoURL, "issues", "3333"),
|
||||
numericIssueLink(util.URLJoin(TestRepoURL, "issues"), "ref-issue", 3333, "#"))
|
||||
numericIssueLink(util.URLJoin(TestRepoURL, "issues"), "ref-issue", 3333, "#"),
|
||||
TestRepoURL)
|
||||
})
|
||||
|
||||
t.Run("Commit", func(t *testing.T) {
|
||||
// render valid commit URLs
|
||||
tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24</code></a>")
|
||||
tmp += "#diff-2"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24</code></a>", TestRepoURL)
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">"+TestOrgRepo+"@d8a994ef24</code></a>", "https://localhost/forgejo/forgejo")
|
||||
test(
|
||||
tmp+"#diff-2",
|
||||
"<a href=\""+tmp+"#diff-2\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>",
|
||||
TestRepoURL,
|
||||
)
|
||||
test(
|
||||
tmp+"#diff-953bb4f01b7c77fa18f0cd54211255051e647dbc",
|
||||
"<a href=\""+tmp+"#diff-953bb4f01b7c77fa18f0cd54211255051e647dbc\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-953bb4f01b)</code></a>",
|
||||
TestRepoURL,
|
||||
)
|
||||
|
||||
// render other commit URLs
|
||||
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">go-gitea/gitea@d8a994ef24 (diff-2)</code></a>", TestRepoURL)
|
||||
|
||||
tmp = "http://localhost:3000/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
|
||||
|
||||
tmp = "http://localhost:3000/sub/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/sub/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/gogs@190d949293</code></a>", "http://localhost:3000/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
|
||||
|
||||
tmp = "http://localhost:3000/sub1/sub2/sub3/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">190d949293</code></a>", "http://localhost:3000/sub1/sub2/sub3/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/gogs@190d949293</code></a>", "http://localhost:3000/sub1/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/gogs@190d949293</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
|
||||
|
||||
// if the repository happens to be named like one of the known app routes (e.g. `src`),
|
||||
// we can parse the URL correctly, if there is no sub path
|
||||
tmp = "http://localhost:3000/gogits/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/src@190d949293</code></a>", TestRepoURL)
|
||||
tmp = "http://localhost:3000/gogits/src/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">gogits/src@190d949293</code></a>", TestRepoURL)
|
||||
// but if there is a sub path, we cannot reliably distinguish the repo name from the app route
|
||||
tmp = "http://localhost:3000/sub/gogits/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">sub/gogits@190d949293</code></a>", TestRepoURL)
|
||||
})
|
||||
|
||||
t.Run("Compare", func(t *testing.T) {
|
||||
tmp := util.URLJoin(TestRepoURL, "compare", "d8a994ef243349f321568f9e36d5c3f444b99cae..190d9492934af498c3f669d6a2431dc5459e5b20")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">d8a994ef24..190d949293</code></a>", TestRepoURL)
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">"+TestOrgRepo+"@d8a994ef24..190d949293</code></a>", "https://localhost/forgejo/forgejo")
|
||||
|
||||
tmp = "http://localhost:3000/sub/gogits/gogs/compare/190d9492934af498c3f669d6a2431dc5459e5b20..d8a994ef243349f321568f9e36d5c3f444b99cae"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">190d949293..d8a994ef24</code></a>", "http://localhost:3000/sub/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">gogits/gogs@190d949293..d8a994ef24</code></a>", "http://localhost:3000/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">gogits/gogs@190d949293..d8a994ef24</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
|
||||
|
||||
tmp = "http://localhost:3000/sub1/sub2/sub3/gogits/gogs/compare/190d9492934af498c3f669d6a2431dc5459e5b20..d8a994ef243349f321568f9e36d5c3f444b99cae"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">190d949293..d8a994ef24</code></a>", "http://localhost:3000/sub1/sub2/sub3/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">gogits/gogs@190d949293..d8a994ef24</code></a>", "http://localhost:3000/sub1/gogits/gogs")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">gogits/gogs@190d949293..d8a994ef24</code></a>", "https://external-link.gitea.io/go-gitea/gitea")
|
||||
|
||||
tmp = "https://codeberg.org/forgejo/forgejo/compare/8bbac4c679bea930c74849c355a60ed3c52f8eb5...e2278e5a38187a1dc84dc41d583ec8b44e7257c1?files=options/locale/locale_fi-FI.ini"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">8bbac4c679...e2278e5a38 (options/locale/locale_fi-FI.ini)</code></a>", "https://codeberg.org/forgejo/forgejo")
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">forgejo/forgejo@8bbac4c679...e2278e5a38 (options/locale/locale_fi-FI.ini)</code></a>", TestRepoURL)
|
||||
test(tmp+".", "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">forgejo/forgejo@8bbac4c679...e2278e5a38 (options/locale/locale_fi-FI.ini)</code></a>.", TestRepoURL)
|
||||
|
||||
tmp = "https://codeberg.org/forgejo/forgejo/compare/8bbac4c679bea930c74849c355a60ed3c52f8eb5...e2278e5a38187a1dc84dc41d583ec8b44e7257c1?files=options/locale/locale_fi-FI.ini#L2"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"compare\"><code class=\"nohighlight\">8bbac4c679...e2278e5a38 (options/locale/locale_fi-FI.ini#L2)</code></a>", "https://codeberg.org/forgejo/forgejo")
|
||||
})
|
||||
|
||||
t.Run("Invalid URLs", func(t *testing.T) {
|
||||
tmp := "https://local host/gogits/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
test(tmp, "<a href=\"https://local\" class=\"link\">https://local</a> host/gogits/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20", TestRepoURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRender_IssueIndexPatternRef(t *testing.T) {
|
||||
|
|
@ -429,45 +497,79 @@ func TestRegExp_hashCurrentPattern(t *testing.T) {
|
|||
func TestRegExp_anySHA1Pattern(t *testing.T) {
|
||||
testCases := map[string][]string{
|
||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
|
||||
"jquery/jquery/blob",
|
||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
||||
"/test/unit/event.js",
|
||||
"",
|
||||
"#L2703",
|
||||
},
|
||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
|
||||
"jquery/jquery/blob",
|
||||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
||||
"/test/unit/event.js",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
|
||||
"jquery/jquery/commit",
|
||||
"0705be475092aede1eddae01319ec931fb9c65fc",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
|
||||
"jquery/jquery/tree",
|
||||
"0705be475092aede1eddae01319ec931fb9c65fc",
|
||||
"/src",
|
||||
"",
|
||||
"",
|
||||
},
|
||||
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
|
||||
"gogs/gogs/commit",
|
||||
"d8a994ef243349f321568f9e36d5c3f444b99cae",
|
||||
"",
|
||||
"",
|
||||
"#diff-2",
|
||||
},
|
||||
"https://codeberg.org/forgejo/forgejo/src/commit/949ab9a5c4cac742f84ae5a9fa186f8d6eb2cdc0/RELEASE-NOTES.md?display=source&w=1#L7-L9": {
|
||||
"forgejo/forgejo/src/commit",
|
||||
"949ab9a5c4cac742f84ae5a9fa186f8d6eb2cdc0",
|
||||
"/RELEASE-NOTES.md",
|
||||
"?display=source&w=1",
|
||||
"#L7-L9",
|
||||
},
|
||||
"http://localhost:3000/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3": {
|
||||
"gogits/gogs/src/commit",
|
||||
"190d9492934af498c3f669d6a2431dc5459e5b20",
|
||||
"/path/to/file.go",
|
||||
"",
|
||||
"#L2-L3",
|
||||
},
|
||||
"http://localhost:3000/sub/gogits/gogs/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3": {
|
||||
"sub/gogits/gogs/commit",
|
||||
"190d9492934af498c3f669d6a2431dc5459e5b20",
|
||||
"/path/to/file.go",
|
||||
"",
|
||||
"#L2-L3",
|
||||
},
|
||||
"http://localhost:3000/sub/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3": {
|
||||
"sub/gogits/gogs/src/commit",
|
||||
"190d9492934af498c3f669d6a2431dc5459e5b20",
|
||||
"/path/to/file.go",
|
||||
"",
|
||||
"#L2-L3",
|
||||
},
|
||||
"http://localhost:3000/sub1/sub2/sub3/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3": {
|
||||
"sub1/sub2/sub3/gogits/gogs/src/commit",
|
||||
"190d9492934af498c3f669d6a2431dc5459e5b20",
|
||||
"/path/to/file.go",
|
||||
"",
|
||||
"#L2-L3",
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range testCases {
|
||||
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v)
|
||||
assert.Equal(t, v, anyHashPattern.FindStringSubmatch(k)[1:])
|
||||
}
|
||||
|
||||
for _, v := range []string{"https://codeberg.org/forgejo/forgejo/attachments/774421a1-b0ae-4501-8fba-983874b76811"} {
|
||||
|
|
|
|||
|
|
@ -241,6 +241,45 @@ func TestRender_links(t *testing.T) {
|
|||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
||||
func TestRender_PullReviewCommitLink(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
|
||||
sha := "190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
prCommitLink := util.URLJoin(markup.TestRepoURL, "pulls", "1", "commits", sha)
|
||||
|
||||
test := func(input, expected, base string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: base,
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
test(prCommitLink, `<p><a href="`+prCommitLink+`" rel="nofollow">!1 (commit <code>`+sha[0:10]+`</code>)</a></p>`, markup.TestRepoURL)
|
||||
|
||||
prCommitLink = util.URLJoin(markup.TestAppURL, "sub1", "sub2", markup.TestOrgRepo, "pulls", "1", "commits", sha)
|
||||
test(
|
||||
prCommitLink,
|
||||
`<p><a href="`+prCommitLink+`" rel="nofollow">!1 (commit <code>`+sha[0:10]+`</code>)</a></p>`,
|
||||
util.URLJoin(markup.TestAppURL, "sub1", "sub2", markup.TestOrgRepo),
|
||||
)
|
||||
test(
|
||||
prCommitLink,
|
||||
`<p><a href="`+prCommitLink+`" rel="nofollow">`+markup.TestOrgRepo+`@!1 (commit <code>`+sha[0:10]+`</code>)</a></p>`,
|
||||
markup.TestRepoURL,
|
||||
)
|
||||
|
||||
prCommitLink = "https://codeberg.org/forgejo/forgejo/pulls/7979/commits/4d968c08e0a8d24bd2f3fb2a3a48b37e6d84a327#diff-7649acfa98a9ee3faf0d28b488bbff428317fc72"
|
||||
test(prCommitLink, `<p><a href="`+prCommitLink+`" rel="nofollow">!7979 (commit <code>4d968c08e0</code>)</a></p>`, "https://codeberg.org/forgejo/forgejo")
|
||||
test(prCommitLink, `<p><a href="`+prCommitLink+`" rel="nofollow">forgejo/forgejo@!7979 (commit <code>4d968c08e0</code>)</a></p>`, markup.TestRepoURL)
|
||||
}
|
||||
|
||||
func TestRender_email(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
|
||||
|
|
@ -686,6 +725,9 @@ func TestIssue18471(t *testing.T) {
|
|||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
Links: markup.Links{
|
||||
Base: "http://domain/org/repo",
|
||||
},
|
||||
}, strings.NewReader(data), &res)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
|
@ -723,6 +765,9 @@ func TestRender_FilePreview(t *testing.T) {
|
|||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
Metas: metas,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
|
|
@ -835,7 +880,7 @@ func TestRender_FilePreview(t *testing.T) {
|
|||
|
||||
testRender(
|
||||
urlWithSub,
|
||||
`<p><a href="http://localhost:3000/sub/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3" rel="nofollow"><code>190d949293/path/to/file.go (L2-L3)</code></a></p>`,
|
||||
`<p><a href="http://localhost:3000/sub/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3" rel="nofollow"><code>gogits/gogs@190d949293/path/to/file.go (L2-L3)</code></a></p>`,
|
||||
localMetas,
|
||||
)
|
||||
|
||||
|
|
@ -1253,4 +1298,20 @@ func TestRender_FilePreview(t *testing.T) {
|
|||
localMetas,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("file previews followed by new line", func(t *testing.T) {
|
||||
testRender(
|
||||
commitFileURLFirstLine+"\nand\n"+commitFileURLFirstLine,
|
||||
"<p></p>"+filePreviewBox+"<p><br/>\nand<br/>\n</p>"+filePreviewBox+"<p></p>",
|
||||
localMetas,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("file previews followed by new line in div", func(t *testing.T) {
|
||||
testRender(
|
||||
"<div>"+commitFileURLFirstLine+"\nand\n"+commitFileURLFirstLine+"</div>",
|
||||
"<div>"+filePreviewBox+"\nand\n"+filePreviewBox+"</div>",
|
||||
localMetas,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/util"
|
||||
"forgejo.org/modules/util/donotpanic"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
|
@ -267,6 +268,15 @@ sandbox="allow-scripts"
|
|||
return err
|
||||
}
|
||||
|
||||
func postProcessOrCopy(ctx *RenderContext, renderer Renderer, reader io.Reader, writer io.Writer) (err error) {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
err = PostProcess(ctx, reader, writer)
|
||||
} else {
|
||||
_, err = io.Copy(writer, reader)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
|
|
@ -293,7 +303,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
|||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = SanitizeReader(pr2, renderer.Name(), output)
|
||||
err = donotpanic.SafeFuncWithError(func() error { return SanitizeReader(pr2, renderer.Name(), output) })
|
||||
_ = pr2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
|
@ -303,11 +313,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
|||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
err = PostProcess(ctx, pr, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr)
|
||||
}
|
||||
err = donotpanic.SafeFuncWithError(func() error { return postProcessOrCopy(ctx, renderer, pr, pw2) })
|
||||
_ = pr.Close()
|
||||
_ = pw2.Close()
|
||||
wg.Done()
|
||||
|
|
@ -320,7 +326,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
|||
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
||||
// Append a short script to the iframe's contents, which will communicate the scroll height of the embedded document via postMessage, either once loaded (in case the containing page loads first) in response to a postMessage from external.js, in case the iframe loads first
|
||||
// We use '*' as a target origin for postMessage, because can be certain we are embedded on the same domain, due to X-Frame-Options configured elsewhere. (Plus, the offsetHeight of an embedded document is likely not sensitive data anyway.)
|
||||
_, _ = pw.Write([]byte("<script>{let postHeight = () => {window.parent.postMessage({frameHeight: document.documentElement.offsetHeight}, '*')}; window.addEventListener('load', postHeight); window.addEventListener('message', (event) => {if (event.source === window.parent && event.data.requestOffsetHeight) postHeight()});}</script>"))
|
||||
_, _ = pw.Write([]byte("<script>{let postHeight = () => {window.parent.postMessage({frameHeight: document.documentElement.offsetHeight || document.documentElement.scrollHeight}, '*')}; window.addEventListener('load', postHeight); window.addEventListener('message', (event) => {if (event.source === window.parent && event.data.requestOffsetHeight) postHeight()});}</script>"))
|
||||
}
|
||||
_ = pw.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,49 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package markup_test
|
||||
package markup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type failReader struct{}
|
||||
|
||||
func (*failReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("FAIL")
|
||||
}
|
||||
|
||||
func TestRender_postProcessOrCopy(t *testing.T) {
|
||||
renderContext := &RenderContext{Ctx: t.Context()}
|
||||
|
||||
t.Run("CopyOK", func(t *testing.T) {
|
||||
input := "SOMETHING"
|
||||
output := &bytes.Buffer{}
|
||||
require.NoError(t, postProcessOrCopy(renderContext, nil, strings.NewReader(input), output))
|
||||
assert.Equal(t, input, output.String())
|
||||
})
|
||||
|
||||
renderer := GetRendererByType("markdown")
|
||||
|
||||
t.Run("PostProcessOK", func(t *testing.T) {
|
||||
input := "SOMETHING"
|
||||
output := &bytes.Buffer{}
|
||||
defer test.MockVariableValue(&defaultProcessors, []processor{})()
|
||||
require.NoError(t, postProcessOrCopy(renderContext, renderer, strings.NewReader(input), output))
|
||||
assert.Equal(t, input, output.String())
|
||||
})
|
||||
|
||||
t.Run("PostProcessError", func(t *testing.T) {
|
||||
input := &failReader{}
|
||||
defer test.MockVariableValue(&defaultProcessors, []processor{})()
|
||||
assert.ErrorContains(t, postProcessOrCopy(renderContext, renderer, input, &bytes.Buffer{}), "FAIL")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ func PushUpdateAddTag(ctx context.Context, repo *repo_model.Repository, gitRepo
|
|||
NumCommits: commitsCount,
|
||||
CreatedUnix: timeutil.TimeStamp(createdAt.Unix()),
|
||||
IsTag: true,
|
||||
Note: tag.Message,
|
||||
}
|
||||
if author != nil {
|
||||
rel.PublisherID = author.ID
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/git"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_calcSync(t *testing.T) {
|
||||
|
|
@ -74,3 +80,56 @@ func Test_calcSync(t *testing.T) {
|
|||
assert.Equal(t, *gitTags[1], *updates[0], "updates equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncReleasesWithTags(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Can be any repository that doesn't have the git tag releases.
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
t.Run("SHA1", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
require.NoError(t, git.InitRepository(t.Context(), tmpDir, false, git.Sha1ObjectFormat.Name()))
|
||||
gitRepo, err := git.OpenRepository(t.Context(), tmpDir)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, "README.md"), []byte("testing the testing"), 0o666))
|
||||
require.NoError(t, git.AddChanges(tmpDir, true))
|
||||
require.NoError(t, git.CommitChanges(tmpDir, git.CommitChangesOptions{Message: "Add README"}))
|
||||
require.NoError(t, gitRepo.CreateAnnotatedTag("v1.0.0", "First release \\o/", "HEAD"))
|
||||
|
||||
require.NoError(t, SyncReleasesWithTags(t.Context(), repo, gitRepo))
|
||||
|
||||
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.0.0"})
|
||||
assert.Equal(t, "First release \\o/\n", release.Note)
|
||||
assert.True(t, release.IsTag)
|
||||
assert.EqualValues(t, 1, release.NumCommits)
|
||||
})
|
||||
|
||||
t.Run("SHA256", func(t *testing.T) {
|
||||
if !git.SupportHashSha256 {
|
||||
t.Skip("skipping because installed Git version doesn't support SHA256")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
require.NoError(t, git.InitRepository(t.Context(), tmpDir, false, git.Sha256ObjectFormat.Name()))
|
||||
gitRepo, err := git.OpenRepository(t.Context(), tmpDir)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, "README.md"), []byte("testing the testing"), 0o666))
|
||||
require.NoError(t, git.AddChanges(tmpDir, true))
|
||||
require.NoError(t, git.CommitChanges(tmpDir, git.CommitChangesOptions{Message: "Add README"}))
|
||||
require.NoError(t, gitRepo.CreateAnnotatedTag("v2.0.0", "Second release \\o/", "HEAD"))
|
||||
|
||||
require.NoError(t, SyncReleasesWithTags(t.Context(), repo, gitRepo))
|
||||
|
||||
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v2.0.0"})
|
||||
assert.Equal(t, "Second release \\o/\n", release.Note)
|
||||
assert.True(t, release.IsTag)
|
||||
assert.EqualValues(t, 1, release.NumCommits)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
28
modules/util/donotpanic/donotpanic.go
Normal file
28
modules/util/donotpanic/donotpanic.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package donotpanic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forgejo.org/modules/log"
|
||||
)
|
||||
|
||||
type FuncWithError func() error
|
||||
|
||||
func SafeFuncWithError(fun FuncWithError) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("PANIC recovered: %v\nStacktrace: %s", r, log.Stack(2))
|
||||
rErr, ok := r.(error)
|
||||
if ok {
|
||||
err = fmt.Errorf("PANIC recover with error: %w", rErr)
|
||||
} else {
|
||||
err = fmt.Errorf("PANIC recover: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return fun()
|
||||
}
|
||||
28
modules/util/donotpanic/donotpanic_test.go
Normal file
28
modules/util/donotpanic/donotpanic_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package donotpanic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDoNotPanic_SafeFuncWithError(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
assert.NoError(t, SafeFuncWithError(func() error { return nil }))
|
||||
})
|
||||
|
||||
t.Run("PanickString", func(t *testing.T) {
|
||||
errorMessage := "ERROR MESSAGE"
|
||||
assert.ErrorContains(t, SafeFuncWithError(func() error { panic(errorMessage) }), fmt.Sprintf("recover: %s", errorMessage))
|
||||
})
|
||||
|
||||
t.Run("PanickError", func(t *testing.T) {
|
||||
errorMessage := "ERROR MESSAGE"
|
||||
assert.ErrorContains(t, SafeFuncWithError(func() error { panic(errors.New(errorMessage)) }), fmt.Sprintf("recover with error: %s", errorMessage))
|
||||
})
|
||||
}
|
||||
|
|
@ -1887,7 +1887,7 @@ org_full_name_holder = Пълно име на организацията
|
|||
teams = Екипи
|
||||
lower_members = участници
|
||||
lower_repositories = хранилища
|
||||
settings.repoadminchangeteam = Админ. на хранилището да може да добавя и премахва достъп за екипи
|
||||
settings.repoadminchangeteam = Админ. на хранилище да може да добавя и премахва достъп за екипи
|
||||
settings.email = Ел. поща за връзка
|
||||
settings.delete_account = Изтриване на тази организация
|
||||
settings.delete_org_title = Изтриване на организацията
|
||||
|
|
@ -1954,6 +1954,9 @@ settings.change_orgname_redirect_prompt.with_cooldown.one = Старото им
|
|||
teams.add_nonexistent_repo = Хранилището, което се опитвате да добавите, не съществува, моля, първо го създайте.
|
||||
teams.invite.by = Поканен от %s
|
||||
|
||||
teams.can_create_org_repo_helper = Участниците могат да създават нови хранилища в организацията. Създателят ще получи администраторски достъп до новото хранилище.
|
||||
teams.owners_permission_desc = Притежателите имат пълен достъп до <strong>всички хранилища</strong> и имат <strong>администраторски достъп</strong> до организацията.
|
||||
|
||||
[install]
|
||||
admin_password = Парола
|
||||
user = Потребителско име
|
||||
|
|
@ -2068,7 +2071,7 @@ projects = Проекти
|
|||
code = Код
|
||||
overview = Обзор
|
||||
watched = Наблюдавани хранилища
|
||||
unfollow = Прекратяване на следването
|
||||
unfollow = Отследване
|
||||
block = Блокиране
|
||||
settings = Потребителски настройки
|
||||
starred = Отбелязани хранилища
|
||||
|
|
|
|||
|
|
@ -484,13 +484,13 @@ team_invite.subject = %[1]s us convida a unir-vos a l'organització %[2]s
|
|||
release.title = Títol: %s
|
||||
release.downloads = Baixades:
|
||||
release.download.zip = Codi font (ZIP)
|
||||
repo.transfer.subject_to_you = %s us vol transferir el repositori "%s"
|
||||
repo.transfer.subject_to_you = %s us vol transferir el repositori «%s»
|
||||
repo.collaborator.added.subject = %s us ha afegit a %s com a col·laborador
|
||||
repo.collaborator.added.text = Us han afegit com a col·laborador al repositori:
|
||||
issue_assigned.issue = @%[1]s us ha assignat l'incidència %[2]s del repositori %[3]s.
|
||||
issue.x_mentioned_you = <b>@%s</b> us ha mencionat:
|
||||
issue.action.new = <b>@%[1]s</b> ha creat #%[2]d.
|
||||
repo.transfer.subject_to = %s vol transferir el repositori "%s" a %s
|
||||
repo.transfer.subject_to = %s vol transferir el repositori «%s» a %s
|
||||
repo.transfer.to_you = tu
|
||||
register_notify.text_3 = Si algú altre us ha fet aquest compte, necessitareu <a href="%s">configurar la vostra contrasenya</a> abans.
|
||||
password_change.subject = S'ha canviat la vostra contrasenya
|
||||
|
|
@ -508,6 +508,28 @@ issue.action.approve = <b>@%[1]s</b> ha aprovat aquesta sol·licitud d'extracci
|
|||
issue.action.reject = <b>@%[1]s</b> ha sol·licitat canvis en aquesta sol·licitud d'extracció.
|
||||
register_notify.text_2 = Podeu iniciar sessió al vostre compte fent servir el vostre nom d'usuari: %s
|
||||
|
||||
register_notify.text_1 = aquest és el vostre correu electrònic de confirmació pel registre de %s!
|
||||
password_change.text_1 = S'acaba de canviar la contrasenya pel vostre compte.
|
||||
issue.action.review = <b>@%[1]s</b> ha deixat un comentari a la vostra sol·licitud d'extracció.
|
||||
issue.action.review_dismissed = <b>@%[1]s</b> ha rebutjat l'última revisió de %[2]s per aquesta sol·licitud d'extracció.
|
||||
|
||||
reset_password = Recupereu el vostre compte
|
||||
reset_password.text = Si heu sigut vós, cliqueu el següent enllaç per recuperar el vostre compte abans de <b>%s</b>:
|
||||
primary_mail_change.text_1 = S'ha canviat la vostra adreça de correu electrònic principal a %[1]s. Això vol dir que aquesta adreça de correu electrònic no rebrà més notificacions de correu pel vostre compte.
|
||||
totp_disabled.text_1 = S'han desactivat les contrasenyes d'un sol ús basades en el temps (TOTP) pel vostre compte.
|
||||
totp_disabled.no_2fa = Ja no hi ha altres mètodes d'autenticació de doble factor configurats, per la qual cosa ja no és necessari iniciar sessió al vostre compte mitjançat autenticació de doble factor.
|
||||
removed_security_key.no_2fa = Ja no hi ha altres mètodes d'autenticació de doble factor configurats, per la qual cosa ja no és necessari iniciar sessió al vostre compte mitjançat autenticació de doble factor.
|
||||
totp_enrolled.text_1.has_webauthn = Heu habilitat el TOTP per al vostre compte. Això vol dir que, per a totes les futures connexions al vostre compte, podreu utilitzar el TOTP com a mètode d'autenticació en dos passos (2FA) o bé utilitzar qualsevol de les vostres claus de seguretat.
|
||||
issue.action.force_push = <b>%[1]s</b> ha realitzat un «force push» de <b>%[2]s</b> des de %[3]s fins a %[4]s.
|
||||
issue.action.push_1 = <b>@%[1]s</b> ha pujat $[3]d commit a %[2]s
|
||||
issue.action.push_n = <b>@%[1]s</b> ha pujat $[3]d commits a %[2]s
|
||||
issue.action.ready_for_review = <b>@%[1]s</b> ha marcat aquesta «pull request» com a preparada per a la revisió.
|
||||
issue.in_tree_path = A %s:
|
||||
release.new.text = <b>@%[1]s</b> ha publicat %[2]s a %[3]s
|
||||
release.note = Nota:
|
||||
|
||||
release.new.subject = %s a %s llançat
|
||||
|
||||
[modal]
|
||||
yes = Sí
|
||||
no = No
|
||||
|
|
@ -580,6 +602,26 @@ min_size_error = ` ha de contenir %s caràcters com a mínim.`
|
|||
max_size_error = ` ha de contenir %s caràcters com a màxim.`
|
||||
email_domain_is_not_allowed = El domini de l'adreça de correu electrònic <b>%s</b> de l'usuari entra en conflicte amb EMAIL_DOMAIN_ALLOWLIST o EMAIL_DOMAIN_BLOCKLIST. Assegureu-vos d'haver introduït l'adreça de correu electrònic correctament.
|
||||
|
||||
Website = Lloc web
|
||||
Location = Ubicació
|
||||
|
||||
AdminEmail = Correu electrònic de l'administrador
|
||||
AccessToken = Testimoni d'accés
|
||||
CommitSummary = Resum del commit
|
||||
CommitMessage = Missatge del commit
|
||||
CommitChoice = Selecció de commit
|
||||
TreeName = Camí del fitxer
|
||||
alpha_dash_error = ` hauria de contenir només caràcters alfanumèrics, guions ("-") i barres baixes ("_").`
|
||||
alpha_dash_dot_error = ` hauria de contenir només caràcters alfanumèrics, guions ("-"), barres baixes ("_") i punts (".").`
|
||||
regex_pattern_error = ` el patró d'expressió regular no és vàlid: %s.`
|
||||
required_prefix = L'entrada ha de començar amb "%s"
|
||||
|
||||
PayloadUrl = URL de càrrega útil
|
||||
git_ref_name_error = ` ha de ser un nom correcte de referència de Git.`
|
||||
size_error = ` ha de tenir una mida de %s.`
|
||||
include_error = ` ha de contenir la subcadena «%s».`
|
||||
glob_pattern_error = ` el patró glob no és vàlid: %s.`
|
||||
|
||||
[settings]
|
||||
pronouns = Pronoms
|
||||
change_username_prompt = Nota: canviar el vostre nom d'usuari també canvia l'URL del vostre compte.
|
||||
|
|
@ -739,6 +781,108 @@ generate_token_name_duplicate = Ja s'ha usat <strong>%s</strong> com a nom d'apl
|
|||
delete_token = Eliminar
|
||||
access_token_deletion =
|
||||
|
||||
biography_placeholder = Explica una mica sobre tu als altres! (Compatible amb Markdown)
|
||||
delete_email = Eliminar
|
||||
add_new_email = Afegir una adreça de correu electrònic
|
||||
add_new_openid = Afegir una nova URI d'OpenID
|
||||
add_email = Afegir una adreça de correu electrònic
|
||||
add_openid = Afegir una URI d'OpenID
|
||||
keep_email_private = Oculta l'adreça de correu electrònic
|
||||
delete_key = Eliminar
|
||||
added_on = Afegit el %s
|
||||
last_used = Usat per últim cop el
|
||||
can_read_info = Llegir
|
||||
can_write_info = Escriure
|
||||
show_openid = Mostrar al perfil
|
||||
hide_openid = Ocultar del perfil
|
||||
ssh_disabled = SSH està deshabilitat
|
||||
access_token_deletion_desc = Eliminar un testimoni revocarà l'accés al vostre compte de les aplicacions que l'estiguin fent servir. Aquesta acció no es pot desfer. Voleu continuar?
|
||||
delete_token_success = S'ha eliminat el testimoni. Les aplicacions que l'estiguin fent servir ja no tenen accés al vostre compte.
|
||||
regenerate_token = Regenerar
|
||||
access_token_regeneration = Regenerar un testimoni d'accés
|
||||
access_token_regeneration_desc = Regenerar un testimoni revocarà l'accés al vostre compte de les aplicacions que l'estiguin fent servir. Aquesta acció no es pot desfer. Voleu continuar?
|
||||
regenerate_token_success = S'ha regenerat el testimoni. Les aplicacions que el fan servir ja no tenen accés al vostre compte i s'han actualitzar al nou testimoni.
|
||||
permissions_public_only = Només públic
|
||||
permissions_access_all = Tot (públic, privat i limitat)
|
||||
select_permissions = Selecció de permisos
|
||||
permission_no_access = Sense accés
|
||||
permission_read = Lectura
|
||||
permission_write = Lectura i escriptura
|
||||
at_least_one_permission = Heu de seleccionar un permís com a mínim per crear un testimoni
|
||||
permissions_list = Permisos:
|
||||
manage_oauth2_applications = Gestionar aplicacions OAuth2
|
||||
edit_oauth2_application = Modificar aplicació OAuth2
|
||||
remove_oauth2_application = Eliminar aplicació OAuth2
|
||||
remove_oauth2_application_success = S'ha eliminat l'aplicació.
|
||||
create_oauth2_application = Crear una nova aplicació OAuth2
|
||||
create_oauth2_application_button = Crear aplicació
|
||||
create_oauth2_application_success = Heu creat amb èxit una nova aplicació OAuth2.
|
||||
update_oauth2_application_success = Heu actualitzat amb èxit l'aplicació OAuth2.
|
||||
oauth2_application_name = Nom de l'aplicació
|
||||
oauth2_redirect_uris = URIs de redirecció. Si us plau, poseu cada URI en una línia nova.
|
||||
save_application = Guardar
|
||||
oauth2_client_id = ID del client
|
||||
oauth2_client_secret = Secret del client
|
||||
oauth2_regenerate_secret = Regenerar secret
|
||||
oauth2_regenerate_secret_hint = Heu perdut el vostre secret?
|
||||
oauth2_application_edit = Modificar
|
||||
authorized_oauth2_applications = Aplicacions OAuth2 autoritzades
|
||||
authorized_oauth2_applications_description = Heu permès accés al vostre compte personal de Forgejo a aquestes aplicacions de tercers. Si us plau, revoqueu l'accés de les aplicacions que ja no feu servir.
|
||||
revoke_key = Revocar
|
||||
revoke_oauth2_grant = Revocar l'accés
|
||||
revoke_oauth2_grant_description = Revocar l'accés d'aquesta aplicació de tercers impedirà que accedeixi a la vostra informació. N'esteu segurs?
|
||||
revoke_oauth2_grant_success = L'accés s'ha revocat amb èxit.
|
||||
twofa_desc = Per protegir el vostre compte del robatori de contrasenyes, podeu fer servir un telèfon intel·ligent o un altre dispositiu per rebre contrasenyes d'un sol ús basades en el temps ("TOTP").
|
||||
twofa_recovery_tip = Si perdeu el vostre dispositiu, podreu fer servir una clau de recuperació d'un sol ús per recuperar l'accés al vostre compte.
|
||||
twofa_disable = Desactivar l'autenticació de doble factor
|
||||
twofa_scratch_token_regenerate = Regenerar la clau de recuperació d'un sol ús
|
||||
twofa_scratch_token_regenerated = La vostra clau de recuperació d'un sol ús ara és %s. Emmagatzemeu-la en un lloc segur, ja que no es tornarà a mostrar.
|
||||
twofa_disable_note = L'autenticació de doble factor es pot desactivar si és necessari.
|
||||
twofa_disable_desc = Desactivar l'autenticació de doble factor farà que el vostre compte sigui menys segur. Voleu continuar?
|
||||
regenerate_scratch_token_desc = Si heu perdur la vostra clau de recuperació o ja l'heu fet servir per iniciar sessió, la podeu reiniciar aquí.
|
||||
twofa_disabled = S'ha desactivat l'autenticació de doble factor.
|
||||
scan_this_image = Escanegeu aquesta imatge amb la vostra aplicació d'autenticació:
|
||||
or_enter_secret = O introduïu el secret: %s
|
||||
then_enter_passcode = I introduïu el codi d'accés que es mostra a l'aplicació:
|
||||
passcode_invalid = El codi d'accés és incorrecte. Intenteu-ho de nou.
|
||||
twofa_failed_get_secret = No s'ha pogut obtenir el secret.
|
||||
webauthn_desc = Les claus de seguretat són dispositius que contenen claus criptogràfiques. Es poden fer servir per l'autenticació de doble factor. Les claus de seguretat han de ser compatibles amb l'estàndard <a rel="noreferrer" target="_blank" href="%s">WebAuthn Authenticator</a>.
|
||||
webauthn_register_key = Afegir una clau de seguretat
|
||||
webauthn_nickname = Sobrenom
|
||||
webauthn_delete_key_desc = Si elimineu una clau de seguretat no la podreu fer servir per iniciar sessió. Voleu continuar?
|
||||
webauthn_key_loss_warning = Si perdeu les vostres claus de seguretat, perdreu també l'accés al vostre compte.
|
||||
manage_account_links = Comptes vinculats
|
||||
manage_account_links_desc = Aquests comptes externs estan vinculats al vostre compte de Forgejo.
|
||||
link_account = Vincular un compte
|
||||
remove_account_link_desc = Eliminar un compte vinculat revocarà el seu accés al vostre compte de Forgejo. Voleu continuar?
|
||||
remove_account_link_success = S'ha eliminat el compte vinculat.
|
||||
orgs_none = No sou membres de cap organització.
|
||||
repos_none = No sou propietaris de cap repositori.
|
||||
blocked_users_none = No hi ha cap usuari bloquejat.
|
||||
delete_account = Eliminar el vostre compte
|
||||
delete_prompt = Aquest acció eliminarà permanentment el vostre compte. Aquesta acció <strong>no</strong> es pot desfer.
|
||||
confirm_delete_account = Confirmar l'eliminació
|
||||
delete_account_title = Eliminar el compte d'usuari
|
||||
delete_account_desc = Esteu segurs de voler eliminar permanentment aquest compte d'usuari?
|
||||
email_notifications.enable = Activar les notificacions per correu electrònic
|
||||
email_notifications.disable = Desactivar les notificacions per correu electrònic
|
||||
visibility.public_tooltip = Visible per a tothom
|
||||
visibility.limited_tooltip = Visible només pels usuaris que hagin iniciat sessió
|
||||
visibility.private_tooltip = Visible només pels membres de les organitzacions a les quals us hàgiu unit
|
||||
user_unblock_success = S'ha desbloquejat l'usuari amb èxit.
|
||||
user_block_success = S'ha bloquejat l'usuari amb èxit.
|
||||
quota.sizes.repos.all = Repositoris
|
||||
quota.sizes.repos.public = Repositoris públics
|
||||
quota.sizes.repos.private = Repositoris privats
|
||||
quota.sizes.git.all = Contingut de Git
|
||||
quota.sizes.git.lfs = Git LFS
|
||||
quota.sizes.assets.attachments.all = Fitxers adjunts
|
||||
quota.sizes.assets.attachments.issues = Fitxers adjunts a incidències
|
||||
quota.sizes.assets.artifacts = Artefactes
|
||||
quota.sizes.assets.packages.all = Paquets
|
||||
|
||||
enable_custom_avatar = Usa un avatar personalitzat
|
||||
|
||||
[repo]
|
||||
settings.basic_settings = Configuració bàsica
|
||||
settings.event_issues = Modificació
|
||||
|
|
@ -952,3 +1096,5 @@ public_activity.visibility_hint.admin_public = Aquesta activitat és visible per
|
|||
public_activity.visibility_hint.self_private = La vostra activitat és visible només per vós i pels administradors de la instància. <a href="%s">Configurar</a>.
|
||||
public_activity.visibility_hint.admin_private = Aquesta activitat és visible per vós perquè sou un administrador, però l'usuari vol que romangui privada.
|
||||
public_activity.visibility_hint.self_private_profile = La vostra activitat és visible només per vós i pels administradors de la instància perquè el vostre perfil és privat. <a href="%s">Configurar</a>.
|
||||
change_avatar = Canvieu el vostre avatar…
|
||||
joined_on = S'ha unit el %s
|
||||
|
|
@ -3683,7 +3683,7 @@ owner.settings.cleanuprules.keep.count=Zachovat nejnovější
|
|||
owner.settings.cleanuprules.keep.count.1=1 verze na balíček
|
||||
owner.settings.cleanuprules.keep.count.n=%d verzí na balíček
|
||||
owner.settings.cleanuprules.keep.pattern=Ponechat odpovídající verze
|
||||
owner.settings.cleanuprules.keep.pattern.container=U balíčků Container je vždy zachována <code>nejnovější</code> verze.
|
||||
owner.settings.cleanuprules.keep.pattern.container=U balíčků Container je vždy zachována <code>nejnovější</code> verze.
|
||||
owner.settings.cleanuprules.remove.title=Verze, které odpovídají těmto pravidlům, jsou odstraněny, pokud výše uvedené pravidlo neukládá jejich zachování.
|
||||
owner.settings.cleanuprules.remove.days=Odstranit verze starší než
|
||||
owner.settings.cleanuprules.remove.pattern=Odstranit odpovídající verze
|
||||
|
|
@ -3691,7 +3691,7 @@ owner.settings.cleanuprules.success.update=Pravidlo pro čištění bylo aktuali
|
|||
owner.settings.cleanuprules.success.delete=Pravidlo pro čištění bylo odstraněno.
|
||||
owner.settings.chef.title=Registr Chef
|
||||
owner.settings.chef.keypair=Generovat pár klíčů
|
||||
owner.settings.chef.keypair.description=Žádosti odeslané do registru Chef musí být kryptograficky podepsané jako způsob ověření. Při generování páru klíčů je ve službě Forgejo uložen pouze veřejný klíč. Soukromý klíč je poskytnut vám, abyste jej mohli použít s programem knife. Vygenerováním nového páru klíčů přepíšete ten předchozí.
|
||||
owner.settings.chef.keypair.description=Žádosti odeslané do registru Chef musí být kryptograficky podepsané jako způsob ověření. Při generování páru klíčů je ve službě Forgejo uložen pouze veřejný klíč. Soukromý klíč je poskytnut vám, abyste jej mohli použít s programem knife. Vygenerováním nového páru klíčů přepíšete ten předchozí.
|
||||
owner.settings.cargo.rebuild.description = Opětovné sestavení může být užitečné, pokud není index synchronizován s uloženými balíčky Cargo.
|
||||
owner.settings.cargo.rebuild.no_index = Opětovné vytvoření selhalo, nebyl inicializován žádný index.
|
||||
npm.dependencies.bundle = Přidružené závislosti
|
||||
|
|
@ -3726,7 +3726,7 @@ secrets=Tajné klíče
|
|||
description=Tejné klíče budou předány určitým akcím a nelze je přečíst jinak.
|
||||
none=Zatím zde nejsou žádné tajné klíče.
|
||||
creation=Přidat tajný klíč
|
||||
creation.name_placeholder=nerozlišovat velká a malá písmena, pouze alfanumerické znaky nebo podtržítka, nemohou začínat na GITEA_ nebo GITHUB_
|
||||
creation.name_placeholder=nerozlišovat velká a malá písmena, pouze alfanumerické znaky nebo podtržítka, nemohou začínat na GITEA_ nebo GITHUB_
|
||||
creation.value_placeholder=Vložte jakýkoliv obsah. Mezery na začátku a konci budou vynechány.
|
||||
creation.success=Tajný klíč „%s“ byl přidán.
|
||||
creation.failed=Nepodařilo se přidat tajný klíč.
|
||||
|
|
|
|||
|
|
@ -776,7 +776,7 @@ activate_email=Aktivierung senden
|
|||
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 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_success=Die E-Mail-Adresse wurde entfernt.
|
||||
theme_update_success=Deine Theme-Auswahl wurde gespeichert.
|
||||
|
|
@ -1653,8 +1653,8 @@ issues.add_time=Zeit manuell hinzufügen
|
|||
issues.del_time=Diese Zeiterfassung löschen
|
||||
issues.add_time_short=Zeit hinzufügen
|
||||
issues.add_time_cancel=Abbrechen
|
||||
issues.add_time_history=`hat %s den Zeitaufwand hinzugefügt`
|
||||
issues.del_time_history=`hat %s den Zeitaufwand gelöscht`
|
||||
issues.add_time_history=`hat den Zeitaufwand von %s hinzugefügt`
|
||||
issues.del_time_history=`hat den Zeitaufwand von %s gelöscht`
|
||||
issues.add_time_hours=Stunden
|
||||
issues.add_time_minutes=Minuten
|
||||
issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben.
|
||||
|
|
@ -3523,7 +3523,7 @@ mark_as_read=Als gelesen markieren
|
|||
mark_as_unread=Als ungelesen markieren
|
||||
mark_all_as_read=Alle als gelesen markieren
|
||||
subscriptions=Abonnements
|
||||
watching=Gefolgt
|
||||
watching=Beobachtet
|
||||
no_subscriptions=Keine Abonnements
|
||||
|
||||
[gpg]
|
||||
|
|
|
|||
|
|
@ -1579,6 +1579,7 @@ issues.filter_poster = Author
|
|||
issues.filter_poster_no_select = All authors
|
||||
issues.filter_type = Type
|
||||
issues.filter_type.all_pull_requests = All pull requests
|
||||
issues.filter_type.all_issues = All issues
|
||||
issues.filter_type.assigned_to_you = Assigned to you
|
||||
issues.filter_type.created_by_you = Created by you
|
||||
issues.filter_type.mentioning_you = Mentioning you
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ ok = Bone
|
|||
download_logs = Elsuti protokolojn
|
||||
unknown = Nekonata
|
||||
issues = Eraroj
|
||||
error404 = Aŭ tiu ĉi paĝo <strong>ne ekzistas</strong>, <strong>estis forigita</strong> aŭ <strong>vi ne rajtas</strong> vidi ĝin.
|
||||
error404 = Aŭ tiu ĉi paĝo <strong>ne ekzistas</strong>, <strong>estas forigita</strong> aŭ <strong>vi ne rajtas</strong> vidi ĝin.
|
||||
retry = Reprovi
|
||||
activities = Aktivecoj
|
||||
confirm_delete_selected = Konfirmi forigon de ĉiu elektito?
|
||||
|
|
@ -142,9 +142,11 @@ new_org.link = Novan organizaĵon
|
|||
error413 = Vi plenkonsumis vian kvoton.
|
||||
twofa_scratch = Sukuranta kodo por duobla aŭtentikigo
|
||||
|
||||
copy_path = Kopii dosiervojon
|
||||
|
||||
[editor]
|
||||
buttons.list.ordered.tooltip = Aldoni nombran liston
|
||||
buttons.bold.tooltip = Aldoni grasan tekston
|
||||
buttons.bold.tooltip = Aldoni grasan tekston (Ctrl+B / ⌘B)
|
||||
buttons.quote.tooltip = Citi tekston
|
||||
buttons.code.tooltip = Aldoni kodtekston
|
||||
buttons.list.unordered.tooltip = Aldoni punktan liston
|
||||
|
|
@ -154,7 +156,7 @@ buttons.ref.tooltip = Citi eraron aŭ tirpeton
|
|||
buttons.list.task.tooltip = Aldoni liston de taskoj
|
||||
buttons.enable_monospace_font = Ŝalti egallarĝan signoformaron
|
||||
buttons.mention.tooltip = Mencii uzanton aŭ grupon
|
||||
buttons.italic.tooltip = Aldoni oblikvan tekston
|
||||
buttons.italic.tooltip = Aldoni oblikvan tekston (Ctrl+I / ⌘I)
|
||||
buttons.link.tooltip = Aldoni ligilon
|
||||
buttons.disable_monospace_font = Malsalti egallarĝan signoformaron
|
||||
buttons.indent.tooltip = Krommarĝeni erojn je unu nivelo
|
||||
|
|
@ -167,6 +169,10 @@ table_modal.label.rows = Horizontaloj
|
|||
table_modal.label.columns = Vertikaloj
|
||||
link_modal.description = Priskribo
|
||||
|
||||
link_modal.header = Aldoni ligilon
|
||||
link_modal.url = Url
|
||||
link_modal.paste_reminder = Aludo: kun URL en via tondujo, vi povas alglui senpere al la redaktilo por krei ligilon.
|
||||
|
||||
[aria]
|
||||
navbar = Esplora breto
|
||||
footer.software = Pri ĉi tiu programaro
|
||||
|
|
@ -203,6 +209,8 @@ lightweight_desc = Forgejo ne penigos vian servilon, kaj eĉ ruleblas je Raspber
|
|||
platform = Plursistema
|
||||
license_desc = Ek, prenu <a target="_blank" rel="noopener noreferrer" href="%[1]s">Forgejon</a>! Aliĝu kaj <a target="_blank" rel="noopener noreferrer" href="%[2]s">helpu</a> nin plibonigi la projekton. Ne timu kontribui!
|
||||
|
||||
platform_desc = Forgejo estas konfirmita ruli sur liberaj operaciumoj kiel Linukso kaj FreeBSD, kaj malsamaj arkitekturprocesoroj. Elektu tion, kion vi preferas.
|
||||
|
||||
[install]
|
||||
title = Komenca agordado
|
||||
install = Instalado
|
||||
|
|
@ -311,7 +319,7 @@ enable_update_checker = Aktivigi novversian kontrolanton
|
|||
password_algorithm = Pasvorthaketiga algoritmo
|
||||
env_config_keys = Mediagordoj
|
||||
invalid_password_algorithm = Malvalida pasvorthakeita algoritmo
|
||||
password_algorithm_helper = Agordas la pasvorthaketigan algoritmon. Algoritmoj havas malsamajn postulojn kaj efikecojn. La algoritmo argon2 sufiĉe sekuras, sed postulas multan memoron kaj eble ne taŭgas por nepotencaj serviloj.
|
||||
password_algorithm_helper = Agordu la pasvorthaketigan algoritmon. Algoritmoj havas malsamajn postulojn kaj efikecojn. La algoritmo argon2 sufiĉe sekuras, sed postulas multan memoron kaj eble ne taŭgas por nepotencaj serviloj.
|
||||
internal_token_failed = Malsukcesis krei internan ĵetonon: %v
|
||||
smtp_from_invalid = La «Sendu retleterojn kiel» adreso malvalidas
|
||||
allow_only_external_registration = Permesi registriĝon nur per fremdaj servoj
|
||||
|
|
@ -398,7 +406,7 @@ openid_register_title = Krei novan konton
|
|||
email_domain_blacklisted = Vi ne povas registriĝi per via retpoŝtadreso.
|
||||
verify = Konfirmi
|
||||
oauth_signup_submit = Finfari konton
|
||||
prohibit_login_desc = Via konto estas suspendita kaj ne povas interagi kun la instanco. Bonvolu kontakti vian retejestron por regajni aliron.
|
||||
prohibit_login_desc = Via konto estas suspendita kaj ne povas interagi kun la instanco. Bonvolu kontakti la retejestron por regajni aliron.
|
||||
openid_connect_desc = La elektita OpenID URI estas nekonata. Ligu ĝin al nova konto ĉi tie.
|
||||
oauth.signin.error = Eraris traktante aprobpeton. Se plu eraros, bonvolu kunparoli la retejestron.
|
||||
invalid_code = Via konfirmkodo malvalidas aŭ eksdatiĝis.
|
||||
|
|
@ -428,7 +436,7 @@ hint_login = Ĉu vi jam havas konton? <a href="%s">Salutu nun!</a>
|
|||
hint_register = Ĉu vi bezonas konton? <a href="%s">Reĝistriĝi nun.</a>
|
||||
sign_up_button = Reĝistriĝi nun.
|
||||
sign_in_openid = Daŭrigi kun OpenID
|
||||
back_to_sign_in = Reen en la saluton
|
||||
back_to_sign_in = Reen en la ensaluton
|
||||
use_onetime_code = Uzi unufojan kodon
|
||||
|
||||
[mail]
|
||||
|
|
@ -484,7 +492,7 @@ password_change.text_1 = La pasvorto de via konto ĵus ŝanĝiĝis.
|
|||
primary_mail_change.subject = Via ĉefa retpoŝtadreso ŝanĝiĝis
|
||||
totp_disabled.text_1 = La tempobazita unufoja pasvorto (TOTP) en via konto ĵus malaktiviĝis.
|
||||
admin.new_user.text = Bonvolu <a href="%s">klaki ĉi tie</a> por konduki ĉi tiun uzanton el la administranta agordilo.
|
||||
removed_security_key.subject = Sekureca ŝlosilo estas forigita
|
||||
removed_security_key.subject = Sekurŝlosilo estas forigita
|
||||
removed_security_key.text_1 = La sekureca ŝlosilo "%[1]s" ĵus estas forigita de via konton.
|
||||
totp_enrolled.text_1.has_webauthn = Vi ĵus aktivigis TOTP-n por via konto. Tio volas diri ke por ĉiuj venontaj salutoj al via konto, vi povus uzi TOTP-n kiel 2FA metodo aŭ ajnan sekurecan ŝlosilon.
|
||||
totp_enrolled.text_1.no_webauthn = Vi ĵus aktivigis TOTP-n por via konto. Tio volas diri ke por ĉiuj venontaj salutoj al via konto, vi devos uzi TOTP-n kiel 2FA metodo.
|
||||
|
|
@ -492,6 +500,13 @@ removed_security_key.no_2fa = Ne estas aliaj 2FA agorditaj metodoj, tio estas ke
|
|||
totp_disabled.no_2fa = Ne estas plu aliaj 2FA agorditaj metodoj, tio estas ke ne plus necesas uzi 2FA-n por saluti.
|
||||
account_security_caution.text_1 = Se tio estis vi, vi povas sekure ignori ĉi tiun retmesaĝon.
|
||||
|
||||
account_security_caution.text_2 = Se ne estis vi, via konto estas kompromitata. Bonvolu kontakti la administrantojn de la retpaĝaro.
|
||||
totp_enrolled.subject = Vi aktivigis TOTP-n kiel 2FA metodo
|
||||
issue_assigned.pull = @%[1]s asignis al vi la tirpeton %[2]s en la deponejo %[3]s.
|
||||
issue_assigned.issue = @%[1]s asignis al vi ĉi tiun eraron %[2]s en la deponejo %[3]s.
|
||||
issue.action.review_dismissed = <b>@%[1]s</b> maldungis la lastan revizion de %[2]s por ĉi tiu tirpeto.
|
||||
repo.transfer.subject_to_you = %s volas reposedigi la deponejon "%s" al vi
|
||||
|
||||
[form]
|
||||
TeamName = Gruponomo
|
||||
RepoName = Deponejonomo
|
||||
|
|
@ -560,6 +575,16 @@ last_org_owner = Vi ne povas forigi la lastan uzanton de la «posendantoj» grup
|
|||
still_has_org = "Via konto anas de almenaŭ unu organizaĵoj, forlasu ilin unue."
|
||||
invalid_ssh_key = Ne povis konfirmi vian SSH-ŝlosilon: %s
|
||||
|
||||
FullName = Plena nomo
|
||||
Description = Priskribo
|
||||
Pronouns = Pronomoj
|
||||
Biography = Biografio
|
||||
Website = Retpaĝaro
|
||||
Location = Kieo
|
||||
To = Branĉonomo
|
||||
AccessToken = Atingoĵetono
|
||||
required_prefix = La enigaĵo devas komenciĝi per "%s"
|
||||
|
||||
[modal]
|
||||
confirm = Konfirmi
|
||||
no = Ne
|
||||
|
|
@ -728,6 +753,91 @@ permission_write = Lega kaj Skriba
|
|||
key_content = Enhavo
|
||||
key_signature_gpg_placeholder = Komenciĝas per «-----BEGIN PGP SIGNATURE-----»
|
||||
|
||||
storage_overview = Stokada superrigardo
|
||||
quota = Kvoto
|
||||
pronouns = Pronomoj
|
||||
pronouns_unspecified = Nespecifitaj
|
||||
change_username_redirect_prompt.with_cooldown.one = La malnova uzantnomo disponeblos al ĉiuj post atendoperiodo de %[1]d tago. Vi povas ankoraŭ reakiri la malnovan uzantnomon dum ĉi tiu periodo.
|
||||
change_username_redirect_prompt.with_cooldown.few = La malnova uzantnomo disponeblos al ĉiuj post atendoperiodo de %[1]d tagoj. Vi povas ankoraŭ reakiri la malnovan uzantnomon dum ĉi tiu periodo.
|
||||
language.title = Defaŭlta lingvo
|
||||
language.description = Ĉi tiu lingvo estos konservota en via konto kaj estos uzota kiel defaŭlta post kiam vi ensalutos.
|
||||
language.localization_project = Helpu nin traduki Forgejo-n en via lingvo! <a href="%s">Lerni plu</a>.
|
||||
hints = Sugestoj
|
||||
update_hints = Ĝisdatigi la sugestojn
|
||||
update_hints_success = La sugestoj ĝisdatiĝis.
|
||||
hidden_comment_types.ref_tooltip = Komentoj, kie ĉi tiu eraro estas referencita de alia eraro/enmeto/…
|
||||
comment_type_group_reference = Referenco
|
||||
comment_type_group_milestone = Celo
|
||||
comment_type_group_assignee = Asignito
|
||||
comment_type_group_lock = Rigli staton
|
||||
comment_type_group_review_request = Revizia peto
|
||||
comment_type_group_issue_ref = Referenco de la eraro
|
||||
keep_activity_private = Kaŝi la aktivecon de la profilpaĝo
|
||||
keep_activity_private.description = Via <a href="%s">publika aktiveco</a> nur videblos al vi kaj la instancaj administrantoj.
|
||||
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 =
|
||||
gpg_token_required = Vi devas disponigi signaturon por la malsupran ĵetono
|
||||
gpg_token = Ĵetono
|
||||
gpg_token_help = Vi povas generi signaturon uzante:
|
||||
ssh_token_required = Vi devas disponigi signaturon for la malsupran ĵetono
|
||||
ssh_token = Ĵetono
|
||||
ssh_token_help = Vi povas generi signaturon uzante:
|
||||
no_activity = Neniu ĵusa aktiveco
|
||||
token_state_desc = Ĉi tiu ĵetono estis uzata dum la 7 lastaj tagoj
|
||||
manage_access_token = Atingoĵetonoj
|
||||
generate_new_token = Generi novan ĵetonon
|
||||
token_name = Ĵetono-nomo
|
||||
generate_token = Generi ĵetonon
|
||||
generate_token_success = Via nova ĵetono estas generita. Kopiu ĝin nun, ĉar ĝi ne estos montrata ree.
|
||||
access_token_deletion = Forigi atingoĵetonon
|
||||
delete_token_success = La ĵetono estas forigita. Aplikaĵoj uzantaj ĝin ne atingos plu vian konton.
|
||||
regenerate_token = Regeneri
|
||||
access_token_regeneration = Regeneri atingoĵetonon
|
||||
at_least_one_permission = Vi devas selekti almenaŭ unu permeson por krei ĵetonon
|
||||
remove_oauth2_application = Forigi OAuth2-aplikaĵon
|
||||
remove_oauth2_application_success = La aplikaĵo estas forigita.
|
||||
create_oauth2_application = Krei novan OAuth2-aplikaĵon
|
||||
create_oauth2_application_button = Krei aplikaĵon
|
||||
create_oauth2_application_success = Vi sukcese kreis novan OAuth2 aplikaĵon.
|
||||
update_oauth2_application_success = Vi sukcese ĝisdatigis la OAuth2 aplikaĵon.
|
||||
oauth2_application_name = Aplikaĵonomo
|
||||
save_application = Konservi
|
||||
oauth2_client_id = Klienta ID
|
||||
oauth2_client_secret = Klienta sekreto
|
||||
oauth2_regenerate_secret = Regeneri sekreton
|
||||
oauth2_regenerate_secret_hint = Ĉu vi perdis vian sekreton?
|
||||
oauth2_application_edit = Redakti
|
||||
authorized_oauth2_applications = Permesitaj OAuth2-aplikaĵoj
|
||||
visibility = Uzanta videbleco
|
||||
visibility.public = Publika
|
||||
visibility.public_tooltip = Videbla al ĉiuj
|
||||
visibility.limited = Limigata
|
||||
visibility.limited_tooltip = Videbla nur al ensalutitaj uzantoj
|
||||
visibility.private = Privata
|
||||
visibility.private_tooltip = Videbla nur al membroj de organizaĵoj, kiujn vi aliĝis
|
||||
blocked_since = Blokata ekde %s
|
||||
user_unblock_success = La uzanto sukcese estas malblokita.
|
||||
user_block_success = La uzanto sukcese estas blokita.
|
||||
user_block_yourself = Vi ne povas bloki vin mem.
|
||||
quota.applies_to_user = La sekvantaj kvotaj reguloj aplikiĝas al via konto
|
||||
quota.applies_to_org = La sekvantaj kvotaj reguloj aplikiĝas al ĉi tiu organizaĵo
|
||||
quota.rule.exceeded = Superita
|
||||
quota.sizes.all = Ĉio
|
||||
quota.sizes.repos.all = Deponejoj
|
||||
quota.sizes.repos.public = Publikaj deponejoj
|
||||
quota.sizes.repos.private = Privataj deponejoj
|
||||
quota.sizes.git.all = Git-enhavo
|
||||
quota.sizes.git.lfs = Git LFS
|
||||
quota.sizes.assets.attachments.all = Kunsendaĵoj
|
||||
quota.sizes.assets.attachments.issues = Kunsendaĵoj de eraro
|
||||
quota.sizes.assets.attachments.releases = Kunsendaĵoj de eldono
|
||||
quota.sizes.assets.artifacts = Artfaritaĵoj
|
||||
quota.sizes.assets.packages.all = Pakaĵoj
|
||||
quota.sizes.wiki = Vikio
|
||||
|
||||
[user]
|
||||
form.name_reserved = La uzantonomo «%s» estas protektita.
|
||||
joined_on = Aliĝis je %s
|
||||
|
|
@ -756,6 +866,22 @@ block_user = Bloki uzanton
|
|||
change_avatar = Ŝanĝi vian profilbildon…
|
||||
|
||||
|
||||
activity = Publika aktiveco
|
||||
followers.title.one = Sekvanto
|
||||
followers.title.few = Sekvantoj
|
||||
following.title.one = Sekvata
|
||||
following.title.few = Sekvataj
|
||||
followers_one = %d sekvanto
|
||||
following_one = %d sekvataj
|
||||
overview = Superrigardo
|
||||
disabled_public_activity = Ĉi tiu uzanto malaktivigis la publikan videblecon de sia aktiveco.
|
||||
public_activity.visibility_hint.self_public = Via aktiveco videblas al ĉiuj, escepte de interagoj en privataj spacoj. <a href="%s">Agordi</a>.
|
||||
public_activity.visibility_hint.admin_public = Ĉi tiu aktiveco videblas al ĉiuj, sed kiel administranto vi povas ankaŭ vidi interagojn en privataj spacoj.
|
||||
public_activity.visibility_hint.self_private = Via aktiveco nur videblas al vi kaj la instancaj administrantoj. <a href="%s">Agordi</a>.
|
||||
public_activity.visibility_hint.admin_private = Ĉi tiu aktiveco videblas al vi ĉar vi estas administranto, sed la uzanto volas ke ĝi restu privata.
|
||||
public_activity.visibility_hint.self_private_profile = Via aktiveco nur videblas al vi kaj la instancaj administrantoj, ĉar via profilo estas privata. <a href="%s">Agordi</a>.
|
||||
form.name_pattern_not_allowed = La ŝablono "%s" ne estas permesata en uzantnomo.
|
||||
|
||||
[repo]
|
||||
editor.add_file = Aldoni dosieron
|
||||
settings.tags = Etikedoj
|
||||
|
|
@ -803,6 +929,8 @@ settings.archive.text = Arĥivigi la deponejon igus ĝin sole legebla. Ĝi kaŝi
|
|||
migrate_items_releases = Eldonoj
|
||||
commits.commits = Enmetoj
|
||||
|
||||
rss.must_be_on_branch = Vi devas esti en branĉo por havi RSS-fluon.
|
||||
|
||||
[org]
|
||||
code = Fontkodo
|
||||
settings = Agordoj
|
||||
|
|
@ -838,3 +966,26 @@ fuzzy = Svaga
|
|||
branch_kind = Serĉi disbranĉigojn…
|
||||
runner_kind = Serĉi rulantojn…
|
||||
pull_kind = Serĉi tirpetojn…
|
||||
|
||||
[actions]
|
||||
variables.creation.success = La variablo "%s" estas aldonita.
|
||||
variables.update.failed = Ne eblas redakti la variablon.
|
||||
variables.update.success = La variablo estas redaktita.
|
||||
|
||||
[projects]
|
||||
deleted.display_name = Forigita projekto
|
||||
type-1.display_name = Persona projekto
|
||||
type-2.display_name = Deponejoprojekto
|
||||
|
||||
[git.filemode]
|
||||
changed_filemode = %[1]s → %[2]s
|
||||
directory = Dosierujo
|
||||
normal_file = Normala dosiero
|
||||
executable_file = Rulebla dosiero
|
||||
symbolic_link = Simbola ligilo
|
||||
submodule = Submodulo
|
||||
|
||||
[markup]
|
||||
filepreview.line = Linio %[1]d en %[2]s
|
||||
filepreview.lines = Linioj %[1]d ĝis %[2]d en %[3]s
|
||||
filepreview.truncated = La superrigardo estas mallongigita
|
||||
|
|
@ -1 +1,2 @@
|
|||
[common]
|
||||
home = Etxea
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ webauthn_use_twofa=Käytä kaksivaihesta todennusta puhelimestasi
|
|||
webauthn_error=Turva-avainta ei voitu lukea.
|
||||
webauthn_unsupported_browser=Selaimesi ei tällä hetkellä tue WebAuthnia.
|
||||
webauthn_error_unknown=Tuntematon virhe. Yritä uudelleen.
|
||||
webauthn_error_insecure=WebAuthn tukee vain suojattuja yhteyksiä. Testatessa HTTP-yhteydellä voit käyttää osoitetta "localhost" tai "127.0.0.1"
|
||||
webauthn_error_insecure=WebAuthn tukee vain turvallisia yhteyksiä. Testaamista varten HTTP-välityksellä voit käyttää alkuperää "localhost" tai "127.0.0.1"
|
||||
webauthn_error_unable_to_process=Palvelin ei pystynyt käsittelemään pyyntöä.
|
||||
webauthn_error_duplicated=Turva-avainta ei ole sallittu tässä pyynnössä. Varmista, ettei avainta ole jo rekisteröity.
|
||||
webauthn_error_empty=Sinun täytyy asettaa nimi tälle avaimelle.
|
||||
|
|
@ -848,7 +848,7 @@ twofa_enroll=Ota kaksivaiheinen todennus käyttöön
|
|||
twofa_disabled=Kaksivaiheinen todennus on otettu pois käytöstä.
|
||||
scan_this_image=Skannaa tämä kuva todennussovelluksellasi:
|
||||
or_enter_secret=Tai kirjoita salainen avain: %s
|
||||
twofa_enrolled=Tiliisi on otettu käyttöön kaksivaiheinen todennus. Ota kertakäyttöinen palautusavain (%s) talteen turvalliseen paikkaan, sillä se näytetään vain kerran!
|
||||
twofa_enrolled=Tilisi on otettu mukaan onnistuneesti. Säilytä kertakäyttöistä palautusavainta (%s) turvallisessa paikassa, sillä sitä ei enää näytetä.
|
||||
|
||||
webauthn_nickname=Nimimerkki
|
||||
|
||||
|
|
@ -1013,6 +1013,8 @@ ssh_principal_deletion_success = Prinsipaali on poistettu.
|
|||
principal_state_desc = Tätä prinsipaalia on käytetty viimeisen seitsemän päivän aikana
|
||||
regenerate_token_success = Poletti on luotu uudelleen. Sovellukset, jotka käyttivät polettia, eivät enää pääse tilillesi. Kyseiset sovellukset tulee päivittää uudella poletilla.
|
||||
|
||||
quota.sizes.assets.all = Resurssit
|
||||
|
||||
[repo]
|
||||
owner=Omistaja
|
||||
owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska tietovarastojen enimmäismäärää on rajoitettu.
|
||||
|
|
@ -1072,7 +1074,7 @@ migrate_items_pullrequests=Vetopyynnöt
|
|||
migrate_items_releases=Julkaisut
|
||||
migrate_repo=Suorita tietovaraston migraatio
|
||||
migrate.clone_address=Migraatio/kloonaus URL-osoitteesta
|
||||
migrate.github_token_desc=Voit laittaa yhden tai useamman pääsypoletin pilkulla erotellen tähän nopeuttaaksesi migraatiota GitHubin rajapinnan tahtirajojen takia. VAROITUS: Tämän ominaisuuden väärinkäyttö voi rikkoa palveluntarjoajan ehtoja ja johtaa tilin estämiseen.
|
||||
migrate.github_token_desc=Voit lisätä tähän yhden tai useamman tunnuksen pilkuilla erotettuna nopeuttaaksesi migraatiota kiertämällä GitHub API:n nopeusrajoituksen. Varoitus: Tämän ominaisuuden väärinkäyttö voi rikkoa palveluntarjoajan käytäntöä ja johtaa tiliesi sulkemiseen.
|
||||
migrate.permission_denied=Paikallisten tietovarastojen tuominen ei ole sallittua.
|
||||
migrate.failed=Migraatio epäonnistui: %v
|
||||
migrate.migrate_items_options=Lisäkohteiden migraatiota varten vaaditaan pääsypoletti
|
||||
|
|
@ -1081,7 +1083,7 @@ migrate.migrating_failed=Migraatio lähteestä <b>%s</b> epäonnistui.
|
|||
migrate.migrating_git=Suoritetaan Git-datan migraatiota
|
||||
|
||||
mirror_from=peili kohteelle
|
||||
forked_from=forkattu lähteestä
|
||||
forked_from=forkattu tietovarastosta
|
||||
unwatch=Lopeta tarkkailu
|
||||
watch=Tarkkaile
|
||||
unstar=Poista tähti
|
||||
|
|
@ -1117,7 +1119,7 @@ file_permalink=Pysyväislinkki
|
|||
|
||||
video_not_supported_in_browser=Selaimesi ei tue HTML5:n video-tagia.
|
||||
audio_not_supported_in_browser=Selaimesi ei tue HTML5:n audio-tagia.
|
||||
blame=Blame
|
||||
blame=Syyllistynyt
|
||||
download_file=Lataa tiedosto
|
||||
normal_view=Normaali näkymä
|
||||
line=rivi
|
||||
|
|
@ -1180,7 +1182,7 @@ projects.open=Avaa
|
|||
projects.close=Sulje
|
||||
|
||||
issues.desc=Ongelmien, tehtävien ja merkkipaalujen hallinta.
|
||||
issues.filter_assignees=Suodata käyttäjiä
|
||||
issues.filter_assignees=Suodata vastuuhenkilö
|
||||
issues.filter_milestones=Suodata merkkipaalu
|
||||
issues.new=Uusi ongelma
|
||||
issues.new.labels=Nimilaput
|
||||
|
|
@ -1215,7 +1217,7 @@ issues.self_assign_at=`itse otti tämän käsittelyyn %s`
|
|||
issues.change_title_at=`muutti otsikon <b><strike>%s</strike></b> otsikoksi <b>%s</b> %s`
|
||||
issues.delete_branch_at=`poisti haaran <b>%s</b> %s`
|
||||
issues.filter_label=Nimilappu
|
||||
issues.filter_label_exclude=`Käytä <code>alt</code> + <code>napsautus/rivinvaihto</code> poissulkeaksesi nimilappuja`
|
||||
issues.filter_label_exclude=Käytä <kbd>Alt</kbd> + <kbd>Click</kbd>-näppäinyhdistelmää nimiöiden poissulkemiseksi
|
||||
issues.filter_label_no_select=Kaikki nimilaput
|
||||
issues.filter_milestone=Merkkipaalu
|
||||
issues.filter_project=Projekti
|
||||
|
|
@ -1264,9 +1266,9 @@ issues.close_comment_issue=Kommentoi ja sulje
|
|||
issues.reopen_issue=Avaa uudelleen
|
||||
issues.reopen_comment_issue=Kommentoi ja avaa uudelleen
|
||||
issues.create_comment=Kommentoi
|
||||
issues.closed_at=`sulki tämän ongelman %s`
|
||||
issues.reopened_at=`uudelleenavasi tämän ongelman %s`
|
||||
issues.commit_ref_at=`viittasi tähän ongelmaan kommitissa %s`
|
||||
issues.closed_at=`sulki tämän tukipyynnön %s`
|
||||
issues.reopened_at=`avasi tämän tukipyynnön uudelleen %s`
|
||||
issues.commit_ref_at=`viittasi tähän tukipyyntöön sitoumuksesta %s`
|
||||
issues.author=Tekijä
|
||||
issues.role.owner=Omistaja
|
||||
issues.role.member=Jäsen
|
||||
|
|
@ -1547,7 +1549,7 @@ settings.web_hook_name_larksuite_only =Lark Suite
|
|||
settings.web_hook_name_packagist=Packagist
|
||||
settings.deploy_keys=Toimitusavaimet
|
||||
settings.add_deploy_key=Lisää toimitusavain
|
||||
settings.deploy_key_desc=Toimitusavaimilla on pelkkä lukuoikeus tietovarastoon.
|
||||
settings.deploy_key_desc=Käyttöönottoavaimilla voi olla vain luku- tai luku-kirjoitusoikeudet tietovarastoon.
|
||||
settings.is_writable_info=Salli tämän toimitusavaimen <strong>työntää</strong> tietovarastoon.
|
||||
settings.no_deploy_keys=Toimitusavaimia ei ole käytössä vielä.
|
||||
settings.title=Otsikko
|
||||
|
|
@ -1586,7 +1588,7 @@ settings.archive.header=Arkistoi tämä tietovarasto
|
|||
settings.archive.tagsettings_unavailable=Tagi-asetukset eivät ole käytettävissä arkistoiduissa tietovarastoissa.
|
||||
settings.lfs=LFS
|
||||
settings.lfs_filelist=Tähän tietovarastoon tallennetut LFS-tiedostot
|
||||
settings.lfs_no_lfs_files=LFS-tiedostoja ei ole tallennettu tähän tietovarastoon.
|
||||
settings.lfs_no_lfs_files=Tähän tietovarastoon ei ole tallennettu LFS-tiedostoja
|
||||
settings.lfs_findcommits=Etsi kommitteja
|
||||
settings.lfs_lfs_file_no_commits=Tälle LFS-tiedostolle ei löytynyt kommitteja
|
||||
settings.lfs_noattribute=Tällä polulla ei ole lukittavaa attribuuttia oletushaarassa
|
||||
|
|
@ -2148,7 +2150,7 @@ issues.add_label = lisäsi nimilapun %s %s
|
|||
issues.due_date_added = lisäsi eräpäivän %s %s
|
||||
issues.review.add_review_request = pyysi katselmointia käyttäjältä %[1]s %[2]s
|
||||
issues.ref_pull_from = `<a href="%[2]s">viittasi tähän vetopyyntöön %[3]s</a> %[1]s`
|
||||
pulls.commit_ref_at = `viittasi tähän vetopyyntöön kommitista %s`
|
||||
pulls.commit_ref_at = `viittasi tähän vetopyyntöön sitoumuksesta %s`
|
||||
issues.review.comment = katselmoi %s
|
||||
issues.add_labels = lisäsi nimilaput %s %s
|
||||
issues.review.add_review_requests = pyysi katselmointeja käyttäjiltä %[1]s %[2]s
|
||||
|
|
@ -2329,9 +2331,9 @@ wiki.page_name_desc = Kirjoita tämän wikisivun nimi. Joitain erikoisnimiä ova
|
|||
pulls.blocked_by_changed_protected_files_1 = Tämä vetopyyntö sisältää suojatun tiedoston ja on siksi estetty:
|
||||
pulls.status_checks_warning = Jotkin tarkistukset raportoivat varoituksia
|
||||
pulls.status_checks_error = Jotkin tarkistukset raportoivat virheitä
|
||||
pulls.reopened_at = `avasi uudelleen tämän vetopyynnön %s`
|
||||
pulls.reopened_at = `avasi tämän vetopyynnön uudelleen %s`
|
||||
pulls.auto_merge_when_succeed = Yhdistä automaatisesti kun kaikki tarkistukset onnistuvat
|
||||
signing.wont_sign.error = Tapahtui virhe tarkistaessa voiko kommitin allekirjoittaa.
|
||||
signing.wont_sign.error = Tapahtui virhe tarkistettaessa, voiko sitoumus allekirjoittaa.
|
||||
signing.wont_sign.twofa = Sinulla tulee olla kaksivaiheinen todennus käytössä, jotta kommitit voi allekirjoittaa.
|
||||
pulls.data_broken = Tämä vetopyyntö on rikki johtuen puuttuvasta forkkitiedosta.
|
||||
pulls.files_conflicted = Tämä vetopyyntö sisältää muutoksia, jotka ovat ristiriidassa kohdehaaran kanssa.
|
||||
|
|
@ -3051,7 +3053,7 @@ auths.attribute_username_placeholder = Jätä tyhjäksi käyttääksesi Forgejo:
|
|||
auths.oauth2_authURL = Valtuutuksen URL-osoite
|
||||
auths.new_success = Todennus "%s" on lisätty.
|
||||
users.still_own_repo = Tämä käyttäjä omistaa yhden tai useamman tietovaraston. Poista tai siirrä nämä tietovarastot ensin.
|
||||
dashboard.cleanup_hook_task_table = Siivoa hook_task-taulu
|
||||
dashboard.cleanup_hook_task_table = Siivoa koukku -_tehtävätaulukko
|
||||
dashboard.delete_old_actions = Poista kaikki vanhat aktiviteetit tietokannasta
|
||||
auths.attribute_mail = Sähköpostiosoitteen attribuutti
|
||||
auths.attribute_ssh_public_key = Julkisen SSH-avaimen attribuutti
|
||||
|
|
@ -3119,7 +3121,7 @@ dashboard.resync_all_sshprincipals = Päivitä ".ssh/authorized_principals"-tied
|
|||
create_repo=loi tietovaraston <a href="%s">%s</a>
|
||||
rename_repo=asetti tietovaraston <code>%[1]s</code> uudeksi nimeksi <a href="%[2]s">%[3]s</a>
|
||||
transfer_repo=siirsi tietovaraston <code>%s</code> käyttäjälle <a href="%s">%s</a>
|
||||
push_tag=työnsi tagin <a href="%[2]s">%[3]s</a> kohteeseen <a href="%[1]s">%[4]s</a>
|
||||
push_tag=työnsi tagin <a href="%[2]s">%[3]s</a> tietovarastoon <a href="%[1]s">%[4]s</a>
|
||||
delete_tag=poisti tagin %[2]s kohteesta <a href="%[1]s">%[3]s</a>
|
||||
compare_commits_general=Vertaa kommitteja
|
||||
create_branch=loi haaran <a href="%[2]s">%[3]s</a> tietovarastossa <a href="%[1]s">%[4]s</a>
|
||||
|
|
@ -3143,6 +3145,8 @@ approve_pull_request = `hyväksyi <a href="%[1]s">%[3]s#%[2]s</a>`
|
|||
starred_repo = lisäsi tähden tietovarastolle <a href="%[1]s">%[2]s</a>
|
||||
reject_pull_request = `ehdotti muutoksia kohteeseen <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
|
||||
publish_release = `julkaisi <a href="%[2]s">%[4]s</a> tietovarastossa <a href="%[1]s">%[3]s</a>`
|
||||
|
||||
[tool]
|
||||
now=nyt
|
||||
1s=1 sekunti
|
||||
|
|
@ -3233,7 +3237,7 @@ helm.install = Asenna paketti komennolla:
|
|||
owner.settings.chef.keypair = Luo avainpari
|
||||
settings.delete.error = Paketin poistaminen epäonnistui.
|
||||
requirements = Vaatimukset
|
||||
published_by_in = Julkaistu %[1]s, julkaisija <a href="%[2]s">%[3]s</a> projektissa <a href="%[4]s"><strong>%[5]s</strong></a>
|
||||
published_by_in = Julkaistu %[1]s, julkaisija <a href="%[2]s">%[3]s</a> tietovarastossa <a href="%[4]s"><strong>%[5]s</strong></a>
|
||||
pypi.requires = Vaatii Pythonin
|
||||
alpine.install = Asenna paketti seuraavalla komennolla:
|
||||
debian.repository.components = Komponentit
|
||||
|
|
@ -3356,6 +3360,8 @@ conda.registry = Määritä tämä rekisteri Conda-tietovarastoksi <code>.condar
|
|||
container.labels = Nimilaput
|
||||
settings.link.description = Jos linkität paketin tietovarastoon, paketti listataan tietovaraston pakettilistalla.
|
||||
|
||||
assets = Resurssit
|
||||
|
||||
[secrets]
|
||||
creation.failed = Salaisuuden lisääminen epäonnistui.
|
||||
deletion = Poista salaisuus
|
||||
|
|
@ -3523,4 +3529,4 @@ wiki.write = <b>Kirjoita:</b> Luo, päivitä ja poista integroidun wikin sivuja.
|
|||
filepreview.truncated = Esikatselu on typistetty
|
||||
|
||||
[translation_meta]
|
||||
test = This is a test string. It is not displayed in Forgejo UI but is used for testing purposes. Feel free to enter "ok" to save time (or a fun fact of your choice) to hit that sweet 100% completion mark :) :) :)
|
||||
test = Tämä on testimerkkijono. Sitä ei näytetä Forgejo-käyttöliittymässä, mutta sitä käytetään testaustarkoituksiin. Voit vapaasti kirjoittaa "selvä" säästääksesi aikaa (tai käyttää hauskaa faktaa) ja saavuttaaksesi sen makean 100 %:n valmistumisrajan :)
|
||||
|
|
@ -225,3 +225,98 @@ admin_email = ईमेल एड्रेस
|
|||
config_location_hint = ये सञ्चालन के तरीके यहाँ सेव होंगे :
|
||||
install_btn_confirm = इनस्टॉल फॉरगेजो
|
||||
test_git_failed = git कमांड टेस्ट नहीं हुई: %v
|
||||
sqlite3_not_available = इस फॉरगेजो के वर्शन में SQLite३ नहीं है। कृपया डाउनलोड करें बाइनरी वर्शन यहाँ से %s (“gobuild” वर्शन नहीं करें)।
|
||||
invalid_db_setting = ये डेटाबेस सेटिंग्स मान्य नहीं हैं %v
|
||||
invalid_db_table = डेटाबेस टेबल “%s” मान्य नहीं: %v
|
||||
invalid_repo_path = इस रिपॉजिटरी की मूल पथ मान्य नहीं है: %v
|
||||
invalid_app_data_path = एप्प की डाटा पथ अमान्य है: %v
|
||||
run_user_not_match = “यूजर को ऐसे चलाएं” उसरनाम अभी का उसरनाम नहीं है: %s -> %s
|
||||
internal_token_failed = अंदरूनी टोकन बना नहीं पाए: %v
|
||||
secret_key_failed = गुप्त चाभी बना नहीं पाए: %v
|
||||
save_config_failed = संरूपण को सेव नहीं कर पाए: %v
|
||||
enable_update_checker_helper_forgejo = ये समय-समय पर चेक करेगा फॉरगेजो वर्शन नया है की नहीं TXT DNS रिकॉर्ड से यहाँ release.forgejo.org।
|
||||
invalid_admin_setting = प्रशासक अकाउंट सेटिंग अमान्य है: %v
|
||||
invalid_log_root_path = लॉग का रास्ता अमान्य है: %v
|
||||
no_reply_address = गुप्त ईमेल डोमेन
|
||||
no_reply_address_helper = डोमेन का नाम उसेर्स के गुप्त ईमेल पते से हैं। जैसे की, जब यूजर “जोई” लॉग करेगा Git पे “जोई@noreply.example.org” अगर गुप्त ईमेल पता पड़ा है “noreply.example.org”।
|
||||
password_algorithm = पासवर्ड का हैश कलन विधि (algorithm)
|
||||
invalid_password_algorithm = अमान्य पासवर्ड का हैश कलन विधि (algorithm)
|
||||
password_algorithm_helper = पासवर्ड हैश अल्गोरिथम सेट करें। अल्गोरिथ्म्स की अलग-अलग ज़रूरतें और मज़बूतियाँ हैं। आर्गन2 अल्गोरिथम मज़बूत है पर मेमोरी ज़्यादा लेता है और छोटे सिस्टम्स के लिए सही नहीं होता।
|
||||
enable_update_checker = अपडेट चेकर चालू करें
|
||||
env_config_keys = पर्यावरण संरूपण
|
||||
env_config_keys_prompt = पर्यावरण वेरिएबल्स को संरूपण फाइल पर भी लागू किया जायेगा:
|
||||
|
||||
[home]
|
||||
uname_holder = उसरनाम या ईमेल एड्रेस
|
||||
switch_dashboard_context = डैशबोर्ड का सन्दर्भ बदलें
|
||||
my_repos = रेपोसिटोरिएस
|
||||
my_orgs = संस्थाएं
|
||||
view_home = व्यू: %s
|
||||
filter = बाकी फिल्टर्स
|
||||
filter_by_team_repositories = फ़िल्टर करें टीम रेपोसिट्रीज़ से
|
||||
feed_of = फीड इसकी "%s"
|
||||
show_archived = संग्रहीत
|
||||
show_both_archived_unarchived = संग्रहीत और असंग्रहीत देखिए
|
||||
show_only_archived = सिर्फ संग्रहीत देखिए
|
||||
show_only_unarchived = सिर्फ असंग्रहीत देखिए
|
||||
show_private = निजी
|
||||
show_both_private_public = निजी और सार्वजनिक देखिए
|
||||
show_only_private = सिर्फ निजी देखिए
|
||||
show_only_public = सिर्फ सार्वजनिक देखिए
|
||||
issues.in_your_repos = आपकी रेपोसिटोरिस में
|
||||
|
||||
[explore]
|
||||
repos = रेपोसिटोरिस
|
||||
users = उसेर्स
|
||||
stars_one = %d सितारा
|
||||
stars_few = %d सितारे
|
||||
forks_one = %d फोर्क
|
||||
forks_few = %d फोर्कस
|
||||
organizations = संस्थाएं
|
||||
go_to = जाएं इधर
|
||||
code = कोड
|
||||
code_last_indexed_at = आखरी संस्थापित %s
|
||||
relevant_repositories_tooltip = रेपोसिटोरिस जो की फोर्क्स या जिनका शीर्षक नहीं है, आइकॉन नहीं और विश्लेषण नहीं गुप्त हैं।
|
||||
relevant_repositories = सिर्फ इस काम की रेपोस्टिरिस दिखाई जा रहीं हैं. <a href="%s">बिना फ़िल्टर के रिजल्ट दिखाएं</a>।
|
||||
|
||||
[auth]
|
||||
create_new_account = अकाउंट रजिस्टर करें
|
||||
disable_register_prompt = रजिस्ट्रेशन खुला नहीं। कृपया साइट प्रशासक से बात करें।
|
||||
disable_register_mail = ईमेल से रजिस्ट्रेशन पक्का करना बंद हैं अभी।
|
||||
manual_activation_only = अपने साइट प्रशासक बात करें एक्टिवेशन पूरा करने के लिए।
|
||||
remember_me = इस डिवाइस को याद रखें
|
||||
forgot_password_title = पासवर्ड भूल गए
|
||||
forgot_password = पासवर्ड भूल गए?
|
||||
hint_login = पहले से अकाउंट है? <a href="%s">अभी सिग्न इन करें!</a>
|
||||
hint_register = अकाउंट चाहिए? <a href="%s">रजिस्टर करें</a>
|
||||
sign_up_button = अभी रजिस्टर करें।
|
||||
sign_up_successful = अकाउंट बन गया है। स्वागत!
|
||||
back_to_sign_in = sign in में फिर से जाएं
|
||||
sign_in_openid = OpenID के साथ आगे बढ़ें
|
||||
|
||||
[mail]
|
||||
link_not_working_do_paste = क्या कड़ी चल नहीं पाई? उसे कॉपी करके URL बार में जोड़ें।
|
||||
hi_user_x = नमस्ते <b>%s</b>,
|
||||
activate_account = कृपया अपना खाता चालू करें
|
||||
activate_account.text_1 = नमस्ते <b>%[1]s</b>, संपादित करने के लिए शुक्रिया यहां %[2]s!
|
||||
activate_account.text_2 = कृपया यहां की कड़ी को क्लिक करें अपने अकाउंट को चालू करने के लिए <b>%s</b>:
|
||||
activate_email = अपना ईमेल एड्रेस पुख्ता करें
|
||||
activate_email.text = कृपया इस कड़ी को क्लिक करें अपना अकाउंट पुख्ता करने के लिए <b>%s</b>:
|
||||
admin.new_user.subject = नया यूजर %s अभी sign up हुआ
|
||||
admin.new_user.user_info = यूजर की जानकारी
|
||||
admin.new_user.text = कृपया <a href="%s"> क्लिक करें यहाँ </a> अपने यूजर को प्रशासक पैनल से चलाने के लिए।
|
||||
register_notify = स्वागत है %s
|
||||
register_notify.text_1 = ये आपका रजिस्टर्ड ईमेल पुख्ता करने के लिए है %s!
|
||||
register_notify.text_2 = आप अपने अकाउंट में हस्ताक्षर कर सकते हैं इस उसर-नाम के साथ: %s
|
||||
register_notify.text_3 = अगर किसी और ने आपका अकाउंट बनाया है, तो आपको चाहिए <a href="%s"> पासवर्ड संरक्षित करें </a> पहले।
|
||||
reset_password = अपना अकाउंट निकाल पाएं
|
||||
reset_password.text = अगर ये आप थे, कृपया इस कड़ी को क्लिक करें अपने अकाउंट को फिर से चालू करने को <b>%s</b>:
|
||||
password_change.subject = आपका पासवर्ड बदला गया
|
||||
password_change.text_1 = आपके अकाउंट का पासवर्ड बदला गया है।
|
||||
primary_mail_change.subject = आपका प्राथमिक ईमेल बदला गया है
|
||||
primary_mail_change.text_1 = आपका प्राथमिक ईमेल बदला गया है इससे %[1]s. यानी इस ईमेल एड्रेस से आप ईमेल अधिसूचना प्राप्त नहीं कर पाएंगे।
|
||||
totp_disabled.subject = टीओटीपी रोक दिया गया
|
||||
totp_disabled.text_1 = समय निर्धारित एक बार के पासवर्ड (टीओटीपी) आपके अकाउंट पर रोक दिया गया।
|
||||
totp_disabled.no_2fa = और कोई 2FA के तरीके संचालित नहीं हैं, यानी आपके अकाउंट पर 2FA से लॉगिन की ज़रुरत नहीं होगी।
|
||||
removed_security_key.subject = सुरक्षक चाभी हटाई गई
|
||||
removed_security_key.text_1 = सुरक्षक चाभी "%[1]s" अभी-अभी आपके अकाउंट से हटाई गई।
|
||||
|
|
@ -786,7 +786,7 @@ add_key=Aggiungi chiave
|
|||
ssh_desc=Queste chiavi SSH pubbliche sono associate al tuo profilo. Le corrispondenti chiavi private consentono l'accesso completo ai tuoi progetti. Le chiavi SSH che sono state verificate possono essere usate per verificare commit Git firmati tramite SSH.
|
||||
principal_desc=Queste entità di certificato SSH sono associate al tuo profilo e forniscono l'accesso completo ai tuoi repositori.
|
||||
gpg_desc=Queste chiavi GPG pubbliche sono associate con il tuo profilo e sono usate per verificare i tuoi commit. Proteggi le tue chiavi private perché permettono di firmare i commit con la tua identità.
|
||||
ssh_helper=<strong> Hai bisogno di aiuto?</strong> Dài un'occhiata alla guida per<a href="%s">creare le tue chiavi SSH </a> o risolvere quei <a href="%s"> problemi comuni </a> in cui potresti imbatterti utilizzando SSH.
|
||||
ssh_helper=<strong> Hai bisogno di aiuto?</strong> Dai un'occhiata alla guida per <a href="%s">creare le tue chiavi SSH </a> o risolvere quei <a href="%s"> problemi comuni </a> in cui potresti imbatterti utilizzando SSH.
|
||||
gpg_helper=<strong>Hai bisogno di aiuto?</strong> Dai un'occhiata alla guida di GitHub <a href="%s">riguardo il GPG</a>.
|
||||
key_content_ssh_placeholder=Inizia con "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "sk-ecdsa-sha2-nistp256@openssh.com", o "sk-ssh-ed25519@openssh.com"
|
||||
key_content_gpg_placeholder=Inizia con "-----BEGIN PGP PUBLIC KEY BLOCK-----"
|
||||
|
|
@ -2845,7 +2845,7 @@ members.membership_visibility=Visibilità appartenenza:
|
|||
members.public=Visibile
|
||||
members.public_helper=nascondi
|
||||
members.private=Nascosto
|
||||
members.private_helper=rendi visibile
|
||||
members.private_helper=Rendi visibile
|
||||
members.member_role=Ruolo del membro:
|
||||
members.owner=Proprietario
|
||||
members.member=Membro
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
|
||||
|
||||
[common]
|
||||
home = Agejdan
|
||||
username = Isem n useqdac
|
||||
|
|
@ -43,7 +40,7 @@ captcha = CAPČA
|
|||
passcode = Angal n wadduf
|
||||
settings = Iɣewwaren
|
||||
your_profile = Amaɣnu
|
||||
your_settings = Iɣewwaren
|
||||
your_settings = Tawila
|
||||
activities = Irmad
|
||||
ok = Ih
|
||||
view = Wali
|
||||
|
|
@ -56,6 +53,19 @@ concept_system_global = Amatu
|
|||
concept_user_organization = Tuddsa
|
||||
filter = Imsizdeg
|
||||
|
||||
your_starred = S yitran
|
||||
new_repo.title = Akufi amaynut
|
||||
new_repo.link = Akufi amaynut
|
||||
issues = Uguren
|
||||
preview = Taskant
|
||||
loading = Aɛebbi…
|
||||
|
||||
sign_in = Qqen
|
||||
sign_out = Senser tuqqna
|
||||
link_account = Qqen amiḍan
|
||||
email = Tansa imayl
|
||||
access_token = Ajuṭu n unekcum
|
||||
|
||||
[user]
|
||||
code = Tangalt
|
||||
repositories = Ikufan
|
||||
|
|
@ -69,9 +79,20 @@ following.title.one = Yeṭṭafaṛ
|
|||
following.title.few = Yeṭṭafaṛ
|
||||
unfollow = Ur ṭṭafar ara
|
||||
|
||||
overview = Tamuɣli s umata
|
||||
|
||||
[org]
|
||||
code = Tanglat
|
||||
|
||||
org_desc = Aglam
|
||||
team_desc = Aglam
|
||||
|
||||
lower_repositories = Ikufan
|
||||
settings.options = Tuddsa
|
||||
settings.website = Asmel web
|
||||
members.remove = Kkes
|
||||
teams.settings = Iɣewwaren
|
||||
|
||||
[repo]
|
||||
release.source_code = Tangalt taɣbalut
|
||||
settings.slack_username = Isem n useqdac
|
||||
|
|
@ -125,6 +146,127 @@ projects.title = Azwel
|
|||
projects.type.none = Ula yiwen
|
||||
projects.template.desc = Tamudemt
|
||||
|
||||
repo_name = Isem n ukufi
|
||||
repo_size = Tiddi n ukufi
|
||||
create_repo = Snulfu-d akufi
|
||||
mirror_sync = yemtawa
|
||||
migrate_items_issues = Uguren
|
||||
star = Rnu-yas itri
|
||||
no_desc = Ur yesɛi ara aglam
|
||||
branches = Tiṣeḍwa
|
||||
issues = Uguren
|
||||
projects.description = Aglam (d afrayan)
|
||||
projects.column.edit_title = Isem
|
||||
projects.column.new_title = Isem
|
||||
projects.column.color = Ini
|
||||
projects.open = Ldi
|
||||
projects.close = Mdel
|
||||
issues.new.labels = Tibzimin
|
||||
issues.new.projects = Isenfaren
|
||||
issues.choose.open_external_link = Ldi
|
||||
issues.choose.blank = Amezwer
|
||||
issues.new_label_desc_placeholder = Aglam
|
||||
issues.filter_label = Tabzimt
|
||||
issues.filter_project = Asenfar
|
||||
issues.filter_poster = Ameskar
|
||||
issues.filter_type = Tawsit
|
||||
issues.filter_sort = Semyizwer
|
||||
issues.action_open = Ldi
|
||||
issues.action_close = Mdel
|
||||
issues.action_label = Tabzimt
|
||||
issues.previous = Uzwir
|
||||
issues.next = Uḍfir
|
||||
issues.all_title = Akk
|
||||
issues.draft_title = Arewway
|
||||
issues.context.edit = Ẓreg
|
||||
issues.context.delete = Kkes
|
||||
issues.reopen_issue = Ales tulya
|
||||
issues.create_comment = Awennit
|
||||
issues.author = Ameskar
|
||||
issues.role.owner = Bab-is
|
||||
issues.role.member = Aɛeggal
|
||||
issues.role.contributor = Amɛiwen
|
||||
issues.edit = Ẓreg
|
||||
issues.cancel = Semmet
|
||||
issues.save = Sekles
|
||||
issues.label_title = Isem
|
||||
issues.label_description = Aglam
|
||||
issues.label_color = Ini
|
||||
issues.label_edit = Ẓreg
|
||||
issues.label_delete = Kkes
|
||||
issues.label.filter_sort.alphabetically = S ugemmay
|
||||
issues.delete = Kkes
|
||||
issues.add_time_cancel = Semmet
|
||||
issues.add_time_hours = Isragen
|
||||
issues.due_date_form = yyyy-mm-dd
|
||||
issues.due_date_form_edit = Ẓreg
|
||||
issues.dependency.cancel = Semmet
|
||||
issues.content_history.options = Tixtiṛiyin
|
||||
pulls.made_using_agit = AGit
|
||||
milestones.open = Ldi
|
||||
milestones.close = Mdel
|
||||
milestones.title = Azwel
|
||||
milestones.desc = Aglam
|
||||
milestones.clear = Sfeḍ
|
||||
milestones.cancel = Semmet
|
||||
milestones.filter_sort.name = Isem
|
||||
wiki = Awiki
|
||||
wiki.page = Asebter
|
||||
wiki.new_page = Asebter
|
||||
wiki.cancel = Semmet
|
||||
wiki.edit_page_button = Ẓreg
|
||||
wiki.pages = Isebtar
|
||||
activity = Armud
|
||||
activity.navbar.contributors = Iwiziwen
|
||||
activity.overview = Tamuɣli s umata
|
||||
settings.delete = Kkes akufi-a
|
||||
settings.confirm_delete = Kkes akufi
|
||||
settings.slack_color = Ini
|
||||
|
||||
all_branches = Akk tiseḍwa
|
||||
editor.add_file = Rnu afaylu
|
||||
editor.new_file = Afaylu amaynut
|
||||
editor.upload_file = Sali afaylu
|
||||
editor.edit_file = Ẓreg afaylu
|
||||
editor.edit_this_file = Ẓreg afaylu
|
||||
editor.delete_this_file = Kkes afaylu
|
||||
commits.search_branch = Taseṭṭa-a
|
||||
commits.search_all = Akk tiseḍwa
|
||||
issues.filter_project_all = Akk isenfaren
|
||||
issues.due_date_form_remove = Kkes
|
||||
issues.dependency.remove = Kkes
|
||||
pulls.filter_branch = Sizdeg taseṭṭa
|
||||
activity.period.daily = 1 n wass
|
||||
activity.period.halfweekly = 3 n wussan
|
||||
activity.period.weekly = 1 n umalas
|
||||
activity.period.monthly = 1 n wayyur
|
||||
activity.period.quarterly = 3 n wayyuren
|
||||
activity.period.semiyearly = 6 n wayyuren
|
||||
activity.period.yearly = 1 n useggas
|
||||
activity.git_stats_on_default_branch = Ɣef %s,
|
||||
settings.site = Asmel web
|
||||
settings.delete_collaborator = Kkes
|
||||
settings.webhook.payload = Agbur
|
||||
settings.event_issue_comment = Iwenniten
|
||||
settings.event_pull_request_comment = Iwenniten
|
||||
settings.web_hook_name_gitea = Gitea
|
||||
settings.web_hook_name_forgejo = Forgejo
|
||||
settings.web_hook_name_gogs = Gogs
|
||||
settings.web_hook_name_slack = Slack
|
||||
settings.web_hook_name_discord = Discord
|
||||
settings.web_hook_name_dingtalk = DingTalk
|
||||
settings.web_hook_name_telegram = Telegram
|
||||
settings.web_hook_name_matrix = Matrix
|
||||
settings.web_hook_name_feishu_only = Feishu
|
||||
settings.web_hook_name_packagist = Packagist
|
||||
settings.title = Azwel
|
||||
settings.deploy_key_content = Agbur
|
||||
settings.branches = Tiṣeḍwa
|
||||
settings.edit_protected_branch = Ẓreg
|
||||
release.edit = Ẓreg
|
||||
release.cancel = Semmet
|
||||
release.downloads = Isidar
|
||||
|
||||
[install]
|
||||
admin_password = Awal n uɛeddi
|
||||
password = Awal n uɛeddi
|
||||
|
|
@ -135,6 +277,8 @@ db_schema = Azenziɣ
|
|||
ssl_mode = SSL
|
||||
path = Abrid
|
||||
|
||||
db_name = Isem n taffa n yisefka
|
||||
|
||||
[admin]
|
||||
notices.type_1 = Akufi
|
||||
packages.repository = Akufi
|
||||
|
|
@ -142,6 +286,26 @@ config.db_user = Isem n useqdac
|
|||
users.name = Isem n useqdac
|
||||
emails.filter_sort.name = Isem n useqdac
|
||||
|
||||
orgs.name = Isem
|
||||
repos.name = Isem
|
||||
packages.name = Isem
|
||||
auths.name = Isem
|
||||
config.db_name = Isem
|
||||
config.mailer_name = Isem
|
||||
monitor.name = Isem
|
||||
monitor.queue.name = Isem
|
||||
notices.desc = Aglam
|
||||
|
||||
dashboard = Tafelwit n usenqed
|
||||
organizations = Tuddsiwin
|
||||
repositories = Ikufan
|
||||
config = Tawila
|
||||
config_summary = Agzul
|
||||
config_settings = Iɣewwaren
|
||||
dashboard.statistic = Agzul
|
||||
users.2fa = 2FA
|
||||
config.db_ssl_mode = SSL
|
||||
|
||||
[search]
|
||||
code_kind = Nadi tangalt…
|
||||
search = Nadi…
|
||||
|
|
@ -160,11 +324,19 @@ table_modal.label.columns = Tigejda
|
|||
link_modal.url = Url
|
||||
link_modal.description = Aglam
|
||||
|
||||
table_modal.placeholder.header = Aqeṛṛu
|
||||
table_modal.label.rows = Izirigen
|
||||
|
||||
buttons.new_table.tooltip = Rnu tafelwit
|
||||
table_modal.header = Rnu tafelwit
|
||||
|
||||
[explore]
|
||||
code = Tanglat
|
||||
repos = Ikufan
|
||||
users = Iseqdacen
|
||||
|
||||
organizations = Tuddsiwin
|
||||
|
||||
[form]
|
||||
UserName = Isem n useqdac
|
||||
Password = Awal n uɛeddi
|
||||
|
|
@ -174,6 +346,12 @@ Content = Agbur
|
|||
Biography = Tameddurt
|
||||
Location = Asun
|
||||
|
||||
RepoName = Isem n ukufi
|
||||
TeamName = Isem n terbaɛt
|
||||
|
||||
Email = Tansa imayl
|
||||
Retype = Sentem awal n uɛeddi
|
||||
|
||||
[aria]
|
||||
footer.links = Iseɣwan
|
||||
footer = Aḍar n usebter
|
||||
|
|
@ -184,14 +362,20 @@ more = Ugar
|
|||
[startpage]
|
||||
lightweight = D afessas
|
||||
|
||||
license = Aɣbalu yeldin
|
||||
|
||||
[home]
|
||||
my_repos = Ikufan
|
||||
show_private = Uslig
|
||||
|
||||
my_orgs = Tuddsiwin
|
||||
|
||||
[auth]
|
||||
openid_connect_submit = Tuqqna
|
||||
verify = Senqed
|
||||
|
||||
login_userpass = Qqen
|
||||
|
||||
[modal]
|
||||
yes = Ih
|
||||
no = Uhu
|
||||
|
|
@ -235,3 +419,42 @@ quota.sizes.repos.all = Ikufan
|
|||
quota.sizes.assets.attachments.all = Imeddayen
|
||||
quota.sizes.assets.packages.all = Ikemmusen
|
||||
quota.sizes.wiki = Wiki
|
||||
appearance = Udem
|
||||
avatar = Avataṛ
|
||||
orgs = Tuddsiwin
|
||||
organization = Tuddsiwin
|
||||
quota = Amur
|
||||
location = Asun
|
||||
comment_type_group_reference = Tamsisɣelt
|
||||
visibility.private = Uslig
|
||||
quota.rule.no_limit = War talast
|
||||
quota.sizes.assets.all = Igburen
|
||||
|
||||
update_language = Beddel tutlayt
|
||||
language.title = Tutlayt tamezwarut
|
||||
delete_email = Kkes
|
||||
|
||||
[packages]
|
||||
alpine.repository.branches = Tiṣeḍwa
|
||||
alpine.repository.repositories = Ikufan
|
||||
arch.version.description = Aglam
|
||||
settings.link.select = Fren akufi
|
||||
|
||||
alpine.repository.architectures = Tisegda
|
||||
container.images.title = Tugniwin
|
||||
container.details.platform = Tiɣerɣert
|
||||
debian.repository.architectures = Tisegda
|
||||
rpm.repository.architectures = Tisegda
|
||||
alt.repository.architectures = Tisegda
|
||||
|
||||
[actions]
|
||||
runners.name = Isem
|
||||
runners.description = Aglam
|
||||
|
||||
runners.task_list.repository = Akufi
|
||||
|
||||
[projects]
|
||||
type-2.display_name = Asenfar n ukufi
|
||||
|
||||
[error]
|
||||
network_error = Tuccḍa deg uẓeṭṭa
|
||||
|
|
@ -1249,7 +1249,7 @@ file_history=Vēsture
|
|||
file_view_source=Skatīt avotu
|
||||
file_view_rendered=Skatīt atveidojumu
|
||||
file_view_raw=Apskatīt neapstrādātu
|
||||
file_permalink=Patstāvīgā saite
|
||||
file_permalink=Pastāvīgā saite
|
||||
file_too_large=Datne ir pārāk liela, lai to parādītu.
|
||||
invisible_runes_header=Šī datne satur neredzamas unikoda rakstzīmes
|
||||
invisible_runes_description=`Šī datne satur neredzamas unikoda rakstzīmes, kas ir neatšķiramas cilvēkiem, bet dators tās var apstrādāt atšķirīgi. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
|
||||
|
|
@ -1921,7 +1921,7 @@ milestones.filter_sort.least_issues=Vismazāk pieteikumu
|
|||
|
||||
signing.will_sign=Šis iesūtījums tiks parakstīts ar atslēgu "%s".
|
||||
signing.wont_sign.error=Atgadījās kļūda pārbaudot, vai iesūtījums var tikt parakstīts.
|
||||
signing.wont_sign.nokey=Nav pieejamas atslēgas, ar ko parakstīt šo iesūtījumu.
|
||||
signing.wont_sign.nokey=Šajā serverī nav atslēgas, ar ko parakstīt šo iesūtījumu.
|
||||
signing.wont_sign.never=Iesūtījumi nekad netiek parakstīti.
|
||||
signing.wont_sign.always=Iesūtījumi vienmēr tiek parakstīti.
|
||||
signing.wont_sign.pubkey=Iesūtījums netiks parakstīts, jo kontam nav piesaistīta publiska atslēga.
|
||||
|
|
@ -2307,7 +2307,7 @@ settings.packagist_api_token=API pilnvara
|
|||
settings.packagist_package_url=Packagist pakotnes URL
|
||||
settings.deploy_keys=Izvietošanas atslēgas
|
||||
settings.add_deploy_key=Pievienot izvietošanas atslēgu
|
||||
settings.deploy_key_desc=Izvietošanas atslēgām ir lasīšanas piekļuve glabātavai.
|
||||
settings.deploy_key_desc=Izvietošanas atslēgām var būt tikai lasīšanas vai lasīšanas un rakstīšanas piekļuve glabātavai.
|
||||
settings.is_writable=Iespējot rakstīšanas piekļuvi
|
||||
settings.is_writable_info=Atļaut šai izvietošanas atslēgai <strong>aizgādāt</strong> uz glabātavu.
|
||||
settings.no_deploy_keys=Pagaidām nav nevienas izvietošanas atslēgas.
|
||||
|
|
@ -2364,7 +2364,7 @@ settings.protect_protected_file_patterns=Aizsargāto datņu paraugs (vairākus a
|
|||
settings.protect_protected_file_patterns_desc=Aizsargātās datnes nav ļauts tiešā veidā mainīt, pat ja lietotājam šajā zarā ir tiesības pievienot, labot vai izdzēst datnes. Vairākus paraugus var atdalīt ar semikolu (";"). Paraugu pieraksts ir skatāms <a href="%[1]s">%[2]s</a> dokumentācijā. Piemēri: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
settings.protect_unprotected_file_patterns=Neaizsargāto datņu paraugs (vairākus atdala ar semikolu ";")
|
||||
settings.protect_unprotected_file_patterns_desc=Neaizsargātās datnes, kuras ir ļauts izmainīt tiešā veidā, apejot aizgādāšanas ierobežojumu, ja lietotājam ir rakstīšanas piekļuve. Vairāki paraugi ir atdalāmi ar semikolu (";"). Paraugu pierakstu skatīt <a href="%[1]s">%[2]s</a> dokumentācijā. Piemēri: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
settings.update_protect_branch_success=Zara aizsargāšanas kārtula "%s" tika atjaunināta.
|
||||
settings.update_protect_branch_success=Zara aizsargāšanas kārtula “%s” tika atjaunināta.
|
||||
settings.remove_protected_branch_success=Zara aizsargāšanas kārtula "%s" tika noņemta.
|
||||
settings.remove_protected_branch_failed=Zara aizsargāšanas kārtulas "%s" noņemšana neizdevās.
|
||||
settings.protected_branch_deletion=Izdzēst zara aizsargāšanu
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ contributions_format = {contributions} op {month} {day}, {year}
|
|||
|
||||
[editor]
|
||||
buttons.heading.tooltip = Koptekst toevoegen
|
||||
buttons.bold.tooltip = Vetgedrukte tekst toevoegen
|
||||
buttons.bold.tooltip = Vetgedrukte tekst toevoegen (Ctrl+B / ⌘B)
|
||||
buttons.quote.tooltip = Tekst citeren
|
||||
buttons.code.tooltip = Code toevoegen
|
||||
buttons.link.tooltip = Link toevoegen
|
||||
|
|
@ -188,7 +188,7 @@ buttons.mention.tooltip = Vermeld een gebruiker of team
|
|||
buttons.ref.tooltip = Verwijs naar een issue of pull verzoek
|
||||
buttons.switch_to_legacy.tooltip = Gebruik in plaats daarvan de oude editor
|
||||
buttons.enable_monospace_font = Lettertype monospace inschakelen
|
||||
buttons.italic.tooltip = Schuingedrukte tekst toevoegen
|
||||
buttons.italic.tooltip = Schuingedrukte tekst toevoegen (Ctrl+I / ⌘I)
|
||||
buttons.list.task.tooltip = Een lijst met taken toevoegen
|
||||
buttons.disable_monospace_font = Lettertype monospace uitschakelen
|
||||
buttons.indent.tooltip = Items één niveau lager plaatsen
|
||||
|
|
@ -2035,7 +2035,7 @@ settings.packagist_api_token=API token
|
|||
settings.packagist_package_url=Packagist pakket URL
|
||||
settings.deploy_keys=Deploy sleutels
|
||||
settings.add_deploy_key=Deploy sleutel toevoegen
|
||||
settings.deploy_key_desc=Deploy keys hebben alleen-lezen pull-toegang tot de repository.
|
||||
settings.deploy_key_desc=Deploy sleutels kunnen alleen-lezen of alleen-lezen-schrijftoegang hebben tot de repository.
|
||||
settings.is_writable=Schrijftoegang inschakelen
|
||||
settings.is_writable_info=Sta deze deploy toets toe om te <strong>pushen</strong> naar de repository.
|
||||
settings.no_deploy_keys=Er zijn nog geen deploy sleutels.
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ new_repo.link = Novo repositório
|
|||
new_migrate.link = Nova migração
|
||||
new_org.link = Nova organização
|
||||
test = Teste
|
||||
error413 = Você esgotou sua cota.
|
||||
error413 = Você exauriu sua cota.
|
||||
copy_path = Copiar caminho
|
||||
|
||||
[aria]
|
||||
|
|
@ -222,7 +222,7 @@ platform=Multi-plataforma
|
|||
lightweight=Leve e rápido
|
||||
lightweight_desc=Forgejo utiliza poucos recursos e consegue mesmo rodar no barato Raspberry Pi. Economize energia elétrica da sua máquina!
|
||||
license=Código aberto
|
||||
license_desc=Está tudo no <a target="_blank" rel="noopener noreferrer" href="%[1]s">Forgejo</a>! Contribua e torne este projeto ainda melhor. Não tenha vergonha de contribuir!
|
||||
license_desc=Está tudo no <a target="_blank" rel="noopener noreferrer" href="%[1]s">Forgejo</a>! Junte-se a nós e <a target="_blank" rel="noopener noreferrer" href="%[2]s">contribua</a> para tornar este projeto ainda melhor. Não tenha vergonha de contribuir!
|
||||
install_desc = Apenas <a target="_blank" rel="noopener noreferrer" href="%[1]s">rode o binário</a> para a sua plataforma, execute-o com <a target="_blank" rel="noopener noreferrer" href="%[2]s">Docker</a>, ou obtenha-o <a target="_blank" rel="noopener noreferrer" href="%[3]s">empacotado</a>.
|
||||
platform_desc = Forgejo roda em sistemas operacionais livres, como Linux e FreeBSD, assim como em diferentes arquiteturas de processador. Escolha o seu sistema preferido!
|
||||
|
||||
|
|
@ -528,7 +528,7 @@ totp_disabled.subject = A autenticação em dois fatores foi desabilitada
|
|||
removed_security_key.subject = Uma chave de segurança foi removida
|
||||
removed_security_key.text_1 = A chave de segurança "%[1]s" foi removida de sua conta.
|
||||
account_security_caution.text_1 = Caso tenha sido você, este e-mail pode ser ignorado.
|
||||
totp_enrolled.subject = Você ativou TOTP como método 2FA
|
||||
totp_enrolled.subject = Ativação de TOTP como método de A2F
|
||||
totp_disabled.text_1 = A senha de uso único baseada em tempo (TOTP) na sua conta foi desativada.
|
||||
totp_disabled.no_2fa = Já não existem mais outros métodos de autenticação em dois fatores (2FA) configurados, ou seja, não é mais necessário acessar sua conta com 2FA.
|
||||
removed_security_key.no_2fa = Já não existem mais outros métodos de autenticação em dois fatores (2FA) configurados, ou seja, não é mais necessário acessar sua conta com 2FA.
|
||||
|
|
@ -644,7 +644,7 @@ email_domain_is_not_allowed = O domínio do endereço de email da conta <b>%s</b
|
|||
|
||||
|
||||
[user]
|
||||
change_avatar=Altere seu avatar...
|
||||
change_avatar=Altere seu avatar…
|
||||
joined_on=Inscreveu-se em %s
|
||||
repositories=Repositórios
|
||||
activity=Atividade pública
|
||||
|
|
@ -681,7 +681,7 @@ following.title.one = seguindo
|
|||
followers.title.one = seguidor
|
||||
followers.title.few = seguidores
|
||||
public_activity.visibility_hint.self_private = Sua atividade está visível apenas para você e para os administradores da instância. <a href="%s">Configurar</a>.
|
||||
public_activity.visibility_hint.self_public = Sua atividade está visível para todos, exceto interações em espaços privados. <a href="%s">Configurar</a>.
|
||||
public_activity.visibility_hint.self_public = Sua atividade está visível para todos, exceto as interações em espaços privados. <a href="%s">Clique para configurar</a>.
|
||||
public_activity.visibility_hint.admin_public = Sua atividade está visível para todos, mas como um administrador você também pode ver interações em espaços privados.
|
||||
public_activity.visibility_hint.admin_private = Essa atividade está visível para você porque você é um administrador, mas o usuário dejesa que ela seja mantida em privado.
|
||||
public_activity.visibility_hint.self_private_profile = Sua atividade só é visível para você e para os administradores do servidor porque seu perfil é privado. <a href="%s">Configurar</a>.
|
||||
|
|
@ -992,7 +992,7 @@ pronouns_unspecified = Não especificado
|
|||
language.title = Idioma padrão
|
||||
additional_repo_units_hint = Sugira habilitar unidades de repositório adicionais
|
||||
additional_repo_units_hint_description = Exibir uma sugestão para "Habilitar mais" em repositórios que não possuem todas as unidades disponíveis habilitadas.
|
||||
update_hints = Dicas de atualização
|
||||
update_hints = Atualizar dicas
|
||||
update_hints_success = As dicas foram atualizadas.
|
||||
keep_activity_private.description = A sua <a href="%s">atividade pública</a> estará visível apenas para si e para os administradores do servidor.
|
||||
language.localization_project = Ajude-nos a traduzir Forgejo para o seu idioma! <a href="%s">Mais informações</a>.
|
||||
|
|
@ -1303,14 +1303,14 @@ editor.patch=Aplicar correção
|
|||
editor.patching=Corrigindo:
|
||||
editor.fail_to_apply_patch=`Não foi possível aplicar a correção "%s"`
|
||||
editor.new_patch=Novo patch
|
||||
editor.commit_message_desc=Adicione uma descrição detalhada (opcional)...
|
||||
editor.commit_message_desc=Adicione uma descrição detalhada (opcional)…
|
||||
editor.signoff_desc=Adicione um assinado-por-committer no final do log do commit.
|
||||
editor.commit_directly_to_this_branch=Commit diretamente no branch <strong class="%[2]s">%[1]s</strong>.
|
||||
editor.create_new_branch=Crie um <strong>novo branch</strong> para este commit e crie um pull request.
|
||||
editor.create_new_branch_np=Crie um <strong>novo branch</strong> para este commit.
|
||||
editor.propose_file_change=Propor alteração de arquivo
|
||||
editor.new_branch_name=Nome do novo branch para este commit
|
||||
editor.new_branch_name_desc=Novo nome do branch...
|
||||
editor.new_branch_name_desc=Nome do novo ramo…
|
||||
editor.cancel=Cancelar
|
||||
editor.filename_cannot_be_empty=Nome do arquivo não pode ser em branco.
|
||||
editor.filename_is_invalid=O nome do arquivo é inválido: "%s".
|
||||
|
|
@ -1331,7 +1331,7 @@ editor.fail_to_update_file_summary=Mensagem de erro:
|
|||
editor.push_rejected_no_message=A alteração foi rejeitada pelo servidor sem uma mensagem. Por favor, verifique os Git hooks .
|
||||
editor.push_rejected=A alteração foi rejeitada pelo servidor. Por favor, verifique os Git hooks .
|
||||
editor.push_rejected_summary=Mensagem completa de rejeição:
|
||||
editor.add_subdir=Adicionar um subdiretório...
|
||||
editor.add_subdir=Adicionar uma pasta…
|
||||
editor.unable_to_upload_files=Ocorreu um erro ao enviar arquivos para "%s": %v
|
||||
editor.upload_file_is_locked=Arquivo "%s" está bloqueado por %s.
|
||||
editor.upload_files_to_dir=`Enviar arquivos para "%s"`
|
||||
|
|
@ -1523,7 +1523,7 @@ issues.action_assignee_no_select=Sem responsável
|
|||
issues.action_check=Marcar/Desmarcar
|
||||
issues.action_check_all=Marcar/Desmarcar todos os itens
|
||||
issues.opened_by=aberto por <a href="%[2]s">%[3]s</a> %[1]s
|
||||
pulls.merged_by=por <a href="%[2]s">%[3]s</a> foi aplicado em %[1]s
|
||||
pulls.merged_by=por <a href="%[2]s">%[3]s</a> foi aplicado %[1]s
|
||||
pulls.merged_by_fake=por %[2]s foi aplicado %[1]s
|
||||
issues.closed_by=por <a href="%[2]s">%[3]s</a> foi fechada %[1]s
|
||||
issues.opened_by_fake=%[1]s abertas por %[2]s
|
||||
|
|
@ -2258,7 +2258,7 @@ settings.packagist_api_token=Token de API
|
|||
settings.packagist_package_url=URL do pacote do Packagist
|
||||
settings.deploy_keys=Chaves de deploy
|
||||
settings.add_deploy_key=Adicionar chave de deploy
|
||||
settings.deploy_key_desc=As chaves de deploy possuem somente acesso de leitura (pull) ao repositório.
|
||||
settings.deploy_key_desc=As chaves de deploy podem ter acesso ao repositório somente para leitura ou para leitura e escrita.
|
||||
settings.is_writable=Habilitar acesso de escrita
|
||||
settings.is_writable_info=Permitir que esta chave de deploy faça <strong>push</strong> para o repositório.
|
||||
settings.no_deploy_keys=Não há nenhuma chave de deploy ainda.
|
||||
|
|
@ -2321,7 +2321,7 @@ settings.block_outdated_branch_desc=O merge não será possível quando o branch
|
|||
settings.default_branch_desc=Selecione um branch padrão para pull requests e commits de código:
|
||||
settings.merge_style_desc=Estilos de merge
|
||||
settings.default_merge_style_desc=Estilo de merge padrão
|
||||
settings.choose_branch=Escolha um branch...
|
||||
settings.choose_branch=Escolha um ramo…
|
||||
settings.no_protected_branch=Não há branches protegidos.
|
||||
settings.edit_protected_branch=Editar
|
||||
settings.protected_branch_required_rule_name=Nome da regra é obrigatório
|
||||
|
|
@ -2768,8 +2768,8 @@ issues.filter_no_results_placeholder = Tente ajustar seus filtros de pesquisa.
|
|||
archive.nocomment = Não é possível comentar pois o repositório foi arquivado.
|
||||
migrate.repo_desc_helper = Deixe em branco para importar a descrição existente
|
||||
comment.blocked_by_user = Não é possível comentar pois você foi bloqueado pelo dono ou autor do repositório.
|
||||
sync_fork.branch_behind_few = Este branch está %[1]d commits atrás de %[2]s
|
||||
sync_fork.branch_behind_one = Este branch está %[1]d commit atrás de %[2]s
|
||||
sync_fork.branch_behind_few = Este ramo está %[1]d commits atrás de %[2]s
|
||||
sync_fork.branch_behind_one = Este ramo está %[1]d commit(s) atrás de %[2]s
|
||||
sync_fork.button = Sincronizar
|
||||
settings.event_header_action = Eventos de execução de Actions
|
||||
settings.event_action_failure = Falha
|
||||
|
|
@ -3001,9 +3001,9 @@ dashboard.delete_old_actions.started=A exclusão de todas as atividades antigas
|
|||
dashboard.update_checker=Verificador de atualização
|
||||
dashboard.delete_old_system_notices=Excluir todos os avisos de sistema antigos do banco de dados
|
||||
dashboard.gc_lfs=Coletar lixos dos meta-objetos LFS
|
||||
dashboard.stop_zombie_tasks=Parar tarefas de actions zumbi
|
||||
dashboard.stop_endless_tasks=Parar tarefas infinitas de actions
|
||||
dashboard.cancel_abandoned_jobs=Cancelar trabalhos abandonados de actions
|
||||
dashboard.stop_zombie_tasks=Parar tarefas de Actions zumbis
|
||||
dashboard.stop_endless_tasks=Parar tarefas de Actions intermináveis
|
||||
dashboard.cancel_abandoned_jobs=Cancelar trabalhos abandonados de Actions
|
||||
|
||||
users.user_manage_panel=Gerenciar contas de usuário
|
||||
users.new_account=Criar conta de usuário
|
||||
|
|
@ -3406,7 +3406,7 @@ notices.delete_success=Os avisos do sistema foram excluídos.
|
|||
identity_access = Identidade e acesso
|
||||
settings = Configurações de administrador
|
||||
users.bot = Robô
|
||||
dashboard.start_schedule_tasks = Iniciar tarefas de actions programadas
|
||||
dashboard.start_schedule_tasks = Iniciar tarefas de Actions programadas
|
||||
users.reserved = Reservado
|
||||
emails.change_email_text = Tem certeza de que deseja atualizar este endereço de e-mail?
|
||||
self_check = Autoverificação
|
||||
|
|
@ -3817,7 +3817,7 @@ variables.id_not_exist = A variável com o ID %d não existe.
|
|||
variables.deletion.failed = Falha ao remover a variável.
|
||||
variables.creation.failed = Falha ao adicionar a variável.
|
||||
runs.no_results = Nenhum resultado.
|
||||
variables.description = As variáveis serão passadas para certas ações e não poderão ser lidas de outra forma.
|
||||
variables.description = As variáveis serão passadas para certas Actions e não poderão ser lidas de outra forma.
|
||||
workflow.dispatch.trigger_found = Este workflow tem um disparador de evento <c>workflow_dispatch</c>.
|
||||
workflow.dispatch.run = Executar workflow
|
||||
runs.no_runs = O workflow ainda não foi executado.
|
||||
|
|
|
|||
|
|
@ -2319,7 +2319,7 @@ settings.packagist_api_token=Código da API
|
|||
settings.packagist_package_url=URL do pacote Packagist
|
||||
settings.deploy_keys=Chaves de instalação
|
||||
settings.add_deploy_key=Adicionar chave de instalação
|
||||
settings.deploy_key_desc=Chaves de instalação têm acesso para puxar do repositório apenas em modo de leitura.
|
||||
settings.deploy_key_desc=Chaves de instalação podem ter acesso ao repositório apenas para leitura ou de leitura e escrita.
|
||||
settings.is_writable=Habilitar acesso de escrita
|
||||
settings.is_writable_info=Permitir a esta chave de instalação <strong>enviar</strong> para o repositório.
|
||||
settings.no_deploy_keys=Ainda não existem quaisquer chaves de instalação.
|
||||
|
|
|
|||
|
|
@ -1092,7 +1092,7 @@ mirror_password_placeholder=(Неизменный)
|
|||
mirror_password_blank_placeholder=(Отменено)
|
||||
mirror_password_help=Смените имя пользователя для удаления пароля.
|
||||
watchers=Наблюдатели
|
||||
stargazers=Звездочеты
|
||||
stargazers=Добавившие в избранное
|
||||
stars_remove_warning=Данное действие удалит все звёзды из этого репозитория.
|
||||
forks=Ответвления
|
||||
reactions_more=и ещё %d
|
||||
|
|
@ -2271,7 +2271,7 @@ settings.packagist_api_token=Токен API
|
|||
settings.packagist_package_url=Ссылка на пакет Packagist
|
||||
settings.deploy_keys=Ключи развёртывания
|
||||
settings.add_deploy_key=Добавить ключ развёртывания
|
||||
settings.deploy_key_desc=Ключи развёртывания предоставляют доступ к репозиторию только для чтения.
|
||||
settings.deploy_key_desc=Ключи развёртывания дают доступ к просмотру репозитория и опционально к отправке изменений.
|
||||
settings.is_writable=Разрешить запись
|
||||
settings.is_writable_info=Может ли этот ключ быть использован для выполнения <strong>push</strong> в репозиторий? Ключи развёртывания всегда имеют доступ на pull.
|
||||
settings.no_deploy_keys=Вы не добавляли ключи развёртывания.
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ buttons.quote.tooltip = Citera text
|
|||
buttons.code.tooltip = Lägg till kod
|
||||
buttons.link.tooltip = Lägg till en länk
|
||||
buttons.heading.tooltip = Lägg till rubrik
|
||||
buttons.bold.tooltip = Lägg till fetstilt text
|
||||
buttons.italic.tooltip = Lägg till kursiv text
|
||||
buttons.bold.tooltip = Lägg till fetstilt text (CTRL+B / ⌘B)
|
||||
buttons.italic.tooltip = Lägg till kursiv text (CTRL+I / ⌘I)
|
||||
buttons.list.unordered.tooltip = Lägg till en punktlista
|
||||
buttons.list.ordered.tooltip = Lägg till en numrerad lista
|
||||
buttons.list.task.tooltip = Lägg till en lista med sysslor
|
||||
|
|
@ -203,6 +203,8 @@ buttons.disable_monospace_font = Avaktivera jämnbrett typsnitt
|
|||
link_modal.paste_reminder = Tips: Med ett URL i ditt klippbord, kan du klistra in direkt i textredigeraren för att skapa en länk.
|
||||
buttons.enable_monospace_font = Aktivera jämnbrett typsnitt
|
||||
|
||||
buttons.indent.tooltip = Inplacera föremål med en nivå
|
||||
|
||||
[filter]
|
||||
string.asc = A - Ö
|
||||
string.desc = Ö - A
|
||||
|
|
@ -321,7 +323,7 @@ app_slogan_helper = Skriv in din slogan här. Lämna tom för att stänga av.
|
|||
domain = Serverdomän
|
||||
domain_helper = Domän eller värdadress för servern.
|
||||
reinstall_error = Du försöker att installera i en existerande Forgejo-databas
|
||||
password_algorithm_helper = Ställ in hashalgoritmen för lösenord. Algoritmer har olika krav och styrka. Argon2-algoritmen är ganska säker men använder mycket minne och kan vara olämplig för små system.
|
||||
password_algorithm_helper = Ställ in hashalgoritmen för lösenord. Algoritmer har olika krav och styrkor. Argon2-algoritmen är ganska säker men använder mycket minne och kan vara olämplig för små system.
|
||||
config_location_hint = Dessa konfigurationsinställningar kommer att sparas i:
|
||||
invalid_db_table = Databastabellen "%s" är ogiltig: %v
|
||||
secret_key_failed = Misslyckades att generera hemlig nyckel: %v
|
||||
|
|
@ -335,6 +337,8 @@ invalid_password_algorithm = Ogiltig hashalgoritm för lösenord
|
|||
env_config_keys_prompt = Följande miljövariabler kommer också att tillämpas på din konfigurationsfil:
|
||||
smtp_from_invalid = "Skicka E-post som" adressen är ogiltig
|
||||
|
||||
reinstall_confirm_check_1 = Data krypterad av HEMLIG_NYCKEL i app.ini kan gå förlorad: användare kommer kanske inte att kunna logga in med 2FA/OTP & speglar funkar kanske inte korrekt. Genom att kryssa i rutan godkänner du att den nuvarande app.ini innehåller korrekta HEMLIG_NYCKEL.
|
||||
|
||||
[home]
|
||||
uname_holder=Användarnamn eller e-postadress
|
||||
switch_dashboard_context=Växla visad instrumentpanel
|
||||
|
|
@ -650,7 +654,7 @@ activate_email=Skicka aktivering
|
|||
activations_pending=Väntar på aktivering
|
||||
delete_email=Ta Bort
|
||||
email_deletion=Ta bort e-postadress
|
||||
email_deletion_desc=Mejladressen och relaterad information kommer tas bort från ditt konto. Git-commits med denna mejladress förblir oförändrade. Vill du fortsätta?
|
||||
email_deletion_desc=Denna mejladress och relaterad information kommer tas bort från ditt konto. Git-commits med denna mejladress förblir oförändrade. Vill du fortsätta?
|
||||
email_deletion_success=Mejladressen har tagits bort.
|
||||
theme_update_success=Ditt tema ändrades.
|
||||
theme_update_error=Det valda temat finns inte.
|
||||
|
|
@ -822,7 +826,7 @@ license_helper_desc=En licens styr vad andra kan och inte kan göra med din kod.
|
|||
readme=README
|
||||
readme_helper=Välj en mall för README-filen
|
||||
readme_helper_desc=Här kan du skriva en fullständig beskrivning för ditt projekt.
|
||||
auto_init=Initiera utvecklingskatalog (Lägger till .gitignore, License and README)
|
||||
auto_init=Initiera utvecklingskatalog
|
||||
create_repo=Skapa utvecklingskatalog
|
||||
default_branch=Standardgren
|
||||
default_branch_helper=Den förvalda grenen är bas-gren för pull requests och kod-commits.
|
||||
|
|
@ -866,7 +870,7 @@ migrate_items_wiki=Wiki
|
|||
migrate_items_milestones=Milstenar
|
||||
migrate_items_labels=Etiketter
|
||||
migrate_items_issues=Ärenden
|
||||
migrate_items_pullrequests=Pull Requester
|
||||
migrate_items_pullrequests=Pull-förfrågningar
|
||||
migrate_items_merge_requests=Begäran om sammanslagning
|
||||
migrate_items_releases=Releaser
|
||||
migrate_repo=Migrera utvecklingskatalog
|
||||
|
|
@ -966,7 +970,7 @@ editor.propose_file_change=Föreslå filändring
|
|||
editor.new_branch_name_desc=Nytt branchnamn…
|
||||
editor.cancel=Avbryt
|
||||
editor.filename_cannot_be_empty=Filnamnet kan inte vara tomt.
|
||||
editor.file_changed_while_editing=Filens innehåll har ändrats sedan du påbörjade din ändring.<a target="_blank" rel="noopener noreferrer" href="%s">Klicka här</a> för att se ändringarna eller <strong>commita ändringarna igen</strong> för att skriva över dem.
|
||||
editor.file_changed_while_editing=Filens innehåll har ändrats sedan du öppnade filen.<a target="_blank" rel="noopener noreferrer" href="%s">Klicka här</a> för att se ändringarna eller <strong>commita ändringarna igen</strong> för att skriva över dem.
|
||||
editor.commit_empty_file_header=Committa en tom fil
|
||||
editor.commit_empty_file_text=Filen du vill committa är tom. Vill du fortsätta?
|
||||
editor.no_changes_to_show=Det finns inga ändringar att visa.
|
||||
|
|
@ -1062,7 +1066,7 @@ issues.remove_self_assignment=`tog bort sin tilldelning %s`
|
|||
issues.change_title_at='ändrade titeln från <b><strike>%s</strike></b> till <b>%s</b> %s'
|
||||
issues.delete_branch_at='tog bort grenen <b>%s</b> %s'
|
||||
issues.filter_label=Etikett
|
||||
issues.filter_label_exclude=`Använd <code>alt</code> + <code>klicka/enter</code> för att exkludera etiketter`
|
||||
issues.filter_label_exclude=Använd <kbd>Alt</kbd> + <kbd>klicka/enter</kbd> för att exkludera etiketter
|
||||
issues.filter_label_no_select=Alla etiketter
|
||||
issues.filter_milestone=Milsten
|
||||
issues.filter_project_none=Inget projekt
|
||||
|
|
@ -2247,7 +2251,7 @@ team_kind = Sök team…
|
|||
org_kind = Sök organisationer…
|
||||
issue_kind = Sök ärenden…
|
||||
regexp_tooltip = Tolka söktermen som ett reguljärt uttryck
|
||||
code_search_unavailable = Kodsökning är för närvarande inte tillgänglig. Vänligen kontakta webbplatsadministratören.
|
||||
code_search_unavailable = Kodsökning är för närvarande inte tillgängligt. Vänligen kontakta webbplatsadministratören.
|
||||
fuzzy_tooltip = Inkludera resultat som är närliggande till söktermen
|
||||
no_results = Inga matchande resultat hittades.
|
||||
fuzzy = Ungefärlig
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ logo=Logo
|
|||
sign_in=Giriş Yap
|
||||
sign_in_with_provider=%s ile oturum aç
|
||||
sign_in_or=veya
|
||||
sign_out=Çıkış Yap
|
||||
sign_out=Çıkış yap
|
||||
sign_up=Kaydol
|
||||
link_account=Bağlantı hesabı
|
||||
link_account=Hesap Bağla
|
||||
register=Üye Ol
|
||||
version=Sürüm
|
||||
powered_by=%s tarafından desteklenen
|
||||
|
|
@ -159,7 +159,7 @@ toggle_menu = Menüyü aç-kapa
|
|||
new_migrate.title = Yeni göç
|
||||
new_migrate.link = Yeni göç
|
||||
copy_path = Dizini kopyala
|
||||
confirm_delete_artifact = "%s" adlı öğeyi silmek istediğinizden emin misiniz?
|
||||
confirm_delete_artifact = "%s" adlı öğeyi silmek istediğinize emin misiniz?
|
||||
|
||||
[aria]
|
||||
navbar=Gezinti çubuğu
|
||||
|
|
@ -178,8 +178,8 @@ contributions_format = {day} {month} {year} tarihinde {contributions} katkı
|
|||
|
||||
[editor]
|
||||
buttons.heading.tooltip=Başlık ekle
|
||||
buttons.bold.tooltip=Kalın metin ekle
|
||||
buttons.italic.tooltip=Eğik metin ekle
|
||||
buttons.bold.tooltip=Kalın metin ekle (Ctrl+B / ⌘B)
|
||||
buttons.italic.tooltip=Eğik metin ekle (Ctrl+I / ⌘I)
|
||||
buttons.quote.tooltip=Metni alıntıla
|
||||
buttons.code.tooltip=Kod ekle
|
||||
buttons.link.tooltip=Bağlantı ekle
|
||||
|
|
@ -228,7 +228,7 @@ platform_desc = Forgejo'nun Linux ve FreeBSD gibi özgür işletim sistemlerinde
|
|||
|
||||
[install]
|
||||
install=Kurulum
|
||||
title=Başlangıç Yapılandırması
|
||||
title=Başlangıç yapılandırması
|
||||
docker_helper=Eğer Forgejo'yı Docker içerisinde çalıştırıyorsanız, lütfen herhangi bir değişiklik yapmadan önce <a target="_blank" rel="noopener noreferrer" href="%s">belgeleri</a> okuyun.
|
||||
require_db_desc=Forgejo MySQL, PostgreSQL, SQLite3 veya TiDB (MySQL protokolü) gerektirir.
|
||||
db_title=Veritabanı ayarları
|
||||
|
|
@ -257,7 +257,7 @@ err_admin_name_is_invalid=Yönetici kullanıcı adı geçersiz
|
|||
|
||||
general_title=Genel ayarlar
|
||||
app_name=Site Başlığı
|
||||
app_name_helper=Şirket adınızı buraya girebilirsiniz.
|
||||
app_name_helper=Buraya örnek adınızı girin. Her sayfada görüntülenecektir.
|
||||
repo_path=Depo kök dizini
|
||||
repo_path_helper=Tüm uzak Git depoları bu dizine kaydedilecektir.
|
||||
lfs_path=Git LFS kök dizini
|
||||
|
|
@ -266,9 +266,9 @@ run_user=Şu Kullanıcı Olarak Çalıştır
|
|||
run_user_helper=Forgejo'nin çalışacağı işletim sistemi kullanıcı adı. Bu kullanıcının depo kök yoluna erişiminin olması gerektiğini unutmayın.
|
||||
domain=Sunucu alan adı
|
||||
domain_helper=Sunucu için alan adı veya ana bilgisayar adresi.
|
||||
ssh_port=SSH Sunucu Portu
|
||||
ssh_port_helper=SSH sunucusunun dinleyeceği port numarası. Etkisizleştimek için boş bırakın.
|
||||
http_port=Forgejo HTTP Dinleme Portu
|
||||
ssh_port=SSH sunucu portu
|
||||
ssh_port_helper=SSH sunucusunun dinleyeceği port numarası. SSH sunucusunu devre dışı bırakmak için boş bırakın.
|
||||
http_port=HTTP dinleme portu
|
||||
http_port_helper=Forgejo'nın web sunucusunun dinleyeceği port numarası.
|
||||
app_url=Forgejo Kök URL
|
||||
app_url_helper=HTTP(S) kopyalama URL'leri ve e-posta bildirimleri için temel adres.
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ register_notify.text_2=Ви можете ввійти до свого облік
|
|||
register_notify.text_3=Якщо цей обліковий запис було створено не вами, будь ласка, спочатку <a href="%s">встановіть свій пароль</a>.
|
||||
|
||||
reset_password=Відновлення вашого облікового запису
|
||||
reset_password.text=Перейдіть за цим посиланням, щоб відновити свій обліковий запис в <b>%s</b>:
|
||||
reset_password.text=Перейдіть за посиланням (маєте <b>%s</b>), щоб відновити обліковий запис:
|
||||
|
||||
register_success=Реєстрація успішна
|
||||
|
||||
|
|
@ -776,7 +776,7 @@ ssh_desc=Ці відкриті ключі SSH повʼязані з вашим
|
|||
principal_desc=Ці настройки SSH сертифікатів вказані у вашому обліковому записі та надають повний доступ до ваших репозиторіїв.
|
||||
gpg_desc=Ці відкриті ключі GPG пов'язані з вашим обліковим записом і використовуються для підтвердження комітів. Тримайте свої приватні ключі в безпеці, оскільки вони дозволяють підписувати коміти вашим особистим підписом.
|
||||
ssh_helper=<strong>Потрібна допомога?</strong> Дивіться гід на GitHub з <a href="%s"> генерації ключів SSH</a> або виправлення <a href="%s">типових неполадок SSH</a>.
|
||||
gpg_helper=<strong> Потрібна допомога? </strong> Перегляньте посібник GitHub <a href="%s"> про GPG </a>.
|
||||
gpg_helper=<strong>Потрібна допомога?</strong> Перегляньте посібник <a href="%s">про GPG</a>.
|
||||
key_content_ssh_placeholder=Починається з «ssh-ed25519», «ssh-rsa», «ecdsa-sha2-nistp256», «ecdsa-sha2-nistp384», «ecdsa-sha2-nistp521», «sk-ecdsa-sha2-nistp256@openssh.com» або «sk-ssh-ed25519@openssh.com»
|
||||
key_content_gpg_placeholder=Починається з «-----BEGIN PGP PUBLIC KEY BLOCK-----»
|
||||
add_new_principal=Додати принципал
|
||||
|
|
@ -1030,7 +1030,7 @@ ssh_token_help_ssh_agent = або, якщо ви використовуєте а
|
|||
|
||||
[repo]
|
||||
owner=Власник
|
||||
owner_helper=Деякі організації можуть не відображатися у випадаючому списку через максимальну кількість репозиторііїв.
|
||||
owner_helper=Деякі організації можуть не відображатися у випадаючому списку через обмеження на максимальну кількість репозиторіїв.
|
||||
repo_name=Назва репозиторію
|
||||
repo_name_helper=Хороші назви репозиторіїв використовують короткі, унікальні ключові слова що легко запам'ятати.
|
||||
repo_size=Розмір репозиторію
|
||||
|
|
@ -1039,7 +1039,7 @@ template_select=Виберіть шаблон
|
|||
template_helper=Зробити репозиторій шаблоном
|
||||
template_description=Шаблонні репозиторії дозволяють користувачам генерувати нові репозиторії із такою ж структурою директорій, файлами та додатковими налаштуваннями.
|
||||
visibility=Видимість
|
||||
visibility_description=Тільки власник або члени організації які мають віповідні права, зможуть побачити.
|
||||
visibility_description=Тільки власник або члени організації, які мають відповідні права, зможуть побачити.
|
||||
visibility_helper_forced=Адміністратор вашого сайту налаштував параметри: всі нові репозиторії будуть приватними.
|
||||
visibility_fork_helper=(Буде змінено видимість усіх форків.)
|
||||
clone_helper=Потрібна допомога у клонуванні? Відвідайте сторінку <a target="_blank" rel="noopener" href="%s">Допомога</a>.
|
||||
|
|
@ -1384,7 +1384,7 @@ issues.filter_assignee=Виконавець
|
|||
issues.filter_assginee_no_select=Всі виконавці
|
||||
issues.filter_assginee_no_assignee=Немає виконавця
|
||||
issues.filter_type=Тип
|
||||
issues.filter_type.all_issues=Всі задачі
|
||||
issues.filter_type.all_issues=Усі задачі
|
||||
issues.filter_type.assigned_to_you=Призначене вам
|
||||
issues.filter_type.created_by_you=Створено вами
|
||||
issues.filter_type.mentioning_you=Вас згадано
|
||||
|
|
@ -1508,7 +1508,7 @@ issues.add_time_minutes=Хвилини
|
|||
issues.add_time_sum_to_small=Час не введено.
|
||||
issues.time_spent_total=Загальний витрачений час
|
||||
issues.time_spent_from_all_authors=`Загальний витрачений час: %s`
|
||||
issues.due_date=Дата завершення
|
||||
issues.due_date=Термін виконання
|
||||
issues.push_commit_1=додає %d коміт %s
|
||||
issues.push_commits_n=додає %d коміти(-ів) %s
|
||||
issues.force_push_codes=`примусово залито %[1]s з <a class="%[7]s" href="%[3]s"><code>%[2]s</code></a> %[8]s до <a class="%[7]s" href="%[5]s"><code>%[4]s</code></a> %[9]s %[6]s`
|
||||
|
|
@ -1517,10 +1517,10 @@ issues.due_date_form=рррр-мм-дд
|
|||
issues.due_date_form_edit=Редагувати
|
||||
issues.due_date_form_remove=Видалити
|
||||
issues.due_date_not_set=Термін виконання не встановлено.
|
||||
issues.due_date_added=додав(ла) дату завершення %s %s
|
||||
issues.due_date_remove=видалив(ла) дату завершення %s %s
|
||||
issues.due_date_added=додає термін виконання %s %s
|
||||
issues.due_date_remove=видаляє термін виконання %s %s
|
||||
issues.due_date_overdue=Прострочено
|
||||
issues.due_date_invalid=Термін дії недійсний або знаходиться за межами допустимого діапазону. Будь ласка, використовуйте формат «рррр-мм-дд».
|
||||
issues.due_date_invalid=Термін виконання недійсний або знаходиться за межами допустимого діапазону. Будь ласка, використовуйте формат «рррр-мм-дд».
|
||||
issues.dependency.title=Залежності
|
||||
issues.dependency.add=Додати залежність…
|
||||
issues.dependency.cancel=Відмінити
|
||||
|
|
@ -1548,17 +1548,17 @@ issues.dependency.add_error_cannot_create_circular=Ви не можете ств
|
|||
issues.dependency.add_error_dep_not_same_repo=Обидві задачі повинні бути в одному репозиторії.
|
||||
issues.review.self.approval=Ви не можете схвалити власний пулл-реквест.
|
||||
issues.review.self.rejection=Ви не можете надіслати запит на зміну на власний пулл-реквест.
|
||||
issues.review.approve=зміни затверджено %s
|
||||
issues.review.comment=рецензовано %s
|
||||
issues.review.dismissed=відхилено відгук %s %s
|
||||
issues.review.approve=схвалює зміни %s
|
||||
issues.review.comment=надає відгук %s
|
||||
issues.review.dismissed=відхиляє відгук %s %s
|
||||
issues.review.dismissed_label=Відхилено
|
||||
issues.review.left_comment=додав коментар
|
||||
issues.review.content.empty=Запрошуючи зміни, ви зобов'язані залишити коментар з поясненнями своїх побажань відносно Pull Request'а.
|
||||
issues.review.reject=зробив запит змін %s
|
||||
issues.review.reject=запитує зміни %s
|
||||
issues.review.wait=попросив рецензію %s
|
||||
issues.review.add_review_request=попросив рецензію від %s %s
|
||||
issues.review.remove_review_request=видалив запит на рецензію до %s %s
|
||||
issues.review.remove_review_request_self=відмовився рецензувати %s
|
||||
issues.review.add_review_request=запитує відгук від %[1]s %[2]s
|
||||
issues.review.remove_review_request=видаляє запит на відгук від %[1]s %[2]s
|
||||
issues.review.remove_review_request_self=відмовляється рецензувати %s
|
||||
issues.review.pending=Очікування
|
||||
issues.review.reviewers=Рецензенти
|
||||
issues.review.outdated=Застарілі
|
||||
|
|
@ -1631,7 +1631,7 @@ pulls.no_merge_desc=Цей запити на злиття неможливо з
|
|||
pulls.no_merge_helper=Увімкніть параметри злиття в налаштуваннях репозиторія або злийте запити на злиття вручну.
|
||||
pulls.no_merge_wip=Цей пулл-реквест не можливо об'єднати, тому-що він вже виконується.
|
||||
pulls.no_merge_not_ready=Цей запит не готовий до злиття, перевірте статус рецензіювання і статус перевірки.
|
||||
pulls.no_merge_access=Ви не авторизовані, щоб виконати цей запит на злиття.
|
||||
pulls.no_merge_access=У вас нема дозволу злити цей запит.
|
||||
pulls.merge_pull_request=Створити коміт зі злиттям
|
||||
pulls.rebase_merge_pull_request=Перебазувати, а потім виконати злиття перемотуванням
|
||||
pulls.rebase_merge_commit_pull_request=Перебазувати, а потім створити коміт злиття
|
||||
|
|
@ -1679,7 +1679,7 @@ milestones.completeness=%d%% завершено
|
|||
milestones.create=Створити етап
|
||||
milestones.title=Заголовок
|
||||
milestones.desc=Опис
|
||||
milestones.due_date=Дата завершення (опціонально)
|
||||
milestones.due_date=Термін виконання (необов'язково)
|
||||
milestones.clear=Очистити
|
||||
milestones.invalid_due_date_format=Термін виконання має бути у форматі «рррр-мм-дд».
|
||||
milestones.edit=Редагувати етап
|
||||
|
|
@ -2406,7 +2406,7 @@ already_forked = Ви вже створили форк %s
|
|||
fork_to_different_account = Створити форк до іншого облікового запису
|
||||
fork_no_valid_owners = Неможливо створити форк цього репозиторію, оскільки тут немає дійсних власників.
|
||||
pulls.agit_explanation = Створено через робочий потік AGit. AGit дозволяє учасникам пропонувати зміни за допомогою «git push» без створення форку або нової гілки.
|
||||
diff.review.self_approve = Автори запитів на злиття не можуть схвалювати власні запити на злиття
|
||||
diff.review.self_approve = Автори запитів на злиття не можуть схвалювати власні запити
|
||||
settings.event_pull_request_approvals = Схвалення запитів на злиття
|
||||
diff.git-notes.add = Додати примітку
|
||||
diff.git-notes.remove-header = Видалити примітку
|
||||
|
|
@ -2695,7 +2695,7 @@ issues.reaction.alt_many = %[1]s і ще %[2]d реагують %[3]s.
|
|||
settings.pulls.default_allow_edits_from_maintainers = За замовчуванням дозволити редагування від супроводжувачів
|
||||
pulls.blocked_by_changed_protected_files_1 = Цей запит на злиття заблоковано, оскільки він змінює захищений файл:
|
||||
pulls.delete.text = Ви дійсно хочете видалити цей запит на злиття? (Весь його вміст буде остаточно видалено. Можливо, варто його закрити і зберегти в архіві)
|
||||
pulls.blocked_by_rejection = Цей запит на злиття містить зміни, запропоновані офіційним рецензентом.
|
||||
pulls.blocked_by_rejection = Цей запит на злиття містить запит змін від офіційного рецензента.
|
||||
signing.wont_sign.parentsigned = Цей коміт не буде підписано, оскільки не підписано батьківський коміт.
|
||||
settings.ignore_stale_approvals = Ігнорувати застарілі схвалення
|
||||
pulls.blocked_by_approvals = Цей запит на злиття ще не має достатньої кількості схвалень. Отримано %d з %d схвалень.
|
||||
|
|
@ -2738,6 +2738,20 @@ tree_path_not_found.tag = Шлях %[1]s не існує в тегу %[2]s
|
|||
tree_path_not_found.branch = Шлях %[1]s не існує в гілці %[2]s
|
||||
pulls.recently_pushed_new_branches = Ви надіслали зміни до гілки <a href="%[3]s"><strong>%[1]s</strong></a> %[2]s
|
||||
|
||||
issues.due_date_modified = змінює термін виконання з %[2]s на %[1]s %[3]s
|
||||
milestones.filter_sort.earliest_due_data = Найближчий термін виконання
|
||||
milestones.filter_sort.latest_due_date = Найдальший термін виконання
|
||||
|
||||
settings.mirror_settings.pushed_repository = Цільовий репозиторій
|
||||
|
||||
issues.label_exclusive_desc = Назвіть мітку <code>область/елемент</code>, щоб зробити її взаємовиключною з іншими мітками <code>область/</code>.
|
||||
issues.label_exclusive_warning = Усі конфліктні мітки будуть видалені під час редагування міток задачі або запиту на злиття.
|
||||
issues.review.add_review_requests = запитує відгуки від %[1]s %[2]s
|
||||
issues.review.remove_review_requests = видаляє запити на відгук від %[1]s %[2]s
|
||||
issues.review.add_remove_review_requests = запитує відгуки від %[1]s і видаляє запити на відгук від %[2]s %[3]s
|
||||
settings.ignore_stale_approvals_desc = Не враховувати схвалення, зроблені для старих комітів (застарілі відгуки), до кількості схвалень, які має запит на злиття. Цей параметр не діє, якщо застарілі відгуки вже відхилено.
|
||||
diff.review.self_reject = Автори запитів на злиття не можуть запитувати зміни у власних запитах
|
||||
|
||||
[graphs]
|
||||
contributors.what = внески
|
||||
component_loading_info = Це може зайняти деякий час…
|
||||
|
|
|
|||
|
|
@ -173,8 +173,8 @@ contributions_zero=没有贡献
|
|||
less=较少
|
||||
more=较多
|
||||
contributions_format = {year}年{month}{day}日有{contributions}
|
||||
contributions_few = 贡献
|
||||
contributions_one = 贡献
|
||||
contributions_few = 项贡献
|
||||
contributions_one = 项贡献
|
||||
|
||||
[editor]
|
||||
buttons.heading.tooltip=添加标题
|
||||
|
|
@ -328,7 +328,7 @@ default_allow_create_organization=默认情况下允许创建组织
|
|||
default_allow_create_organization.description=默认允许新用户创建组织。禁用此选项时,管理员必须向新用户授予创建组织的权限。
|
||||
default_enable_timetracking=默认情况下启用时间跟踪
|
||||
default_enable_timetracking.description=默认允许新仓库使用时间跟踪功能。
|
||||
no_reply_address=隐藏电子邮件
|
||||
no_reply_address=隐藏的电子邮件域名
|
||||
no_reply_address_helper=用于设置隐藏电子邮件地址的用户使用的电子邮件域名。例如,如果用于隐藏电子邮件地址的域名设为“noreply.example.org”,则用户名 “joe” 在 Git 中将以 “joe@noreply.example.org” 表示。
|
||||
password_algorithm=密码哈希算法
|
||||
invalid_password_algorithm=无效的密码哈希算法
|
||||
|
|
@ -445,8 +445,8 @@ password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" h
|
|||
password_pwned_err=无法完成对 HaveIBeenPwned 的请求
|
||||
last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
|
||||
change_unconfirmed_email = 如果您在注册时提供了错误的邮箱地址,您可以在下方修改,激活邮件会发送到修改后的邮箱地址。
|
||||
change_unconfirmed_email_summary = 修改用来接收激活邮件的邮箱地址。
|
||||
change_unconfirmed_email_error = 无法修改邮箱地址:%v
|
||||
change_unconfirmed_email_summary = 更改接收激活邮件的电子邮件地址。
|
||||
change_unconfirmed_email_error = 无法更改电子邮件地址:%v
|
||||
hint_login = 已经有账户了吗?<a href="%s">立即登录!</a>
|
||||
back_to_sign_in = 返回登录
|
||||
sign_in_openid = 继续使用 OpenID
|
||||
|
|
@ -762,7 +762,7 @@ password_incorrect=当前密码不正确。
|
|||
change_password_success=您的密码已更新。从现在开始请使用您的新密码登录。
|
||||
password_change_disabled=非本地帐户不能通过 Forgejo 的 web 界面更改密码。
|
||||
|
||||
manage_emails=管理邮箱地址
|
||||
manage_emails=管理电子邮件地址
|
||||
manage_themes=默认主题
|
||||
manage_openid=OpenID 地址
|
||||
email_desc=您的主要电子邮件地址将用于通知、密码恢复,基于网页界面的Git操作(只要它不是设置为隐藏的)。
|
||||
|
|
@ -783,15 +783,15 @@ theme_update_error=所选主题不存在。
|
|||
openid_deletion=移除 OpenID 地址
|
||||
openid_deletion_desc=删除此 OpenID 地址将会阻止你使用它进行登录。你确定要继续吗?
|
||||
openid_deletion_success=OpenID地址已被移除。
|
||||
add_new_email=添加邮箱地址
|
||||
add_new_email=添加电子邮件地址
|
||||
add_new_openid=添加新的 OpenID URI
|
||||
add_email=增加电子邮件地址
|
||||
add_email=添加电子邮件地址
|
||||
add_openid=添加 OpenID URI
|
||||
add_email_confirmation_sent=确认邮件已发送至“%s”。请检查您的收件箱并在接下来的 %s 内点击提供的链接以确认您的电子邮件地址。
|
||||
add_email_success=新的电子邮件地址已添加。
|
||||
email_preference_set_success=电子邮件首选项已成功设置。
|
||||
add_openid_success=新的 OpenID 地址已添加。
|
||||
keep_email_private=隐藏邮箱地址
|
||||
keep_email_private=隐藏电子邮件地址
|
||||
keep_email_private_popup=您的邮件地址不会在个人资料中显示,也不会成为通过网页界面提交的默认地址,例如文件上传、编辑和合并提交。相反,可以使用特殊地址 %s 将提交链接到您的账号。此选项不会影响现有提交。
|
||||
openid_desc=OpenID 让你可以将认证转发到外部服务。
|
||||
|
||||
|
|
@ -1670,7 +1670,7 @@ issues.push_commit_1=于 %[2]s 推送了 %[1]d 个提交
|
|||
issues.push_commits_n=于 %[2]s 推送了 %[1]d 个提交
|
||||
issues.force_push_codes=`于 %[6]s 强制推送 %[1]s,从 <a class="%[7]s" href="%[3]s"><code>%[2]s</code></a> %[8]s,至 <a class="%[7]s" href="%[5]s"><code>%[4]s</code></a> %[9]s`
|
||||
issues.force_push_compare=比较
|
||||
issues.due_date_form=yyyy-mm-dd
|
||||
issues.due_date_form=yyyy年mm月dd日
|
||||
issues.due_date_form_edit=编辑
|
||||
issues.due_date_form_remove=删除
|
||||
issues.due_date_not_set=未设置到期时间。
|
||||
|
|
@ -2315,7 +2315,7 @@ settings.packagist_api_token=API 令牌
|
|||
settings.packagist_package_url=Packagist 软件包 URL
|
||||
settings.deploy_keys=部署密钥
|
||||
settings.add_deploy_key=添加部署密钥
|
||||
settings.deploy_key_desc=部署密钥具有对仓库的只读拉取权限。
|
||||
settings.deploy_key_desc=部署密钥对仓库可具有只读或读写访问权限。
|
||||
settings.is_writable=启用写权限
|
||||
settings.is_writable_info=允许此部署密钥 <strong>推送</strong> 提交到仓库。
|
||||
settings.no_deploy_keys=没有部署密钥。
|
||||
|
|
@ -3298,7 +3298,7 @@ config.enable_timetracking=启用时间跟踪
|
|||
config.default_enable_timetracking=默认情况下启用时间跟踪
|
||||
config.allow_dots_in_usernames = 允许用户在用户名中使用英文句号。不影响已有的帐户。
|
||||
config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间
|
||||
config.no_reply_address=隐藏电子邮件域名
|
||||
config.no_reply_address=隐藏的电子邮件域名
|
||||
config.default_visibility_organization=新组织的默认可见性
|
||||
config.default_enable_dependencies=默认情况下启用议题依赖
|
||||
|
||||
|
|
@ -3320,7 +3320,7 @@ config.mailer_sendmail_path=Sendmail 路径
|
|||
config.mailer_sendmail_args=Sendmail 的额外参数
|
||||
config.mailer_sendmail_timeout=Sendmail 超时
|
||||
config.mailer_use_dummy=Dummy
|
||||
config.test_email_placeholder=电子邮址(例如,test@example.com)
|
||||
config.test_email_placeholder=电子邮址(例如:test@example.com)
|
||||
config.send_test_mail=发送测试邮件
|
||||
config.send_test_mail_submit=发送
|
||||
config.test_mail_failed=发送测试邮件至 "%s" 时失败:%v
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
"keys.ssh.link": "Claus SSH",
|
||||
"keys.gpg.link": "Claus GPG",
|
||||
"admin.config.moderation_config": "Configuració de la moderació",
|
||||
"admin.moderation.reports": "Informes",
|
||||
"admin.moderation.reports": "Denúncies",
|
||||
"admin.moderation.moderation_reports": "Informes de moderació",
|
||||
"admin.moderation.no_open_reports": "No hi ha cap denúncia oberta.",
|
||||
"moderation.report_abuse": "Denunciar abús",
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
"admin.config.global_2fa_requirement.admin": "Administradors",
|
||||
"settings.visibility.description": "La visibilitat del perfil afecta la capacitat dels altres per accedir als vostres repositoris no privats. <a href=\"%s\" target=\"_blank\">Saber-ne més</a>.",
|
||||
"settings.twofa_unroll_unavailable": "El vostre compte requereix l'autenticació de doble factor i no es pot desactivar.",
|
||||
"settings.twofa_reenroll.description": "Torneu-vos a inscriure a l'autenticació de doble factor",
|
||||
"settings.twofa_reenroll.description": "Torneu-vos a inscriure a la vostra autenticació de doble factor",
|
||||
"settings.must_enable_2fa": "Aquesta instància de Forgejo requereix que els usuaris habilitin l'autenticació de doble factor abans de poder accedir als seus comptes.",
|
||||
"error.must_enable_2fa": "Aquesta instància de Forgejo requereix que els usuaris habilitin l'autenticació de doble factor abans de poder accedir als seus comptes. Habiliteu-la a: %s",
|
||||
"avatar.constraints_hint": "Els avatars personalitzats no poden pesar més de %[1]s ni tenir una dimensió superior a %[2]dx%[3]d píxels",
|
||||
|
|
@ -143,7 +143,24 @@
|
|||
"migrate.pagure.incorrect_url": "S'ha proporcionat una URL del repositori font incorrecta",
|
||||
"migrate.pagure.project_url": "URL del projecte de Pagure",
|
||||
"migrate.pagure.project_example": "L'URL del projecte de pagure, per exemple: https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Testimoni",
|
||||
"migrate.pagure.token_label": "Testimoni de l'API de Pagure",
|
||||
"migrate.pagure.token_body_a": "Proporcioneu un testimoni d'API de Pagure amb accés a les incidències privades per crear un repositori que contingui només les incidències privades",
|
||||
"meta.last_line": "Molt estudiar anglès i vinga a fer espíquings i ràitings i tal... I què passa amb el català? Eh?! Me'l noto una mica oxidat! També és important."
|
||||
"meta.last_line": "Molt estudiar anglès i vinga a fer espíquings i ràitings i tal... I què passa amb el català? Eh?! Me'l noto una mica oxidat! També és important.",
|
||||
"relativetime.future": "en el futur",
|
||||
"admin.dashboard.cleanup_offline_runners": "Neteja els executors fora de línia",
|
||||
"settings.twofa_reenroll": "Torneu-vos a inscriure a l'autenticació de doble factor",
|
||||
"user.ghost.tooltip": "Aquest usuari ha sigut eliminat, o no es pot trobar.",
|
||||
"migrate.pagure.private_issues.summary": "Incidències privades (opcional)",
|
||||
"migrate.pagure.private_issues.description": "Aquesta funció està pensada per crear un segon repositori amb només les incidències privades del vostre projecte de Pragure amb la finalitat d'arxivar-lo. Primer, feu una migració normal (sense cap testimoni) per importar tot el contingut públic. Després, si teniu incidències privades que voleu preservar, creeu un repositori separat emprant aquesta opció de testimoni per arxivar-les.",
|
||||
"migrate.pagure.private_issues.warning": "Assegureu-vos de configurar la visibilitat del repositori com a privada si esteu usant la clau API per importar incidències privades. Això evitarà exposar accidentalment el contingut privat en un repositori públic.",
|
||||
"migrate.pagure.token.placeholder": "Només per crear un arxiu d'incidències privades",
|
||||
"actions.runs.run_attempt_label": "Intent d'execució #%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "Esteu veient una execució desactualitzada d'aquesta tasca que va ser executada %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Veure l'execució més recent",
|
||||
"actions.workflow.job_parsing_error": "No s'han pogut analitzar les tasques al flux de treball: %v",
|
||||
"actions.workflow.pre_execution_error": "No s'ha executat el flux de treball degut a un error que ha bloquejat l'intent d'execució.",
|
||||
"stars.list.none": "Ningú ha marcat aquest repositori com a favorit.",
|
||||
"mail.actions.run_info_trigger": "Activat per: %[1]s per: %[2]s",
|
||||
"og.repo.summary_card.alt_description": "La targeta de resum del repositori %[1]s, descrita com a: %[2]s",
|
||||
"actions.workflow.event_detection_error": "No s'han pogut analitzar els esdeveniments compatibles al flux de treball: %v"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
"migrate.pagure.incorrect_url": "Byla zadána nesprávná adresa URL zdrojového repozitáře",
|
||||
"migrate.pagure.project_url": "Adresa projektu Pagure",
|
||||
"migrate.pagure.project_example": "Adresa URL projektu Pagure, např. https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_label": "Token API Pagure",
|
||||
"migrate.pagure.token_body_a": "Zadejte token API Pagure s přístupem k soukromým problémům pro vytvoření repozitáře obsahujícího pouze soukromé problémy",
|
||||
"migrate.pagure.token_body_b": "Nezapomeňte nastavit repozitář jako soukromý výše, pokud chcete, aby byl repozitář soukromý",
|
||||
"migrate.github.description": "Migrovat data z github.com nebo serveru GitHub Enterprise.",
|
||||
|
|
@ -156,5 +156,12 @@
|
|||
"migrate.form.error.url_credentials": "Adresa obsahuje údaje, zadejte je do odpovídajících polí uživatelského jména a hesla",
|
||||
"actions.runs.viewing_out_of_date_run": "Prohlížíte si zastaralý běh této úlohy, který byl spuštěn %[1]s.",
|
||||
"actions.runs.run_attempt_label": "Pokus o běh #%[1]s (%[2]s)",
|
||||
"actions.runs.view_most_recent_run": "Zobrazit nejnovější běh"
|
||||
"actions.runs.view_most_recent_run": "Zobrazit nejnovější běh",
|
||||
"migrate.pagure.private_issues.summary": "Soukromé problémy (volitelné)",
|
||||
"migrate.pagure.private_issues.description": "Tato funkce je navržená pro vytvoření druhého repozitáře obsahujícího pouze soukromé problémy z vašeho projektu Pagure pro účely archivace. Nejprve proveďte normální migraci (bez tokenu) pro importování veškerého veřejného obsahu. Poté, pokud máte soukromé problémy k uchování, vytvořte oddělený repozitář pomocí této možnosti tokenu k archivování daných soukromých problémů.",
|
||||
"migrate.pagure.private_issues.warning": "Ujistěte se, že je viditelnost repozitáře výše nastavena na Soukromou, pokud používáte klíč API k importování soukromých problémů. Tím zabráníte nechtěnému zveřejněný soukromého obsahu ve veřejném repozitáři.",
|
||||
"migrate.pagure.token.placeholder": "Pouze pro vytvoření archivu soukromých problémů",
|
||||
"actions.workflow.job_parsing_error": "Nepodařilo se zpracovat úlohy ve workflow: %v",
|
||||
"actions.workflow.event_detection_error": "Nepodařilo se zpracovat podporované události ve workflow: %v",
|
||||
"actions.workflow.pre_execution_error": "Workflow nebyl vykonán z důvodu chyby, která zablokovala pokus o vykonání."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
"migrate.pagure.token_body_a": "Stellen Sie ein Pagure-API-Token mit Zugriff auf die privaten Issues zur Verfügung, um ein Repository mit nur den privaten Issues darin zu erstellen",
|
||||
"migrate.pagure.project_example": "Die URL des Pagure-Projekts, z. B. https://pagure.io/pagure",
|
||||
"migrate.pagure.description": "Daten von pagure.io oder anderen Pagure-Instanzen migrieren.",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_label": "Pagure-API-Token",
|
||||
"migrate.pagure.project_url": "Pagure-Projekt-URL",
|
||||
"admin.config.security": "Sicherheitskonfiguration",
|
||||
"settings.twofa_unroll_unavailable": "Für deinen Account wird die Zwei-Faktor-Authentifizierung benötigt; sie kann nicht deaktiviert werden.",
|
||||
|
|
@ -148,5 +148,12 @@
|
|||
"actions.runs.view_most_recent_run": "Letzten Run betrachten",
|
||||
"actions.runs.viewing_out_of_date_run": "Sie sehen einen veralteten Run dieses Jobs, der %[1]s ausgeführt wurde.",
|
||||
"actions.runs.run_attempt_label": "Run-Versuch #%[1]s (%[2]s)",
|
||||
"migrate.form.error.url_credentials": "Die URL enthält Anmeldedaten; diese sollten in das jeweilige Benutzername- und Passwortfeld eingetragen werden"
|
||||
"migrate.form.error.url_credentials": "Die URL enthält Anmeldedaten; diese sollten in das jeweilige Benutzername- und Passwortfeld eingetragen werden",
|
||||
"migrate.pagure.private_issues.summary": "Private Issues (optional)",
|
||||
"migrate.pagure.private_issues.description": "Dieses Feature wurde dafür entworfen, um ein zweites Repository zu erstellen, das nur private Issues von deinem Pagure-Projekt für Archivierungszwecke enthält. Nimm zuerst eine normale Migration (ohne ein Token) vor, um alle öffentliche Inhalte zu importieren. Anschließend, wenn du private Issues hast, die du aufbewahren willst, erstelle mit diesem Token ein separates Repository, um diese privaten Issues zu archivieren.",
|
||||
"migrate.pagure.private_issues.warning": "Stell sicher, dass du die Repository-Sichtbarkeit oben auf Privat stellst, falls du den API-Schlüssel benutzt, um private Issues zu importieren. Damit verhindest du, dass du aus Versehen private Inhalte in einem öffentlichen Repository offenlegst.",
|
||||
"migrate.pagure.token.placeholder": "Nur für die Erstellung eines privaten Issue-Archivs",
|
||||
"actions.workflow.job_parsing_error": "Jobs im Workflow konnten nicht geparst werden: %v",
|
||||
"actions.workflow.event_detection_error": "Unterstützte Ereignisse im Workflow konnten nicht geparst werden: %v",
|
||||
"actions.workflow.pre_execution_error": "Der Workflow wurde aufgrund eines Fehlers, der den Ausführungsversuch blockierte, nicht ausgeführt."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,11 +142,16 @@
|
|||
"migrate.pagure.incorrect_url": "Incorrect source repository URL has been provided",
|
||||
"migrate.pagure.project_url": "Pagure project URL",
|
||||
"migrate.pagure.project_example": "The Pagure project URL, e.g. https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_body_a": "Provide a Pagure API token with access to the private issues to create a repository with just the private issues in it",
|
||||
"migrate.pagure.token_body_b": "Be sure to set the private repo flag above if you want this repo to be private",
|
||||
"migrate.pagure.token_label": "Pagure API Token",
|
||||
"migrate.pagure.private_issues.summary": "Private Issues (Optional)",
|
||||
"migrate.pagure.private_issues.description": "This feature is designed to create a second repository containing only private issues from your Pagure project for archive purposes. First, perform a normal migration (without a token) to import all public content. Then, if you have private issues to preserve, create a separate repository using this token option to archive those private issues.",
|
||||
"migrate.pagure.private_issues.warning": "Be sure to set the repository visibility above to Private if you are using the API key to import private issues. This prevents accidentally exposing private content in a public repository.",
|
||||
"migrate.pagure.token.placeholder": "Only for creating private issues archive",
|
||||
"actions.runs.run_attempt_label": "Run attempt #%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "You are viewing an out-of-date run of this job that was executed %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "View most recent run",
|
||||
"actions.workflow.job_parsing_error": "Unable to parse jobs in workflow: %v",
|
||||
"actions.workflow.event_detection_error": "Unable to parse supported events in workflow: %v",
|
||||
"actions.workflow.pre_execution_error": "Workflow was not executed due to an error that blocked the execution attempt.",
|
||||
"meta.last_line": "Thank you for translating Forgejo! This line isn't seen by the users but it serves other purposes in the translation management. You can place a fun fact in the translation instead of translating it."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,38 @@
|
|||
{
|
||||
"search.milestone_kind": "Serĉi celojn..."
|
||||
"search.milestone_kind": "Serĉi celojn…",
|
||||
"home.welcome.no_activity": "Neniu aktiveco",
|
||||
"home.explore_repos": "Esplori deponejojn",
|
||||
"home.explore_users": "Esplori uzantojn",
|
||||
"home.explore_orgs": "Esplori organizaĵojn",
|
||||
"stars.list.none": "Neniu stelumis ĉi tiun deponejon.",
|
||||
"relativetime.now": "nun",
|
||||
"relativetime.1day": "hieraŭ",
|
||||
"relativetime.2days": "antaŭ du tagoj",
|
||||
"relativetime.1week": "je lasta semajno",
|
||||
"relativetime.2weeks": "antaŭ du semajnoj",
|
||||
"relativetime.1month": "je lasta monato",
|
||||
"relativetime.2months": "antaŭ du monatoj",
|
||||
"relativetime.1year": "je lasta jaro",
|
||||
"relativetime.2years": "antaŭ du jaroj",
|
||||
"migrate.github.description": "Migrigi datumojn el github.com aŭ Github Enterprise server.",
|
||||
"migrate.git.description": "Migrigi nur deponejon el ajna Git servilo.",
|
||||
"migrate.gitea.description": "Migrigi datumojn el gitea.com aŭ aliaj Gitea instancoj.",
|
||||
"migrate.gitlab.description": "Migrigi datumojn el gitlab.com aŭ aliaj GitLab instancoj.",
|
||||
"migrate.gogs.description": "Migrigi datumojn el notabug.org aŭ aliaj Gogs instancoj.",
|
||||
"migrate.onedev.description": "Migrigi datumojn el code.onedev.io aŭ aliaj OneDev instancoj.",
|
||||
"migrate.gitbucket.description": "Migrigi datumojn el GitBucket instancoj.",
|
||||
"migrate.codebase.description": "Migrigi datumojn el codebasehq.com.",
|
||||
"migrate.forgejo.description": "Migrigi datumojn el codeberg.org aŭ aliaj Forgejo instancoj.",
|
||||
"repo.settings.push_mirror.branch_filter.label": "Branĉa filtrilo (malnepra)",
|
||||
"themes.names.forgejo-auto": "Forgejo (konformi kun sistema etoso)",
|
||||
"themes.names.forgejo-light": "Forgejo hela",
|
||||
"themes.names.forgejo-dark": "Forgejo malhela",
|
||||
"error.not_found.title": "Paĝo ne trovita",
|
||||
"alert.range_error": " devas esti nombro inter %[1]s kaj %[2]s.",
|
||||
"profile.edit.link": "Redakti profilon",
|
||||
"feed.atom.link": "Atomfluo",
|
||||
"keys.ssh.link": "SSH ŝlosiloj",
|
||||
"keys.gpg.link": "GPG ŝlosiloj",
|
||||
"admin.moderation.reports": "Raportoj",
|
||||
"admin.moderation.no_open_reports": "Ne estas malfermataj raportoj."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,5 +132,14 @@
|
|||
"avatar.constraints_hint": "Mukautettu profiilikuva voi olla kooltaan enintään %[1]s tai enintään %[2]dx%[3]d pikseliä",
|
||||
"repo.commit.load_tags_failed": "Tagien lataaminen epäonnistui sisäisen virheen vuoksi",
|
||||
"actions.runs.run_attempt_label": "Suoritusyritys #%[1]s (%[2]s)",
|
||||
"migrate.pagure.description": "Tee migraatio pagure.io:sta tai muista Pagure-instansseista."
|
||||
"migrate.pagure.description": "Tee migraatio pagure.io:sta tai muista Pagure-instansseista.",
|
||||
"repo.form.cannot_create": "Kaikki tilat, joihin voit luoda tietovarastoja, ovat saavuttaneet tietovarastojen rajan.",
|
||||
"migrate.form.error.url_credentials": "URL-osoite sisältää valtuustiedot, laita ne vastaavasti käyttäjänimi- ja salasanakenttiin",
|
||||
"warning.repository.out_of_sync": "Tämän tietotietovaraston tietokantaesitys ei ole synkronoitu. Jos tämä varoitus näkyy edelleen tämän tietovaraston sitoumuksen lähettämisen jälkeen, ota yhteyttä järjestelmänvalvojaan.",
|
||||
"admin.moderation.deleted_content_ref": "Ilmoitettua sisältöä, jonka tyyppi on %[1]v ja tunnus %[2]d, ei enää ole olemassa",
|
||||
"settings.twofa_reenroll.description": "Ota mukaan kaksivaiheinen todennus uudelleen",
|
||||
"user.ghost.tooltip": "Tämä käyttäjä on poistettu tai häntä ei voida yhdistää.",
|
||||
"og.repo.summary_card.alt_description": "Yhteenvetokortti tietovarastosta %[1]s, kuvattu seuraavasti: %[2]s",
|
||||
"migrate.pagure.incorrect_url": "Virheellinen lähdetietovaraston URL-osoite on syötetty",
|
||||
"actions.runs.viewing_out_of_date_run": "Katselet tämän työn vanhentunutta suorituskertaa, joka suoritettiin %[1]s."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
"repo.pulls.already_merged": "Nabigo ang pagsasama: Naisama na ang hiling sa paghila na ito.",
|
||||
"migrate.pagure.incorrect_url": "Maling pinagmulang URL ng repositoryo ang ibinigay",
|
||||
"migrate.pagure.project_url": "URL ng Pagure na proyekto",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_label": "Pagure API Token",
|
||||
"migrate.pagure.token_body_a": "Magbigay ng Pagure API token na may access sa mga pribadong isyu para gumawa ng repositoryo na may mga pribadong isyu lang",
|
||||
"migrate.pagure.project_example": "Ang URL ng Pagure na proyekto, hal. https://pagure.io/pagure",
|
||||
"migrate.pagure.token_body_b": "Siguraduhin na itakda ang pribadong repositoryo na flag sa itaas kung gusto mo na pribado ang repositoryong ito",
|
||||
|
|
@ -148,5 +148,12 @@
|
|||
"migrate.form.error.url_credentials": "Naglalaman ng mga kredensyal ang URL, ilagay ang mga ito sa mga patlang ng username at password ayon sa pagkakabanggit",
|
||||
"actions.runs.viewing_out_of_date_run": "Tumitingin ka sa isang hindi napapanahong paktabo ng trabahong ito na na-execute %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Tignan ang pinakabagong pagtakbo",
|
||||
"actions.runs.run_attempt_label": "Tangka sa pagtakbo #%[1]s (%[2]s)"
|
||||
"actions.runs.run_attempt_label": "Tangka sa pagtakbo #%[1]s (%[2]s)",
|
||||
"migrate.pagure.private_issues.summary": "Mga Pribadong Isyu (Opsyonal)",
|
||||
"migrate.pagure.private_issues.description": "Dinisenyo ang feature na ito na gumawa ng isa pang repositoryo na naglalaman lamang ng mga pribadong isyu mula sa iyong proyekto sa Pagure para sa layunin ng pag-archive. Una, magsagawa ng isang normal na pag-migrate (nang walang token) para i-import ang lahat ng mga nilalaman. At, kung may mga pribadong isyu na gusto mong panatilihin, gumawa ng isang hiwalay na repositoryo gamit ng opsyon ng token na ito para i-archive ang mga pribadong isyu.",
|
||||
"migrate.pagure.private_issues.warning": "Siguraduhing itakda ang visibility ng repositoryo sa itaas sa Pribado kung ginagamit mo ang API key para i-import ang mga pribadong isyu. Pinipigilan nito ang paglalantag ng mga pribadong nilalaman sa isang pampublikong repositoryo.",
|
||||
"migrate.pagure.token.placeholder": "Para lamang sa paggawa ng archive ng mga pribadong isyu",
|
||||
"actions.workflow.job_parsing_error": "Hindi ma-parse ang mga trabaho sa workflow: %v",
|
||||
"actions.workflow.event_detection_error": "Hindi ma-parse ang mga sinusuportahang event sa workflow: %v",
|
||||
"actions.workflow.pre_execution_error": "Hindi na-execute ang workflow dahil sa isang error na hinarang ang pagtangka sa pag-execute."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,5 +156,10 @@
|
|||
"migrate.pagure.project_example": "L'URL du projet Pagure, ex. https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Jeton",
|
||||
"migrate.pagure.token_body_a": "Veuillez fournir un jeton API Pagure avec l'accès aux problèmes privés pour créer un dépôt contenant seulement les problèmes privés",
|
||||
"migrate.pagure.token_body_b": "Assurez-vous de définir le drapeau dépôt privé ci-dessus si vous souhaitez que ce dépôt soit privé"
|
||||
"migrate.pagure.token_body_b": "Assurez-vous de définir le drapeau dépôt privé ci-dessus si vous souhaitez que ce dépôt soit privé",
|
||||
"migrate.pagure.private_issues.summary": "Tickets privés (Optionnel)",
|
||||
"migrate.pagure.private_issues.description": "Cette fonctionnalité est conçue pour créer un deuxième dépôt contenant uniquement des tickets privés de votre projet Pagure à des fins d'archive. Premièrement, effectuer une migration normale (sans jeton) pour importer tout le contenu public. Ensuite, si vous avez des tickets privés à préserver, créez un dépôt séparé en utilisant cette option de jeton pour archiver ces tickets privés.",
|
||||
"migrate.pagure.private_issues.warning": "Assurez-vous de définir la visibilité du dépôt ci-dessus à Privé si vous utilisez la clé API pour importer des tickets privés. Cela empêche d'exposer accidentellement du contenu privé dans un dépôt public.",
|
||||
"migrate.pagure.token.placeholder": "Seulement pour créer des archives de tickets privés",
|
||||
"actions.workflow.pre_execution_error": "Workflow n'a pas été exécuté en raison d'une erreur qui a bloqué la tentative d'exécution."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,133 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": "commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[4]s",
|
||||
"repo.pulls.title_desc": "ingin menggabungkan komit %[1]d dari <code>%[2]s</code> menuju <code id=\"%[4]s\">%[3]s</code>",
|
||||
"moderation.abuse_category.malware": "Perangkat pembahaya"
|
||||
"moderation.abuse_category.malware": "Perangkat pembahaya",
|
||||
"home.welcome.no_activity": "Tidak ada aktivitas",
|
||||
"home.welcome.activity_hint": "Belum ada konten di feed Anda. Aktivitas dan tindakan Anda dari repositori yang Anda ikuti akan ditampilkan di sini.",
|
||||
"home.explore_repos": "Jelajahi repositori",
|
||||
"home.explore_users": "Jelajahi pengguna",
|
||||
"home.explore_orgs": "Jelajahi organisasi",
|
||||
"stars.list.none": "Tidak ada yang memberikan bintang pada repositori ini.",
|
||||
"watch.list.none": "Tidak ada yang memantau repositori ini.",
|
||||
"followers.incoming.list.self.none": "Tidak ada yang mengikuti profil Anda.",
|
||||
"followers.incoming.list.none": "Tidak ada yang mengikuti pengguna ini.",
|
||||
"followers.outgoing.list.self.none": "Anda tidak mengikuti siapa pun.",
|
||||
"followers.outgoing.list.none": "%s tidak mengikuti siapa pun.",
|
||||
"relativetime.now": "sekarang",
|
||||
"relativetime.future": "di masa depan",
|
||||
"relativetime.mins": "%d menit yang lalu",
|
||||
"relativetime.hours": "%d jam yang lalu",
|
||||
"relativetime.days": "%d hari yang lalu",
|
||||
"relativetime.weeks": "%d minggu yang lalu",
|
||||
"relativetime.months": "%d bulan yang lalu",
|
||||
"relativetime.years": "%d tahun yang lalu",
|
||||
"relativetime.1day": "kemarin",
|
||||
"relativetime.2days": "dua hari yang lalu",
|
||||
"relativetime.1week": "minggu lalu",
|
||||
"relativetime.2weeks": "dua minggu yang lalu",
|
||||
"relativetime.1month": "bulan lalu",
|
||||
"relativetime.2months": "dua bulan yang lalu",
|
||||
"relativetime.1year": "tahun lalu",
|
||||
"relativetime.2years": "dua tahun yang lalu",
|
||||
"repo.pulls.already_merged": "Penggabungan (merge) gagal: Permintaan pull (pull request) ini sudah digabungkan.",
|
||||
"repo.form.cannot_create": "Semua ruang di mana Anda dapat membuat repositori telah mencapai batas jumlah repositori.",
|
||||
"migrate.form.error.url_credentials": "URL tersebut mengandung kredensial, masukkan kredensial tersebut ke dalam kolom nama pengguna dan kata sandi masing-masing",
|
||||
"migrate.github.description": "Migrasikan data dari github.com atau server GitHub Enterprise.",
|
||||
"migrate.git.description": "Migrasikan repositori hanya dari layanan Git mana pun.",
|
||||
"migrate.gitea.description": "Migrasikan data dari gitea.com atau instance Gitea lainnya.",
|
||||
"migrate.gitlab.description": "Migrasikan data dari gitlab.com atau instance GitLab lainnya.",
|
||||
"migrate.gogs.description": "Migrasikan data dari notabug.org atau instance Gogs lainnya.",
|
||||
"migrate.onedev.description": "Migrasikan data dari code.onedev.io atau instance OneDev lainnya.",
|
||||
"migrate.gitbucket.description": "Migrasikan data dari instance GitBucket.",
|
||||
"migrate.codebase.description": "Migrasikan data dari codebasehq.com.",
|
||||
"migrate.forgejo.description": "Migrasikan data dari codeberg.org atau instance Forgejo lainnya.",
|
||||
"repo.issue_indexer.title": "Indeks Masalah",
|
||||
"search.milestone_kind": "Cari milestone…",
|
||||
"repo.settings.push_mirror.branch_filter.label": "Filter cabang (opsional)",
|
||||
"repo.settings.push_mirror.branch_filter.description": "Cabang yang akan disalin. Biarkan kosong untuk menyalin semua cabang. Lihat <a href=\"%[1]s\">%[2]s dokumentasi</a> untuk sintaksis. Contoh: <code>main, release/*</code>",
|
||||
"incorrect_root_url": "Instance Forgejo ini dikonfigurasi untuk diakses melalui “%s”. Saat ini Anda mengakses Forgejo melalui URL yang berbeda, yang dapat menyebabkan sebagian fungsi aplikasi tidak berfungsi dengan baik. URL kanonik dikendalikan oleh admin Forgejo melalui pengaturan ROOT_URL di berkas app.ini.",
|
||||
"themes.names.forgejo-auto": "Forgejo (mengikuti tema sistem)",
|
||||
"themes.names.forgejo-light": "Forgejo terang",
|
||||
"themes.names.forgejo-dark": "Forgejo gelap",
|
||||
"error.not_found.title": "Halaman tidak ditemukan",
|
||||
"warning.repository.out_of_sync": "Representasi basis data repositori ini tidak sinkron. Jika peringatan ini masih ditampilkan setelah melakukan commit ke repositori ini, hubungi administrator.",
|
||||
"alert.asset_load_failed": "Gagal memuat berkas aset dari {path}. Pastikan berkas aset dapat diakses.",
|
||||
"alert.range_error": " Harus berupa angka antara %[1]s dan %[2]s.",
|
||||
"install.invalid_lfs_path": "Tidak dapat membuat direktori root LFS di jalur yang ditentukan: %[1]s",
|
||||
"profile.actions.tooltip": "Lebih banyak tindakan",
|
||||
"profile.edit.link": "Edit profil",
|
||||
"feed.atom.link": "Atom feed",
|
||||
"keys.ssh.link": "Kunci SSH",
|
||||
"keys.gpg.link": "Kunci GPG",
|
||||
"admin.config.moderation_config": "Konfigurasi moderasi",
|
||||
"admin.moderation.moderation_reports": "Konfigurasi moderasi",
|
||||
"admin.moderation.reports": "Laporan",
|
||||
"admin.moderation.no_open_reports": "Saat ini tidak ada laporan yang terbuka.",
|
||||
"admin.moderation.deleted_content_ref": "Konten yang dilaporkan dengan tipe %[1]v dan ID %[2]d tidak lagi tersedia",
|
||||
"moderation.report_abuse": "Laporkan penyalahgunaan",
|
||||
"moderation.report_content": "Isi laporan",
|
||||
"moderation.report_abuse_form.header": "Laporkan penyalahgunaan kepada administrator",
|
||||
"moderation.report_abuse_form.details": "Formulir ini harus digunakan untuk melaporkan pengguna yang membuat profil spam, repositori, masalah, komentar, atau berperilaku tidak pantas.",
|
||||
"moderation.report_abuse_form.invalid": "Argumen tidak valid",
|
||||
"moderation.report_abuse_form.already_reported": "Anda sudah melaporkan konten ini",
|
||||
"moderation.abuse_category": "Kategori",
|
||||
"moderation.abuse_category.placeholder": "Pilih kategori",
|
||||
"moderation.abuse_category.spam": "Spam",
|
||||
"moderation.abuse_category.illegal_content": "Konten ilegal",
|
||||
"moderation.abuse_category.other_violations": "Pelanggaran lain terhadap aturan platform",
|
||||
"moderation.report_remarks": "Catatan",
|
||||
"moderation.report_remarks.placeholder": "Silakan berikan beberapa detail mengenai penyalahgunaan yang Anda laporkan.",
|
||||
"moderation.submit_report": "Kirimkan laporan",
|
||||
"moderation.reporting_failed": "Tidak dapat kirimkan laporan penyalahgunaan baru: %v",
|
||||
"moderation.reported_thank_you": "Terima kasih atas laporan yang Anda sampaikan. Pihak administrasi telah mengetahui hal tersebut.",
|
||||
"mail.actions.successful_run_after_failure_subject": "Alur kerja %[1] telah dipulihkan di repositori %[2]",
|
||||
"mail.actions.not_successful_run_subject": "Alur kerja %[1] gagal di repositori %[2]",
|
||||
"mail.actions.successful_run_after_failure": "Alur kerja %[1] telah dipulihkan di repositori %[2]",
|
||||
"mail.actions.not_successful_run": "Alur kerja %[1] gagal di repositori %[2]",
|
||||
"mail.actions.run_info_cur_status": "Status Lari Ini: %[1]s (baru saja diperbarui dari %[2]s)",
|
||||
"mail.actions.run_info_previous_status": "Status Eksekusi Sebelumnya: %[1]s",
|
||||
"mail.actions.run_info_sha": "Komitmen: %[1]s",
|
||||
"mail.actions.run_info_trigger": "Dipicu oleh: %[1]s oleh: %[2]s",
|
||||
"repo.diff.commit.next-short": "Selanjutnya",
|
||||
"repo.diff.commit.previous-short": "Sebelumnya",
|
||||
"discussion.locked": "Diskusi ini telah dikunci. Komentar hanya dapat dilakukan oleh kontributor.",
|
||||
"discussion.sidebar.reference": "Referensi",
|
||||
"editor.textarea.tab_hint": "Baris sudah diindentasi. Tekan <kbd>Tab</kbd> lagi atau <kbd>Escape</kbd> untuk keluar dari editor.",
|
||||
"editor.textarea.shift_tab_hint": "Tidak ada indentasi pada baris ini. Tekan <kbd>Shift</kbd> + <kbd>Tab</kbd> lagi atau <kbd>Escape</kbd> untuk keluar dari editor.",
|
||||
"admin.auths.allow_username_change": "Izinkan perubahan nama pengguna",
|
||||
"admin.auths.allow_username_change.description": "Izinkan pengguna untuk mengubah nama pengguna mereka di pengaturan profil",
|
||||
"admin.dashboard.cleanup_offline_runners": "Pembersihan pelari offline",
|
||||
"admin.dashboard.remove_resolved_reports": "Hapus laporan yang telah diselesaikan",
|
||||
"admin.config.security": "Konfigurasi keamanan",
|
||||
"admin.config.global_2fa_requirement.title": "Persyaratan dua faktor global",
|
||||
"admin.config.global_2fa_requirement.none": "Tidak",
|
||||
"admin.config.global_2fa_requirement.all": "Semua pengguna",
|
||||
"admin.config.global_2fa_requirement.admin": "Administrator",
|
||||
"settings.visibility.description": "Visibilitas profil memengaruhi kemampuan orang lain untuk mengakses repositori non-pribadi Anda. <a href=\"%s\" target=\"_blank\">Pelajari lebih lanjut</a>.",
|
||||
"settings.twofa_unroll_unavailable": "Otentikasi dua faktor diwajibkan untuk akun Anda dan tidak dapat dinonaktifkan.",
|
||||
"settings.twofa_reenroll": "Aktifkan kembali otentikasi dua faktor",
|
||||
"settings.twofa_reenroll.description": "Daftarkan ulang otentikasi dua faktor Anda",
|
||||
"settings.must_enable_2fa": "Instance Forgejo ini mengharuskan pengguna untuk mengaktifkan otentikasi dua faktor sebelum mereka dapat mengakses akun mereka.",
|
||||
"error.must_enable_2fa": "Instance Forgejo ini mengharuskan pengguna untuk mengaktifkan otentikasi dua faktor sebelum mereka dapat mengakses akun mereka. Aktifkan di: %s",
|
||||
"avatar.constraints_hint": "Avatar kustom tidak boleh melebihi %[1]s dalam ukuran atau lebih besar dari %[2]dx%[3]d piksel",
|
||||
"user.ghost.tooltip": "Pengguna ini telah dihapus, atau tidak dapat diidentifikasi.",
|
||||
"og.repo.summary_card.alt_description": "Kartu ringkasan repositori %[1]s, dijelaskan sebagai: %[2]s",
|
||||
"repo.commit.load_tags_failed": "Pemuatan tag gagal karena kesalahan internal",
|
||||
"compare.branches.title": "Bandingkan cabang",
|
||||
"migrate.pagure.description": "Migrasikan data dari pagure.io atau instance Pagure lainnya.",
|
||||
"migrate.pagure.incorrect_url": "URL repositori sumber yang salah telah disediakan",
|
||||
"migrate.pagure.project_url": "URL Proyek Pagure",
|
||||
"migrate.pagure.project_example": "URL proyek Pagure, misalnya https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Token API Pagure",
|
||||
"migrate.pagure.private_issues.summary": "Masalah Pribadi (Opsional)",
|
||||
"migrate.pagure.private_issues.description": "Fitur ini dirancang untuk membuat repositori kedua yang hanya berisi masalah pribadi dari proyek Pagure Anda untuk tujuan arsip. Pertama, lakukan migrasi normal (tanpa token) untuk mengimpor semua konten publik. Kemudian, jika Anda memiliki masalah pribadi yang ingin disimpan, buat repositori terpisah menggunakan opsi token ini untuk mengarsipkan masalah pribadi tersebut.",
|
||||
"migrate.pagure.private_issues.warning": "Pastikan untuk mengatur visibilitas repositori di atas menjadi Pribadi jika Anda menggunakan kunci API untuk mengimpor masalah pribadi. Hal ini mencegah pengungkapan konten pribadi secara tidak sengaja di repositori publik.",
|
||||
"migrate.pagure.token.placeholder": "Hanya untuk membuat arsip masalah pribadi",
|
||||
"actions.runs.run_attempt_label": "Upaya eksekusi #%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "Anda sedang melihat hasil eksekusi yang sudah kadaluwarsa dari tugas ini yang dijalankan pada %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Lihat hasil terbaru",
|
||||
"actions.workflow.job_parsing_error": "Tidak dapat memproses pekerjaan dalam alur kerja: %v",
|
||||
"actions.workflow.event_detection_error": "Tidak dapat memproses peristiwa yang didukung dalam alur kerja: %v",
|
||||
"actions.workflow.pre_execution_error": "Alur kerja tidak dijalankan karena terjadi kesalahan yang menghalangi upaya eksekusi.",
|
||||
"meta.last_line": "Terima kasih telah menerjemahkan Forgejo! Baris ini tidak terlihat oleh pengguna, tetapi memiliki fungsi lain dalam manajemen terjemahan. Anda dapat menambahkan fakta menarik dalam terjemahan alih-alih menerjemahkannya."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,5 +156,6 @@
|
|||
"migrate.pagure.project_url": "URL del progetto Pagure",
|
||||
"migrate.pagure.project_example": "L'URL del progetto Pagure, ad esempio https://pagure.io/pagure",
|
||||
"mail.actions.not_successful_run": "Flusso di lavoro %[1]s non riuscito nel repositorio %[2]s",
|
||||
"settings.twofa_reenroll.description": "Reinserisci la verifica in due passaggi"
|
||||
"settings.twofa_reenroll.description": "Reinserisci la verifica in due passaggi",
|
||||
"migrate.pagure.private_issues.summary": "Segnalazioni Private (opzionale)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,25 @@
|
|||
"repo.diff.commit.previous-short": "Uzwir",
|
||||
"admin.config.global_2fa_requirement.none": "Uhu",
|
||||
"admin.config.global_2fa_requirement.admin": "Inedbalen",
|
||||
"migrate.pagure.token_label": "Ajiṭun"
|
||||
"migrate.pagure.token_label": "Ajiṭun API n Pagure",
|
||||
"home.explore_repos": "Snirem ikufan",
|
||||
"home.explore_users": "Snirem iseqdacen",
|
||||
"relativetime.2days": "sin n wussan aya",
|
||||
"relativetime.1week": "dduṛt yezrin",
|
||||
"relativetime.1month": "ayyur yezrin",
|
||||
"relativetime.2months": "sin n wayyuren aya",
|
||||
"relativetime.1year": "ilindi",
|
||||
"relativetime.2years": "sin n iseggasen aya",
|
||||
"themes.names.forgejo-auto": "Forgejo (akken i yella usentel n unagraw)",
|
||||
"themes.names.forgejo-light": "Forgejo aceɛlal",
|
||||
"themes.names.forgejo-dark": "Forgejo ubrik",
|
||||
"error.not_found.title": "Asebtar ulac-it",
|
||||
"profile.actions.tooltip": "Ugar n tigawin",
|
||||
"profile.edit.link": "Ẓreg amaɣnu",
|
||||
"keys.ssh.link": "Tasarut SSH",
|
||||
"keys.gpg.link": "Tisura GPG",
|
||||
"moderation.abuse_category.placeholder": "Fren taggayt",
|
||||
"admin.config.security": "Tawila n tɣellist",
|
||||
"admin.config.global_2fa_requirement.all": "Akk iseqdacen",
|
||||
"migrate.pagure.project_url": "Tansa URL n usenfar Pagure"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
"migrate.pagure.incorrect_url": "Ir norādīts nepareizs avota glabātavas URL",
|
||||
"migrate.pagure.project_url": "Pagure projekta URL",
|
||||
"migrate.pagure.project_example": "Pagure projekta URL, piem., https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Pilnvara",
|
||||
"migrate.pagure.token_label": "Pagure API ilnvara",
|
||||
"migrate.pagure.token_body_a": "Jānorāda Pagura API pilnvara ar piekļuvi privātajiem pieteikumiem, lai izveidotu glabātavu tikai ar privātiem pieteikumiem tajā",
|
||||
"migrate.pagure.token_body_b": "Jāpārliecinās, ka augstāk ir iestatīts privātas glabātavas karogs, ja vēlies, lai šī glabātava būtu privāta",
|
||||
"migrate.github.description": "Pārcelt datus no github.com vai GitHub Enterprise servera.",
|
||||
|
|
@ -156,5 +156,12 @@
|
|||
"migrate.form.error.url_credentials": "URL satur pieteikšanās datus, tie ir attiecīgi jāievieto lietotājvārda un paroles laukā",
|
||||
"actions.runs.view_most_recent_run": "Apskatīt visnesenāko palaišanu",
|
||||
"actions.runs.run_attempt_label": "Palaišanas mēģinājums #%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "Tu skati novecojušu šī darba palaišanu, kas tika izpildīta %[1]s."
|
||||
"actions.runs.viewing_out_of_date_run": "Tu skati novecojušu šī darba palaišanu, kas tika izpildīta %[1]s.",
|
||||
"migrate.pagure.private_issues.summary": "Privātie pieteikumi (izvēles)",
|
||||
"migrate.pagure.private_issues.description": "Šī iespēja ir izstrādāta, lai izveidotu otrēju glabātavu, kurā arhivēšanas nolūkā atrodas tikai privātie pieteikumi no Pagure projekta. Vispirms jāveic parasta pārcelšana (bez pilnvaras), lai ievietotu visu publisko saturu. Tad, ja ir saglabājami privāti pieteikumi, jāizveido atsevišķa glabātava, izmantojot šo pilnvaras iespēju, lai arhivētu privātos pieteikumus.",
|
||||
"migrate.pagure.private_issues.warning": "Jāpārliecinās, ka glabātavas redzamība augstāk ir iestatīta kā privāta, ja izmanto API atslēgu, lai ievietotu privātos pieteikumus. Tas novērš nejaušu privāta satura atklāšanu publiskā glabātavā.",
|
||||
"migrate.pagure.token.placeholder": "Tikai privātu pieteikumu arhīva izveidošanai",
|
||||
"actions.workflow.job_parsing_error": "Nebija iespējams apstrādāt darbus darbplūsmā: %v",
|
||||
"actions.workflow.event_detection_error": "Nebija iespējams apstrādāt atbalstītos notikumus darbplūsmā: %v",
|
||||
"actions.workflow.pre_execution_error": "Darbplūsma netika izpildīta kļūdas, kura aizturēja izpildes mēģinājumu, dēļ."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
"migrate.pagure.description": "Daten vun pagure.io of anner Pagure-Instanzen umtrecken.",
|
||||
"migrate.pagure.project_url": "Pagure-Projekt-URL",
|
||||
"migrate.pagure.project_example": "De URL vum Projekt up Pagure, to’n Bispööl https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Teken",
|
||||
"migrate.pagure.token_label": "Pagure-API-Teken",
|
||||
"migrate.pagure.token_body_b": "Wees wiss, boven the Flagg för een privaates Repo to setten, wenn du willst, dat deeses Repo privaat wesen sall",
|
||||
"migrate.pagure.incorrect_url": "Ungültige Quell-Repositoriums-URL is angeven worden",
|
||||
"migrate.pagure.token_body_a": "Giff een Pagure-API-Teken mit Togang to de privaaten Gefallens an, um een Repositorium mit blots the privaaten Gefallens daarin to maken",
|
||||
|
|
@ -148,5 +148,12 @@
|
|||
"migrate.form.error.url_credentials": "De URL enthollt Anmell-Daten, legg se in de Felden för Brukernaam un Passwoord",
|
||||
"actions.runs.viewing_out_of_date_run": "Du bekiekst eenen verollten Loop vun deeser Upgaav, wat %[1]s utföhrt worden is.",
|
||||
"actions.runs.view_most_recent_run": "Neeisten Loop ankieken",
|
||||
"actions.runs.run_attempt_label": "Loop-Versöök #%[1]s (%[2]s)"
|
||||
"actions.runs.run_attempt_label": "Loop-Versöök #%[1]s (%[2]s)",
|
||||
"migrate.pagure.private_issues.summary": "Privaate Gefallens (wenn du willst)",
|
||||
"migrate.pagure.private_issues.description": "Deese Funktioon lett di een twedes Repositorium maken, wat blots privaate Gefallens ut dienem Pagure-Projekt för Archivzwecken enthollt. Maak toeerst eenen normaalen Umtreck (sünner een Teken), um alle publiken Inhollen to importeren. Dann, wenn du privaate Gefallens hest, wat du behollen willst, maak noch een anner Repositorium mit deesem Teken, um deese privaaten Gefallens to archiveren.",
|
||||
"migrate.pagure.private_issues.warning": "Wees wiss, de Sichtbaarkeid vun de Repositorium up Privaat to setten, wenn du de API-Slötel bruukst, um privaate Gefallens to importeren. Dat verhinnert, dat du privaaten Inholl ut Versehn in eenem publiken Repositorium blootmaakst.",
|
||||
"migrate.pagure.token.placeholder": "Blots, um een Archiv vun privaaten Gefallens to maken",
|
||||
"actions.workflow.job_parsing_error": "Kann de Upgaven in de Warkwies nich lesen: %v",
|
||||
"actions.workflow.event_detection_error": "Kann de unnerstütt Vörfallen in de Warkwies nich lesen: %v",
|
||||
"actions.workflow.pre_execution_error": "Warkwies is nich utföhrt worden, denn een Fehler is uptreden, wat de Utföhrens-Versöök blockeert hett."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@
|
|||
"migrate.pagure.incorrect_url": "Er is een onjuiste URL voor de bronrepository opgegeven",
|
||||
"migrate.pagure.project_url": "Pagure-project URL",
|
||||
"migrate.pagure.project_example": "De url van het Pagure-project, bijvoorbeeld https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_label": "Pagure API-token",
|
||||
"migrate.pagure.token_body_a": "Geef een Pagure API-token met toegang tot de privé-issues om een repository te maken met alleen de privé-issues erin",
|
||||
"migrate.pagure.token_body_b": "Zorg ervoor dat u de vlag voor privé-repository hierboven instelt als u wilt dat deze repository privé is",
|
||||
"migrate.github.description": "Migreer gegevens van github.com of GitHub Enterprise server.",
|
||||
|
|
@ -146,5 +146,14 @@
|
|||
"user.ghost.tooltip": "Deze gebruiker is verwijderd of kan niet worden gevonden.",
|
||||
"error.must_enable_2fa": "Deze Forgejo-instantie vereist dat gebruikers tweefactorauthenticatie inschakelen voordat ze toegang krijgen tot hun accounts. Schakel dit in op: %s",
|
||||
"migrate.form.error.url_credentials": "De URL bevat inloggegevens. Vul deze in de velden voor gebruikersnaam en wachtwoord respectievelijk",
|
||||
"actions.runs.view_most_recent_run": "Bekijk de meest recente run"
|
||||
"actions.runs.view_most_recent_run": "Bekijk de meest recente run",
|
||||
"actions.runs.run_attempt_label": "Uitvoerpoging #%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "U bekijkt een verouderde uitvoer van deze opdracht die %[1]s is uitgevoerd.",
|
||||
"migrate.pagure.private_issues.summary": "Privé-issues (optioneel)",
|
||||
"migrate.pagure.private_issues.description": "Deze functie is ontworpen om een tweede repository aan te maken met alleen privé-issues van je Pagure project voor archiefdoeleinden. Voer eerst een normale migratie uit (zonder token) om alle publieke content te importeren. Als u vervolgens privé-issues wilt bewaren, maak dan een aparte repository aan met deze tokenoptie om die privé-issues te archiveren.",
|
||||
"migrate.pagure.private_issues.warning": "Zorg ervoor dat je de repository zichtbaarheid hierboven op Privé zet als je de API sleutel gebruikt om privézaken te importeren. Dit voorkomt dat je per ongeluk privé-inhoud blootstelt in een openbare repository.",
|
||||
"migrate.pagure.token.placeholder": "Alleen voor het aanmaken van een privé-archief met issues",
|
||||
"actions.workflow.job_parsing_error": "Taken in workflow kunnen niet worden geparseerd: %v",
|
||||
"actions.workflow.event_detection_error": "Ondersteunde gebeurtenissen in workflow kunnen niet worden geparseerd: %v",
|
||||
"actions.workflow.pre_execution_error": "Workflow is niet uitgevoerd vanwege een fout die de uitvoeringspoging blokkeerde."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"search.milestone_kind": "Pesquisar marcos…",
|
||||
"home.welcome.no_activity": "Sem atividade",
|
||||
"home.welcome.activity_hint": "Ainda não tem nada no seu feed. Suas ações e atividade dos seus repositórios vigiados aparecerão aqui.",
|
||||
"home.welcome.activity_hint": "Ainda não tem nada no seu feed. Suas Actions e atividade dos seus repositórios vigiados aparecerão aqui.",
|
||||
"home.explore_repos": "Explorar repositórios",
|
||||
"home.explore_users": "Explorar usuários",
|
||||
"home.explore_orgs": "Explorar organizações",
|
||||
|
|
@ -140,7 +140,7 @@
|
|||
"migrate.pagure.incorrect_url": "Uma URL incorreta do repositório fonte foi fornecida",
|
||||
"migrate.pagure.project_example": "URL do projeto Pagure, por exemplo: https://pagure.io/pagure",
|
||||
"migrate.pagure.project_url": "URL do projeto Pagure",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_label": "Token de API Pagure",
|
||||
"migrate.pagure.token_body_b": "Certifique-se de definir o sinalizador de repositório privado acima se você deseja que este repositório seja privado",
|
||||
"admin.config.global_2fa_requirement.title": "Exigência global de segundo fator",
|
||||
"admin.config.global_2fa_requirement.none": "Não",
|
||||
|
|
@ -156,5 +156,12 @@
|
|||
"migrate.form.error.url_credentials": "A URL contém credenciais, coloque-as nos campos de usuário e senha respectivamente",
|
||||
"actions.runs.viewing_out_of_date_run": "Você está visualizando uma execução desatualizada deste trabalho que foi executada %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Ver execução mais recente",
|
||||
"actions.runs.run_attempt_label": "Tentativa de execução #%[1]s (%[2]s)"
|
||||
"actions.runs.run_attempt_label": "Tentativa de execução #%[1]s (%[2]s)",
|
||||
"migrate.pagure.private_issues.summary": "Questões Privadas (Opcional)",
|
||||
"migrate.pagure.private_issues.description": "Esta funcionalidade foi projetada para criar um segundo repositório contendo somente questões privadas do seu projeto Pagure para a finalidade de arquivo. Primeiro, faça uma migração normal (sem token) para importar todo o conteúdo público. Depois, se você tem questões privadas que deseja preservar, crie um repositório separado usando esta opção de token para arquivar estas questões privadas.",
|
||||
"migrate.pagure.private_issues.warning": "Certifique-se de definir a visibilidade do repositório acima para Privado se você está usando a chave de API para importar questões privadas. Isso evita expor acidentalmente conteúdo privado em um repositório público.",
|
||||
"migrate.pagure.token.placeholder": "Somente para criar um arquivo de questões privadas",
|
||||
"actions.workflow.job_parsing_error": "Não foi possível processar alguns trabalhos do workflow: %v",
|
||||
"actions.workflow.event_detection_error": "Não foi possível processar eventos suportados no workflow: %v",
|
||||
"actions.workflow.pre_execution_error": "Workflow não foi executado devido a um erro que bloqueou a tentativa de execução."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
"migrate.pagure.incorrect_url": "Foi fornecida uma URL incorreta do repositório de origem",
|
||||
"migrate.pagure.project_url": "URL do projeto Pagure",
|
||||
"migrate.pagure.project_example": "URL do projeto Pagure, por exemplo, https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Token",
|
||||
"migrate.pagure.token_label": "Código da API do Pagure",
|
||||
"migrate.pagure.token_body_a": "Forneça um token da API Pagure com acesso às questões privadas para criar um repositório contendo apenas as questões privadas",
|
||||
"migrate.pagure.token_body_b": "Certifique-se de definir o marcador de repositório privado acima se desejar que este repositório seja privado",
|
||||
"migrate.github.description": "Migrar dados do github.com ou do GitHub Enterprise server.",
|
||||
|
|
@ -156,5 +156,12 @@
|
|||
"user.ghost.tooltip": "Este utilizador foi eliminado ou não é possível encontrar uma correspondência.",
|
||||
"actions.runs.run_attempt_label": "Tentativa de execução #%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "Está a visualizar uma execução desatualizada deste trabalho que foi executado %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Ver execução mais recente"
|
||||
"actions.runs.view_most_recent_run": "Ver execução mais recente",
|
||||
"migrate.pagure.private_issues.summary": "Questões privadas (opcional)",
|
||||
"migrate.pagure.private_issues.description": "Esta funcionalidade está desenhada para criar um segundo repositório contendo apenas questões privadas do seu repositório Pagure para efeitos de arquivo. Primeiro, faça uma migração normal (sem um código) para importar todo o conteúdo público. Depois, se tiver questões privadas a preservar, crie um repositório separado usando esta opção com código para arquivar essas questões privadas.",
|
||||
"migrate.pagure.private_issues.warning": "Certifique-se que passa a visibilidade do repositório para Privado se estiver a usar a chave de API para importar questões privadas. Isso irá evitar que exponha acidentalmente conteúdo privado num repositório público.",
|
||||
"migrate.pagure.token.placeholder": "Somente para criar arquivo com questões privadas",
|
||||
"actions.workflow.job_parsing_error": "Não foi possível analisar os trabalhos na sequência de trabalho: %v",
|
||||
"actions.workflow.event_detection_error": "Não foi possível analisar eventos suportados na sequência de trabalho: %v",
|
||||
"actions.workflow.pre_execution_error": "A sequência de trabalho não foi executada por causa de um erro que bloqueou a tentativa de execução."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"alert.asset_load_failed": "Не удалось получить ресурсы из {path}. Убедитесь, что файлы ресурсов доступны.",
|
||||
"install.invalid_lfs_path": "Не удалось расположить корень LFS по указанному пути: %[1]s",
|
||||
"alert.range_error": " - число должно быть в диапазоне от %[1]s-%[2]s.",
|
||||
"meta.last_line": "This magic string will cause Codeberg Translate to create a new pull request in the Forgejo repository. Test.",
|
||||
"meta.last_line": "This magic string will cause Codeberg Translate to create a new pull request in the Forgejo repository.",
|
||||
"mail.actions.not_successful_run_subject": "Провал раб. потока %[1]s в репозитории %[2]s",
|
||||
"mail.actions.successful_run_after_failure_subject": "Возобновление раб. потока %[1]s в репозитории %[2]s",
|
||||
"mail.actions.run_info_trigger": "Причина срабатывания: %[1]s by: %[2]s",
|
||||
|
|
@ -140,7 +140,7 @@
|
|||
"migrate.pagure.incorrect_url": "Введена неправильная ссылка на источник",
|
||||
"migrate.pagure.project_url": "Ссылка проекта на Pagure",
|
||||
"migrate.pagure.project_example": "Ссылка проекта. Например, https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Токен",
|
||||
"migrate.pagure.token_label": "Токен API Pagure",
|
||||
"migrate.pagure.token_body_a": "Если предоставить токен API Pagure с доступом к секретным задачам проекта, будет создан отдельный репозиторий, в котором они будут размещены",
|
||||
"migrate.pagure.token_body_b": "Поставьте флажок на пункте видимости репозитория выше, если хотите, чтобы этот репозиторий был частным",
|
||||
"migrate.github.description": "Перенести данные с github.com или сервера GitHub Enterprise.",
|
||||
|
|
@ -156,5 +156,12 @@
|
|||
"migrate.form.error.url_credentials": "Ссылка содержит данные авторизации. Поместите их в соответствующие поля",
|
||||
"actions.runs.run_attempt_label": "Попытка №%[1]s (%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "Это устаревший результат. Задание было выполнено %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Перейти к актуальному"
|
||||
"actions.runs.view_most_recent_run": "Перейти к актуальному",
|
||||
"migrate.pagure.private_issues.summary": "Скрытые задачи (необязательно)",
|
||||
"migrate.pagure.private_issues.description": "Эта функция позволяет создать дополнительный репозиторий для архивации в нём скрытых задач из вашего проекта Pagure. Сперва выполните нормальный перенос репозитория без токена, чтобы скопировать всё публичное содержимое. Затем, если нужно, выполните ещё один перенос и укажите токен, чтобы разместить скрытые задачи в дополнительном репозитории.",
|
||||
"migrate.pagure.private_issues.warning": "Не забудьте указать видимость репозитория как Частную при переносе скрытых задач, чтобы их никто не увидел.",
|
||||
"migrate.pagure.token.placeholder": "Только для переноса скрытых задач",
|
||||
"actions.workflow.job_parsing_error": "Не удалось прочитать задачи в раб. потоке: %v",
|
||||
"actions.workflow.event_detection_error": "Не удалось прочитать поддерживаемые события в раб. потоке: %v",
|
||||
"actions.workflow.pre_execution_error": "Рабочий поток не был выполнен из-за ошибки, возникшей при попытке начать выполнение."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
"moderation.abuse_category.placeholder": "Välj en kategori",
|
||||
"moderation.abuse_category.spam": "Skräppost",
|
||||
"moderation.abuse_category.malware": "Skadlig kod",
|
||||
"settings.visibility.description": "Profilens synlighet påverkar andras möjlighet att komma åt dina icke-privata förråd. <a href=\"%s\" target=\"_blank\">Läs mer</a>",
|
||||
"settings.visibility.description": "Profilens synlighet påverkar andras möjlighet att komma åt dina icke-privata förråd. <a href=\"%s\" target=\"_blank\">Läs mer</a>.",
|
||||
"avatar.constraints_hint": "Anpassade avatarer får inte vara större än %[1] eller %[2]dx%[3] bildpunkter",
|
||||
"og.repo.summary_card.alt_description": "Sammanfattningskort för arkivet %[1]s, beskrivet som: %[2]s",
|
||||
"profile.actions.tooltip": "Fler åtgärder",
|
||||
|
|
@ -102,5 +102,56 @@
|
|||
"keys.ssh.link": "SSH-nycklar",
|
||||
"repo.diff.commit.next-short": "Nästa",
|
||||
"repo.diff.commit.previous-short": "Föreg",
|
||||
"feed.atom.link": "Atom-flöde"
|
||||
"feed.atom.link": "Atom-flöde",
|
||||
"repo.pulls.already_merged": "Sammanfogning misslyckades: Denna pulka förfrågning har redan blivit sammanfogad.",
|
||||
"migrate.form.error.url_credentials": "URL:en innehåller inloggningsuppgifter, sätt dem i respektive användarnamn- och lösenordsfält",
|
||||
"migrate.github.description": "Migrera data från github.com eller GitHub Enterprise server.",
|
||||
"migrate.git.description": "Migrera endast utvecklingskatalogen från vilken Git-tjänst som helst.",
|
||||
"migrate.gitea.description": "Migrera data från gitea.com eller andra Gitea-instanser.",
|
||||
"migrate.gitlab.description": "Migrera data från gitlab.com eller annan GitLab-instans.",
|
||||
"migrate.gogs.description": "Migrera data från notabug.org eller annan Gogs-instans.",
|
||||
"migrate.onedev.description": "Migrera data från code.onedev.io eller annan OneDev-instans.",
|
||||
"migrate.gitbucket.description": "Migrera data från GitBucket-instans.",
|
||||
"migrate.codebase.description": "Migrera data från codebasehq.com.",
|
||||
"migrate.forgejo.description": "Migrera data från codeberg.org eller annan Forgejo-instans.",
|
||||
"repo.settings.push_mirror.branch_filter.label": "Grenfilter (frivilligt)",
|
||||
"repo.settings.push_mirror.branch_filter.description": "Grenar att speglas. Lämna tom för att spegla alla grenar. Se <a href=\"%[1]s\">%[2]s dokumentation</a> för syntax. Exempel: <code>main, release/*</code>",
|
||||
"admin.moderation.moderation_reports": "Moderationsrapporter",
|
||||
"admin.moderation.reports": "Rapporter",
|
||||
"admin.moderation.no_open_reports": "Det finns för tillfället inga öppna rapporter.",
|
||||
"mail.actions.run_info_sha": "Commit: %[1]s",
|
||||
"discussion.sidebar.reference": "Referens",
|
||||
"admin.auths.allow_username_change": "Tillåt användarnamnsändring",
|
||||
"admin.auths.allow_username_change.description": "Tillåt användare att ändra sitt användarnamn i profilinställningarna",
|
||||
"admin.dashboard.remove_resolved_reports": "Ta bort lösta rapporter",
|
||||
"admin.config.security": "Säkerhetskonfiguration",
|
||||
"admin.config.global_2fa_requirement.title": "Globalt tvåfaktorskrav",
|
||||
"admin.config.global_2fa_requirement.none": "Nej",
|
||||
"admin.config.global_2fa_requirement.all": "Alla användare",
|
||||
"admin.config.global_2fa_requirement.admin": "Administratörer",
|
||||
"settings.twofa_unroll_unavailable": "Tvåfaktorsautentisering krävs för ditt konto och kan inte inaktiveras.",
|
||||
"settings.must_enable_2fa": "Denna Forgejo-instans kräver användare att aktivera tvåfaktorsautentisering innan de kan komma at deras konto.",
|
||||
"error.must_enable_2fa": "Denna Forgejo-instans kräver användare att aktivera tvåfaktorsautentisering innan de kan komma åt deras konto. Aktivera det på: %s",
|
||||
"repo.commit.load_tags_failed": "Laddning av taggar misslyckades på grund av internt fel",
|
||||
"compare.branches.title": "Jämför grenar",
|
||||
"migrate.pagure.description": "Migrera data från pagure.io eller andra Pagure-instanser.",
|
||||
"migrate.pagure.project_url": "Pagure projekt-URL",
|
||||
"migrate.pagure.project_example": "Pagure-projektets URL, t.ex. https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "Pagure API-token",
|
||||
"actions.runs.run_attempt_label": "Kör försök #%[1]s (%[2]s)",
|
||||
"warning.repository.out_of_sync": "Databasrepresentationen av detta arkiv är inte synkroniserad. Om denna varning fortfarande visas efter att du har skickat en commit till detta arkiv, kontakta administratören.",
|
||||
"admin.moderation.deleted_content_ref": "Rapporterat innehåll med typ %[1]v och id %[2]d finns inte längre",
|
||||
"settings.twofa_reenroll": "Registrera om tvåfaktorsautentisering",
|
||||
"settings.twofa_reenroll.description": "Registrera om din tvåfaktorsautentisering",
|
||||
"user.ghost.tooltip": "Denna användare har tagits bort eller kan inte matchas.",
|
||||
"migrate.pagure.incorrect_url": "Felaktig URL till källarkivet har angetts",
|
||||
"migrate.pagure.private_issues.summary": "Privata ärenden (valfritt)",
|
||||
"migrate.pagure.private_issues.description": "Denna funktion är utformad för att skapa ett andra arkiv som endast innehåller privata ärenden från ditt Pagure-projekt för arkiveringsändamål. Utför först en normal migrering (utan token) för att importera allt publika innehåll. Om du har privata ärenden som du vill bevara skapar du sedan ett separat arkiv med hjälp av denna token-option för att arkivera dessa privata ärenden.",
|
||||
"migrate.pagure.private_issues.warning": "Se till att ställa in synligheten för arkivet ovan till Privat om du använder API-nyckeln för att importera privata ärenden. Detta förhindrar att privat innehåll av misstag exponeras i ett publikt arkiv.",
|
||||
"migrate.pagure.token.placeholder": "Endast för att skapa privata ärendearkiv",
|
||||
"actions.runs.viewing_out_of_date_run": "Du tittar på en föråldrad körning av detta jobb som utfördes %[1]s.",
|
||||
"actions.runs.view_most_recent_run": "Visa senaste körning",
|
||||
"actions.workflow.job_parsing_error": "Kunde inte tolka jobb i arbetsflöde: %v",
|
||||
"actions.workflow.event_detection_error": "Det går inte att tolka stödda händelser i arbetsflödet: %v",
|
||||
"actions.workflow.pre_execution_error": "Arbetsflödet kördes inte på grund av ett fel som blockerade körningsförsöket."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@
|
|||
"warning.repository.out_of_sync": "База даних цього репозиторію не синхронізована. Якщо ви знову бачите це попередження після відправлення коміту в цей репозиторій, зверніться до адміністратора.",
|
||||
"repo.pulls.already_merged": "Не вдалося об'єднати: цей запит на злиття вже об'єднано.",
|
||||
"migrate.pagure.description": "Перенести дані з pagure.io або інших екземплярів Pagure.",
|
||||
"migrate.pagure.token_label": "Токен",
|
||||
"migrate.pagure.token_label": "Токен API Pagure",
|
||||
"migrate.pagure.project_url": "URL-адреса проєкту Pagure",
|
||||
"migrate.pagure.project_example": "URL-адреса проєкту Pagure, наприклад, https://pagure.io/pagure",
|
||||
"migrate.pagure.token_body_b": "Якщо хочете зробити цей репозиторій приватним, обов'язково встановіть прапорець «Приватний репозиторій» вище",
|
||||
|
|
@ -156,5 +156,12 @@
|
|||
"migrate.form.error.url_credentials": "URL-адреса містить дані для входу, введіть їх у поля «Ім'я користувача» і «Пароль» відповідно",
|
||||
"actions.runs.run_attempt_label": "Спроба запуску №%[1]s (%[2]s)",
|
||||
"actions.runs.view_most_recent_run": "Переглянути останній запуск",
|
||||
"actions.runs.viewing_out_of_date_run": "Ви переглядаєте застарілий запуск цього завдання, який було виконано %[1]s."
|
||||
"actions.runs.viewing_out_of_date_run": "Ви переглядаєте застарілий запуск цього завдання, який було виконано %[1]s.",
|
||||
"migrate.pagure.private_issues.summary": "Приватні задачі (необов'язково)",
|
||||
"migrate.pagure.private_issues.description": "Ця функція дозволяє створити додатковий репозиторій для архівування в ньому приватних задач із вашого проєкту Pagure. Спочатку виконайте звичайну міграцію (без токена), щоб імпортувати весь публічний вміст. Потім, якщо у вас є приватні задачі, які потрібно зберегти, створіть окремий репозиторій, використовуючи токен, щоб архівувати ці приватні задачі.",
|
||||
"migrate.pagure.private_issues.warning": "Обов'язково встановіть видимість репозиторію на «Приватний», якщо використовуєте ключ API для імпорту приватних задач. Це запобігає випадковому розкриттю приватного вмісту в публічному репозиторії.",
|
||||
"migrate.pagure.token.placeholder": "Тільки для створення архіву приватних задач",
|
||||
"actions.workflow.job_parsing_error": "Не вдалося розібрати завдання в робочому потоці: %v",
|
||||
"actions.workflow.event_detection_error": "Не вдалося розібрати підтримувані події в робочому потоці: %v",
|
||||
"actions.workflow.pre_execution_error": "Спробу виконання робочого потоку було заблоковано через помилку."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,21 @@
|
|||
{}
|
||||
{
|
||||
"home.explore_repos": "Omborlarni kashf etish",
|
||||
"home.explore_users": "Foydalanuvchilarni kashf etish",
|
||||
"home.explore_orgs": "`tashkilotlarni` kashf etish",
|
||||
"stars.list.none": "Hech kim bu omborga yulduz bosmagan.",
|
||||
"watch.list.none": "Bu ombor hech kimning ko'ruv'ida emas",
|
||||
"followers.incoming.list.self.none": "Hech kim sizning profilingizga tirkalmagan.",
|
||||
"followers.incoming.list.none": "Hech kim bu foydalanuvchiga tirkalmagan.",
|
||||
"followers.outgoing.list.self.none": "Siz hech kimga tirkalmagansiz.",
|
||||
"followers.outgoing.list.none": "%s hech kimga tirkalmagan.",
|
||||
"relativetime.now": "hozir",
|
||||
"relativetime.future": "kelajakda",
|
||||
"relativetime.1day": "kecha",
|
||||
"relativetime.2days": "2 kun oldin",
|
||||
"relativetime.1week": "oxirgi hafta",
|
||||
"relativetime.2weeks": "ikki hafta oldin",
|
||||
"relativetime.1month": "oxirgi oy",
|
||||
"relativetime.2months": "ikki oy oldin",
|
||||
"relativetime.1year": "oxirgi yil",
|
||||
"relativetime.2years": "ikki yil oldin"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"alert.asset_load_failed": "无法从 {path} 加载资源文件。请确保资源文件可被访问。",
|
||||
"install.invalid_lfs_path": "无法在指定路径创建 LFS 根目录:%[1]s",
|
||||
"alert.range_error": " 必须是一个介于 %[1]s 和 %[2]s 之间的数字。",
|
||||
"meta.last_line": "感谢各位对Forgejo翻译的支持和帮助!不需要翻译这个。",
|
||||
"meta.last_line": "感谢各位对Forgejo翻译的支持和帮助!不需要翻译这个。(三维鱼)。",
|
||||
"mail.actions.successful_run_after_failure_subject": "仓库 %[2]s 中的工作流 %[1]s 已恢复",
|
||||
"mail.actions.not_successful_run_subject": "仓库 %[2]s 中的工作流 %[1]s 已失败",
|
||||
"mail.actions.successful_run_after_failure": "仓库 %[2]s 中的工作流 %[1]s 已恢复",
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
"migrate.pagure.incorrect_url": "提供了错误的源仓库URL",
|
||||
"migrate.pagure.project_url": "Pagure 项目 URL",
|
||||
"migrate.pagure.project_example": "Pagure 项目 URL,例如:https://pagure.io/pagure",
|
||||
"migrate.pagure.token_label": "令牌",
|
||||
"migrate.pagure.token_label": "Pagure API 令牌",
|
||||
"migrate.pagure.token_body_a": "提供可访问私有议题的 Pagure API 令牌以创建一个只有私有议题的仓库",
|
||||
"migrate.pagure.token_body_b": "如果你希望此仓库为私有,记得选择上面的私有仓库选项!",
|
||||
"repo.pulls.already_merged": "合并失败:此合并请求已被合并。",
|
||||
|
|
@ -124,5 +124,12 @@
|
|||
"migrate.form.error.url_credentials": "URL 包含凭据,请分别将凭据填入用户名和密码字段",
|
||||
"actions.runs.view_most_recent_run": "查看最新运行",
|
||||
"actions.runs.run_attempt_label": "运行尝试 #%[1]s(%[2]s)",
|
||||
"actions.runs.viewing_out_of_date_run": "您正在查看于 %[1]s 执行的过期运行。"
|
||||
"actions.runs.viewing_out_of_date_run": "您正在查看于 %[1]s 执行的过期运行。",
|
||||
"migrate.pagure.private_issues.summary": "私有议题(可选)",
|
||||
"migrate.pagure.private_issues.description": "此功能将创建一个单独的、只包含私有议题的仓库,以供存档。首先,不带令牌进行一次普通迁移以导入公开内容,然后若有需要,填写令牌并再次迁移以创建一个新的仓库并存档私有议题。",
|
||||
"migrate.pagure.private_issues.warning": "请确保在导入私有议题时将仓库可见性设置为私有,以避免意外将私有内容泄漏至公开仓库。",
|
||||
"migrate.pagure.token.placeholder": "仅用于创建私有议题归档",
|
||||
"actions.workflow.job_parsing_error": "无法解析工作流中的任务:%v",
|
||||
"actions.workflow.event_detection_error": "无法解析工作流中受支持的事件:%v",
|
||||
"actions.workflow.pre_execution_error": "因有错误阻止了执行尝试,工作流未被执行。"
|
||||
}
|
||||
|
|
|
|||
1
release-notes/9689.md
Normal file
1
release-notes/9689.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Uploaded avatar images can sometimes contain unexpected metadata such as the location where the image was created, or the device the image was created with, stored in a format called EXIF. Forgejo now removes EXIF data when custom user and repository images are uploaded in order to reduce the risk of personally identifiable information being leaked unexpectedly. A new CLI subcommand `forgejo doctor avatar-strip-exif` can be used to strip EXIF information from all existing avatars; we recommend that administrators run this command once after upgrade in order to minimize this risk for existing stored files.
|
||||
4
release-notes/9849.md
Normal file
4
release-notes/9849.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/8885844e72fedc3585a1f42f16c430ab0cbeb90f) prevent commit API from leaking user's hidden email address on valid GPG signed commits
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/449b5bf10e3ba4baf2ef33b535b5e8ba15711838) prevent writing to out-of-repo symlink destinations while evaluating template repos
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/afbf1efe028a33fb79c1af208d28a993db8ca2af) prevent .forgejo/template from being out-of-repo content
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/fa1a2ba669301238cf3da6a3e746912d76e47f36) return on error if an LFS token cannot be parsed
|
||||
|
|
@ -631,7 +631,7 @@ func CommonRoutes() *web.Route {
|
|||
baseURLPattern = regexp.MustCompile(`\A(.*?)\.repo\z`)
|
||||
uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`)
|
||||
baseRepoPattern = regexp.MustCompile(`(\S+)\.repo/(\S+)\/base/(\S+)`)
|
||||
rpmsRepoPattern = regexp.MustCompile(`(\S+)\.repo/(\S+)\.(\S+)\/([a-zA-Z0-9_-]+)-([\d.]+-[a-zA-Z0-9_.-]+)\.(\S+)\.rpm`)
|
||||
rpmsRepoPattern = regexp.MustCompile(`\A/(.+?)\.repo/([^/]+)/RPMS\.([^/]+)/(.+)-([0-9][^-]*-[^-]*)\.([^.]+)\.rpm\z`)
|
||||
)
|
||||
|
||||
r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func ForgotPasswdPost(ctx *context.Context) {
|
|||
email := ctx.FormString("email")
|
||||
ctx.Data["Email"] = email
|
||||
|
||||
u, err := user_model.GetUserByEmail(ctx, email)
|
||||
u, err := user_model.GetUserByEmailSimple(ctx, email)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ type ViewRunInfo struct {
|
|||
Done bool `json:"done"`
|
||||
Jobs []*ViewJob `json:"jobs"`
|
||||
Commit ViewCommit `json:"commit"`
|
||||
PreExecutionError string `json:"preExecutionError"`
|
||||
}
|
||||
|
||||
type ViewCurrentJob struct {
|
||||
|
|
@ -285,6 +286,7 @@ func getViewResponse(ctx *context_module.Context, req *ViewRequest, runIndex, jo
|
|||
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead of 'null' in json
|
||||
resp.State.Run.Status = run.Status.String()
|
||||
resp.State.Run.PreExecutionError = run.PreExecutionError
|
||||
|
||||
// It's possible for the run to be marked with a finalized status (eg. failure) because of a single job within the
|
||||
// run; eg. one job fails, the run fails. But other jobs can still be running. The frontend RepoActionView uses the
|
||||
|
|
|
|||
|
|
@ -172,7 +172,8 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
|
|||
ctx.ServerError("SearchVersions", err)
|
||||
return
|
||||
}
|
||||
for _, pv := range pvs[pcr.KeepCount:] {
|
||||
keep := min(len(pvs), pcr.KeepCount)
|
||||
for _, pv := range pvs[keep:] {
|
||||
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
|
||||
ctx.ServerError("ShouldBeSkipped", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/translation"
|
||||
"forgejo.org/modules/util"
|
||||
webhook_module "forgejo.org/modules/webhook"
|
||||
"forgejo.org/services/convert"
|
||||
|
|
@ -378,13 +379,25 @@ func handleWorkflows(
|
|||
continue
|
||||
}
|
||||
|
||||
jobs, err := jobParser(dwf.Content, jobparser.WithVars(vars))
|
||||
if err != nil {
|
||||
var jobs []*jobparser.SingleWorkflow
|
||||
if dwf.EventDetectionError != nil { // don't even bother trying to parse jobs due to event detection error
|
||||
tr := translation.NewLocale(input.Doer.Language)
|
||||
run.PreExecutionError = tr.TrString("actions.workflow.event_detection_error", dwf.EventDetectionError)
|
||||
run.Status = actions_model.StatusFailure
|
||||
log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
|
||||
jobs = []*jobparser.SingleWorkflow{{
|
||||
Name: dwf.EntryName,
|
||||
}}
|
||||
} else {
|
||||
jobs, err = jobParser(dwf.Content, jobparser.WithVars(vars))
|
||||
if err != nil {
|
||||
log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
|
||||
tr := translation.NewLocale(input.Doer.Language)
|
||||
run.PreExecutionError = tr.TrString("actions.workflow.job_parsing_error", err)
|
||||
run.Status = actions_model.StatusFailure
|
||||
jobs = []*jobparser.SingleWorkflow{{
|
||||
Name: dwf.EntryName,
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
// cancel running jobs if the event is push or pull_request_sync
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
|
|
@ -144,3 +145,88 @@ func Test_OpenForkPullRequestEvent(t *testing.T) {
|
|||
assert.Equal(t, webhook_module.HookEventPullRequest, runs[0].Event)
|
||||
assert.True(t, runs[0].IsForkPullRequest)
|
||||
}
|
||||
|
||||
func TestActionsPreExecutionErrorInvalidJobs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||
|
||||
commit := &git.Commit{
|
||||
ID: git.MustIDFromString("0000000000000000000000000000000000000000"),
|
||||
CommitMessage: "test",
|
||||
}
|
||||
detectedWorkflows := []*actions_module.DetectedWorkflow{
|
||||
{
|
||||
EntryName: "test.yml",
|
||||
TriggerEvent: &jobparser.Event{
|
||||
Name: "pull_request",
|
||||
},
|
||||
Content: []byte("{ on: pull_request, jobs: 'hello, I am the jobs!' }"),
|
||||
},
|
||||
}
|
||||
input := ¬ifyInput{
|
||||
Repo: repo,
|
||||
Doer: doer,
|
||||
Event: webhook_module.HookEventPullRequestSync,
|
||||
PullRequest: pr,
|
||||
Payload: &api.PullRequestPayload{},
|
||||
}
|
||||
|
||||
err := handleWorkflows(db.DefaultContext, detectedWorkflows, commit, input, "refs/head/main")
|
||||
require.NoError(t, err)
|
||||
|
||||
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, runs, 1)
|
||||
createdRun := runs[0]
|
||||
|
||||
assert.Equal(t, actions_model.StatusFailure, createdRun.Status)
|
||||
assert.Contains(t, createdRun.PreExecutionError, "actions.workflow.job_parsing_error%!(EXTRA *fmt.wrapError=")
|
||||
}
|
||||
|
||||
func TestActionsPreExecutionEventDetectionError(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||
|
||||
commit := &git.Commit{
|
||||
ID: git.MustIDFromString("0000000000000000000000000000000000000000"),
|
||||
CommitMessage: "test",
|
||||
}
|
||||
detectedWorkflows := []*actions_module.DetectedWorkflow{
|
||||
{
|
||||
EntryName: "test.yml",
|
||||
TriggerEvent: &jobparser.Event{
|
||||
Name: "pull_request",
|
||||
},
|
||||
Content: []byte("{ on: nothing, jobs: { j1: {} }}"),
|
||||
EventDetectionError: errors.New("nothing is not a valid event"),
|
||||
},
|
||||
}
|
||||
input := ¬ifyInput{
|
||||
Repo: repo,
|
||||
Doer: doer,
|
||||
Event: webhook_module.HookEventPullRequestSync,
|
||||
PullRequest: pr,
|
||||
Payload: &api.PullRequestPayload{},
|
||||
}
|
||||
|
||||
err := handleWorkflows(db.DefaultContext, detectedWorkflows, commit, input, "refs/head/main")
|
||||
require.NoError(t, err)
|
||||
|
||||
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, runs, 1)
|
||||
createdRun := runs[0]
|
||||
|
||||
assert.Equal(t, actions_model.StatusFailure, createdRun.Status)
|
||||
assert.Equal(t, "actions.workflow.event_detection_error%!(EXTRA *errors.errorString=nothing is not a valid event)", createdRun.PreExecutionError)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,6 @@ func (ctx *Context) validateTwoFactorRequirement() {
|
|||
hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
log.ErrorWithSkip(2, "Error getting 2fa: %s", err)
|
||||
// fallthrough to set the variables
|
||||
}
|
||||
ctx.Data["MustEnableTwoFactor"] = !hasTwoFactor
|
||||
ctx.Data["HideNavbarLinks"] = !hasTwoFactor
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerifi
|
|||
if verif.SigningUser != nil {
|
||||
commitVerification.Signer = &api.PayloadUser{
|
||||
Name: verif.SigningUser.Name,
|
||||
Email: verif.SigningUser.Email,
|
||||
Email: verif.SigningEmail,
|
||||
}
|
||||
}
|
||||
return commitVerification
|
||||
|
|
|
|||
105
services/convert/convert_test.go
Normal file
105
services/convert/convert_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
api "forgejo.org/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToVerification(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("models/fixtures/TestParseCommitWithSSHSignature")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Change the user's primary email address to ensure this value isn't ambiguous with any other return value from
|
||||
// signature verification.
|
||||
userModel := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
userModel.Email = "secret-email@example.com"
|
||||
db.GetEngine(t.Context()).ID(userModel.ID).Cols("email").Update(userModel)
|
||||
|
||||
t.Run("SSH Key Signature", func(t *testing.T) {
|
||||
commit := &git.Commit{
|
||||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||
author user2 <user2@example.com> 1699707877 +0100
|
||||
committer user2 <user2@example.com> 1699707877 +0100
|
||||
|
||||
Add content
|
||||
`,
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQBe2Fwk/FKY3SBCnG6jSYcO6ucyahp2SpQ/0P+otslzIHpWNW8cQ0fGLdhhaFynJXQ
|
||||
fs9cMpZVM9BfIKNUSO8QY=
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
},
|
||||
}
|
||||
commitVerification := ToVerification(t.Context(), commit)
|
||||
require.NotNil(t, commitVerification)
|
||||
assert.Equal(t, &api.PayloadCommitVerification{
|
||||
Verified: true,
|
||||
Reason: "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4",
|
||||
Signature: "-----BEGIN SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95\nf5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQBe2Fwk/FKY3SBCnG6jSYcO6ucyahp2SpQ/0P+otslzIHpWNW8cQ0fGLdhhaFynJXQ\nfs9cMpZVM9BfIKNUSO8QY=\n-----END SSH SIGNATURE-----\n",
|
||||
Signer: &api.PayloadUser{
|
||||
Name: "user2",
|
||||
Email: "user2@example.com", // expected email will match the commit's committer's email, regardless of `KeepEmailPrivate`.
|
||||
},
|
||||
Payload: "tree 853694aae8816094a0d875fee7ea26278dbf5d0f\nparent c2780d5c313da2a947eae22efd7dacf4213f4e7f\nauthor user2 <user2@example.com> 1699707877 +0100\ncommitter user2 <user2@example.com> 1699707877 +0100\n\nAdd content\n",
|
||||
}, commitVerification)
|
||||
})
|
||||
|
||||
t.Run("GPG Signature", func(t *testing.T) {
|
||||
commit := &git.Commit{
|
||||
ID: git.MustIDFromString("e20aa0bcd2878f65a93de68a3eed9045d6efdd74"),
|
||||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: `tree e20aa0bcd2878f65a93de68a3eed9045d6efdd74
|
||||
parent 5cd9b9847563eb730d63d23c1f1b84868e52ae7d
|
||||
author user2 <user2+committer@example.com> 1759956520 -0600
|
||||
committer user2 <user2+committer@example.com> 1759956520 -0600
|
||||
|
||||
Add content
|
||||
`,
|
||||
Signature: `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAABCgAdFiEEdlqhn25IEoMmvK5vmDaXTfEZWRMFAmjmzigACgkQmDaXTfEZ
|
||||
WROC4ggAs8mD8csA6FV5e2v/4HcxuaZKCN+D8Gvku2JUigODQCA+NOX0FF2jDnCh
|
||||
tXylBPB4HJw1spKkDLtOpnCUSOniBdl9NcZjnBt6sP/OSnEfLznXFra+9fCHzsu0
|
||||
9uhDn3Wn1iHWXQ2ZglUwVS0ja6pNgEip8wNZBysv8+XbO1CEEW0m7zQA6tunzIwp
|
||||
yiPZDUJrKtpKAK0+v19EccT2VjYAa+Vo+p3/E0piaTYNbsTqtFRy63tdjDkf+mo+
|
||||
l/PaPhrMqdnbxv3/sd/63VCNdvPH3f0+OuydcC7mXyysmvap99EC+QKnpsrm7RAP
|
||||
uf51WIBywxztet6vi+jYJK1jFoY4iA==
|
||||
=Lnrt
|
||||
-----END PGP SIGNATURE-----`,
|
||||
},
|
||||
}
|
||||
commitVerification := ToVerification(t.Context(), commit)
|
||||
require.NotNil(t, commitVerification)
|
||||
assert.Equal(t, &api.PayloadCommitVerification{
|
||||
Verified: true,
|
||||
Reason: "user2 / 9836974DF1195913",
|
||||
Signature: "-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCgAdFiEEdlqhn25IEoMmvK5vmDaXTfEZWRMFAmjmzigACgkQmDaXTfEZ\nWROC4ggAs8mD8csA6FV5e2v/4HcxuaZKCN+D8Gvku2JUigODQCA+NOX0FF2jDnCh\ntXylBPB4HJw1spKkDLtOpnCUSOniBdl9NcZjnBt6sP/OSnEfLznXFra+9fCHzsu0\n9uhDn3Wn1iHWXQ2ZglUwVS0ja6pNgEip8wNZBysv8+XbO1CEEW0m7zQA6tunzIwp\nyiPZDUJrKtpKAK0+v19EccT2VjYAa+Vo+p3/E0piaTYNbsTqtFRy63tdjDkf+mo+\nl/PaPhrMqdnbxv3/sd/63VCNdvPH3f0+OuydcC7mXyysmvap99EC+QKnpsrm7RAP\nuf51WIBywxztet6vi+jYJK1jFoY4iA==\n=Lnrt\n-----END PGP SIGNATURE-----",
|
||||
Signer: &api.PayloadUser{
|
||||
Name: "user2",
|
||||
Email: "user2+signingkey@example.com", // expected email will match the signing key's email
|
||||
},
|
||||
Payload: "tree e20aa0bcd2878f65a93de68a3eed9045d6efdd74\nparent 5cd9b9847563eb730d63d23c1f1b84868e52ae7d\nauthor user2 <user2+committer@example.com> 1759956520 -0600\ncommitter user2 <user2+committer@example.com> 1759956520 -0600\n\nAdd content\n",
|
||||
}, commitVerification)
|
||||
})
|
||||
}
|
||||
|
|
@ -580,9 +580,6 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
|
|||
}
|
||||
|
||||
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
|
||||
if !strings.Contains(tokenSHA, ".") {
|
||||
return nil, nil
|
||||
}
|
||||
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
|
|
@ -590,7 +587,7 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
|
|||
return setting.LFS.JWTSecretBytes, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
claims, claimsOk := token.Claims.(*Claims)
|
||||
|
|
|
|||
82
services/lfs/server_test.go
Normal file
82
services/lfs/server_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
perm_model "forgejo.org/models/perm"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/services/contexttest"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
type authTokenOptions struct {
|
||||
Op string
|
||||
UserID int64
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
func getLFSAuthTokenWithBearer(opts authTokenOptions) (string, error) {
|
||||
now := time.Now()
|
||||
claims := Claims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
},
|
||||
RepoID: opts.RepoID,
|
||||
Op: opts.Op,
|
||||
UserID: opts.UserID,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign LFS JWT token: %w", err)
|
||||
}
|
||||
return "Bearer " + tokenString, nil
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
token2, _ := getLFSAuthTokenWithBearer(authTokenOptions{Op: "download", UserID: 2, RepoID: 1})
|
||||
_, token2, _ = strings.Cut(token2, " ")
|
||||
ctx, _ := contexttest.MockContext(t, "/")
|
||||
|
||||
t.Run("handleLFSToken", func(t *testing.T) {
|
||||
u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, u)
|
||||
|
||||
u, err = handleLFSToken(ctx, "invalid", repo1, perm_model.AccessModeRead)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, u)
|
||||
|
||||
u, err = handleLFSToken(ctx, token2, repo1, perm_model.AccessModeRead)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 2, u.ID)
|
||||
})
|
||||
|
||||
t.Run("authenticate", func(t *testing.T) {
|
||||
const prefixBearer = "Bearer "
|
||||
assert.False(t, authenticate(ctx, repo1, "", true, false))
|
||||
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
|
||||
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false))
|
||||
})
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"forgejo.org/modules/log"
|
||||
base "forgejo.org/modules/migration"
|
||||
"forgejo.org/modules/proxy"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
|
@ -279,6 +280,11 @@ func (d *PagureDownloader) callAPI(endpoint string, parameter map[string]string,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// pagure.io is protected by Anubis and requires proper headers
|
||||
req.Header.Add("Accept", "*/*")
|
||||
req.Header.Add("User-Agent", "Forgejo/"+setting.AppVer)
|
||||
|
||||
if d.privateIssuesOnlyRepo {
|
||||
req.Header.Set("Authorization", "token "+d.token)
|
||||
}
|
||||
|
|
@ -344,7 +350,7 @@ func (d *PagureDownloader) GetMilestones() ([]*base.Milestone, error) {
|
|||
func (d *PagureDownloader) GetLabels() ([]*base.Label, error) {
|
||||
rawLabels := PagureLabelsList{}
|
||||
|
||||
err := d.callAPI("/api/0/"+d.repoName+"/tags", nil, &rawLabels)
|
||||
err := d.callAPI("/api/0/"+d.repoName+"/tags/", nil, &rawLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
|
|||
return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err)
|
||||
}
|
||||
versionDeleted := false
|
||||
for _, pv := range pvs[pcr.KeepCount:] {
|
||||
keep := min(len(pvs), pcr.KeepCount)
|
||||
for _, pv := range pvs[keep:] {
|
||||
if pcr.Type == packages_model.TypeContainer {
|
||||
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
|
|||
// If the line got changed the comment is going to be invalidated.
|
||||
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *git.Repository, branch string) error {
|
||||
// FIXME differentiate between previous and proposed line
|
||||
commit, err := repo.LineBlame(branch, c.TreePath, c.UnsignedLine())
|
||||
commit, _, err := repo.LineBlame(branch, c.TreePath, c.UnsignedLine())
|
||||
if err != nil && (errors.Is(err, git.ErrBlameFileDoesNotExist) || errors.Is(err, git.ErrBlameFileNotEnoughLines)) {
|
||||
c.Invalidated = true
|
||||
return issues_model.UpdateCommentInvalidate(ctx, c)
|
||||
|
|
@ -178,7 +178,8 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
|
|||
|
||||
// CreateCodeCommentKnownReviewID creates a plain code comment at the specified line / path
|
||||
func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) {
|
||||
var commitID, patch string
|
||||
var commitID, blamedCommitID, patch string
|
||||
blamedLine := line
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadPullRequest: %w", err)
|
||||
}
|
||||
|
|
@ -226,12 +227,15 @@ func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User,
|
|||
// FIXME validate treePath
|
||||
// Get latest commit referencing the commented line
|
||||
// No need for get commit for base branch changes
|
||||
commit, err := gitRepo.LineBlame(head, treePath, uint64(line))
|
||||
commit, lineres, err := gitRepo.LineBlame(head, treePath, uint64(line))
|
||||
if err == nil {
|
||||
commitID = commit.ID.String()
|
||||
blamedCommitID = commit.ID.String()
|
||||
blamedLine = int64(lineres)
|
||||
} else if !errors.Is(err, git.ErrBlameFileDoesNotExist) && !errors.Is(err, git.ErrBlameFileNotEnoughLines) {
|
||||
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
|
||||
}
|
||||
} else {
|
||||
blamedCommitID = commitID
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -243,6 +247,9 @@ func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User,
|
|||
return nil, fmt.Errorf("GetRefCommitID[%s]: %w", head, err)
|
||||
}
|
||||
}
|
||||
if len(blamedCommitID) == 0 {
|
||||
blamedCommitID = commitID
|
||||
}
|
||||
reader, writer := io.Pipe()
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
|
|
@ -268,9 +275,9 @@ func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User,
|
|||
Repo: repo,
|
||||
Issue: issue,
|
||||
Content: content,
|
||||
LineNum: line,
|
||||
LineNum: blamedLine,
|
||||
TreePath: treePath,
|
||||
CommitSHA: commitID,
|
||||
CommitSHA: blamedCommitID,
|
||||
ReviewID: reviewID,
|
||||
Patch: patch,
|
||||
Invalidated: invalidated,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
|
|||
func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
username := "user2"
|
||||
unadoptedList := []string{path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
|
||||
unadoptedList := []string{path.Join(username, "rendering-test"), path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
|
||||
for _, unadopted := range unadoptedList {
|
||||
_ = os.Mkdir(path.Join(setting.RepoRootPath, unadopted+".git"), 0o755)
|
||||
}
|
||||
|
|
@ -77,13 +77,13 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
|
|||
opts := db.ListOptions{Page: 1, PageSize: 1}
|
||||
repoNames, count, err := ListUnadoptedRepositories(db.DefaultContext, "", &opts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, 3, count)
|
||||
assert.Equal(t, unadoptedList[0], repoNames[0])
|
||||
|
||||
opts = db.ListOptions{Page: 2, PageSize: 1}
|
||||
repoNames, count, err = ListUnadoptedRepositories(db.DefaultContext, "", &opts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, 3, count)
|
||||
assert.Equal(t, unadoptedList[1], repoNames[0])
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ func TestAdoptRepository(t *testing.T) {
|
|||
path.Join(setting.RepoRootPath, username, unadopted+".git"),
|
||||
))
|
||||
|
||||
opts := db.ListOptions{Page: 1, PageSize: 1}
|
||||
opts := db.ListOptions{Page: 2, PageSize: 1}
|
||||
repoNames, _, err := ListUnadoptedRepositories(db.DefaultContext, "", &opts)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, repoNames, path.Join(username, unadopted))
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ type ChangeRepoFile struct {
|
|||
ContentReader io.ReadSeeker
|
||||
SHA string
|
||||
Options *RepoFileOptions
|
||||
Symlink bool
|
||||
}
|
||||
|
||||
// ChangeRepoFilesOptions holds the repository files update options
|
||||
|
|
@ -62,6 +63,7 @@ type RepoFileOptions struct {
|
|||
treePath string
|
||||
fromTreePath string
|
||||
executable bool
|
||||
symlink bool
|
||||
}
|
||||
|
||||
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
|
||||
|
|
@ -116,6 +118,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||
treePath: treePath,
|
||||
fromTreePath: fromTreePath,
|
||||
executable: false,
|
||||
symlink: file.Symlink,
|
||||
}
|
||||
treePaths = append(treePaths, treePath)
|
||||
}
|
||||
|
|
@ -427,15 +430,15 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
|
|||
}
|
||||
|
||||
// Add the object to the index
|
||||
mode := "100644" // regular file
|
||||
if file.Options.executable {
|
||||
if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil {
|
||||
return err
|
||||
mode = "100755"
|
||||
} else if file.Options.symlink {
|
||||
mode = "120644"
|
||||
}
|
||||
} else {
|
||||
if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil {
|
||||
if err := t.AddObjectToIndex(mode, objectHash, file.Options.treePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if lfsMetaObject != nil {
|
||||
// We have an LFS object - create it
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -121,143 +119,6 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
|
|||
return gt.globs
|
||||
}
|
||||
|
||||
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
|
||||
configDirs := []string{".forgejo", ".gitea"}
|
||||
var templateFilePath string
|
||||
|
||||
for _, dir := range configDirs {
|
||||
candidatePath := filepath.Join(tmpDir, dir, "template")
|
||||
if _, err := os.Stat(candidatePath); err == nil {
|
||||
templateFilePath = candidatePath
|
||||
break
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if templateFilePath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(templateFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GiteaTemplate{
|
||||
Path: templateFilePath,
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
|
||||
commitTimeStr := time.Now().Format(time.RFC3339)
|
||||
authorSig := repo.Owner.NewGitSig()
|
||||
|
||||
// Because this may call hooks we should pass in the environment
|
||||
env := append(os.Environ(),
|
||||
"GIT_AUTHOR_NAME="+authorSig.Name,
|
||||
"GIT_AUTHOR_EMAIL="+authorSig.Email,
|
||||
"GIT_AUTHOR_DATE="+commitTimeStr,
|
||||
"GIT_COMMITTER_NAME="+authorSig.Name,
|
||||
"GIT_COMMITTER_EMAIL="+authorSig.Email,
|
||||
"GIT_COMMITTER_DATE="+commitTimeStr,
|
||||
)
|
||||
|
||||
// Clone to temporary path and do the init commit.
|
||||
templateRepoPath := templateRepo.RepoPath()
|
||||
if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
|
||||
Depth: 1,
|
||||
Branch: templateRepo.DefaultBranch,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("git clone: %w", err)
|
||||
}
|
||||
|
||||
if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
|
||||
return fmt.Errorf("remove git dir: %w", err)
|
||||
}
|
||||
|
||||
// Variable expansion
|
||||
gt, err := checkGiteaTemplate(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checkGiteaTemplate: %w", err)
|
||||
}
|
||||
|
||||
if gt != nil {
|
||||
if err := util.Remove(gt.Path); err != nil {
|
||||
return fmt.Errorf("remove .giteatemplate: %w", err)
|
||||
}
|
||||
|
||||
// Avoid walking tree if there are no globs
|
||||
if len(gt.Globs()) > 0 {
|
||||
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
|
||||
if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
|
||||
for _, g := range gt.Globs() {
|
||||
if g.Match(base) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path,
|
||||
[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
|
||||
0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
substPath := filepath.FromSlash(filepath.Join(tmpDirSlash,
|
||||
generateExpansion(base, templateRepo, generateRepo, true)))
|
||||
|
||||
// Create parent subdirectories if needed or continue silently if it exists
|
||||
if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Substitute filename variables
|
||||
if err := os.Rename(path, substPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoPath := repo.RepoPath()
|
||||
if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath).
|
||||
SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
|
||||
RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
|
||||
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
|
||||
return fmt.Errorf("git remote add: %w", err)
|
||||
}
|
||||
|
||||
// set default branch based on whether it's specified in the newly generated repo or not
|
||||
defaultBranch := repo.DefaultBranch
|
||||
if strings.TrimSpace(defaultBranch) == "" {
|
||||
defaultBranch = templateRepo.DefaultBranch
|
||||
}
|
||||
|
||||
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
|
||||
}
|
||||
|
||||
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
|
||||
if err != nil {
|
||||
|
|
|
|||
178
services/repository/generate_repo_commit.go
Normal file
178
services/repository/generate_repo_commit.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build go1.25
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
|
||||
commitTimeStr := time.Now().Format(time.RFC3339)
|
||||
authorSig := repo.Owner.NewGitSig()
|
||||
|
||||
// Because this may call hooks we should pass in the environment
|
||||
env := append(os.Environ(),
|
||||
"GIT_AUTHOR_NAME="+authorSig.Name,
|
||||
"GIT_AUTHOR_EMAIL="+authorSig.Email,
|
||||
"GIT_AUTHOR_DATE="+commitTimeStr,
|
||||
"GIT_COMMITTER_NAME="+authorSig.Name,
|
||||
"GIT_COMMITTER_EMAIL="+authorSig.Email,
|
||||
"GIT_COMMITTER_DATE="+commitTimeStr,
|
||||
)
|
||||
|
||||
// Clone to temporary path and do the init commit.
|
||||
templateRepoPath := templateRepo.RepoPath()
|
||||
if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
|
||||
Depth: 1,
|
||||
Branch: templateRepo.DefaultBranch,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("git clone: %w", err)
|
||||
}
|
||||
|
||||
if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
|
||||
return fmt.Errorf("remove git dir: %w", err)
|
||||
}
|
||||
|
||||
// Variable expansion
|
||||
gt, err := checkGiteaTemplate(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checkGiteaTemplate: %w", err)
|
||||
}
|
||||
|
||||
if gt != nil {
|
||||
if err := util.Remove(gt.Path); err != nil {
|
||||
return fmt.Errorf("remove .giteatemplate: %w", err)
|
||||
}
|
||||
|
||||
// Avoid walking tree if there are no globs
|
||||
if len(gt.Globs()) > 0 {
|
||||
// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
|
||||
root, err := os.OpenRoot(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open root: %w", err)
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
|
||||
if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
|
||||
for _, g := range gt.Globs() {
|
||||
if g.Match(base) {
|
||||
// `path` will be an absolute filepath from `WalkDir`, but `os.Root` requires all accesses are
|
||||
// relative file paths from the root -- use `relPath` from here out.
|
||||
relPath, err := filepath.Rel(tmpDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := root.ReadFile(relPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := root.WriteFile(relPath,
|
||||
[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
|
||||
0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
substPath := generateExpansion(relPath, templateRepo, generateRepo, true)
|
||||
|
||||
// Create parent subdirectories if needed or continue silently if it exists
|
||||
if err := root.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Substitute filename variables
|
||||
if err := root.Rename(relPath, substPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoPath := repo.RepoPath()
|
||||
if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath).
|
||||
SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
|
||||
RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
|
||||
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
|
||||
return fmt.Errorf("git remote add: %w", err)
|
||||
}
|
||||
|
||||
// set default branch based on whether it's specified in the newly generated repo or not
|
||||
defaultBranch := repo.DefaultBranch
|
||||
if strings.TrimSpace(defaultBranch) == "" {
|
||||
defaultBranch = templateRepo.DefaultBranch
|
||||
}
|
||||
|
||||
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
|
||||
}
|
||||
|
||||
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
|
||||
configDirs := []string{".forgejo", ".gitea"}
|
||||
var templateFilePath string
|
||||
|
||||
// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
|
||||
root, err := os.OpenRoot(tmpDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open root: %w", err)
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
for _, dir := range configDirs {
|
||||
candidatePath := filepath.Join(dir, "template")
|
||||
if _, err := root.Stat(candidatePath); err == nil {
|
||||
templateFilePath = candidatePath
|
||||
break
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if templateFilePath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content, err := root.ReadFile(templateFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GiteaTemplate{
|
||||
Path: templateFilePath,
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue