mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-19 23:38:29 +02:00
Merge branch 'main' into add-file-tree-to-file-view-page
This commit is contained in:
commit
b1b24282ae
@ -674,7 +674,7 @@ module.exports = {
|
|||||||
'no-this-before-super': [2],
|
'no-this-before-super': [2],
|
||||||
'no-throw-literal': [2],
|
'no-throw-literal': [2],
|
||||||
'no-undef-init': [2],
|
'no-undef-init': [2],
|
||||||
'no-undef': [0],
|
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
|
||||||
'no-undefined': [0],
|
'no-undefined': [0],
|
||||||
'no-underscore-dangle': [0],
|
'no-underscore-dangle': [0],
|
||||||
'no-unexpected-multiline': [2],
|
'no-unexpected-multiline': [2],
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -9,6 +9,10 @@ _test
|
|||||||
|
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# IntelliJ Gateway
|
||||||
|
.uuid
|
||||||
|
|
||||||
# Goland's output filename can not be set manually
|
# Goland's output filename can not be set manually
|
||||||
/go_build_*
|
/go_build_*
|
||||||
/gitea_*
|
/gitea_*
|
||||||
|
@ -31,7 +31,6 @@ Gary Kim <gary@garykim.dev> (@gary-kim)
|
|||||||
Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
|
Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
|
||||||
Mura Li <typeless@ctli.io> (@typeless)
|
Mura Li <typeless@ctli.io> (@typeless)
|
||||||
6543 <6543@obermui.de> (@6543)
|
6543 <6543@obermui.de> (@6543)
|
||||||
jaqra <jaqra@hotmail.com> (@jaqra)
|
|
||||||
David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson)
|
David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson)
|
||||||
a1012112796 <1012112796@qq.com> (@a1012112796)
|
a1012112796 <1012112796@qq.com> (@a1012112796)
|
||||||
Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
|
Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
|
||||||
@ -63,3 +62,4 @@ Yu Liu <1240335630@qq.com> (@HEREYUA)
|
|||||||
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
|
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
|
||||||
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
|
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
|
||||||
hiifong <i@hiif.ong> (@hiifong)
|
hiifong <i@hiif.ong> (@hiifong)
|
||||||
|
metiftikci <metiftikci@hotmail.com> (@metiftikci)
|
||||||
|
10
Makefile
10
Makefile
@ -806,22 +806,22 @@ $(DIST_DIRS):
|
|||||||
|
|
||||||
.PHONY: release-windows
|
.PHONY: release-windows
|
||||||
release-windows: | $(DIST_DIRS)
|
release-windows: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
||||||
ifeq (,$(findstring gogit,$(TAGS)))
|
ifeq (,$(findstring gogit,$(TAGS)))
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: release-linux
|
.PHONY: release-linux
|
||||||
release-linux: | $(DIST_DIRS)
|
release-linux: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
|
||||||
|
|
||||||
.PHONY: release-darwin
|
.PHONY: release-darwin
|
||||||
release-darwin: | $(DIST_DIRS)
|
release-darwin: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
|
||||||
|
|
||||||
.PHONY: release-freebsd
|
.PHONY: release-freebsd
|
||||||
release-freebsd: | $(DIST_DIRS)
|
release-freebsd: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
|
||||||
|
|
||||||
.PHONY: release-copy
|
.PHONY: release-copy
|
||||||
release-copy: | $(DIST_DIRS)
|
release-copy: | $(DIST_DIRS)
|
||||||
|
60
assets/go-licenses.json
generated
60
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@ -69,6 +69,10 @@ var microcmdUserCreate = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runCreateUser(c *cli.Context) error {
|
func runCreateUser(c *cli.Context) error {
|
||||||
|
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
|
||||||
|
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
|
||||||
|
setting.LoadSettings()
|
||||||
|
|
||||||
if err := argsSet(c, "email"); err != nil {
|
if err := argsSet(c, "email"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,7 @@ func NewMainApp(appVer AppVersion) *cli.App {
|
|||||||
app.Commands = append(app.Commands, subCmdWithConfig...)
|
app.Commands = append(app.Commands, subCmdWithConfig...)
|
||||||
app.Commands = append(app.Commands, subCmdStandalone...)
|
app.Commands = append(app.Commands, subCmdStandalone...)
|
||||||
|
|
||||||
|
setting.InitGiteaEnvVars()
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -113,37 +112,17 @@ func TestCliCmd(t *testing.T) {
|
|||||||
_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
var envBackup []string
|
|
||||||
for _, s := range os.Environ() {
|
|
||||||
if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") {
|
|
||||||
envBackup = append(envBackup, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clearGiteaEnv := func() {
|
|
||||||
for _, s := range os.Environ() {
|
|
||||||
if strings.HasPrefix(s, "GITEA_") {
|
|
||||||
_ = os.Unsetenv(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
clearGiteaEnv()
|
|
||||||
for _, s := range envBackup {
|
|
||||||
k, v, _ := strings.Cut(s, "=")
|
|
||||||
_ = os.Setenv(k, v)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
clearGiteaEnv()
|
t.Run(c.cmd, func(t *testing.T) {
|
||||||
for k, v := range c.env {
|
for k, v := range c.env {
|
||||||
_ = os.Setenv(k, v)
|
t.Setenv(k, v)
|
||||||
}
|
}
|
||||||
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
||||||
r, err := runTestApp(app, args...)
|
r, err := runTestApp(app, args...)
|
||||||
assert.NoError(t, err, c.cmd)
|
assert.NoError(t, err, c.cmd)
|
||||||
assert.NotEmpty(t, c.exp, c.cmd)
|
assert.NotEmpty(t, c.exp, c.cmd)
|
||||||
assert.Contains(t, r.Stdout, c.exp, c.cmd)
|
assert.Contains(t, r.Stdout, c.exp, c.cmd)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
var CmdMigrate = &cli.Command{
|
var CmdMigrate = &cli.Command{
|
||||||
Name: "migrate",
|
Name: "migrate",
|
||||||
Usage: "Migrate the database",
|
Usage: "Migrate the database",
|
||||||
Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.",
|
Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`,
|
||||||
Action: runMigrate,
|
Action: runMigrate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
cmd/web.go
11
cmd/web.go
@ -12,6 +12,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
||||||
|
|
||||||
@ -115,6 +116,16 @@ func showWebStartupMessage(msg string) {
|
|||||||
log.Info("* CustomPath: %s", setting.CustomPath)
|
log.Info("* CustomPath: %s", setting.CustomPath)
|
||||||
log.Info("* ConfigFile: %s", setting.CustomConf)
|
log.Info("* ConfigFile: %s", setting.CustomConf)
|
||||||
log.Info("%s", msg) // show startup message
|
log.Info("%s", msg) // show startup message
|
||||||
|
|
||||||
|
if setting.CORSConfig.Enabled {
|
||||||
|
log.Info("CORS Service Enabled")
|
||||||
|
}
|
||||||
|
if setting.DefaultUILocation != time.Local {
|
||||||
|
log.Info("Default UI Location is %v", setting.DefaultUILocation.String())
|
||||||
|
}
|
||||||
|
if setting.MailService != nil {
|
||||||
|
log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveInstall(ctx *cli.Context) error {
|
func serveInstall(ctx *cli.Context) error {
|
||||||
|
@ -54,7 +54,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
altTLSALPNPort = p
|
altTLSALPNPort = p
|
||||||
}
|
}
|
||||||
|
|
||||||
magic := certmagic.NewDefault()
|
magic := &certmagic.Default
|
||||||
magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
|
magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
|
||||||
// Try to use private CA root if provided, otherwise defaults to system's trust
|
// Try to use private CA root if provided, otherwise defaults to system's trust
|
||||||
var certPool *x509.CertPool
|
var certPool *x509.CertPool
|
||||||
|
@ -1485,6 +1485,10 @@ LEVEL = Info
|
|||||||
;REPO_INDEXER_EXCLUDE =
|
;REPO_INDEXER_EXCLUDE =
|
||||||
;;
|
;;
|
||||||
;MAX_FILE_SIZE = 1048576
|
;MAX_FILE_SIZE = 1048576
|
||||||
|
;;
|
||||||
|
;; Bleve engine has performance problems with fuzzy search, so we limit the fuzziness to 0 by default to disable it.
|
||||||
|
;; If you'd like to enable it, you can set it to a value between 0 and 2.
|
||||||
|
;TYPE_BLEVE_MAX_FUZZINESS = 0
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
10
go.mod
10
go.mod
@ -28,7 +28,6 @@ require (
|
|||||||
github.com/PuerkitoBio/goquery v1.10.0
|
github.com/PuerkitoBio/goquery v1.10.0
|
||||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
|
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0
|
github.com/alecthomas/chroma/v2 v2.14.0
|
||||||
github.com/aws/aws-sdk-go v1.55.5
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.42
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.42
|
||||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3
|
github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||||
@ -61,7 +60,6 @@ require (
|
|||||||
github.com/go-redsync/redsync/v4 v4.13.0
|
github.com/go-redsync/redsync/v4 v4.13.0
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/go-swagger/go-swagger v0.31.0
|
github.com/go-swagger/go-swagger v0.31.0
|
||||||
github.com/go-testfixtures/testfixtures/v3 v3.11.0
|
|
||||||
github.com/go-webauthn/webauthn v0.11.2
|
github.com/go-webauthn/webauthn v0.11.2
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||||
@ -145,8 +143,6 @@ require (
|
|||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||||
github.com/ClickHouse/ch-go v0.63.1 // indirect
|
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.24.0 // indirect
|
|
||||||
github.com/DataDog/zstd v1.5.6 // indirect
|
github.com/DataDog/zstd v1.5.6 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||||
@ -204,8 +200,6 @@ require (
|
|||||||
github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568 // indirect
|
github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||||
github.com/go-faster/city v1.0.1 // indirect
|
|
||||||
github.com/go-faster/errors v0.7.1 // indirect
|
|
||||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
@ -270,7 +264,6 @@ require (
|
|||||||
github.com/oklog/ulid v1.3.1 // indirect
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
github.com/paulmach/orb v0.11.1 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
@ -285,7 +278,6 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
@ -310,8 +302,6 @@ require (
|
|||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.etcd.io/bbolt v1.3.11 // indirect
|
go.etcd.io/bbolt v1.3.11 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.1 // indirect
|
go.mongodb.org/mongo-driver v1.17.1 // indirect
|
||||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
|
62
go.sum
62
go.sum
@ -59,10 +59,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS
|
|||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM=
|
|
||||||
github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0=
|
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.24.0 h1:L/n/pVVpk95KtkHOiKuSnO7cu2ckeW4gICbbOh5qs74=
|
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.24.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ=
|
|
||||||
github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
|
github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
|
||||||
github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||||
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
|
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
|
||||||
@ -109,8 +105,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
|||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
|
||||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk=
|
github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
|
github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM=
|
||||||
@ -237,8 +231,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||||
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
|
|
||||||
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
|
||||||
@ -317,10 +309,6 @@ github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqV
|
|||||||
github.com/go-enry/go-enry/v2 v2.9.1/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
|
github.com/go-enry/go-enry/v2 v2.9.1/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 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
|
||||||
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
||||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
|
||||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
|
||||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
|
||||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
|
||||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
|
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
|
||||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
@ -372,8 +360,6 @@ github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/
|
|||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/go-testfixtures/testfixtures/v3 v3.11.0 h1:XxQr8AnPORcZkyNd7go5UNLPD3dULN8ixYISlzrlfEQ=
|
|
||||||
github.com/go-testfixtures/testfixtures/v3 v3.11.0/go.mod h1:THmudHF1Ixq++J2/UodcJpxUphfyEd77m83TvDtryqE=
|
|
||||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||||
github.com/go-webauthn/x v0.1.15 h1:eG1OhggBJTkDE8gUeOlGRbRe8E/PSVG26YG4AyFbwkU=
|
github.com/go-webauthn/x v0.1.15 h1:eG1OhggBJTkDE8gUeOlGRbRe8E/PSVG26YG4AyFbwkU=
|
||||||
@ -385,7 +371,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
|
|||||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
|
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
|
||||||
@ -410,7 +395,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
|||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
@ -497,22 +481,6 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
|
|||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
|
||||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
|
||||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
|
||||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
|
||||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
|
||||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
|
|
||||||
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
|
||||||
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
|
|
||||||
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
|
||||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
@ -533,10 +501,6 @@ github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bB
|
|||||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||||
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
|
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
|
||||||
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
|
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
@ -550,11 +514,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
|
|||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4=
|
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4=
|
||||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
|
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
@ -629,7 +590,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
|
||||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
|
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
|
||||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
@ -670,9 +630,6 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
|
|||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
|
||||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
|
||||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
@ -735,8 +692,6 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng
|
|||||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||||
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
|
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
|
||||||
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
|
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
|
||||||
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
@ -783,7 +738,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@ -797,7 +751,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
|
||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||||
@ -823,9 +776,6 @@ github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+D
|
|||||||
github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
|
github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
|
||||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
|
||||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
@ -842,9 +792,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
|
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
|
||||||
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
|
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@ -863,13 +811,8 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l
|
|||||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
|
||||||
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
|
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
|
||||||
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
|
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
|
||||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
|
||||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
|
||||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
@ -941,7 +884,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
@ -1022,10 +964,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
@ -1046,8 +986,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
|||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *
|
|||||||
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
||||||
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
||||||
And("issue.is_pull = ?", false).
|
And("issue.is_pull = ?", false).
|
||||||
And("issue.created_unix >= ?", fromTime.Unix()).
|
And(builder.Or(
|
||||||
Or("issue.closed_unix >= ?", fromTime.Unix())
|
builder.Gte{"issue.created_unix": fromTime.Unix()},
|
||||||
|
builder.Gte{"issue.closed_unix": fromTime.Unix()},
|
||||||
|
))
|
||||||
|
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
@ -64,11 +64,9 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID})
|
||||||
|
|
||||||
doer := &user_model.User{ID: tc.doerID}
|
var doer *user_model.User
|
||||||
_, err := unittest.LoadBeanIfExists(doer)
|
if tc.doerID != 0 {
|
||||||
assert.NoError(t, err)
|
doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID})
|
||||||
if tc.doerID == 0 {
|
|
||||||
doer = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the action for comparison
|
// get the action for comparison
|
||||||
|
@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) {
|
|||||||
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
|
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
|
||||||
cred.SignCount = 1
|
cred.SignCount = 1
|
||||||
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
|
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
|
||||||
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
|
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
|
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
|
||||||
@ -52,7 +52,7 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
|
|||||||
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
|
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
|
||||||
cred.SignCount = 0xffffffff
|
cred.SignCount = 0xffffffff
|
||||||
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
|
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
|
||||||
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
|
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateCredential(t *testing.T) {
|
func TestCreateCredential(t *testing.T) {
|
||||||
@ -63,5 +63,5 @@ func TestCreateCredential(t *testing.T) {
|
|||||||
assert.Equal(t, "WebAuthn Created Credential", res.Name)
|
assert.Equal(t, "WebAuthn Created Credential", res.Name)
|
||||||
assert.Equal(t, []byte("Test"), res.CredentialID)
|
assert.Equal(t, []byte("Test"), res.CredentialID)
|
||||||
|
|
||||||
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
|
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,13 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument}
|
||||||
// ErrNameEmpty name is empty error
|
|
||||||
ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument}
|
|
||||||
|
|
||||||
// AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-)
|
|
||||||
AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNameReserved represents a "reserved name" error.
|
// ErrNameReserved represents a "reserved name" error.
|
||||||
type ErrNameReserved struct {
|
type ErrNameReserved struct {
|
||||||
@ -82,20 +75,20 @@ func (err ErrNameCharsNotAllowed) Unwrap() error {
|
|||||||
|
|
||||||
// IsUsableName checks if name is reserved or pattern of name is not allowed
|
// IsUsableName checks if name is reserved or pattern of name is not allowed
|
||||||
// based on given reserved names and patterns.
|
// based on given reserved names and patterns.
|
||||||
// Names are exact match, patterns can be prefix or suffix match with placeholder '*'.
|
// Names are exact match, patterns can be a prefix or suffix match with placeholder '*'.
|
||||||
func IsUsableName(names, patterns []string, name string) error {
|
func IsUsableName(reservedNames, reservedPatterns []string, name string) error {
|
||||||
name = strings.TrimSpace(strings.ToLower(name))
|
name = strings.TrimSpace(strings.ToLower(name))
|
||||||
if utf8.RuneCountInString(name) == 0 {
|
if utf8.RuneCountInString(name) == 0 {
|
||||||
return ErrNameEmpty
|
return ErrNameEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range names {
|
for i := range reservedNames {
|
||||||
if name == names[i] {
|
if name == reservedNames[i] {
|
||||||
return ErrNameReserved{name}
|
return ErrNameReserved{name}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pat := range patterns {
|
for _, pat := range reservedPatterns {
|
||||||
if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) ||
|
if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) ||
|
||||||
(pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) {
|
(pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) {
|
||||||
return ErrNamePatternNotAllowed{pat}
|
return ErrNamePatternNotAllowed{pat}
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
name: job2
|
name: job2
|
||||||
attempt: 1
|
attempt: 1
|
||||||
job_id: job2
|
job_id: job2
|
||||||
needs: [job1]
|
needs: '["job1"]'
|
||||||
task_id: 51
|
task_id: 51
|
||||||
status: 5
|
status: 5
|
||||||
started: 1683636528
|
started: 1683636528
|
||||||
|
@ -2,23 +2,23 @@
|
|||||||
id: 1
|
id: 1
|
||||||
repo_id: 4
|
repo_id: 4
|
||||||
name_pattern: /v.+/
|
name_pattern: /v.+/
|
||||||
allowlist_user_i_ds: []
|
allowlist_user_i_ds: "[]"
|
||||||
allowlist_team_i_ds: []
|
allowlist_team_i_ds: "[]"
|
||||||
created_unix: 1715596037
|
created_unix: 1715596037
|
||||||
updated_unix: 1715596037
|
updated_unix: 1715596037
|
||||||
-
|
-
|
||||||
id: 2
|
id: 2
|
||||||
repo_id: 1
|
repo_id: 1
|
||||||
name_pattern: v-*
|
name_pattern: v-*
|
||||||
allowlist_user_i_ds: []
|
allowlist_user_i_ds: "[]"
|
||||||
allowlist_team_i_ds: []
|
allowlist_team_i_ds: "[]"
|
||||||
created_unix: 1715596037
|
created_unix: 1715596037
|
||||||
updated_unix: 1715596037
|
updated_unix: 1715596037
|
||||||
-
|
-
|
||||||
id: 3
|
id: 3
|
||||||
repo_id: 1
|
repo_id: 1
|
||||||
name_pattern: v-1.1
|
name_pattern: v-1.1
|
||||||
allowlist_user_i_ds: [2]
|
allowlist_user_i_ds: "[2]"
|
||||||
allowlist_team_i_ds: []
|
allowlist_team_i_ds: "[]"
|
||||||
created_unix: 1715596037
|
created_unix: 1715596037
|
||||||
updated_unix: 1715596037
|
updated_unix: 1715596037
|
||||||
|
@ -197,6 +197,20 @@ func (t CommentType) HasMailReplySupport() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t CommentType) CountedAsConversation() bool {
|
||||||
|
for _, ct := range ConversationCountedCommentType() {
|
||||||
|
if t == ct {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConversationCountedCommentType returns the comment types that are counted as a conversation
|
||||||
|
func ConversationCountedCommentType() []CommentType {
|
||||||
|
return []CommentType{CommentTypeComment, CommentTypeReview}
|
||||||
|
}
|
||||||
|
|
||||||
// RoleInRepo presents the user's participation in the repo
|
// RoleInRepo presents the user's participation in the repo
|
||||||
type RoleInRepo string
|
type RoleInRepo string
|
||||||
|
|
||||||
@ -592,26 +606,26 @@ func (c *Comment) LoadAttachments(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAttachments update attachments by UUIDs for the comment
|
// UpdateCommentAttachments update attachments by UUIDs for the comment
|
||||||
func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error {
|
func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) error {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
if len(uuids) == 0 {
|
||||||
if err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
||||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
|
||||||
}
|
|
||||||
for i := 0; i < len(attachments); i++ {
|
|
||||||
attachments[i].IssueID = c.IssueID
|
|
||||||
attachments[i].CommentID = c.ID
|
|
||||||
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
|
||||||
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
|
||||||
}
|
}
|
||||||
}
|
for i := 0; i < len(attachments); i++ {
|
||||||
return committer.Commit()
|
attachments[i].IssueID = c.IssueID
|
||||||
|
attachments[i].CommentID = c.ID
|
||||||
|
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
||||||
|
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Attachments = attachments
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
|
// LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
|
||||||
@ -878,7 +892,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
|||||||
// Check comment type.
|
// Check comment type.
|
||||||
switch opts.Type {
|
switch opts.Type {
|
||||||
case CommentTypeCode:
|
case CommentTypeCode:
|
||||||
if err = updateAttachments(ctx, opts, comment); err != nil {
|
if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if comment.ReviewID != 0 {
|
if comment.ReviewID != 0 {
|
||||||
@ -893,12 +907,12 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
|||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case CommentTypeComment:
|
case CommentTypeComment:
|
||||||
if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
|
if err := UpdateIssueNumComments(ctx, opts.Issue.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case CommentTypeReview:
|
case CommentTypeReview:
|
||||||
if err = updateAttachments(ctx, opts, comment); err != nil {
|
if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case CommentTypeReopen, CommentTypeClose:
|
case CommentTypeReopen, CommentTypeClose:
|
||||||
@ -910,23 +924,6 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
|||||||
return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
|
return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error {
|
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
|
|
||||||
}
|
|
||||||
for i := range attachments {
|
|
||||||
attachments[i].IssueID = opts.Issue.ID
|
|
||||||
attachments[i].CommentID = comment.ID
|
|
||||||
// No assign value could be 0, so ignore AllCols().
|
|
||||||
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
|
|
||||||
return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
comment.Attachments = attachments
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
|
func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
|
||||||
var content string
|
var content string
|
||||||
var commentType CommentType
|
var commentType CommentType
|
||||||
@ -1182,8 +1179,8 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment.Type == CommentTypeComment {
|
if comment.Type.CountedAsConversation() {
|
||||||
if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil {
|
if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1300,6 +1297,21 @@ func (c *Comment) HasOriginalAuthor() bool {
|
|||||||
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
|
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateIssueNumCommentsBuilder(issueID int64) *builder.Builder {
|
||||||
|
subQuery := builder.Select("COUNT(*)").From("`comment`").Where(
|
||||||
|
builder.Eq{"issue_id": issueID}.And(
|
||||||
|
builder.In("`type`", ConversationCountedCommentType()),
|
||||||
|
))
|
||||||
|
|
||||||
|
return builder.Update(builder.Eq{"num_comments": subQuery}).
|
||||||
|
From("`issue`").Where(builder.Eq{"id": issueID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateIssueNumComments(ctx context.Context, issueID int64) error {
|
||||||
|
_, err := db.GetEngine(ctx).Exec(UpdateIssueNumCommentsBuilder(issueID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// InsertIssueComments inserts many comments of issues.
|
// InsertIssueComments inserts many comments of issues.
|
||||||
func InsertIssueComments(ctx context.Context, comments []*Comment) error {
|
func InsertIssueComments(ctx context.Context, comments []*Comment) error {
|
||||||
if len(comments) == 0 {
|
if len(comments) == 0 {
|
||||||
@ -1332,8 +1344,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, issueID := range issueIDs {
|
for _, issueID := range issueIDs {
|
||||||
if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
|
if err := UpdateIssueNumComments(ctx, issueID); err != nil {
|
||||||
issueID, CommentTypeComment, issueID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,24 @@ func TestCreateComment(t *testing.T) {
|
|||||||
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_UpdateCommentAttachment(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
|
||||||
|
attachment := repo_model.Attachment{
|
||||||
|
Name: "test.txt",
|
||||||
|
}
|
||||||
|
assert.NoError(t, db.Insert(db.DefaultContext, &attachment))
|
||||||
|
|
||||||
|
err := issues_model.UpdateCommentAttachments(db.DefaultContext, comment, []string{attachment.UUID})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
attachment2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: attachment.ID})
|
||||||
|
assert.EqualValues(t, attachment.Name, attachment2.Name)
|
||||||
|
assert.EqualValues(t, comment.ID, attachment2.CommentID)
|
||||||
|
assert.EqualValues(t, comment.IssueID, attachment2.IssueID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetchCodeComments(t *testing.T) {
|
func TestFetchCodeComments(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
@ -97,3 +115,12 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
|
|||||||
|
|
||||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
|
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_UpdateIssueNumComments(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||||
|
|
||||||
|
assert.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID))
|
||||||
|
issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||||
|
assert.EqualValues(t, 1, issue2.NumComments)
|
||||||
|
}
|
||||||
|
@ -34,7 +34,7 @@ func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
|
func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
|
||||||
// Reload the issue
|
// Reload the issue
|
||||||
currentIssue, err := GetIssueByID(ctx, issue.ID)
|
currentIssue, err := GetIssueByID(ctx, issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -134,7 +134,7 @@ func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comm
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
comment, err := changeIssueStatus(ctx, issue, doer, true, false)
|
comment, err := ChangeIssueStatus(ctx, issue, doer, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -159,7 +159,7 @@ func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Com
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
comment, err := changeIssueStatus(ctx, issue, doer, false, false)
|
comment, err := ChangeIssueStatus(ctx, issue, doer, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -405,19 +405,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.Attachments) > 0 {
|
if err := UpdateIssueAttachments(ctx, opts.Issue.ID, opts.Attachments); err != nil {
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
|
return err
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(attachments); i++ {
|
|
||||||
attachments[i].IssueID = opts.Issue.ID
|
|
||||||
if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
|
|
||||||
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = opts.Issue.LoadAttributes(ctx); err != nil {
|
if err = opts.Issue.LoadAttributes(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_NewIssueUsers(t *testing.T) {
|
func Test_NewIssueUsers(t *testing.T) {
|
||||||
@ -27,9 +28,8 @@ func Test_NewIssueUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// artificially insert new issue
|
// artificially insert new issue
|
||||||
unittest.AssertSuccessfulInsert(t, newIssue)
|
require.NoError(t, db.Insert(db.DefaultContext, newIssue))
|
||||||
|
require.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue))
|
||||||
assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue))
|
|
||||||
|
|
||||||
// issue_user table should now have entries for new issue
|
// issue_user table should now have entries for new issue
|
||||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID})
|
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID})
|
||||||
|
@ -387,7 +387,7 @@ func TestDeleteIssueLabel(t *testing.T) {
|
|||||||
|
|
||||||
expectedNumIssues := label.NumIssues
|
expectedNumIssues := label.NumIssues
|
||||||
expectedNumClosedIssues := label.NumClosedIssues
|
expectedNumClosedIssues := label.NumClosedIssues
|
||||||
if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) {
|
if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil {
|
||||||
expectedNumIssues--
|
expectedNumIssues--
|
||||||
if issue.IsClosed {
|
if issue.IsClosed {
|
||||||
expectedNumClosedIssues--
|
expectedNumClosedIssues--
|
||||||
|
@ -499,65 +499,6 @@ func (pr *PullRequest) IsFromFork() bool {
|
|||||||
return pr.HeadRepoID != pr.BaseRepoID
|
return pr.HeadRepoID != pr.BaseRepoID
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMerged sets a pull request to merged and closes the corresponding issue
|
|
||||||
func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) {
|
|
||||||
if pr.HasMerged {
|
|
||||||
return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index)
|
|
||||||
}
|
|
||||||
if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil {
|
|
||||||
return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index)
|
|
||||||
}
|
|
||||||
|
|
||||||
pr.HasMerged = true
|
|
||||||
sess := db.GetEngine(ctx)
|
|
||||||
|
|
||||||
if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pr.Issue = nil
|
|
||||||
if err := pr.LoadIssue(ctx); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else if tmpPr.HasMerged {
|
|
||||||
if pr.Issue.IsClosed {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID)
|
|
||||||
} else if pr.Issue.IsClosed {
|
|
||||||
return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pr.Issue.Repo.LoadOwner(ctx); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil {
|
|
||||||
return false, fmt.Errorf("Issue.changeStatus: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset the conflicted files as there cannot be any if we're merged
|
|
||||||
pr.ConflictedFiles = []string{}
|
|
||||||
|
|
||||||
// We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging.
|
|
||||||
if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil {
|
|
||||||
return false, fmt.Errorf("Failed to update pr[%d]: %w", pr.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPullRequest creates new pull request with labels for repository.
|
// NewPullRequest creates new pull request with labels for repository.
|
||||||
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) {
|
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
@ -639,6 +639,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return committer.Commit()
|
return committer.Commit()
|
||||||
|
@ -76,7 +76,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
|||||||
t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err)
|
t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err)
|
||||||
return x, deferFn
|
return x, deferFn
|
||||||
}
|
}
|
||||||
if err := unittest.LoadFixtures(x); err != nil {
|
if err := unittest.LoadFixtures(); err != nil {
|
||||||
t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err)
|
t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err)
|
||||||
return x, deferFn
|
return x, deferFn
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ func TestAddOrgUser(t *testing.T) {
|
|||||||
testSuccess := func(orgID, userID int64, isPublic bool) {
|
testSuccess := func(orgID, userID int64, isPublic bool) {
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
||||||
expectedNumMembers := org.NumMembers
|
expectedNumMembers := org.NumMembers
|
||||||
if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
|
if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil {
|
||||||
expectedNumMembers++
|
expectedNumMembers++
|
||||||
}
|
}
|
||||||
assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID))
|
assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID))
|
||||||
|
@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) {
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages
|
||||||
|
func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error {
|
||||||
|
var cond builder.Cond = builder.Eq{
|
||||||
|
"package.id": packageID,
|
||||||
|
"package.owner_id": ownerID,
|
||||||
|
"package.type": packageType,
|
||||||
|
"package.is_internal": false,
|
||||||
|
}
|
||||||
|
_, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetPackageByName gets a package by name
|
// GetPackageByName gets a package by name
|
||||||
func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) {
|
func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) {
|
||||||
var cond builder.Cond = builder.Eq{
|
var cond builder.Cond = builder.Eq{
|
||||||
|
@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint
|
||||||
|
return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint
|
||||||
|
return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID)
|
||||||
|
}
|
||||||
|
|
||||||
// Link returns the project's relative URL.
|
// Link returns the project's relative URL.
|
||||||
func (p *Project) Link(ctx context.Context) string {
|
func (p *Project) Link(ctx context.Context) string {
|
||||||
if p.OwnerID > 0 {
|
if p.OwnerID > 0 {
|
||||||
@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string {
|
|||||||
log.Error("LoadOwner: %v", err)
|
log.Error("LoadOwner: %v", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID)
|
return ProjectLinkForOrg(p.Owner, p.ID)
|
||||||
}
|
}
|
||||||
if p.RepoID > 0 {
|
if p.RepoID > 0 {
|
||||||
err := p.LoadRepo(ctx)
|
err := p.LoadRepo(ctx)
|
||||||
@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string {
|
|||||||
log.Error("LoadRepo: %v", err)
|
log.Error("LoadRepo: %v", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID)
|
return ProjectLinkForRepo(p.Repo, p.ID)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Init initialize model
|
// Init initialize model
|
||||||
@ -24,7 +26,7 @@ func Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type repoChecker struct {
|
type repoChecker struct {
|
||||||
querySQL func(ctx context.Context) ([]map[string][]byte, error)
|
querySQL func(ctx context.Context) ([]int64, error)
|
||||||
correctSQL func(ctx context.Context, id int64) error
|
correctSQL func(ctx context.Context, id int64) error
|
||||||
desc string
|
desc string
|
||||||
}
|
}
|
||||||
@ -35,8 +37,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) {
|
|||||||
log.Error("Select %s: %v", checker.desc, err)
|
log.Error("Select %s: %v", checker.desc, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, result := range results {
|
for _, id := range results {
|
||||||
id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id)
|
log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id)
|
||||||
@ -51,21 +52,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatsCorrectSQL(ctx context.Context, sql string, id int64) error {
|
func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error {
|
||||||
_, err := db.GetEngine(ctx).Exec(sql, id, id)
|
args := []any{sql}
|
||||||
|
args = append(args, ids...)
|
||||||
|
_, err := db.GetEngine(ctx).Exec(args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func repoStatsCorrectNumWatches(ctx context.Context, id int64) error {
|
func repoStatsCorrectNumWatches(ctx context.Context, id int64) error {
|
||||||
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id)
|
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func repoStatsCorrectNumStars(ctx context.Context, id int64) error {
|
func repoStatsCorrectNumStars(ctx context.Context, id int64) error {
|
||||||
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id)
|
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func labelStatsCorrectNumIssues(ctx context.Context, id int64) error {
|
func labelStatsCorrectNumIssues(ctx context.Context, id int64) error {
|
||||||
return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id)
|
return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
|
func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
|
||||||
@ -102,11 +105,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userStatsCorrectNumRepos(ctx context.Context, id int64) error {
|
func userStatsCorrectNumRepos(ctx context.Context, id int64) error {
|
||||||
return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id)
|
return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error {
|
func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error {
|
||||||
return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id)
|
return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func repoStatsCorrectNumIssues(ctx context.Context, id int64) error {
|
func repoStatsCorrectNumIssues(ctx context.Context, id int64) error {
|
||||||
@ -125,9 +128,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error {
|
|||||||
return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true)
|
return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) {
|
// statsQuery returns a function that queries the database for a list of IDs
|
||||||
return func(ctx context.Context) ([]map[string][]byte, error) {
|
// sql could be a string or a *builder.Builder
|
||||||
return db.GetEngine(ctx).Query(args...)
|
func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) {
|
||||||
|
return func(ctx context.Context) ([]int64, error) {
|
||||||
|
var ids []int64
|
||||||
|
return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +204,16 @@ func CheckRepoStats(ctx context.Context) error {
|
|||||||
},
|
},
|
||||||
// Issue.NumComments
|
// Issue.NumComments
|
||||||
{
|
{
|
||||||
statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"),
|
statsQuery(builder.Select("`issue`.id").From("`issue`").Where(
|
||||||
|
builder.Neq{
|
||||||
|
"`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where(
|
||||||
|
builder.Expr("issue_id = `issue`.id").And(
|
||||||
|
builder.In("type", issues_model.ConversationCountedCommentType()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
repoStatsCorrectIssueNumComments,
|
repoStatsCorrectIssueNumComments,
|
||||||
"issue count 'num_comments'",
|
"issue count 'num_comments'",
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -60,13 +61,15 @@ func (err ErrRepoIsArchived) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reservedRepoNames = []string{".", "..", "-"}
|
validRepoNamePattern = regexp.MustCompile(`[-.\w]+`)
|
||||||
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
|
invalidRepoNamePattern = regexp.MustCompile(`[.]{2,}`)
|
||||||
|
reservedRepoNames = []string{".", "..", "-"}
|
||||||
|
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsUsableRepoName returns true when repository is usable
|
// IsUsableRepoName returns true when name is usable
|
||||||
func IsUsableRepoName(name string) error {
|
func IsUsableRepoName(name string) error {
|
||||||
if db.AlphaDashDotPattern.MatchString(name) {
|
if !validRepoNamePattern.MatchString(name) || invalidRepoNamePattern.MatchString(name) {
|
||||||
// Note: usually this error is normally caught up earlier in the UI
|
// Note: usually this error is normally caught up earlier in the UI
|
||||||
return db.ErrNameCharsNotAllowed{Name: name}
|
return db.ErrNameCharsNotAllowed{Name: name}
|
||||||
}
|
}
|
||||||
|
@ -217,3 +217,15 @@ func TestComposeSSHCloneURL(t *testing.T) {
|
|||||||
setting.SSH.Port = 123
|
setting.SSH.Port = 123
|
||||||
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsUsableRepoName(t *testing.T) {
|
||||||
|
assert.NoError(t, IsUsableRepoName("a"))
|
||||||
|
assert.NoError(t, IsUsableRepoName("-1_."))
|
||||||
|
assert.NoError(t, IsUsableRepoName(".profile"))
|
||||||
|
|
||||||
|
assert.Error(t, IsUsableRepoName("-"))
|
||||||
|
assert.Error(t, IsUsableRepoName("🌞"))
|
||||||
|
assert.Error(t, IsUsableRepoName("the..repo"))
|
||||||
|
assert.Error(t, IsUsableRepoName("foo.wiki"))
|
||||||
|
assert.Error(t, IsUsableRepoName("foo.git"))
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -22,3 +23,16 @@ func TestDoctorUserStarNum(t *testing.T) {
|
|||||||
|
|
||||||
assert.NoError(t, DoctorUserStarNum(db.DefaultContext))
|
assert.NoError(t, DoctorUserStarNum(db.DefaultContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_repoStatsCorrectIssueNumComments(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||||
|
assert.NotNil(t, issue2)
|
||||||
|
assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here
|
||||||
|
|
||||||
|
assert.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2))
|
||||||
|
// reload the issue
|
||||||
|
issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||||
|
assert.EqualValues(t, 1, issue2.NumComments)
|
||||||
|
}
|
||||||
|
@ -1,97 +1,33 @@
|
|||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//nolint:forbidigo
|
|
||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/go-testfixtures/testfixtures/v3"
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fixturesLoader *testfixtures.Loader
|
type FixturesLoader interface {
|
||||||
|
Load() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var fixturesLoader FixturesLoader
|
||||||
|
|
||||||
// GetXORMEngine gets the XORM engine
|
// GetXORMEngine gets the XORM engine
|
||||||
func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) {
|
func GetXORMEngine() (x *xorm.Engine) {
|
||||||
if len(engine) == 1 {
|
|
||||||
return engine[0]
|
|
||||||
}
|
|
||||||
return db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
return db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitFixtures initialize test fixtures for a test database
|
func loadFixtureResetSeqPgsql(e *xorm.Engine) error {
|
||||||
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
results, err := e.QueryString(`SELECT 'SELECT SETVAL(' ||
|
||||||
e := GetXORMEngine(engine...)
|
|
||||||
var fixtureOptionFiles func(*testfixtures.Loader) error
|
|
||||||
if opts.Dir != "" {
|
|
||||||
fixtureOptionFiles = testfixtures.Directory(opts.Dir)
|
|
||||||
} else {
|
|
||||||
fixtureOptionFiles = testfixtures.Files(opts.Files...)
|
|
||||||
}
|
|
||||||
dialect := "unknown"
|
|
||||||
switch e.Dialect().URI().DBType {
|
|
||||||
case schemas.POSTGRES:
|
|
||||||
dialect = "postgres"
|
|
||||||
case schemas.MYSQL:
|
|
||||||
dialect = "mysql"
|
|
||||||
case schemas.MSSQL:
|
|
||||||
dialect = "mssql"
|
|
||||||
case schemas.SQLITE:
|
|
||||||
dialect = "sqlite3"
|
|
||||||
default:
|
|
||||||
fmt.Println("Unsupported RDBMS for integration tests")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
loaderOptions := []func(loader *testfixtures.Loader) error{
|
|
||||||
testfixtures.Database(e.DB().DB),
|
|
||||||
testfixtures.Dialect(dialect),
|
|
||||||
testfixtures.DangerousSkipTestDatabaseCheck(),
|
|
||||||
fixtureOptionFiles,
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
|
||||||
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
|
|
||||||
}
|
|
||||||
|
|
||||||
fixturesLoader, err = testfixtures.New(loaderOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// register the dummy hash algorithm function used in the test fixtures
|
|
||||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
|
||||||
|
|
||||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFixtures load fixtures for a test database
|
|
||||||
func LoadFixtures(engine ...*xorm.Engine) error {
|
|
||||||
e := GetXORMEngine(engine...)
|
|
||||||
var err error
|
|
||||||
// (doubt) database transaction conflicts could occur and result in ROLLBACK? just try for a few times.
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
if err = fixturesLoader.Load(); err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("LoadFixtures failed after retries: %v\n", err)
|
|
||||||
}
|
|
||||||
// Now if we're running postgres we need to tell it to update the sequences
|
|
||||||
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
|
||||||
results, err := e.QueryString(`SELECT 'SELECT SETVAL(' ||
|
|
||||||
quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
|
quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
|
||||||
', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
|
', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
|
||||||
quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
|
quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
|
||||||
@ -107,22 +43,42 @@ func LoadFixtures(engine ...*xorm.Engine) error {
|
|||||||
AND D.refobjsubid = C.attnum
|
AND D.refobjsubid = C.attnum
|
||||||
AND T.relname = PGT.tablename
|
AND T.relname = PGT.tablename
|
||||||
ORDER BY S.relname;`)
|
ORDER BY S.relname;`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to generate sequence update: %v\n", err)
|
return fmt.Errorf("failed to generate sequence update: %w", err)
|
||||||
return err
|
}
|
||||||
}
|
for _, r := range results {
|
||||||
for _, r := range results {
|
for _, value := range r {
|
||||||
for _, value := range r {
|
_, err = e.Exec(value)
|
||||||
_, err = e.Exec(value)
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to update sequence: %s, error: %w", value, err)
|
||||||
fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitFixtures initialize test fixtures for a test database
|
||||||
|
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||||
|
xormEngine := util.IfZero(util.OptionalArg(engine), GetXORMEngine())
|
||||||
|
fixturesLoader, err = NewFixturesLoader(xormEngine, opts)
|
||||||
|
// fixturesLoader = NewFixturesLoaderVendor(xormEngine, opts)
|
||||||
|
|
||||||
|
// register the dummy hash algorithm function used in the test fixtures
|
||||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadFixtures load fixtures for a test database
|
||||||
|
func LoadFixtures() error {
|
||||||
|
if err := fixturesLoader.Load(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Now if we're running postgres we need to tell it to update the sequences
|
||||||
|
if GetXORMEngine().Dialect().URI().DBType == schemas.POSTGRES {
|
||||||
|
if err := loadFixtureResetSeqPgsql(GetXORMEngine()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
201
models/unittest/fixtures_loader.go
Normal file
201
models/unittest/fixtures_loader.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package unittest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/schemas"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fixtureItem struct {
|
||||||
|
tableName string
|
||||||
|
tableNameQuoted string
|
||||||
|
sqlInserts []string
|
||||||
|
sqlInsertArgs [][]any
|
||||||
|
|
||||||
|
mssqlHasIdentityColumn bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type fixturesLoaderInternal struct {
|
||||||
|
db *sql.DB
|
||||||
|
dbType schemas.DBType
|
||||||
|
files []string
|
||||||
|
fixtures map[string]*fixtureItem
|
||||||
|
quoteObject func(string) string
|
||||||
|
paramPlaceholder func(idx int) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixturesLoaderInternal) mssqlTableHasIdentityColumn(db *sql.DB, tableName string) (bool, error) {
|
||||||
|
row := db.QueryRow(`SELECT COUNT(*) FROM sys.identity_columns WHERE OBJECT_ID = OBJECT_ID(?)`, tableName)
|
||||||
|
var count int
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err error) {
|
||||||
|
for _, m := range row {
|
||||||
|
for k, v := range m {
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
if strings.HasPrefix(s, "0x") {
|
||||||
|
if m[k], err = hex.DecodeString(s[2:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) {
|
||||||
|
fixture := &fixtureItem{}
|
||||||
|
fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".")
|
||||||
|
fixture.tableNameQuoted = f.quoteObject(fixture.tableName)
|
||||||
|
|
||||||
|
if f.dbType == schemas.MSSQL {
|
||||||
|
fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read file %q: %w", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []map[string]any
|
||||||
|
if err = yaml.Unmarshal(data, &rows); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err)
|
||||||
|
}
|
||||||
|
if err = f.preprocessFixtureRow(rows); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqlBuf []byte
|
||||||
|
var sqlArguments []any
|
||||||
|
for _, row := range rows {
|
||||||
|
sqlBuf = append(sqlBuf, fmt.Sprintf("INSERT INTO %s (", fixture.tableNameQuoted)...)
|
||||||
|
for k, v := range row {
|
||||||
|
sqlBuf = append(sqlBuf, f.quoteObject(k)...)
|
||||||
|
sqlBuf = append(sqlBuf, ","...)
|
||||||
|
sqlArguments = append(sqlArguments, v)
|
||||||
|
}
|
||||||
|
sqlBuf = sqlBuf[:len(sqlBuf)-1]
|
||||||
|
sqlBuf = append(sqlBuf, ") VALUES ("...)
|
||||||
|
paramIdx := 1
|
||||||
|
for range row {
|
||||||
|
sqlBuf = append(sqlBuf, f.paramPlaceholder(paramIdx)...)
|
||||||
|
sqlBuf = append(sqlBuf, ',')
|
||||||
|
paramIdx++
|
||||||
|
}
|
||||||
|
sqlBuf[len(sqlBuf)-1] = ')'
|
||||||
|
fixture.sqlInserts = append(fixture.sqlInserts, string(sqlBuf))
|
||||||
|
fixture.sqlInsertArgs = append(fixture.sqlInsertArgs, slices.Clone(sqlArguments))
|
||||||
|
sqlBuf = sqlBuf[:0]
|
||||||
|
sqlArguments = sqlArguments[:0]
|
||||||
|
}
|
||||||
|
return fixture, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) {
|
||||||
|
fixture := f.fixtures[file]
|
||||||
|
if fixture == nil {
|
||||||
|
if fixture, err = f.prepareFixtureItem(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.fixtures[file] = fixture
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fixture.mssqlHasIdentityColumn {
|
||||||
|
_, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", fixture.tableNameQuoted))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", fixture.tableNameQuoted)) }()
|
||||||
|
}
|
||||||
|
for i := range fixture.sqlInserts {
|
||||||
|
_, err = tx.Exec(fixture.sqlInserts[i], fixture.sqlInsertArgs[i]...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixturesLoaderInternal) Load() error {
|
||||||
|
tx, err := f.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
for _, file := range f.files {
|
||||||
|
if err := f.loadFixtures(tx, file); err != nil {
|
||||||
|
return fmt.Errorf("failed to load fixtures from %s: %w", file, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixturesFileFullPaths(dir string, files []string) ([]string, error) {
|
||||||
|
if files != nil && len(files) == 0 {
|
||||||
|
return nil, nil // load nothing
|
||||||
|
}
|
||||||
|
files = slices.Clone(files)
|
||||||
|
if len(files) == 0 {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
files = append(files, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, file := range files {
|
||||||
|
if !filepath.IsAbs(file) {
|
||||||
|
files[i] = filepath.Join(dir, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) {
|
||||||
|
files, err := FixturesFileFullPaths(opts.Dir, opts.Files)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
|
||||||
|
}
|
||||||
|
f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}}
|
||||||
|
switch f.dbType {
|
||||||
|
case schemas.SQLITE:
|
||||||
|
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
|
||||||
|
f.paramPlaceholder = func(idx int) string { return "?" }
|
||||||
|
case schemas.POSTGRES:
|
||||||
|
f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
|
||||||
|
f.paramPlaceholder = func(idx int) string { return fmt.Sprintf(`$%d`, idx) }
|
||||||
|
case schemas.MYSQL:
|
||||||
|
f.quoteObject = func(s string) string { return fmt.Sprintf("`%s`", s) }
|
||||||
|
f.paramPlaceholder = func(idx int) string { return "?" }
|
||||||
|
case schemas.MSSQL:
|
||||||
|
f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) }
|
||||||
|
f.paramPlaceholder = func(idx int) string { return "?" }
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
114
models/unittest/fixtures_test.go
Normal file
114
models/unittest/fixtures_test.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package unittest_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// the old code is kept here in case we are still interested in benchmarking the two implementations
|
||||||
|
func init() {
|
||||||
|
NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) {
|
||||||
|
return NewFixturesLoaderVendorGoTestfixtures(e, opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.FixturesOptions) (*testfixtures.Loader, error) {
|
||||||
|
files, err := unittest.FixturesFileFullPaths(opts.Dir, opts.Files)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get fixtures files: %w", err)
|
||||||
|
}
|
||||||
|
var dialect string
|
||||||
|
switch e.Dialect().URI().DBType {
|
||||||
|
case schemas.POSTGRES:
|
||||||
|
dialect = "postgres"
|
||||||
|
case schemas.MYSQL:
|
||||||
|
dialect = "mysql"
|
||||||
|
case schemas.MSSQL:
|
||||||
|
dialect = "mssql"
|
||||||
|
case schemas.SQLITE:
|
||||||
|
dialect = "sqlite3"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType)
|
||||||
|
}
|
||||||
|
loaderOptions := []func(loader *testfixtures.Loader) error{
|
||||||
|
testfixtures.Database(e.DB().DB),
|
||||||
|
testfixtures.Dialect(dialect),
|
||||||
|
testfixtures.DangerousSkipTestDatabaseCheck(),
|
||||||
|
testfixtures.Files(files...),
|
||||||
|
}
|
||||||
|
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
||||||
|
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
|
||||||
|
}
|
||||||
|
return testfixtures.New(loaderOptions...)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
|
||||||
|
_ = user_model.User{}
|
||||||
|
opts := unittest.FixturesOptions{Dir: filepath.Join(test.SetupGiteaRoot(), "models", "fixtures"), Files: []string{
|
||||||
|
"user.yml",
|
||||||
|
}}
|
||||||
|
require.NoError(t, unittest.CreateTestEngine(opts))
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixturesLoader(t *testing.T) {
|
||||||
|
opts := prepareTestFixturesLoaders(t)
|
||||||
|
loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("Internal", func(t *testing.T) {
|
||||||
|
require.NoError(t, loaderInternal.Load())
|
||||||
|
require.NoError(t, loaderInternal.Load())
|
||||||
|
})
|
||||||
|
t.Run("Vendor", func(t *testing.T) {
|
||||||
|
if loaderVendor == nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
require.NoError(t, loaderVendor.Load())
|
||||||
|
require.NoError(t, loaderVendor.Load())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFixturesLoader(b *testing.B) {
|
||||||
|
opts := prepareTestFixturesLoaders(b)
|
||||||
|
require.NoError(b, unittest.CreateTestEngine(opts))
|
||||||
|
loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts)
|
||||||
|
require.NoError(b, err)
|
||||||
|
loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts)
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
// BenchmarkFixturesLoader/Vendor
|
||||||
|
// BenchmarkFixturesLoader/Vendor-12 1696 719416 ns/op
|
||||||
|
// BenchmarkFixturesLoader/Internal
|
||||||
|
// BenchmarkFixturesLoader/Internal-12 1746 670457 ns/op
|
||||||
|
b.Run("Internal", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
require.NoError(b, loaderInternal.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("Vendor", func(b *testing.B) {
|
||||||
|
if loaderVendor == nil {
|
||||||
|
b.Skip()
|
||||||
|
}
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
require.NoError(b, loaderVendor.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -67,7 +67,7 @@ func SyncDirs(srcPath, destPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// find and delete all untracked files
|
// find and delete all untracked files
|
||||||
destFiles, err := util.StatDir(destPath, true)
|
destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -86,13 +86,13 @@ func SyncDirs(srcPath, destPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sync src files to dest
|
// sync src files to dest
|
||||||
srcFiles, err := util.StatDir(srcPath, true)
|
srcFiles, err := util.ListDirRecursively(srcPath, &util.ListDirOptions{IncludeDir: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, srcFile := range srcFiles {
|
for _, srcFile := range srcFiles {
|
||||||
destFilePath := filepath.Join(destPath, srcFile)
|
destFilePath := filepath.Join(destPath, srcFile)
|
||||||
// util.StatDir appends a slash to the directory name
|
// util.ListDirRecursively appends a slash to the directory name
|
||||||
if strings.HasSuffix(srcFile, "/") {
|
if strings.HasSuffix(srcFile, "/") {
|
||||||
err = os.MkdirAll(destFilePath, os.ModePerm)
|
err = os.MkdirAll(destFilePath, os.ModePerm)
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ func fieldByName(v reflect.Value, field string) reflect.Value {
|
|||||||
}
|
}
|
||||||
f := v.FieldByName(field)
|
f := v.FieldByName(field)
|
||||||
if !f.IsValid() {
|
if !f.IsValid() {
|
||||||
log.Panicf("can not read %s for %v", field, v)
|
panic(fmt.Errorf("can not read %s for %v", field, v))
|
||||||
}
|
}
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
@ -28,16 +28,7 @@ import (
|
|||||||
"xorm.io/xorm/names"
|
"xorm.io/xorm/names"
|
||||||
)
|
)
|
||||||
|
|
||||||
// giteaRoot a path to the gitea root
|
var giteaRoot string
|
||||||
var (
|
|
||||||
giteaRoot string
|
|
||||||
fixturesDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
// FixturesDir returns the fixture directory
|
|
||||||
func FixturesDir() string {
|
|
||||||
return fixturesDir
|
|
||||||
}
|
|
||||||
|
|
||||||
func fatalTestError(fmtStr string, args ...any) {
|
func fatalTestError(fmtStr string, args ...any) {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
|
_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
|
||||||
@ -68,6 +59,7 @@ func InitSettings() {
|
|||||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||||
|
|
||||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||||
|
setting.InitGiteaEnvVarsForTesting()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOptions represents test options
|
// TestOptions represents test options
|
||||||
@ -79,39 +71,14 @@ type TestOptions struct {
|
|||||||
|
|
||||||
// MainTest a reusable TestMain(..) function for unit tests that need to use a
|
// MainTest a reusable TestMain(..) function for unit tests that need to use a
|
||||||
// test database. Creates the test database, and sets necessary settings.
|
// test database. Creates the test database, and sets necessary settings.
|
||||||
func MainTest(m *testing.M, testOpts ...*TestOptions) {
|
func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||||
searchDir, _ := os.Getwd()
|
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||||
for searchDir != "" {
|
giteaRoot = test.SetupGiteaRoot()
|
||||||
if _, err := os.Stat(filepath.Join(searchDir, "go.mod")); err == nil {
|
|
||||||
break // The "go.mod" should be the one for Gitea repository
|
|
||||||
}
|
|
||||||
if dir := filepath.Dir(searchDir); dir == searchDir {
|
|
||||||
searchDir = "" // reaches the root of filesystem
|
|
||||||
} else {
|
|
||||||
searchDir = dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if searchDir == "" {
|
|
||||||
panic("The tests should run in a Gitea repository, there should be a 'go.mod' in the root")
|
|
||||||
}
|
|
||||||
|
|
||||||
giteaRoot = searchDir
|
|
||||||
setting.CustomPath = filepath.Join(giteaRoot, "custom")
|
setting.CustomPath = filepath.Join(giteaRoot, "custom")
|
||||||
InitSettings()
|
InitSettings()
|
||||||
|
|
||||||
fixturesDir = filepath.Join(giteaRoot, "models", "fixtures")
|
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
||||||
var opts FixturesOptions
|
if err := CreateTestEngine(fixturesOpts); err != nil {
|
||||||
if len(testOpts) == 0 || len(testOpts[0].FixtureFiles) == 0 {
|
|
||||||
opts.Dir = fixturesDir
|
|
||||||
} else {
|
|
||||||
for _, f := range testOpts[0].FixtureFiles {
|
|
||||||
if len(f) != 0 {
|
|
||||||
opts.Files = append(opts.Files, filepath.Join(fixturesDir, f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := CreateTestEngine(opts); err != nil {
|
|
||||||
fatalTestError("Error creating test engine: %v\n", err)
|
fatalTestError("Error creating test engine: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,16 +139,16 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
|
|||||||
fatalTestError("git.Init: %v\n", err)
|
fatalTestError("git.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(testOpts) > 0 && testOpts[0].SetUp != nil {
|
if testOpts.SetUp != nil {
|
||||||
if err := testOpts[0].SetUp(); err != nil {
|
if err := testOpts.SetUp(); err != nil {
|
||||||
fatalTestError("set up failed: %v\n", err)
|
fatalTestError("set up failed: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitStatus := m.Run()
|
exitStatus := m.Run()
|
||||||
|
|
||||||
if len(testOpts) > 0 && testOpts[0].TearDown != nil {
|
if testOpts.TearDown != nil {
|
||||||
if err := testOpts[0].TearDown(); err != nil {
|
if err := testOpts.TearDown(); err != nil {
|
||||||
fatalTestError("tear down failed: %v\n", err)
|
fatalTestError("tear down failed: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,17 @@
|
|||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons.
|
// Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons.
|
||||||
@ -51,22 +55,23 @@ func whereOrderConditions(e db.Engine, conditions []any) db.Engine {
|
|||||||
return e.OrderBy(orderBy)
|
return e.OrderBy(orderBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadBeanIfExists loads beans from fixture database if exist
|
func getBeanIfExists(bean any, conditions ...any) (bool, error) {
|
||||||
func LoadBeanIfExists(bean any, conditions ...any) (bool, error) {
|
|
||||||
e := db.GetEngine(db.DefaultContext)
|
e := db.GetEngine(db.DefaultContext)
|
||||||
return whereOrderConditions(e, conditions).Get(bean)
|
return whereOrderConditions(e, conditions).Get(bean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeanExists for testing, check if a bean exists
|
func GetBean[T any](t require.TestingT, bean T, conditions ...any) (ret T) {
|
||||||
func BeanExists(t assert.TestingT, bean any, conditions ...any) bool {
|
exists, err := getBeanIfExists(bean, conditions...)
|
||||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, err)
|
if exists {
|
||||||
return exists
|
return bean
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
|
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
|
||||||
func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T {
|
func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T {
|
||||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
exists, err := getBeanIfExists(bean, conditions...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, exists,
|
require.True(t, exists,
|
||||||
"Expected to find %+v (of type %T, with conditions %+v), but did not",
|
"Expected to find %+v (of type %T, with conditions %+v), but did not",
|
||||||
@ -112,25 +117,11 @@ func GetCount(t assert.TestingT, bean any, conditions ...any) int {
|
|||||||
|
|
||||||
// AssertNotExistsBean assert that a bean does not exist in the test database
|
// AssertNotExistsBean assert that a bean does not exist in the test database
|
||||||
func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) {
|
func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) {
|
||||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
exists, err := getBeanIfExists(bean, conditions...)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, exists)
|
assert.False(t, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssertExistsIf asserts that a bean exists or does not exist, depending on
|
|
||||||
// what is expected.
|
|
||||||
func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) {
|
|
||||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, exists)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertSuccessfulInsert assert that beans is successfully inserted
|
|
||||||
func AssertSuccessfulInsert(t assert.TestingT, beans ...any) {
|
|
||||||
err := db.Insert(db.DefaultContext, beans...)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertCount assert the count of a bean
|
// AssertCount assert the count of a bean
|
||||||
func AssertCount(t assert.TestingT, bean, expected any) bool {
|
func AssertCount(t assert.TestingT, bean, expected any) bool {
|
||||||
return assert.EqualValues(t, expected, GetCount(t, bean))
|
return assert.EqualValues(t, expected, GetCount(t, bean))
|
||||||
@ -155,3 +146,39 @@ func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, e
|
|||||||
return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
|
return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
|
||||||
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
|
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DumpQueryResult dumps the result of a query for debugging purpose
|
||||||
|
func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) {
|
||||||
|
x := db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
||||||
|
goDB := x.DB().DB
|
||||||
|
sql, ok := sqlOrBean.(string)
|
||||||
|
if !ok {
|
||||||
|
sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean))
|
||||||
|
} else if !strings.Contains(sql, " ") {
|
||||||
|
sql = fmt.Sprintf("SELECT * FROM %s", sql)
|
||||||
|
}
|
||||||
|
rows, err := goDB.Query(sql, sqlArgs...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rows.Close()
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql)
|
||||||
|
idx := 0
|
||||||
|
for rows.Next() {
|
||||||
|
row := make([]any, len(columns))
|
||||||
|
rowPointers := make([]any, len(columns))
|
||||||
|
for i := range row {
|
||||||
|
rowPointers[i] = &row[i]
|
||||||
|
}
|
||||||
|
require.NoError(t, rows.Scan(rowPointers...))
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx)
|
||||||
|
for i, col := range columns {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i])
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if idx == 0 {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
|
|||||||
if user := GetVerifyUser(ctx, code); user != nil {
|
if user := GetVerifyUser(ctx, code); user != nil {
|
||||||
// time limit code
|
// time limit code
|
||||||
prefix := code[:base.TimeLimitCodeLength]
|
prefix := code[:base.TimeLimitCodeLength]
|
||||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
|
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
|
||||||
|
data := makeTimeLimitCodeHashData(opts, user)
|
||||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||||
@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
|||||||
|
|
||||||
// Activate/deactivate a user's primary email address and account
|
// Activate/deactivate a user's primary email address and account
|
||||||
if addr.IsPrimary {
|
if addr.IsPrimary {
|
||||||
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
|
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !exist {
|
} else if !exist || !strings.EqualFold(user.Email, email) {
|
||||||
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,8 +39,6 @@ type SearchUserOptions struct {
|
|||||||
IsTwoFactorEnabled optional.Option[bool]
|
IsTwoFactorEnabled optional.Option[bool]
|
||||||
IsProhibitLogin optional.Option[bool]
|
IsProhibitLogin optional.Option[bool]
|
||||||
IncludeReserved bool
|
IncludeReserved bool
|
||||||
|
|
||||||
ExtraParamStrings map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
||||||
|
@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() {
|
|||||||
u.MaxRepoCreation = -1
|
u.MaxRepoCreation = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Organization does not need email
|
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
|
||||||
|
// This trick could be removed in new releases to display the user inputed email as-is.
|
||||||
u.Email = strings.ToLower(u.Email)
|
u.Email = strings.ToLower(u.Email)
|
||||||
if !u.IsOrganization() {
|
if !u.IsOrganization() {
|
||||||
if len(u.AvatarEmail) == 0 {
|
if len(u.AvatarEmail) == 0 {
|
||||||
@ -310,17 +311,6 @@ func (u *User) OrganisationLink() string {
|
|||||||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
|
|
||||||
func (u *User) GenerateEmailActivateCode(email string) string {
|
|
||||||
code := base.CreateTimeLimitCode(
|
|
||||||
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
|
|
||||||
setting.Service.ActiveCodeLives, time.Now(), nil)
|
|
||||||
|
|
||||||
// Add tail hex username
|
|
||||||
code += hex.EncodeToString([]byte(u.LowerName))
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserFollowers returns range of user's followers.
|
// GetUserFollowers returns range of user's followers.
|
||||||
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
||||||
sess := db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
@ -863,12 +853,38 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyUserActiveCode verifies active code when active account
|
type TimeLimitCodePurpose string
|
||||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
|
||||||
|
const (
|
||||||
|
TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account"
|
||||||
|
TimeLimitCodeActivateEmail TimeLimitCodePurpose = "activate_email"
|
||||||
|
TimeLimitCodeResetPassword TimeLimitCodePurpose = "reset_password"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeLimitCodeOptions struct {
|
||||||
|
Purpose TimeLimitCodePurpose
|
||||||
|
NewEmail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTimeLimitCodeHashData(opts *TimeLimitCodeOptions, u *User) string {
|
||||||
|
return fmt.Sprintf("%s|%d|%s|%s|%s|%s", opts.Purpose, u.ID, strings.ToLower(util.IfZero(opts.NewEmail, u.Email)), u.LowerName, u.Passwd, u.Rands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail.
|
||||||
|
// TODO: need to use cache or db to store it to make sure a code can only be consumed once
|
||||||
|
func GenerateUserTimeLimitCode(opts *TimeLimitCodeOptions, u *User) string {
|
||||||
|
data := makeTimeLimitCodeHashData(opts, u)
|
||||||
|
code := base.CreateTimeLimitCode(data, setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||||
|
code += hex.EncodeToString([]byte(u.LowerName)) // Add tail hex username
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyUserTimeLimitCode verifies the time-limit code
|
||||||
|
func VerifyUserTimeLimitCode(ctx context.Context, opts *TimeLimitCodeOptions, code string) (user *User) {
|
||||||
if user = GetVerifyUser(ctx, code); user != nil {
|
if user = GetVerifyUser(ctx, code); user != nil {
|
||||||
// time limit code
|
// time limit code
|
||||||
prefix := code[:base.TimeLimitCodeLength]
|
prefix := code[:base.TimeLimitCodeLength]
|
||||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
data := makeTimeLimitCodeHashData(opts, user)
|
||||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
|
func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
|
||||||
if util.CommonSkip(info.Name()) {
|
if util.IsCommonHiddenFileName(info.Name()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(fileMode) == 0 {
|
if len(fileMode) == 0 {
|
||||||
|
@ -242,7 +242,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTreeLine reads an entry from a tree in a cat-file --batch stream
|
// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream
|
||||||
// This carefully avoids allocations - except where fnameBuf is too small.
|
// This carefully avoids allocations - except where fnameBuf is too small.
|
||||||
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||||
//
|
//
|
||||||
@ -250,7 +250,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
|
|||||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
|
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
|
||||||
//
|
//
|
||||||
// We don't attempt to convert the raw HASH to save a lot of time
|
// We don't attempt to convert the raw HASH to save a lot of time
|
||||||
func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||||
var readBytes []byte
|
var readBytes []byte
|
||||||
|
|
||||||
// Read the Mode & fname
|
// Read the Mode & fname
|
||||||
@ -260,7 +260,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
|
|||||||
}
|
}
|
||||||
idx := bytes.IndexByte(readBytes, ' ')
|
idx := bytes.IndexByte(readBytes, ' ')
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes)
|
log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes)
|
||||||
return mode, fname, sha, n, &ErrNotExist{}
|
return mode, fname, sha, n, &ErrNotExist{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,10 +236,16 @@ type RunOpts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func commonBaseEnvs() []string {
|
func commonBaseEnvs() []string {
|
||||||
// at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it
|
|
||||||
envs := []string{
|
envs := []string{
|
||||||
"HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config
|
// Make Gitea use internal git config only, to prevent conflicts with user's git config
|
||||||
"GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace)
|
// It's better to use GIT_CONFIG_GLOBAL, but it requires git >= 2.32, so we still use HOME at the moment.
|
||||||
|
"HOME=" + HomeDir(),
|
||||||
|
// Avoid using system git config, it would cause problems (eg: use macOS osxkeychain to show a modal dialog, auto installing lfs hooks)
|
||||||
|
// This might be a breaking change in 1.24, because some users said that they have put some configs like "receive.certNonceSeed" in "/etc/gitconfig"
|
||||||
|
// For these users, they need to migrate the necessary configs to Gitea's git config file manually.
|
||||||
|
"GIT_CONFIG_NOSYSTEM=1",
|
||||||
|
// Ignore replace references (https://git-scm.com/docs/git-replace)
|
||||||
|
"GIT_NO_REPLACE_OBJECTS=1",
|
||||||
}
|
}
|
||||||
|
|
||||||
// some environment variables should be passed to git command
|
// some environment variables should be passed to git command
|
||||||
|
78
modules/git/parse.go
Normal file
78
modules/git/parse.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sepSpace = []byte{' '}
|
||||||
|
|
||||||
|
type LsTreeEntry struct {
|
||||||
|
ID ObjectID
|
||||||
|
EntryMode EntryMode
|
||||||
|
Name string
|
||||||
|
Size optional.Option[int64]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
|
||||||
|
// expect line to be of the form:
|
||||||
|
// <mode> <type> <sha> <space-padded-size>\t<filename>
|
||||||
|
// <mode> <type> <sha>\t<filename>
|
||||||
|
|
||||||
|
var err error
|
||||||
|
posTab := bytes.IndexByte(line, '\t')
|
||||||
|
if posTab == -1 {
|
||||||
|
return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := new(LsTreeEntry)
|
||||||
|
|
||||||
|
entryAttrs := line[:posTab]
|
||||||
|
entryName := line[posTab+1:]
|
||||||
|
|
||||||
|
entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
|
||||||
|
_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
|
||||||
|
entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
|
||||||
|
if len(entryAttrs) > 0 {
|
||||||
|
entrySize := entryAttrs // the last field is the space-padded-size
|
||||||
|
size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
|
||||||
|
entry.Size = optional.Some(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(entryMode) {
|
||||||
|
case "100644":
|
||||||
|
entry.EntryMode = EntryModeBlob
|
||||||
|
case "100755":
|
||||||
|
entry.EntryMode = EntryModeExec
|
||||||
|
case "120000":
|
||||||
|
entry.EntryMode = EntryModeSymlink
|
||||||
|
case "160000":
|
||||||
|
entry.EntryMode = EntryModeCommit
|
||||||
|
case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
|
||||||
|
entry.EntryMode = EntryModeTree
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown type: %v", string(entryMode))
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.ID, err = NewIDFromString(string(entryObjectID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entryName) > 0 && entryName[0] == '"' {
|
||||||
|
entry.Name, err = strconv.Unquote(string(entryName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.Name = string(entryName)
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
}
|
@ -10,8 +10,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
|||||||
return parseTreeEntries(data, nil)
|
return parseTreeEntries(data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sepSpace = []byte{' '}
|
// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory
|
||||||
|
|
||||||
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||||
var err error
|
|
||||||
entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
|
entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
|
||||||
for pos := 0; pos < len(data); {
|
for pos := 0; pos < len(data); {
|
||||||
// expect line to be of the form:
|
|
||||||
// <mode> <type> <sha> <space-padded-size>\t<filename>
|
|
||||||
// <mode> <type> <sha>\t<filename>
|
|
||||||
posEnd := bytes.IndexByte(data[pos:], '\n')
|
posEnd := bytes.IndexByte(data[pos:], '\n')
|
||||||
if posEnd == -1 {
|
if posEnd == -1 {
|
||||||
posEnd = len(data)
|
posEnd = len(data)
|
||||||
} else {
|
} else {
|
||||||
posEnd += pos
|
posEnd += pos
|
||||||
}
|
}
|
||||||
|
|
||||||
line := data[pos:posEnd]
|
line := data[pos:posEnd]
|
||||||
posTab := bytes.IndexByte(line, '\t')
|
lsTreeLine, err := parseLsTreeLine(line)
|
||||||
if posTab == -1 {
|
|
||||||
return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := new(TreeEntry)
|
|
||||||
entry.ptree = ptree
|
|
||||||
|
|
||||||
entryAttrs := line[:posTab]
|
|
||||||
entryName := line[posTab+1:]
|
|
||||||
|
|
||||||
entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
|
|
||||||
_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
|
|
||||||
entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
|
|
||||||
if len(entryAttrs) > 0 {
|
|
||||||
entrySize := entryAttrs // the last field is the space-padded-size
|
|
||||||
entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
|
|
||||||
entry.sized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch string(entryMode) {
|
|
||||||
case "100644":
|
|
||||||
entry.entryMode = EntryModeBlob
|
|
||||||
case "100755":
|
|
||||||
entry.entryMode = EntryModeExec
|
|
||||||
case "120000":
|
|
||||||
entry.entryMode = EntryModeSymlink
|
|
||||||
case "160000":
|
|
||||||
entry.entryMode = EntryModeCommit
|
|
||||||
case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
|
|
||||||
entry.entryMode = EntryModeTree
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown type: %v", string(entryMode))
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.ID, err = NewIDFromString(string(entryObjectID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
entry := &TreeEntry{
|
||||||
if len(entryName) > 0 && entryName[0] == '"' {
|
ptree: ptree,
|
||||||
entry.name, err = strconv.Unquote(string(entryName))
|
ID: lsTreeLine.ID,
|
||||||
if err != nil {
|
entryMode: lsTreeLine.EntryMode,
|
||||||
return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
|
name: lsTreeLine.Name,
|
||||||
}
|
size: lsTreeLine.Size.Value(),
|
||||||
} else {
|
sized: lsTreeLine.Size.Has(),
|
||||||
entry.name = string(entryName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pos = posEnd + 1
|
pos = posEnd + 1
|
||||||
entries = append(entries, entry)
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.
|
|||||||
|
|
||||||
loop:
|
loop:
|
||||||
for sz > 0 {
|
for sz > 0 {
|
||||||
mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
|
mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break loop
|
break loop
|
||||||
|
@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
|||||||
case "tree":
|
case "tree":
|
||||||
var n int64
|
var n int64
|
||||||
for n < size {
|
for n < size {
|
||||||
mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
|
mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -63,15 +62,11 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t
|
|||||||
cmd.AddOptionFormat("--format=%s", format.String())
|
cmd.AddOptionFormat("--format=%s", format.String())
|
||||||
cmd.AddDynamicArguments(commitID)
|
cmd.AddDynamicArguments(commitID)
|
||||||
|
|
||||||
// Avoid LFS hooks getting installed because of /etc/gitconfig, which can break pull requests.
|
|
||||||
env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1")
|
|
||||||
|
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
err := cmd.Run(&RunOpts{
|
err := cmd.Run(&RunOpts{
|
||||||
Dir: repo.Path,
|
Dir: repo.Path,
|
||||||
Stdout: target,
|
Stdout: target,
|
||||||
Stderr: &stderr,
|
Stderr: &stderr,
|
||||||
Env: env,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConcatenateError(err, stderr.String())
|
return ConcatenateError(err, stderr.String())
|
||||||
|
66
modules/git/submodule.go
Normal file
66
modules/git/submodule.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplateSubmoduleCommit struct {
|
||||||
|
Path string
|
||||||
|
Commit string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository
|
||||||
|
// This function is only for generating new repos based on existing template, the template couldn't be too large.
|
||||||
|
func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) {
|
||||||
|
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts := &RunOpts{
|
||||||
|
Dir: repoPath,
|
||||||
|
Stdout: stdoutWriter,
|
||||||
|
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
||||||
|
_ = stdoutWriter.Close()
|
||||||
|
defer stdoutReader.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
entry, err := parseLsTreeLine(scanner.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entry.EntryMode == EntryModeCommit {
|
||||||
|
submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scanner.Err()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err)
|
||||||
|
}
|
||||||
|
return submoduleCommits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTemplateSubmoduleIndexes Adds the given submodules to the git index.
|
||||||
|
// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir.
|
||||||
|
func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error {
|
||||||
|
for _, submodule := range submodules {
|
||||||
|
cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path)
|
||||||
|
if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil {
|
||||||
|
log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
48
modules/git/submodule_test.go
Normal file
48
modules/git/submodule_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetTemplateSubmoduleCommits(t *testing.T) {
|
||||||
|
testRepoPath := filepath.Join(testReposDir, "repo4_submodules")
|
||||||
|
submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, submodules, 2)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "<°)))><", submodules[0].Path)
|
||||||
|
assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "libtest", submodules[1].Path)
|
||||||
|
assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTemplateSubmoduleIndexes(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
var err error
|
||||||
|
_, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755)
|
||||||
|
err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir})
|
||||||
|
require.NoError(t, err)
|
||||||
|
submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, submodules, 1)
|
||||||
|
assert.EqualValues(t, "new-dir", submodules[0].Path)
|
||||||
|
assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit)
|
||||||
|
}
|
1
modules/git/tests/repos/repo4_submodules/HEAD
Normal file
1
modules/git/tests/repos/repo4_submodules/HEAD
Normal file
@ -0,0 +1 @@
|
|||||||
|
ref: refs/heads/master
|
4
modules/git/tests/repos/repo4_submodules/config
Normal file
4
modules/git/tests/repos/repo4_submodules/config
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
x<01><>[
|
||||||
|
Β0EύΞ*ζ_<CEB6>ι$MΡ5tifBk IΕ•Ή7ζk~ήΓ9ά<39>—εά ό¦π.jΦΘ ΕOΪδΙ"zΒ`ί#I<>irF…µΝΉΐΨ$%ΉΒης|4)°―?tΌΙ=”Λ:K¦ο#[$DΏ―ϋΏ^<5E><>…΅®Σ’y½HU/<2F>f?G
|
@ -0,0 +1 @@
|
|||||||
|
e1e59caba97193d48862d6809912043871f37437
|
@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubTree get a sub tree by the sub dir path
|
// SubTree get a subtree by the sub dir path
|
||||||
func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
func (t *Tree) SubTree(rpath string) (*Tree, error) {
|
||||||
if len(rpath) == 0 {
|
if len(rpath) == 0 {
|
||||||
return t, nil
|
return t, nil
|
||||||
@ -62,3 +62,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error
|
|||||||
|
|
||||||
return filelist, err
|
return filelist, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTreePathLatestCommit returns the latest commit of a tree path
|
||||||
|
func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) {
|
||||||
|
stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
|
||||||
|
AddDynamicArguments(refName).AddDashesAndList(treePath).
|
||||||
|
RunStdString(&RunOpts{Dir: repo.Path})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return repo.GetCommit(strings.TrimSpace(stdout))
|
||||||
|
}
|
||||||
|
@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
|
|||||||
ptree: t,
|
ptree: t,
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
name: "",
|
name: "",
|
||||||
fullName: "",
|
|
||||||
entryMode: EntryModeTree,
|
entryMode: EntryModeTree,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log"
|
|||||||
|
|
||||||
// TreeEntry the leaf in the git tree
|
// TreeEntry the leaf in the git tree
|
||||||
type TreeEntry struct {
|
type TreeEntry struct {
|
||||||
ID ObjectID
|
ID ObjectID
|
||||||
|
|
||||||
ptree *Tree
|
ptree *Tree
|
||||||
|
|
||||||
entryMode EntryMode
|
entryMode EntryMode
|
||||||
name string
|
name string
|
||||||
|
size int64
|
||||||
size int64
|
sized bool
|
||||||
sized bool
|
|
||||||
fullName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the name of the entry
|
// Name returns the name of the entry
|
||||||
func (te *TreeEntry) Name() string {
|
func (te *TreeEntry) Name() string {
|
||||||
if te.fullName != "" {
|
|
||||||
return te.fullName
|
|
||||||
}
|
|
||||||
return te.name
|
return te.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,3 +25,18 @@ func TestSubTree_Issue29101(t *testing.T) {
|
|||||||
assert.True(t, IsErrNotExist(err))
|
assert.True(t, IsErrNotExist(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_GetTreePathLatestCommit(t *testing.T) {
|
||||||
|
repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer repo.Close()
|
||||||
|
|
||||||
|
commitID, err := repo.GetBranchCommitID("master")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID)
|
||||||
|
|
||||||
|
commit, err := repo.GetTreePathLatestCommit("master", "blame.txt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, commit)
|
||||||
|
assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String())
|
||||||
|
}
|
||||||
|
@ -43,19 +43,20 @@ type contextKey struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
|
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
|
||||||
|
// The caller must call "defer gitRepo.Close()"
|
||||||
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
|
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
|
||||||
ds := reqctx.GetRequestDataStore(ctx)
|
reqCtx := reqctx.FromContext(ctx)
|
||||||
if ds != nil {
|
if reqCtx != nil {
|
||||||
gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo)
|
gitRepo, err := RepositoryFromRequestContextOrOpen(reqCtx, repo)
|
||||||
return gitRepo, util.NopCloser{}, err
|
return gitRepo, util.NopCloser{}, err
|
||||||
}
|
}
|
||||||
gitRepo, err := OpenRepository(ctx, repo)
|
gitRepo, err := OpenRepository(ctx, repo)
|
||||||
return gitRepo, gitRepo, err
|
return gitRepo, gitRepo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context
|
// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context.
|
||||||
// The repo will be automatically closed when the request context is done
|
// Caller shouldn't close the git repo manually, the git repo will be automatically closed when the request context is done.
|
||||||
func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) {
|
func RepositoryFromRequestContextOrOpen(ctx reqctx.RequestContext, repo Repository) (*git.Repository, error) {
|
||||||
ck := contextKey{repoPath: repoPath(repo)}
|
ck := contextKey{repoPath: repoPath(repo)}
|
||||||
if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok {
|
if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok {
|
||||||
return gitRepo, nil
|
return gitRepo, nil
|
||||||
@ -64,7 +65,7 @@ func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ds.AddCloser(gitRepo)
|
ctx.AddCloser(gitRepo)
|
||||||
ds.SetContextValue(ck, gitRepo)
|
ctx.SetContextValue(ck, gitRepo)
|
||||||
return gitRepo, nil
|
return gitRepo, nil
|
||||||
}
|
}
|
||||||
|
@ -123,13 +123,12 @@ func Init() {
|
|||||||
for _, indexerData := range items {
|
for _, indexerData := range items {
|
||||||
log.Trace("IndexerData Process Repo: %d", indexerData.RepoID)
|
log.Trace("IndexerData Process Repo: %d", indexerData.RepoID)
|
||||||
if err := index(ctx, indexer, indexerData.RepoID); err != nil {
|
if err := index(ctx, indexer, indexerData.RepoID); err != nil {
|
||||||
unhandled = append(unhandled, indexerData)
|
|
||||||
if !setting.IsInTesting {
|
if !setting.IsInTesting {
|
||||||
log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err)
|
log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return unhandled
|
return nil // do not re-queue the failed items, otherwise some broken repo will block the queue
|
||||||
}
|
}
|
||||||
|
|
||||||
indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler)
|
indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler)
|
||||||
|
@ -15,6 +15,8 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/indexer/code/bleve"
|
"code.gitea.io/gitea/modules/indexer/code/bleve"
|
||||||
"code.gitea.io/gitea/modules/indexer/code/elasticsearch"
|
"code.gitea.io/gitea/modules/indexer/code/elasticsearch"
|
||||||
"code.gitea.io/gitea/modules/indexer/code/internal"
|
"code.gitea.io/gitea/modules/indexer/code/internal"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
_ "code.gitea.io/gitea/models"
|
_ "code.gitea.io/gitea/models"
|
||||||
_ "code.gitea.io/gitea/models/actions"
|
_ "code.gitea.io/gitea/models/actions"
|
||||||
@ -279,7 +281,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
|
|||||||
|
|
||||||
func TestBleveIndexAndSearch(t *testing.T) {
|
func TestBleveIndexAndSearch(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
|
defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
idx := bleve.NewIndexer(dir)
|
idx := bleve.NewIndexer(dir)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/blevesearch/bleve/v2"
|
"github.com/blevesearch/bleve/v2"
|
||||||
@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) {
|
|||||||
return index, 0, nil
|
return index, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars
|
// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars
|
||||||
// may be different on two string and they still be considered equivalent.
|
// may be different on two string, and they still be considered equivalent.
|
||||||
// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero.
|
// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero.
|
||||||
func GuessFuzzinessByKeyword(s string) int {
|
func GuessFuzzinessByKeyword(s string) int {
|
||||||
tokenizer := unicode_tokenizer.NewUnicodeTokenizer()
|
tokenizer := unicode_tokenizer.NewUnicodeTokenizer()
|
||||||
tokens := tokenizer.Tokenize([]byte(s))
|
tokens := tokenizer.Tokenize([]byte(s))
|
||||||
@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return min(maxFuzziness, len(s)/4)
|
return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4)
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBleveGuessFuzzinessByKeyword(t *testing.T) {
|
func TestBleveGuessFuzzinessByKeyword(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)()
|
||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Input string
|
Input string
|
||||||
Fuzziness int // See util.go for the definition of fuzziness in this particular context
|
Fuzziness int // See util.go for the definition of fuzziness in this particular context
|
||||||
@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) {
|
||||||
assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input))
|
assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
|||||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||||
|
|
||||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td")
|
||||||
|
|
||||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ func LoadRepoConfig() error {
|
|||||||
if isDir, err := util.IsDir(customPath); err != nil {
|
if isDir, err := util.IsDir(customPath); err != nil {
|
||||||
return fmt.Errorf("failed to check custom %s dir: %w", t, err)
|
return fmt.Errorf("failed to check custom %s dir: %w", t, err)
|
||||||
} else if isDir {
|
} else if isDir {
|
||||||
if typeFiles[i].custom, err = util.StatDir(customPath); err != nil {
|
if typeFiles[i].custom, err = util.ListDirRecursively(customPath, &util.ListDirOptions{SkipCommonHiddenNames: true}); err != nil {
|
||||||
return fmt.Errorf("failed to list custom %s files: %w", t, err)
|
return fmt.Errorf("failed to list custom %s files: %w", t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,21 @@ func (r *requestDataStore) cleanUp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestContext interface {
|
||||||
|
context.Context
|
||||||
|
RequestDataStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromContext(ctx context.Context) RequestContext {
|
||||||
|
// here we must use the current ctx and the underlying store
|
||||||
|
// the current ctx guarantees that the ctx deadline/cancellation/values are respected
|
||||||
|
// the underlying store guarantees that the request-specific data is available
|
||||||
|
if store := GetRequestDataStore(ctx); store != nil {
|
||||||
|
return &requestContext{Context: ctx, RequestDataStore: store}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetRequestDataStore(ctx context.Context) RequestDataStore {
|
func GetRequestDataStore(ctx context.Context) RequestDataStore {
|
||||||
if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok {
|
if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok {
|
||||||
return req
|
return req
|
||||||
@ -97,11 +112,11 @@ func GetRequestDataStore(ctx context.Context) RequestDataStore {
|
|||||||
|
|
||||||
type requestContext struct {
|
type requestContext struct {
|
||||||
context.Context
|
context.Context
|
||||||
dataStore *requestDataStore
|
RequestDataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *requestContext) Value(key any) any {
|
func (c *requestContext) Value(key any) any {
|
||||||
if v := c.dataStore.GetContextValue(key); v != nil {
|
if v := c.GetContextValue(key); v != nil {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return c.Context.Value(key)
|
return c.Context.Value(key)
|
||||||
@ -109,9 +124,10 @@ func (c *requestContext) Value(key any) any {
|
|||||||
|
|
||||||
func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) {
|
func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) {
|
||||||
ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true)
|
ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true)
|
||||||
reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}}
|
store := &requestDataStore{values: make(map[any]any)}
|
||||||
|
reqCtx := &requestContext{Context: ctx, RequestDataStore: store}
|
||||||
return reqCtx, func() {
|
return reqCtx, func() {
|
||||||
reqCtx.dataStore.cleanUp()
|
store.cleanUp()
|
||||||
processFinished()
|
processFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,5 +135,5 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co
|
|||||||
// NewRequestContextForTest creates a new RequestContext for testing purposes
|
// NewRequestContextForTest creates a new RequestContext for testing purposes
|
||||||
// It doesn't add the context to the process manager, nor do cleanup
|
// It doesn't add the context to the process manager, nor do cleanup
|
||||||
func NewRequestContextForTest(parentCtx context.Context) context.Context {
|
func NewRequestContextForTest(parentCtx context.Context) context.Context {
|
||||||
return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}}
|
return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}}
|
||||||
}
|
}
|
||||||
|
@ -166,3 +166,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
|||||||
}
|
}
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitGiteaEnvVars initializes the environment variables for gitea
|
||||||
|
func InitGiteaEnvVars() {
|
||||||
|
// Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||||
|
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users,
|
||||||
|
// so at the moment we could still keep "unsetting the unnecessary environments"
|
||||||
|
|
||||||
|
// HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig".
|
||||||
|
// But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist,
|
||||||
|
// then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set.
|
||||||
|
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitGiteaEnvVarsForTesting() {
|
||||||
|
InitGiteaEnvVars()
|
||||||
|
_ = os.Unsetenv("GIT_AUTHOR_NAME")
|
||||||
|
_ = os.Unsetenv("GIT_AUTHOR_EMAIL")
|
||||||
|
_ = os.Unsetenv("GIT_AUTHOR_DATE")
|
||||||
|
_ = os.Unsetenv("GIT_COMMITTER_NAME")
|
||||||
|
_ = os.Unsetenv("GIT_COMMITTER_EMAIL")
|
||||||
|
_ = os.Unsetenv("GIT_COMMITTER_DATE")
|
||||||
|
}
|
||||||
|
@ -5,8 +5,6 @@ package setting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CORSConfig defines CORS settings
|
// CORSConfig defines CORS settings
|
||||||
@ -28,7 +26,4 @@ var CORSConfig = struct {
|
|||||||
|
|
||||||
func loadCorsFrom(rootCfg ConfigProvider) {
|
func loadCorsFrom(rootCfg ConfigProvider) {
|
||||||
mustMapSetting(rootCfg, "cors", &CORSConfig)
|
mustMapSetting(rootCfg, "cors", &CORSConfig)
|
||||||
if CORSConfig.Enabled {
|
|
||||||
log.Info("CORS Service Enabled")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ var Indexer = struct {
|
|||||||
IncludePatterns []*GlobMatcher
|
IncludePatterns []*GlobMatcher
|
||||||
ExcludePatterns []*GlobMatcher
|
ExcludePatterns []*GlobMatcher
|
||||||
ExcludeVendored bool
|
ExcludeVendored bool
|
||||||
|
|
||||||
|
TypeBleveMaxFuzzniess int
|
||||||
}{
|
}{
|
||||||
IssueType: "bleve",
|
IssueType: "bleve",
|
||||||
IssuePath: "indexers/issues.bleve",
|
IssuePath: "indexers/issues.bleve",
|
||||||
@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
|
|||||||
Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true)
|
Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true)
|
||||||
Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024)
|
Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024)
|
||||||
Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second)
|
Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second)
|
||||||
|
Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
|
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
|
||||||
@ -97,7 +100,7 @@ func IndexerGlobFromString(globstr string) []*GlobMatcher {
|
|||||||
expr = strings.TrimSpace(expr)
|
expr = strings.TrimSpace(expr)
|
||||||
if expr != "" {
|
if expr != "" {
|
||||||
if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
|
if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
|
||||||
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
|
log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err)
|
||||||
} else {
|
} else {
|
||||||
extarr = append(extarr, g)
|
extarr = append(extarr, g)
|
||||||
}
|
}
|
||||||
|
@ -255,8 +255,6 @@ func loadMailerFrom(rootCfg ConfigProvider) {
|
|||||||
MailService.OverrideEnvelopeFrom = true
|
MailService.OverrideEnvelopeFrom = true
|
||||||
MailService.EnvelopeFrom = parsed.Address
|
MailService.EnvelopeFrom = parsed.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Mail Service Enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadRegisterMailFrom(rootCfg ConfigProvider) {
|
func loadRegisterMailFrom(rootCfg ConfigProvider) {
|
||||||
@ -267,7 +265,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
Service.RegisterEmailConfirm = true
|
Service.RegisterEmailConfirm = true
|
||||||
log.Info("Register Mail Service Enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNotifyMailFrom(rootCfg ConfigProvider) {
|
func loadNotifyMailFrom(rootCfg ConfigProvider) {
|
||||||
@ -278,7 +275,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
Service.EnableNotifyMail = true
|
Service.EnableNotifyMail = true
|
||||||
log.Info("Notify Mail Service Enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryResolveAddr(addr string) []net.IPAddr {
|
func tryResolveAddr(addr string) []net.IPAddr {
|
||||||
|
@ -13,8 +13,9 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Security settings
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Security settings
|
|
||||||
InstallLock bool
|
InstallLock bool
|
||||||
SecretKey string
|
SecretKey string
|
||||||
InternalToken string // internal access token
|
InternalToken string // internal access token
|
||||||
@ -27,7 +28,7 @@ var (
|
|||||||
ReverseProxyTrustedProxies []string
|
ReverseProxyTrustedProxies []string
|
||||||
MinPasswordLength int
|
MinPasswordLength int
|
||||||
ImportLocalPaths bool
|
ImportLocalPaths bool
|
||||||
DisableGitHooks bool
|
DisableGitHooks = true
|
||||||
DisableWebhooks bool
|
DisableWebhooks bool
|
||||||
OnlyAllowPushIfGiteaEnvironmentSet bool
|
OnlyAllowPushIfGiteaEnvironmentSet bool
|
||||||
PasswordComplexity []string
|
PasswordComplexity []string
|
||||||
|
@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
|||||||
SessionConfig.ProviderConfig = string(shadowConfig)
|
SessionConfig.ProviderConfig = string(shadowConfig)
|
||||||
SessionConfig.OriginalProvider = SessionConfig.Provider
|
SessionConfig.OriginalProvider = SessionConfig.Provider
|
||||||
SessionConfig.Provider = "VirtualSession"
|
SessionConfig.Provider = "VirtualSession"
|
||||||
|
|
||||||
log.Info("Session Service Enabled")
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Load time zone failed: %v", err)
|
log.Fatal("Load time zone failed: %v", err)
|
||||||
}
|
}
|
||||||
log.Info("Default UI Location is %v", zone)
|
|
||||||
}
|
}
|
||||||
if DefaultUILocation == nil {
|
if DefaultUILocation == nil {
|
||||||
DefaultUILocation = time.Local
|
DefaultUILocation = time.Local
|
||||||
|
@ -13,9 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
unittest.MainTest(m, &unittest.TestOptions{
|
unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{ /* load nothing */ }})
|
||||||
FixtureFiles: []string{""}, // load nothing
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type testItem1 struct {
|
type testItem1 struct {
|
||||||
@ -36,8 +34,6 @@ func (*testItem2) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAppStateDB(t *testing.T) {
|
func TestAppStateDB(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
as := &DBStore{}
|
as := &DBStore{}
|
||||||
|
|
||||||
item1 := new(testItem1)
|
item1 := new(testItem1)
|
||||||
|
@ -131,15 +131,9 @@ func NewFuncMap() template.FuncMap {
|
|||||||
"EnableTimetracking": func() bool {
|
"EnableTimetracking": func() bool {
|
||||||
return setting.Service.EnableTimetracking
|
return setting.Service.EnableTimetracking
|
||||||
},
|
},
|
||||||
"DisableGitHooks": func() bool {
|
|
||||||
return setting.DisableGitHooks
|
|
||||||
},
|
|
||||||
"DisableWebhooks": func() bool {
|
"DisableWebhooks": func() bool {
|
||||||
return setting.DisableWebhooks
|
return setting.DisableWebhooks
|
||||||
},
|
},
|
||||||
"DisableImportLocal": func() bool {
|
|
||||||
return !setting.ImportLocalPaths
|
|
||||||
},
|
|
||||||
"UserThemeName": userThemeName,
|
"UserThemeName": userThemeName,
|
||||||
"NotificationSettings": func() map[string]any {
|
"NotificationSettings": func() map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
@ -264,22 +258,42 @@ func userThemeName(user *user_model.User) string {
|
|||||||
return setting.UI.DefaultTheme
|
return setting.UI.DefaultTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isQueryParamEmpty(v any) bool {
|
||||||
|
return v == nil || v == false || v == 0 || v == int64(0) || v == ""
|
||||||
|
}
|
||||||
|
|
||||||
// QueryBuild builds a query string from a list of key-value pairs.
|
// QueryBuild builds a query string from a list of key-value pairs.
|
||||||
// It omits the nil and empty strings, but it doesn't omit other zero values,
|
// It omits the nil, false, zero int/int64 and empty string values,
|
||||||
// because the zero value of number types may have a meaning.
|
// because they are default empty values for "ctx.FormXxx" calls.
|
||||||
|
// If 0 or false need to be included, use string values: "0" and "false".
|
||||||
|
// Build rules:
|
||||||
|
// * Even parameters: always build as query string: a=b&c=d
|
||||||
|
// * Odd parameters:
|
||||||
|
// * * {"/anything", param-pairs...} => "/?param-paris"
|
||||||
|
// * * {"anything?old-params", new-param-pairs...} => "anything?old-params&new-param-paris"
|
||||||
|
// * * Otherwise: {"old¶ms", new-param-pairs...} => "old¶ms&new-param-paris"
|
||||||
|
// * * Other behaviors are undefined yet.
|
||||||
func QueryBuild(a ...any) template.URL {
|
func QueryBuild(a ...any) template.URL {
|
||||||
var s string
|
var reqPath, s string
|
||||||
|
hasTrailingSep := false
|
||||||
if len(a)%2 == 1 {
|
if len(a)%2 == 1 {
|
||||||
if v, ok := a[0].(string); ok {
|
if v, ok := a[0].(string); ok {
|
||||||
if v == "" || (v[0] != '?' && v[0] != '&') {
|
|
||||||
panic("QueryBuild: invalid argument")
|
|
||||||
}
|
|
||||||
s = v
|
s = v
|
||||||
} else if v, ok := a[0].(template.URL); ok {
|
} else if v, ok := a[0].(template.URL); ok {
|
||||||
s = string(v)
|
s = string(v)
|
||||||
} else {
|
} else {
|
||||||
panic("QueryBuild: invalid argument")
|
panic("QueryBuild: invalid argument")
|
||||||
}
|
}
|
||||||
|
hasTrailingSep = s != "&" && strings.HasSuffix(s, "&")
|
||||||
|
if strings.HasPrefix(s, "/") || strings.Contains(s, "?") {
|
||||||
|
if s1, s2, ok := strings.Cut(s, "?"); ok {
|
||||||
|
reqPath = s1 + "?"
|
||||||
|
s = s2
|
||||||
|
} else {
|
||||||
|
reqPath += s + "?"
|
||||||
|
s = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i := len(a) % 2; i < len(a); i += 2 {
|
for i := len(a) % 2; i < len(a); i += 2 {
|
||||||
k, ok := a[i].(string)
|
k, ok := a[i].(string)
|
||||||
@ -290,19 +304,16 @@ func QueryBuild(a ...any) template.URL {
|
|||||||
if va, ok := a[i+1].(string); ok {
|
if va, ok := a[i+1].(string); ok {
|
||||||
v = va
|
v = va
|
||||||
} else if a[i+1] != nil {
|
} else if a[i+1] != nil {
|
||||||
v = fmt.Sprint(a[i+1])
|
if !isQueryParamEmpty(a[i+1]) {
|
||||||
|
v = fmt.Sprint(a[i+1])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// pos1 to pos2 is the "k=v&" part, "&" is optional
|
// pos1 to pos2 is the "k=v&" part, "&" is optional
|
||||||
pos1 := strings.Index(s, "&"+k+"=")
|
pos1 := strings.Index(s, "&"+k+"=")
|
||||||
if pos1 != -1 {
|
if pos1 != -1 {
|
||||||
pos1++
|
pos1++
|
||||||
} else {
|
} else if strings.HasPrefix(s, k+"=") {
|
||||||
pos1 = strings.Index(s, "?"+k+"=")
|
pos1 = 0
|
||||||
if pos1 != -1 {
|
|
||||||
pos1++
|
|
||||||
} else if strings.HasPrefix(s, k+"=") {
|
|
||||||
pos1 = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pos2 := len(s)
|
pos2 := len(s)
|
||||||
if pos1 == -1 {
|
if pos1 == -1 {
|
||||||
@ -315,7 +326,7 @@ func QueryBuild(a ...any) template.URL {
|
|||||||
}
|
}
|
||||||
if v != "" {
|
if v != "" {
|
||||||
sep := ""
|
sep := ""
|
||||||
hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&'))
|
hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && s[pos1-1] == '&')
|
||||||
if !hasPrefixSep {
|
if !hasPrefixSep {
|
||||||
sep = "&"
|
sep = "&"
|
||||||
}
|
}
|
||||||
@ -324,9 +335,22 @@ func QueryBuild(a ...any) template.URL {
|
|||||||
s = s[:pos1] + s[pos2:]
|
s = s[:pos1] + s[pos2:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s != "" && s != "&" && s[len(s)-1] == '&' {
|
if s != "" && s[len(s)-1] == '&' && !hasTrailingSep {
|
||||||
s = s[:len(s)-1]
|
s = s[:len(s)-1]
|
||||||
}
|
}
|
||||||
|
if reqPath != "" {
|
||||||
|
if s == "" {
|
||||||
|
s = reqPath
|
||||||
|
if s != "?" {
|
||||||
|
s = s[:len(s)-1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if s[0] == '&' {
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
s = reqPath + s
|
||||||
|
}
|
||||||
|
}
|
||||||
return template.URL(s)
|
return template.URL(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,3 +118,58 @@ func TestTemplateEscape(t *testing.T) {
|
|||||||
assert.Equal(t, `<a k="""><></a>`, actual)
|
assert.Equal(t, `<a k="""><></a>`, actual)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQueryBuild(t *testing.T) {
|
||||||
|
t.Run("construct", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "", string(QueryBuild()))
|
||||||
|
assert.Equal(t, "", string(QueryBuild("a", nil, "b", false, "c", 0, "d", "")))
|
||||||
|
assert.Equal(t, "a=1&b=true", string(QueryBuild("a", 1, "b", "true")))
|
||||||
|
|
||||||
|
// path with query parameters
|
||||||
|
assert.Equal(t, "/?k=1", string(QueryBuild("/", "k", 1)))
|
||||||
|
assert.Equal(t, "/", string(QueryBuild("/?k=a", "k", 0)))
|
||||||
|
|
||||||
|
// no path but question mark with query parameters
|
||||||
|
assert.Equal(t, "?k=1", string(QueryBuild("?", "k", 1)))
|
||||||
|
assert.Equal(t, "?", string(QueryBuild("?", "k", 0)))
|
||||||
|
assert.Equal(t, "path?k=1", string(QueryBuild("path?", "k", 1)))
|
||||||
|
assert.Equal(t, "path", string(QueryBuild("path?", "k", 0)))
|
||||||
|
|
||||||
|
// only query parameters
|
||||||
|
assert.Equal(t, "&k=1", string(QueryBuild("&", "k", 1)))
|
||||||
|
assert.Equal(t, "", string(QueryBuild("&", "k", 0)))
|
||||||
|
assert.Equal(t, "", string(QueryBuild("&k=a", "k", 0)))
|
||||||
|
assert.Equal(t, "", string(QueryBuild("k=a&", "k", 0)))
|
||||||
|
assert.Equal(t, "a=1&b=2", string(QueryBuild("a=1", "b", 2)))
|
||||||
|
assert.Equal(t, "&a=1&b=2", string(QueryBuild("&a=1", "b", 2)))
|
||||||
|
assert.Equal(t, "a=1&b=2&", string(QueryBuild("a=1&", "b", 2)))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replace", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "a=1&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", 1)))
|
||||||
|
assert.Equal(t, "a=b&c=1&e=f", string(QueryBuild("a=b&c=d&e=f", "c", 1)))
|
||||||
|
assert.Equal(t, "a=b&c=d&e=1", string(QueryBuild("a=b&c=d&e=f", "e", 1)))
|
||||||
|
assert.Equal(t, "a=b&c=d&e=f&k=1", string(QueryBuild("a=b&c=d&e=f", "k", 1)))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replace-&", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "&a=1&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", 1)))
|
||||||
|
assert.Equal(t, "&a=b&c=1&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", 1)))
|
||||||
|
assert.Equal(t, "&a=b&c=d&e=1", string(QueryBuild("&a=b&c=d&e=f", "e", 1)))
|
||||||
|
assert.Equal(t, "&a=b&c=d&e=f&k=1", string(QueryBuild("&a=b&c=d&e=f", "k", 1)))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", "")))
|
||||||
|
assert.Equal(t, "a=b&e=f", string(QueryBuild("a=b&c=d&e=f", "c", "")))
|
||||||
|
assert.Equal(t, "a=b&c=d", string(QueryBuild("a=b&c=d&e=f", "e", "")))
|
||||||
|
assert.Equal(t, "a=b&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "k", "")))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete-&", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", "")))
|
||||||
|
assert.Equal(t, "&a=b&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", "")))
|
||||||
|
assert.Equal(t, "&a=b&c=d", string(QueryBuild("&a=b&c=d&e=f", "e", "")))
|
||||||
|
assert.Equal(t, "&a=b&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "k", "")))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -140,81 +140,51 @@ func IsExist(path string) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
|
func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error {
|
||||||
dir, err := os.Open(dirPath)
|
dir, err := os.Open(fsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
defer dir.Close()
|
defer dir.Close()
|
||||||
|
|
||||||
fis, err := dir.Readdir(0)
|
fis, err := dir.Readdir(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
statList := make([]string, 0)
|
|
||||||
for _, fi := range fis {
|
for _, fi := range fis {
|
||||||
if CommonSkip(fi.Name()) {
|
if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
relPath := path.Join(recordParentPath, fi.Name())
|
||||||
relPath := path.Join(recPath, fi.Name())
|
curPath := filepath.Join(fsDir, fi.Name())
|
||||||
curPath := path.Join(dirPath, fi.Name())
|
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
if includeDir {
|
if opts.IncludeDir {
|
||||||
statList = append(statList, relPath+"/")
|
*result = append(*result, relPath+"/")
|
||||||
}
|
}
|
||||||
s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
|
if err = listDirRecursively(result, curPath, relPath, opts); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
statList = append(statList, s...)
|
|
||||||
} else if !isDirOnly {
|
|
||||||
statList = append(statList, relPath)
|
|
||||||
} else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
|
|
||||||
link, err := os.Readlink(curPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
isDir, err := IsDir(link)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isDir {
|
|
||||||
if includeDir {
|
|
||||||
statList = append(statList, relPath+"/")
|
|
||||||
}
|
|
||||||
s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
statList = append(statList, s...)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
*result = append(*result, relPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return statList, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatDir gathers information of given directory by depth-first.
|
type ListDirOptions struct {
|
||||||
// It returns slice of file list and includes subdirectories if enabled;
|
IncludeDir bool // subdirectories are also included with suffix slash
|
||||||
// it returns error and nil slice when error occurs in underlying functions,
|
SkipCommonHiddenNames bool
|
||||||
// or given path is not a directory or does not exist.
|
}
|
||||||
//
|
|
||||||
// Slice does not include given path itself.
|
|
||||||
// If subdirectories is enabled, they will have suffix '/'.
|
|
||||||
func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
|
|
||||||
if isDir, err := IsDir(rootPath); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !isDir {
|
|
||||||
return nil, errors.New("not a directory or does not exist: " + rootPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
isIncludeDir := false
|
// ListDirRecursively gathers information of given directory by depth-first.
|
||||||
if len(includeDir) != 0 {
|
// The paths are always in "dir/slash/file" format (not "\\" even in Windows)
|
||||||
isIncludeDir = includeDir[0]
|
// Slice does not include given path itself.
|
||||||
|
func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) {
|
||||||
|
if err = listDirRecursively(&res, rootDir, "", opts); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return statDir(rootPath, "", isIncludeDir, false, false)
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOSWindows() bool {
|
func isOSWindows() bool {
|
||||||
@ -265,8 +235,8 @@ func HomeDir() (home string, err error) {
|
|||||||
return home, nil
|
return home, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommonSkip will check a provided name to see if it represents file or directory that should not be watched
|
// IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched
|
||||||
func CommonSkip(name string) bool {
|
func IsCommonHiddenFileName(name string) bool {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -275,9 +245,9 @@ func CommonSkip(name string) bool {
|
|||||||
case '.':
|
case '.':
|
||||||
return true
|
return true
|
||||||
case 't', 'T':
|
case 't', 'T':
|
||||||
return name[1:] == "humbs.db"
|
return name[1:] == "humbs.db" // macOS
|
||||||
case 'd', 'D':
|
case 'd', 'D':
|
||||||
return name[1:] == "esktop.ini"
|
return name[1:] == "esktop.ini" // Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -5,10 +5,12 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFileURLToPath(t *testing.T) {
|
func TestFileURLToPath(t *testing.T) {
|
||||||
@ -210,3 +212,21 @@ func TestCleanPath(t *testing.T) {
|
|||||||
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems)
|
assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListDirRecursively(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
_ = os.WriteFile(tmpDir+"/.config", nil, 0o644)
|
||||||
|
_ = os.Mkdir(tmpDir+"/d1", 0o755)
|
||||||
|
_ = os.WriteFile(tmpDir+"/d1/f-d1", nil, 0o644)
|
||||||
|
_ = os.Mkdir(tmpDir+"/d1/s1", 0o755)
|
||||||
|
_ = os.WriteFile(tmpDir+"/d1/s1/f-d1s1", nil, 0o644)
|
||||||
|
_ = os.Mkdir(tmpDir+"/d2", 0o755)
|
||||||
|
|
||||||
|
res, err := ListDirRecursively(tmpDir, &ListDirOptions{IncludeDir: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, []string{".config", "d1/", "d1/f-d1", "d1/s1/", "d1/s1/f-d1s1", "d2/"}, res)
|
||||||
|
|
||||||
|
res, err = ListDirRecursively(tmpDir, &ListDirOptions{SkipCommonHiddenNames: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res)
|
||||||
|
}
|
||||||
|
@ -4,18 +4,14 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
"code.gitea.io/gitea/modules/htmlutil"
|
"code.gitea.io/gitea/modules/htmlutil"
|
||||||
"code.gitea.io/gitea/modules/reqctx"
|
"code.gitea.io/gitea/modules/reqctx"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
|
||||||
"gitea.com/go-chi/binding"
|
"gitea.com/go-chi/binding"
|
||||||
@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any {
|
|||||||
|
|
||||||
// Router defines a route based on chi's router
|
// Router defines a route based on chi's router
|
||||||
type Router struct {
|
type Router struct {
|
||||||
chiRouter chi.Router
|
chiRouter *chi.Mux
|
||||||
curGroupPrefix string
|
curGroupPrefix string
|
||||||
curMiddlewares []any
|
curMiddlewares []any
|
||||||
}
|
}
|
||||||
@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool {
|
|||||||
return r.Kind() == reflect.Func && r.IsNil()
|
return r.Kind() == reflect.Func && r.IsNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
|
func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
|
||||||
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1)
|
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1)
|
||||||
for _, m := range r.curMiddlewares {
|
for _, m := range curMiddlewares {
|
||||||
if !isNilOrFuncNil(m) {
|
if !isNilOrFuncNil(m) {
|
||||||
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, m := range h {
|
if len(h) == 0 {
|
||||||
|
panic("no endpoint handler provided")
|
||||||
|
}
|
||||||
|
for i, m := range h {
|
||||||
if !isNilOrFuncNil(m) {
|
if !isNilOrFuncNil(m) {
|
||||||
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
||||||
|
} else if i == len(h)-1 {
|
||||||
|
panic("endpoint handler can't be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
middlewares := handlerProviders[:len(handlerProviders)-1]
|
middlewares := handlerProviders[:len(handlerProviders)-1]
|
||||||
@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha
|
|||||||
// Methods adds the same handlers for multiple http "methods" (separated by ",").
|
// Methods adds the same handlers for multiple http "methods" (separated by ",").
|
||||||
// If any method is invalid, the lower level router will panic.
|
// If any method is invalid, the lower level router will panic.
|
||||||
func (r *Router) Methods(methods, pattern string, h ...any) {
|
func (r *Router) Methods(methods, pattern string, h ...any) {
|
||||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
|
||||||
fullPattern := r.getPattern(pattern)
|
fullPattern := r.getPattern(pattern)
|
||||||
if strings.Contains(methods, ",") {
|
if strings.Contains(methods, ",") {
|
||||||
methods := strings.Split(methods, ",")
|
methods := strings.Split(methods, ",")
|
||||||
@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) {
|
|||||||
|
|
||||||
// Any delegate requests for all methods
|
// Any delegate requests for all methods
|
||||||
func (r *Router) Any(pattern string, h ...any) {
|
func (r *Router) Any(pattern string, h ...any) {
|
||||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
|
||||||
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
|
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) {
|
|||||||
r.chiRouter.NotFound(h)
|
r.chiRouter.NotFound(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathProcessorParam struct {
|
|
||||||
name string
|
|
||||||
captureGroup int
|
|
||||||
}
|
|
||||||
|
|
||||||
type PathProcessor struct {
|
|
||||||
methods container.Set[string]
|
|
||||||
re *regexp.Regexp
|
|
||||||
params []pathProcessorParam
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
|
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
|
||||||
normalized := false
|
normalized := false
|
||||||
normalizedPath := req.URL.EscapedPath()
|
normalizedPath := req.URL.EscapedPath()
|
||||||
@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
|
|||||||
next.ServeHTTP(resp, req)
|
next.ServeHTTP(resp, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool {
|
|
||||||
if !p.methods.Contains(chiCtx.RouteMethod) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
path = "/" + path
|
|
||||||
}
|
|
||||||
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
|
||||||
if pathMatches == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var paramMatches [][]int
|
|
||||||
for i := 2; i < len(pathMatches); {
|
|
||||||
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
|
||||||
pmIdx := len(paramMatches) - 1
|
|
||||||
end := pathMatches[i+1]
|
|
||||||
i += 2
|
|
||||||
for ; i < len(pathMatches); i += 2 {
|
|
||||||
if pathMatches[i] >= end {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, pm := range paramMatches {
|
|
||||||
groupIdx := p.params[i].captureGroup * 2
|
|
||||||
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPathProcessor(methods, pattern string) *PathProcessor {
|
|
||||||
p := &PathProcessor{methods: make(container.Set[string])}
|
|
||||||
for _, method := range strings.Split(methods, ",") {
|
|
||||||
p.methods.Add(strings.TrimSpace(method))
|
|
||||||
}
|
|
||||||
re := []byte{'^'}
|
|
||||||
lastEnd := 0
|
|
||||||
for lastEnd < len(pattern) {
|
|
||||||
start := strings.IndexByte(pattern[lastEnd:], '<')
|
|
||||||
if start == -1 {
|
|
||||||
re = append(re, pattern[lastEnd:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
|
||||||
if end == -1 {
|
|
||||||
panic(fmt.Sprintf("invalid pattern: %s", pattern))
|
|
||||||
}
|
|
||||||
re = append(re, pattern[lastEnd:lastEnd+start]...)
|
|
||||||
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
|
||||||
lastEnd += start + end + 1
|
|
||||||
|
|
||||||
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
|
||||||
// it is not used so no need to implement it now
|
|
||||||
param := pathProcessorParam{}
|
|
||||||
if partExp == "*" {
|
|
||||||
re = append(re, "(.*?)/?"...)
|
|
||||||
if lastEnd < len(pattern) {
|
|
||||||
if pattern[lastEnd] == '/' {
|
|
||||||
lastEnd++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
partExp = util.IfZero(partExp, "[^/]+")
|
|
||||||
re = append(re, '(')
|
|
||||||
re = append(re, partExp...)
|
|
||||||
re = append(re, ')')
|
|
||||||
}
|
|
||||||
param.name = partName
|
|
||||||
p.params = append(p.params, param)
|
|
||||||
}
|
|
||||||
re = append(re, '$')
|
|
||||||
reStr := string(re)
|
|
||||||
p.re = regexp.MustCompile(reStr)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combo delegates requests to Combo
|
// Combo delegates requests to Combo
|
||||||
func (r *Router) Combo(pattern string, h ...any) *Combo {
|
func (r *Router) Combo(pattern string, h ...any) *Combo {
|
||||||
return &Combo{r, pattern, h}
|
return &Combo{r, pattern, h}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combo represents a tiny group routes with same pattern
|
// PathGroup creates a group of paths which could be matched by regexp.
|
||||||
type Combo struct {
|
// It is only designed to resolve some special cases which chi router can't handle.
|
||||||
r *Router
|
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
||||||
pattern string
|
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
|
||||||
h []any
|
g := &RouterPathGroup{r: r, pathParam: "*"}
|
||||||
}
|
fn(g)
|
||||||
|
r.Any(pattern, append(h, g.ServeHTTP)...)
|
||||||
// Get delegates Get method
|
|
||||||
func (c *Combo) Get(h ...any) *Combo {
|
|
||||||
c.r.Get(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post delegates Post method
|
|
||||||
func (c *Combo) Post(h ...any) *Combo {
|
|
||||||
c.r.Post(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete delegates Delete method
|
|
||||||
func (c *Combo) Delete(h ...any) *Combo {
|
|
||||||
c.r.Delete(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put delegates Put method
|
|
||||||
func (c *Combo) Put(h ...any) *Combo {
|
|
||||||
c.r.Put(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch delegates Patch method
|
|
||||||
func (c *Combo) Patch(h ...any) *Combo {
|
|
||||||
c.r.Patch(c.pattern, append(c.h, h...)...)
|
|
||||||
return c
|
|
||||||
}
|
}
|
41
modules/web/router_combo.go
Normal file
41
modules/web/router_combo.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
// Combo represents a tiny group routes with same pattern
|
||||||
|
type Combo struct {
|
||||||
|
r *Router
|
||||||
|
pattern string
|
||||||
|
h []any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get delegates Get method
|
||||||
|
func (c *Combo) Get(h ...any) *Combo {
|
||||||
|
c.r.Get(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post delegates Post method
|
||||||
|
func (c *Combo) Post(h ...any) *Combo {
|
||||||
|
c.r.Post(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete delegates Delete method
|
||||||
|
func (c *Combo) Delete(h ...any) *Combo {
|
||||||
|
c.r.Delete(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put delegates Put method
|
||||||
|
func (c *Combo) Put(h ...any) *Combo {
|
||||||
|
c.r.Put(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch delegates Patch method
|
||||||
|
func (c *Combo) Patch(h ...any) *Combo {
|
||||||
|
c.r.Patch(c.pattern, append(c.h, h...)...)
|
||||||
|
return c
|
||||||
|
}
|
135
modules/web/router_path.go
Normal file
135
modules/web/router_path.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RouterPathGroup struct {
|
||||||
|
r *Router
|
||||||
|
pathParam string
|
||||||
|
matchers []*routerPathMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
chiCtx := chi.RouteContext(req.Context())
|
||||||
|
path := chiCtx.URLParam(g.pathParam)
|
||||||
|
for _, m := range g.matchers {
|
||||||
|
if m.matchPath(chiCtx, path) {
|
||||||
|
handler := m.handlerFunc
|
||||||
|
for i := len(m.middlewares) - 1; i >= 0; i-- {
|
||||||
|
handler = m.middlewares[i](handler).ServeHTTP
|
||||||
|
}
|
||||||
|
handler(resp, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchPath matches the request method, and uses regexp to match the path.
|
||||||
|
// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router)
|
||||||
|
// It is only designed to resolve some special cases which chi router can't handle.
|
||||||
|
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
||||||
|
func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
|
||||||
|
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
||||||
|
}
|
||||||
|
|
||||||
|
type routerPathParam struct {
|
||||||
|
name string
|
||||||
|
captureGroup int
|
||||||
|
}
|
||||||
|
|
||||||
|
type routerPathMatcher struct {
|
||||||
|
methods container.Set[string]
|
||||||
|
re *regexp.Regexp
|
||||||
|
params []routerPathParam
|
||||||
|
middlewares []func(http.Handler) http.Handler
|
||||||
|
handlerFunc http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
|
||||||
|
if !p.methods.Contains(chiCtx.RouteMethod) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
||||||
|
if pathMatches == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var paramMatches [][]int
|
||||||
|
for i := 2; i < len(pathMatches); {
|
||||||
|
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
||||||
|
pmIdx := len(paramMatches) - 1
|
||||||
|
end := pathMatches[i+1]
|
||||||
|
i += 2
|
||||||
|
for ; i < len(pathMatches); i += 2 {
|
||||||
|
if pathMatches[i] >= end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, pm := range paramMatches {
|
||||||
|
groupIdx := p.params[i].captureGroup * 2
|
||||||
|
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher {
|
||||||
|
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h)
|
||||||
|
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
|
||||||
|
for _, method := range strings.Split(methods, ",") {
|
||||||
|
p.methods.Add(strings.TrimSpace(method))
|
||||||
|
}
|
||||||
|
re := []byte{'^'}
|
||||||
|
lastEnd := 0
|
||||||
|
for lastEnd < len(pattern) {
|
||||||
|
start := strings.IndexByte(pattern[lastEnd:], '<')
|
||||||
|
if start == -1 {
|
||||||
|
re = append(re, pattern[lastEnd:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
||||||
|
if end == -1 {
|
||||||
|
panic(fmt.Sprintf("invalid pattern: %s", pattern))
|
||||||
|
}
|
||||||
|
re = append(re, pattern[lastEnd:lastEnd+start]...)
|
||||||
|
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
||||||
|
lastEnd += start + end + 1
|
||||||
|
|
||||||
|
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
||||||
|
// it is not used so no need to implement it now
|
||||||
|
param := routerPathParam{}
|
||||||
|
if partExp == "*" {
|
||||||
|
re = append(re, "(.*?)/?"...)
|
||||||
|
if lastEnd < len(pattern) && pattern[lastEnd] == '/' {
|
||||||
|
lastEnd++ // the "*" pattern is able to handle the last slash, so skip it
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partExp = util.IfZero(partExp, "[^/]+")
|
||||||
|
re = append(re, '(')
|
||||||
|
re = append(re, partExp...)
|
||||||
|
re = append(re, ')')
|
||||||
|
}
|
||||||
|
param.name = partName
|
||||||
|
p.params = append(p.params, param)
|
||||||
|
}
|
||||||
|
re = append(re, '$')
|
||||||
|
reStr := string(re)
|
||||||
|
p.re = regexp.MustCompile(reStr)
|
||||||
|
return p
|
||||||
|
}
|
@ -27,17 +27,21 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string {
|
|||||||
}
|
}
|
||||||
m[key] = pathParams.Values[i]
|
m[key] = pathParams.Values[i]
|
||||||
}
|
}
|
||||||
return m
|
return util.Iif(len(m) == 0, nil, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathProcessor(t *testing.T) {
|
func TestPathProcessor(t *testing.T) {
|
||||||
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.RouteMethod = "GET"
|
chiCtx.RouteMethod = "GET"
|
||||||
p := NewPathProcessor("GET", pattern)
|
p := newRouterPathMatcher("GET", pattern, http.NotFound)
|
||||||
assert.True(t, p.ProcessRequestPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
||||||
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the "<...>" is intentionally designed to distinguish from chi's path parameters, because:
|
||||||
|
// 1. their behaviors are totally different, we do not want to mislead developers
|
||||||
|
// 2. we can write regexp in "<name:\w{3,4}>" easily and parse it easily
|
||||||
testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"})
|
testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"})
|
||||||
testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path
|
testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path
|
||||||
testProcess("/<p1:*>", "/", map[string]string{"p1": ""})
|
testProcess("/<p1:*>", "/", map[string]string{"p1": ""})
|
||||||
@ -67,24 +71,31 @@ func TestRouter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
mark := util.OptionalArg(optMark, "")
|
||||||
|
return func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) {
|
||||||
|
h(stop)(resp, req)
|
||||||
|
resp.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r := NewRouter()
|
r := NewRouter()
|
||||||
|
r.NotFound(h("not-found:/"))
|
||||||
r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called
|
r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called
|
||||||
r.Group("/{username}/{reponame}", func() {
|
r.Group("/{username}/{reponame}", func() {
|
||||||
r.Get("/{type:issues|pulls}", h("list-issues-b"))
|
r.Get("/{type:issues|pulls}", h("list-issues-b"))
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
r.Get("/{type:issues|pulls}/{index}", h("view-issue"))
|
r.Get("/{type:issues|pulls}/{index}", h("view-issue"))
|
||||||
}, func(resp http.ResponseWriter, req *http.Request) {
|
}, stopMark())
|
||||||
if stop := req.FormValue("stop"); stop != "" {
|
|
||||||
h(stop)(resp, req)
|
|
||||||
resp.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
r.Group("/issues/{index}", func() {
|
r.Group("/issues/{index}", func() {
|
||||||
r.Post("/update", h("update-issue"))
|
r.Post("/update", h("update-issue"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
m := NewRouter()
|
m := NewRouter()
|
||||||
|
m.NotFound(h("not-found:/api/v1"))
|
||||||
r.Mount("/api/v1", m)
|
r.Mount("/api/v1", m)
|
||||||
m.Group("/repos", func() {
|
m.Group("/repos", func() {
|
||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
@ -96,11 +107,14 @@ func TestRouter(t *testing.T) {
|
|||||||
m.Patch("", h())
|
m.Patch("", h())
|
||||||
m.Delete("", h())
|
m.Delete("", h())
|
||||||
})
|
})
|
||||||
|
m.PathGroup("/*", func(g *RouterPathGroup) {
|
||||||
|
g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path"))
|
||||||
|
}, stopMark("s1"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute := func(methodPath string, expected resultStruct) {
|
testRoute := func(t *testing.T, methodPath string, expected resultStruct) {
|
||||||
t.Run(methodPath, func(t *testing.T) {
|
t.Run(methodPath, func(t *testing.T) {
|
||||||
res = resultStruct{}
|
res = resultStruct{}
|
||||||
methodPathFields := strings.Fields(methodPath)
|
methodPathFields := strings.Fields(methodPath)
|
||||||
@ -111,24 +125,24 @@ func TestRouter(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Root Router", func(t *testing.T) {
|
t.Run("RootRouter", func(t *testing.T) {
|
||||||
testRoute("GET /the-user/the-repo/other", resultStruct{})
|
testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"})
|
||||||
testRoute("GET /the-user/the-repo/pulls", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
||||||
handlerMark: "list-issues-b",
|
handlerMark: "list-issues-b",
|
||||||
})
|
})
|
||||||
testRoute("GET /the-user/the-repo/issues/123", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||||
handlerMark: "view-issue",
|
handlerMark: "view-issue",
|
||||||
})
|
})
|
||||||
testRoute("GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||||
handlerMark: "hijack",
|
handlerMark: "hijack",
|
||||||
})
|
})
|
||||||
testRoute("POST /the-user/the-repo/issues/123/update", resultStruct{
|
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
||||||
handlerMark: "update-issue",
|
handlerMark: "update-issue",
|
||||||
@ -136,31 +150,62 @@ func TestRouter(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Sub Router", func(t *testing.T) {
|
t.Run("Sub Router", func(t *testing.T) {
|
||||||
testRoute("GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"})
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("POST /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||||
})
|
})
|
||||||
|
|
||||||
testRoute("DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("MatchPath", func(t *testing.T) {
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||||
|
handlerMark: "match-path",
|
||||||
|
})
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"},
|
||||||
|
handlerMark: "match-path",
|
||||||
|
})
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
|
||||||
|
handlerMark: "not-found:/api/v1",
|
||||||
|
})
|
||||||
|
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
|
||||||
|
handlerMark: "s1",
|
||||||
|
})
|
||||||
|
|
||||||
|
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
|
||||||
|
method: "GET",
|
||||||
|
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||||
|
handlerMark: "s2",
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouteNormalizePath(t *testing.T) {
|
func TestRouteNormalizePath(t *testing.T) {
|
@ -647,6 +647,7 @@ joined_on=Přidal/a se %s
|
|||||||
repositories=Repozitáře
|
repositories=Repozitáře
|
||||||
activity=Veřejná aktivita
|
activity=Veřejná aktivita
|
||||||
followers=Sledující
|
followers=Sledující
|
||||||
|
show_more=Zobrazit více
|
||||||
starred=Oblíbené repozitáře
|
starred=Oblíbené repozitáře
|
||||||
watched=Sledované repozitáře
|
watched=Sledované repozitáře
|
||||||
code=Kód
|
code=Kód
|
||||||
@ -1011,7 +1012,6 @@ new_repo_helper=Repozitář obsahuje všechny projektové soubory, včetně hist
|
|||||||
owner=Vlastník
|
owner=Vlastník
|
||||||
owner_helper=Některé organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému počtu repozitářů.
|
owner_helper=Některé organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému počtu repozitářů.
|
||||||
repo_name=Název repozitáře
|
repo_name=Název repozitáře
|
||||||
repo_name_helper=Dobrý název repozitáře většinou používá krátká, zapamatovatelná a unikátní klíčová slova.
|
|
||||||
repo_size=Velikost repozitáře
|
repo_size=Velikost repozitáře
|
||||||
template=Šablona
|
template=Šablona
|
||||||
template_select=Vyberte šablonu.
|
template_select=Vyberte šablonu.
|
||||||
@ -2832,6 +2832,7 @@ teams.invite.title=Byli jste pozváni do týmu <strong>%s</strong> v organizaci
|
|||||||
teams.invite.by=Pozvání od %s
|
teams.invite.by=Pozvání od %s
|
||||||
teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
|
teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
maintenance=Údržba
|
maintenance=Údržba
|
||||||
dashboard=Přehled
|
dashboard=Přehled
|
||||||
|
@ -93,6 +93,7 @@ remove_all=Alle entfernen
|
|||||||
remove_label_str=Element "%s " entfernen
|
remove_label_str=Element "%s " entfernen
|
||||||
edit=Bearbeiten
|
edit=Bearbeiten
|
||||||
view=Anzeigen
|
view=Anzeigen
|
||||||
|
test=Test
|
||||||
|
|
||||||
enabled=Aktiviert
|
enabled=Aktiviert
|
||||||
disabled=Deaktiviert
|
disabled=Deaktiviert
|
||||||
@ -103,6 +104,7 @@ copy_url=URL kopieren
|
|||||||
copy_hash=Hash kopieren
|
copy_hash=Hash kopieren
|
||||||
copy_content=Inhalt kopieren
|
copy_content=Inhalt kopieren
|
||||||
copy_branch=Branchnamen kopieren
|
copy_branch=Branchnamen kopieren
|
||||||
|
copy_path=Pfad kopieren
|
||||||
copy_success=Kopiert!
|
copy_success=Kopiert!
|
||||||
copy_error=Kopieren fehlgeschlagen
|
copy_error=Kopieren fehlgeschlagen
|
||||||
copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden
|
copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden
|
||||||
@ -143,6 +145,7 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen?
|
|||||||
|
|
||||||
name=Name
|
name=Name
|
||||||
value=Wert
|
value=Wert
|
||||||
|
readme=Readme
|
||||||
|
|
||||||
filter=Filter
|
filter=Filter
|
||||||
filter.clear=Filter leeren
|
filter.clear=Filter leeren
|
||||||
@ -158,12 +161,15 @@ filter.public=Öffentlich
|
|||||||
filter.private=Privat
|
filter.private=Privat
|
||||||
|
|
||||||
no_results_found=Es wurden keine Ergebnisse gefunden.
|
no_results_found=Es wurden keine Ergebnisse gefunden.
|
||||||
|
internal_error_skipped=Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s
|
||||||
|
|
||||||
[search]
|
[search]
|
||||||
search=Suche ...
|
search=Suche ...
|
||||||
type_tooltip=Suchmodus
|
type_tooltip=Suchmodus
|
||||||
fuzzy=Ähnlich
|
fuzzy=Ähnlich
|
||||||
fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind
|
fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind
|
||||||
|
exact=Exakt
|
||||||
|
exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen
|
||||||
repo_kind=Repositories durchsuchen ...
|
repo_kind=Repositories durchsuchen ...
|
||||||
user_kind=Benutzer durchsuchen ...
|
user_kind=Benutzer durchsuchen ...
|
||||||
org_kind=Organisationen durchsuchen ...
|
org_kind=Organisationen durchsuchen ...
|
||||||
@ -174,9 +180,13 @@ code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" berei
|
|||||||
package_kind=Pakete durchsuchen ...
|
package_kind=Pakete durchsuchen ...
|
||||||
project_kind=Projekte durchsuchen ...
|
project_kind=Projekte durchsuchen ...
|
||||||
branch_kind=Branches durchsuchen ...
|
branch_kind=Branches durchsuchen ...
|
||||||
|
tag_kind=Tags durchsuchen...
|
||||||
|
tag_tooltip=Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen.
|
||||||
commit_kind=Commits durchsuchen ...
|
commit_kind=Commits durchsuchen ...
|
||||||
runner_kind=Runner durchsuchen ...
|
runner_kind=Runner durchsuchen ...
|
||||||
no_results=Es wurden keine passenden Ergebnisse gefunden.
|
no_results=Es wurden keine passenden Ergebnisse gefunden.
|
||||||
|
issue_kind=Issues durchsuchen ...
|
||||||
|
pull_kind=Pull-Requests durchsuchen...
|
||||||
keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
|
keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
|
||||||
|
|
||||||
[aria]
|
[aria]
|
||||||
@ -201,7 +211,10 @@ buttons.link.tooltip=Link hinzufügen
|
|||||||
buttons.list.unordered.tooltip=Liste hinzufügen
|
buttons.list.unordered.tooltip=Liste hinzufügen
|
||||||
buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen
|
buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen
|
||||||
buttons.list.task.tooltip=Aufgabenliste hinzufügen
|
buttons.list.task.tooltip=Aufgabenliste hinzufügen
|
||||||
|
buttons.table.add.tooltip=Tabelle hinzufügen
|
||||||
buttons.table.add.insert=Hinzufügen
|
buttons.table.add.insert=Hinzufügen
|
||||||
|
buttons.table.rows=Zeilen
|
||||||
|
buttons.table.cols=Spalten
|
||||||
buttons.mention.tooltip=Benutzer oder Team erwähnen
|
buttons.mention.tooltip=Benutzer oder Team erwähnen
|
||||||
buttons.ref.tooltip=Issue oder Pull-Request referenzieren
|
buttons.ref.tooltip=Issue oder Pull-Request referenzieren
|
||||||
buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden
|
buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden
|
||||||
@ -214,16 +227,20 @@ string.desc=Z–A
|
|||||||
|
|
||||||
[error]
|
[error]
|
||||||
occurred=Ein Fehler ist aufgetreten
|
occurred=Ein Fehler ist aufgetreten
|
||||||
|
report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf <a href="%s" target="_blank">GitHub</a> nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport.
|
||||||
not_found=Das Ziel konnte nicht gefunden werden.
|
not_found=Das Ziel konnte nicht gefunden werden.
|
||||||
network_error=Netzwerkfehler
|
network_error=Netzwerkfehler
|
||||||
|
|
||||||
[startpage]
|
[startpage]
|
||||||
app_desc=Ein einfacher, selbst gehosteter Git-Service
|
app_desc=Ein einfacher, selbst gehosteter Git-Service
|
||||||
install=Einfach zu installieren
|
install=Einfach zu installieren
|
||||||
|
install_desc=Starte einfach <a target="_blank" rel="noopener noreferrer" href="%[1]s">die Anwendung</a> für deine Plattform oder nutze <a target="_blank" rel="noopener noreferrer" href="%[2]s">Docker</a>. Es existieren auch <a target="_blank" rel="noopener noreferrer" href="%[3]s">paketierte Versionen</a>.
|
||||||
platform=Plattformübergreifend
|
platform=Plattformübergreifend
|
||||||
|
platform_desc=Gitea läuft überall, wo <a target="_blank" rel="noopener noreferrer" href="%s">Go</a> kompiliert: Windows, macOS, Linux, ARM, etc. Wähle das System, das dir am meisten gefällt!
|
||||||
lightweight=Leichtgewicht
|
lightweight=Leichtgewicht
|
||||||
lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden!
|
lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden!
|
||||||
license=Quelloffen
|
license=Quelloffen
|
||||||
|
license_desc=Hol dir den Code unter <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[2]s</a>! Leiste deinen <a target="_blank" rel="noopener noreferrer" href="%[3]s">Beitrag</a> bei der Verbesserung dieses Projekts. Trau dich!
|
||||||
|
|
||||||
[install]
|
[install]
|
||||||
install=Installation
|
install=Installation
|
||||||
@ -337,6 +354,7 @@ enable_update_checker=Aktualisierungsprüfung aktivieren
|
|||||||
enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen.
|
enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen.
|
||||||
env_config_keys=Umgebungskonfiguration
|
env_config_keys=Umgebungskonfiguration
|
||||||
env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
|
env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
|
||||||
|
config_write_file_prompt=Diese Konfigurationsoptionen werden in %s geschrieben
|
||||||
|
|
||||||
[home]
|
[home]
|
||||||
nav_menu=Navigationsmenü
|
nav_menu=Navigationsmenü
|
||||||
@ -377,6 +395,8 @@ relevant_repositories=Es werden nur relevante Repositories angezeigt, <a href="%
|
|||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
create_new_account=Konto anlegen
|
create_new_account=Konto anlegen
|
||||||
|
already_have_account=Du hast bereits ein Konto?
|
||||||
|
sign_in_now=Jetzt anmelden!
|
||||||
disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an den Administrator.
|
disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an den Administrator.
|
||||||
disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert.
|
disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert.
|
||||||
manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen.
|
manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen.
|
||||||
@ -384,6 +404,8 @@ remember_me=Dieses Gerät speichern
|
|||||||
remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten.
|
remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten.
|
||||||
forgot_password_title=Passwort vergessen
|
forgot_password_title=Passwort vergessen
|
||||||
forgot_password=Passwort vergessen?
|
forgot_password=Passwort vergessen?
|
||||||
|
need_account=Noch kein Konto?
|
||||||
|
sign_up_now=Jetzt registrieren.
|
||||||
sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen!
|
sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen!
|
||||||
confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern.
|
confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern.
|
||||||
must_change_password=Aktualisiere dein Passwort
|
must_change_password=Aktualisiere dein Passwort
|
||||||
@ -424,6 +446,7 @@ oauth_signin_submit=Konto verbinden
|
|||||||
oauth.signin.error=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten. Wenn dieser Fehler weiterhin besteht, wende dich bitte an deinen Administrator.
|
oauth.signin.error=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten. Wenn dieser Fehler weiterhin besteht, wende dich bitte an deinen Administrator.
|
||||||
oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt.
|
oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt.
|
||||||
oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut.
|
oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut.
|
||||||
|
oauth_callback_unable_auto_reg=Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktieren den Administrator.
|
||||||
openid_connect_submit=Verbinden
|
openid_connect_submit=Verbinden
|
||||||
openid_connect_title=Mit bestehendem Konto verbinden
|
openid_connect_title=Mit bestehendem Konto verbinden
|
||||||
openid_connect_desc=Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu.
|
openid_connect_desc=Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu.
|
||||||
@ -437,12 +460,16 @@ authorize_application=Anwendung autorisieren
|
|||||||
authorize_redirect_notice=Du wirst zu %s weitergeleitet, wenn du diese Anwendung autorisierst.
|
authorize_redirect_notice=Du wirst zu %s weitergeleitet, wenn du diese Anwendung autorisierst.
|
||||||
authorize_application_created_by=Diese Anwendung wurde von %s erstellt.
|
authorize_application_created_by=Diese Anwendung wurde von %s erstellt.
|
||||||
authorize_application_description=Wenn du diese Anwendung autorisierst, wird sie die Berechtigung erhalten, alle Informationen zu deinem Account zu bearbeiten oder zu lesen. Dies beinhaltet auch private Repositories und Organisationen.
|
authorize_application_description=Wenn du diese Anwendung autorisierst, wird sie die Berechtigung erhalten, alle Informationen zu deinem Account zu bearbeiten oder zu lesen. Dies beinhaltet auch private Repositories und Organisationen.
|
||||||
|
authorize_application_with_scopes=Mit Bereichen: %s
|
||||||
authorize_title=`"%s" den Zugriff auf deinen Account gestatten?`
|
authorize_title=`"%s" den Zugriff auf deinen Account gestatten?`
|
||||||
authorization_failed=Autorisierung fehlgeschlagen
|
authorization_failed=Autorisierung fehlgeschlagen
|
||||||
authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ungültige Anfrage erkannt haben. Bitte kontaktiere den Betreuer der App, die du zu autorisieren versucht hast.
|
authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ungültige Anfrage erkannt haben. Bitte kontaktiere den Betreuer der App, die du zu autorisieren versucht hast.
|
||||||
sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen
|
sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen
|
||||||
|
password_pwned=Das von dir gewählte Passwort befindet sich auf einer <a target="_blank" rel="noopener noreferrer" href="%s">Liste gestohlener Passwörter</a>, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern.
|
||||||
password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden
|
password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden
|
||||||
last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben.
|
last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben.
|
||||||
|
signin_passkey=Mit einem Passkey anmelden
|
||||||
|
back_to_sign_in=Zurück zum Anmelden
|
||||||
|
|
||||||
[mail]
|
[mail]
|
||||||
view_it_on=Auf %s ansehen
|
view_it_on=Auf %s ansehen
|
||||||
@ -459,6 +486,7 @@ activate_email=Bestätige deine E-Mail-Adresse
|
|||||||
activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse
|
activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse
|
||||||
activate_email.text=Bitte klicke innerhalb von <b>%s</b> auf folgenden Link, um dein Konto zu aktivieren:
|
activate_email.text=Bitte klicke innerhalb von <b>%s</b> auf folgenden Link, um dein Konto zu aktivieren:
|
||||||
|
|
||||||
|
register_notify=Willkommen bei %s
|
||||||
register_notify.title=%[1]s, willkommen bei %[2]s
|
register_notify.title=%[1]s, willkommen bei %[2]s
|
||||||
register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s!
|
register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s!
|
||||||
register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden.
|
register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden.
|
||||||
@ -560,6 +588,8 @@ lang_select_error=Wähle eine Sprache aus der Liste aus.
|
|||||||
|
|
||||||
username_been_taken=Der Benutzername ist bereits vergeben.
|
username_been_taken=Der Benutzername ist bereits vergeben.
|
||||||
username_change_not_local_user=Nicht-lokale Benutzer dürfen ihren Nutzernamen nicht ändern.
|
username_change_not_local_user=Nicht-lokale Benutzer dürfen ihren Nutzernamen nicht ändern.
|
||||||
|
change_username_disabled=Ändern des Benutzernamens ist deaktiviert.
|
||||||
|
change_full_name_disabled=Ändern des vollständigen Namens ist deaktiviert.
|
||||||
username_has_not_been_changed=Benutzername wurde nicht geändert
|
username_has_not_been_changed=Benutzername wurde nicht geändert
|
||||||
repo_name_been_taken=Der Repository-Name wird schon verwendet.
|
repo_name_been_taken=Der Repository-Name wird schon verwendet.
|
||||||
repository_force_private=Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden.
|
repository_force_private=Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden.
|
||||||
@ -609,6 +639,7 @@ org_still_own_repo=Diese Organisation besitzt noch ein oder mehrere Repositories
|
|||||||
org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst.
|
org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst.
|
||||||
|
|
||||||
target_branch_not_exist=Der Ziel-Branch existiert nicht.
|
target_branch_not_exist=Der Ziel-Branch existiert nicht.
|
||||||
|
target_ref_not_exist=Zielreferenz existiert nicht %s
|
||||||
|
|
||||||
admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte.
|
admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte.
|
||||||
|
|
||||||
@ -618,6 +649,7 @@ joined_on=Beigetreten am %s
|
|||||||
repositories=Repositories
|
repositories=Repositories
|
||||||
activity=Öffentliche Aktivität
|
activity=Öffentliche Aktivität
|
||||||
followers=Follower
|
followers=Follower
|
||||||
|
show_more=Mehr anzeigen
|
||||||
starred=Favoriten
|
starred=Favoriten
|
||||||
watched=Beobachtete Repositories
|
watched=Beobachtete Repositories
|
||||||
code=Quelltext
|
code=Quelltext
|
||||||
@ -684,6 +716,8 @@ public_profile=Öffentliches Profil
|
|||||||
biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden)
|
biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden)
|
||||||
location_placeholder=Teile Deinen ungefähren Standort mit anderen
|
location_placeholder=Teile Deinen ungefähren Standort mit anderen
|
||||||
profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet.
|
profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet.
|
||||||
|
password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.
|
||||||
|
password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.
|
||||||
full_name=Vollständiger Name
|
full_name=Vollständiger Name
|
||||||
website=Webseite
|
website=Webseite
|
||||||
location=Standort
|
location=Standort
|
||||||
@ -701,6 +735,7 @@ cancel=Abbrechen
|
|||||||
language=Sprache
|
language=Sprache
|
||||||
ui=Theme
|
ui=Theme
|
||||||
hidden_comment_types=Ausgeblendeter Kommentartypen
|
hidden_comment_types=Ausgeblendeter Kommentartypen
|
||||||
|
hidden_comment_types_description=Die hier markierten Kommentartypen werden nicht innerhalb der Issue-Seiten angezeigt. Beispielsweise entfernt das Markieren von "Label" alle "{user} hat {label} hinzugefügt/entfernt"-Kommentare.
|
||||||
hidden_comment_types.ref_tooltip=Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde
|
hidden_comment_types.ref_tooltip=Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde
|
||||||
hidden_comment_types.issue_ref_tooltip=Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert
|
hidden_comment_types.issue_ref_tooltip=Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert
|
||||||
comment_type_group_reference=Verweis auf Mitglieder
|
comment_type_group_reference=Verweis auf Mitglieder
|
||||||
@ -732,6 +767,7 @@ uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild.
|
|||||||
uploaded_avatar_is_too_big=Die hochgeladene Dateigröße (%d KiB) überschreitet die maximale Größe (%d KiB).
|
uploaded_avatar_is_too_big=Die hochgeladene Dateigröße (%d KiB) überschreitet die maximale Größe (%d KiB).
|
||||||
update_avatar_success=Dein Profilbild wurde geändert.
|
update_avatar_success=Dein Profilbild wurde geändert.
|
||||||
update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert.
|
update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert.
|
||||||
|
cropper_prompt=Sie können das Bild vor dem Speichern bearbeiten. Das bearbeitete Bild wird als PNG-Datei gespeichert.
|
||||||
|
|
||||||
change_password=Passwort aktualisieren
|
change_password=Passwort aktualisieren
|
||||||
old_password=Aktuelles Passwort
|
old_password=Aktuelles Passwort
|
||||||
@ -747,6 +783,8 @@ manage_themes=Standard-Theme auswählen
|
|||||||
manage_openid=OpenID-Adressen verwalten
|
manage_openid=OpenID-Adressen verwalten
|
||||||
email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet.
|
email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet.
|
||||||
theme_desc=Dies wird dein Standard-Theme auf der Seite sein.
|
theme_desc=Dies wird dein Standard-Theme auf der Seite sein.
|
||||||
|
theme_colorblindness_help=Hilfe zum Theme für Farbenblinde
|
||||||
|
theme_colorblindness_prompt=Gitea erhält aktuell einfache Unterstützung für Farbenblinde durch einige Themes, die nur wenige Farben definiert haben. Die Arbeit ist noch im Gange. Weitere Verbesserungen können durch die Definition von mehr Farben in den CSS-Theme-Dateien vorgenommen werden.
|
||||||
primary=Primär
|
primary=Primär
|
||||||
activated=Aktiviert
|
activated=Aktiviert
|
||||||
requires_activation=Erfordert Aktivierung
|
requires_activation=Erfordert Aktivierung
|
||||||
@ -871,8 +909,9 @@ repo_and_org_access=Repository- und Organisationszugriff
|
|||||||
permissions_public_only=Nur öffentlich
|
permissions_public_only=Nur öffentlich
|
||||||
permissions_access_all=Alle (öffentlich, privat und begrenzt)
|
permissions_access_all=Alle (öffentlich, privat und begrenzt)
|
||||||
select_permissions=Berechtigungen auswählen
|
select_permissions=Berechtigungen auswählen
|
||||||
|
permission_not_set=Nicht festgelegt
|
||||||
permission_no_access=Kein Zugriff
|
permission_no_access=Kein Zugriff
|
||||||
permission_read=Gelesen
|
permission_read=Lesen
|
||||||
permission_write=Lesen und Schreiben
|
permission_write=Lesen und Schreiben
|
||||||
access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden <a %s>API</a>-Routen. Lies die <a %s>Dokumentation</a> für mehr Informationen.
|
access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden <a %s>API</a>-Routen. Lies die <a %s>Dokumentation</a> für mehr Informationen.
|
||||||
at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen
|
at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen
|
||||||
@ -890,6 +929,7 @@ create_oauth2_application_success=Du hast erfolgreich eine neue OAuth2-Anwendung
|
|||||||
update_oauth2_application_success=Du hast die OAuth2-Anwendung erfolgreich aktualisiert.
|
update_oauth2_application_success=Du hast die OAuth2-Anwendung erfolgreich aktualisiert.
|
||||||
oauth2_application_name=Name der Anwendung
|
oauth2_application_name=Name der Anwendung
|
||||||
oauth2_confidential_client=Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für PCs und Mobilgeräte.
|
oauth2_confidential_client=Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für PCs und Mobilgeräte.
|
||||||
|
oauth2_skip_secondary_authorization=Autorisierung für öffentliche Clients nach einmaliger Gewährung des Zugriffs überspringen. <strong>Dies kann ein Sicherheitsrisiko darstellen.</strong>
|
||||||
oauth2_redirect_uris=URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI.
|
oauth2_redirect_uris=URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI.
|
||||||
save_application=Speichern
|
save_application=Speichern
|
||||||
oauth2_client_id=Client-ID
|
oauth2_client_id=Client-ID
|
||||||
@ -900,6 +940,7 @@ oauth2_client_secret_hint=Das Secret wird nach dem Verlassen oder Aktualisieren
|
|||||||
oauth2_application_edit=Bearbeiten
|
oauth2_application_edit=Bearbeiten
|
||||||
oauth2_application_create_description=OAuth2 Anwendungen geben deiner Drittanwendung Zugriff auf Benutzeraccounts dieser Gitea-Instanz.
|
oauth2_application_create_description=OAuth2 Anwendungen geben deiner Drittanwendung Zugriff auf Benutzeraccounts dieser Gitea-Instanz.
|
||||||
oauth2_application_remove_description=Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren?
|
oauth2_application_remove_description=Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren?
|
||||||
|
oauth2_application_locked=Wenn es in der Konfiguration aktiviert ist, registriert Gitea einige OAuth2-Anwendungen beim Starten vor. Um unerwartetes Verhalten zu verhindern, können diese weder bearbeitet noch entfernt werden. Weitere Informationen findest Du in der OAuth2-Dokumentation.
|
||||||
|
|
||||||
authorized_oauth2_applications=Autorisierte OAuth2-Anwendungen
|
authorized_oauth2_applications=Autorisierte OAuth2-Anwendungen
|
||||||
authorized_oauth2_applications_description=Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt.
|
authorized_oauth2_applications_description=Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt.
|
||||||
@ -908,20 +949,26 @@ revoke_oauth2_grant=Autorisierung widerrufen
|
|||||||
revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher?
|
revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher?
|
||||||
revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen.
|
revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen.
|
||||||
|
|
||||||
|
twofa_desc=Um dein Konto vor Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter ("TOTP") zu erhalten.
|
||||||
twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen.
|
twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen.
|
||||||
twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung <strong>eingeschaltet</strong>.
|
twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung <strong>eingeschaltet</strong>.
|
||||||
twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet.
|
twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet.
|
||||||
twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren
|
twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren
|
||||||
|
twofa_scratch_token_regenerate=Einweg-Wiederherstellungsschlüssel neu generieren
|
||||||
|
twofa_scratch_token_regenerated=Dein Einweg-Wiederherstellungsschlüssel ist jetzt %s. Speichere ihn an einem sicheren Ort, er wird nie wieder angezeigt.
|
||||||
twofa_enroll=Zwei-Faktor-Authentifizierung aktivieren
|
twofa_enroll=Zwei-Faktor-Authentifizierung aktivieren
|
||||||
twofa_disable_note=Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren.
|
twofa_disable_note=Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren.
|
||||||
twofa_disable_desc=Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren?
|
twofa_disable_desc=Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren?
|
||||||
|
regenerate_scratch_token_desc=Wenn du deinen Wiederherstellungsschlüssel verlegt oder bereits benutzt hast, kannst du ihn hier zurücksetzen.
|
||||||
twofa_disabled=Zwei-Faktor-Authentifizierung wurde deaktiviert.
|
twofa_disabled=Zwei-Faktor-Authentifizierung wurde deaktiviert.
|
||||||
scan_this_image=Scanne diese Grafik mit deiner Authentifizierungs-App:
|
scan_this_image=Scanne diese Grafik mit deiner Authentifizierungs-App:
|
||||||
or_enter_secret=Oder gib das Secret ein: %s
|
or_enter_secret=Oder gib das Secret ein: %s
|
||||||
then_enter_passcode=Und gebe dann die angezeigte PIN der Anwendung ein:
|
then_enter_passcode=Und gebe dann die angezeigte PIN der Anwendung ein:
|
||||||
passcode_invalid=Die PIN ist falsch. Probiere es erneut.
|
passcode_invalid=Die PIN ist falsch. Probiere es erneut.
|
||||||
|
twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen Einweg-Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird.
|
||||||
twofa_failed_get_secret=Fehler beim Abrufen des Secrets.
|
twofa_failed_get_secret=Fehler beim Abrufen des Secrets.
|
||||||
|
|
||||||
|
webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard "<a rel="noreferrer" target="_blank" href="%s">WebAuthn</a>" unterstützen.
|
||||||
webauthn_register_key=Sicherheitsschlüssel hinzufügen
|
webauthn_register_key=Sicherheitsschlüssel hinzufügen
|
||||||
webauthn_nickname=Nickname
|
webauthn_nickname=Nickname
|
||||||
webauthn_delete_key=Sicherheitsschlüssel entfernen
|
webauthn_delete_key=Sicherheitsschlüssel entfernen
|
||||||
@ -968,7 +1015,6 @@ new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des
|
|||||||
owner=Besitzer
|
owner=Besitzer
|
||||||
owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist.
|
owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist.
|
||||||
repo_name=Repository-Name
|
repo_name=Repository-Name
|
||||||
repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern.
|
|
||||||
repo_size=Repository-Größe
|
repo_size=Repository-Größe
|
||||||
template=Template
|
template=Template
|
||||||
template_select=Vorlage auswählen
|
template_select=Vorlage auswählen
|
||||||
@ -987,6 +1033,8 @@ fork_to_different_account=Fork in ein anderes Konto erstellen
|
|||||||
fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht geändert werden.
|
fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht geändert werden.
|
||||||
fork_branch=Branch, der zum Fork geklont werden soll
|
fork_branch=Branch, der zum Fork geklont werden soll
|
||||||
all_branches=Alle Branches
|
all_branches=Alle Branches
|
||||||
|
view_all_branches=Alle Branches anzeigen
|
||||||
|
view_all_tags=Alle Tags anzeigen
|
||||||
fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind.
|
fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind.
|
||||||
fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||||
use_template=Dieses Template verwenden
|
use_template=Dieses Template verwenden
|
||||||
@ -998,6 +1046,8 @@ generate_repo=Repository erstellen
|
|||||||
generate_from=Erstelle aus
|
generate_from=Erstelle aus
|
||||||
repo_desc=Beschreibung
|
repo_desc=Beschreibung
|
||||||
repo_desc_helper=Gib eine kurze Beschreibung an (optional)
|
repo_desc_helper=Gib eine kurze Beschreibung an (optional)
|
||||||
|
repo_no_desc=Keine Beschreibung vorhanden
|
||||||
|
repo_lang=Sprachen
|
||||||
repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus.
|
repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus.
|
||||||
repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore.
|
repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore.
|
||||||
issue_labels=Issue Label
|
issue_labels=Issue Label
|
||||||
@ -1005,6 +1055,7 @@ issue_labels_helper=Wähle ein Issue-Label-Set.
|
|||||||
license=Lizenz
|
license=Lizenz
|
||||||
license_helper=Wähle eine Lizenz aus.
|
license_helper=Wähle eine Lizenz aus.
|
||||||
license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>.
|
license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>.
|
||||||
|
multiple_licenses=Mehrere Lizenzen
|
||||||
object_format=Objektformat
|
object_format=Objektformat
|
||||||
object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel.
|
object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel.
|
||||||
readme=README
|
readme=README
|
||||||
@ -1058,13 +1109,16 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht
|
|||||||
blame_prior=Blame vor dieser Änderung anzeigen
|
blame_prior=Blame vor dieser Änderung anzeigen
|
||||||
blame.ignore_revs=Revisionen in <a href="%s">.git-blame-ignore-revs</a> werden ignoriert. Klicke <a href="%s">hier, um das zu umgehen</a> und die normale Blame-Ansicht zu sehen.
|
blame.ignore_revs=Revisionen in <a href="%s">.git-blame-ignore-revs</a> werden ignoriert. Klicke <a href="%s">hier, um das zu umgehen</a> und die normale Blame-Ansicht zu sehen.
|
||||||
blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in <a href="%s">.git-blame-ignore-revs</a>.
|
blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in <a href="%s">.git-blame-ignore-revs</a>.
|
||||||
|
user_search_tooltip=Zeigt maximal 30 Benutzer
|
||||||
|
|
||||||
tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s
|
tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s
|
||||||
tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s
|
tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s
|
||||||
tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s
|
tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s
|
||||||
|
|
||||||
transfer.accept=Übertragung Akzeptieren
|
transfer.accept=Übertragung Akzeptieren
|
||||||
|
transfer.accept_desc=`Übertragung nach "%s"`
|
||||||
transfer.reject=Übertragung Ablehnen
|
transfer.reject=Übertragung Ablehnen
|
||||||
|
transfer.reject_desc=Übertragung nach "%s " abbrechen
|
||||||
transfer.no_permission_to_accept=Du hast keine Berechtigung, diesen Transfer anzunehmen.
|
transfer.no_permission_to_accept=Du hast keine Berechtigung, diesen Transfer anzunehmen.
|
||||||
transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abzulehnen.
|
transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abzulehnen.
|
||||||
|
|
||||||
@ -1139,6 +1193,11 @@ migrate.gogs.description=Daten von notabug.org oder anderen Gogs Instanzen migri
|
|||||||
migrate.onedev.description=Daten von code.onedev.io oder anderen OneDev Instanzen migrieren.
|
migrate.onedev.description=Daten von code.onedev.io oder anderen OneDev Instanzen migrieren.
|
||||||
migrate.codebase.description=Daten von codebasehq.com migrieren.
|
migrate.codebase.description=Daten von codebasehq.com migrieren.
|
||||||
migrate.gitbucket.description=Daten von GitBucket Instanzen migrieren.
|
migrate.gitbucket.description=Daten von GitBucket Instanzen migrieren.
|
||||||
|
migrate.codecommit.description=Daten von AWS CodeCommit migrieren.
|
||||||
|
migrate.codecommit.aws_access_key_id=AWS Access Key ID
|
||||||
|
migrate.codecommit.aws_secret_access_key=AWS Secret Access Key
|
||||||
|
migrate.codecommit.https_git_credentials_username=HTTPS-Git-Nutzername
|
||||||
|
migrate.codecommit.https_git_credentials_password=HTTPS-Git-Passwort
|
||||||
migrate.migrating_git=Git-Daten werden migriert
|
migrate.migrating_git=Git-Daten werden migriert
|
||||||
migrate.migrating_topics=Themen werden migriert
|
migrate.migrating_topics=Themen werden migriert
|
||||||
migrate.migrating_milestones=Meilensteine werden migriert
|
migrate.migrating_milestones=Meilensteine werden migriert
|
||||||
@ -1199,6 +1258,7 @@ releases=Releases
|
|||||||
tag=Tag
|
tag=Tag
|
||||||
released_this=hat released
|
released_this=hat released
|
||||||
tagged_this=hat getaggt
|
tagged_this=hat getaggt
|
||||||
|
file.title=%s in %s
|
||||||
file_raw=Originalformat
|
file_raw=Originalformat
|
||||||
file_history=Verlauf
|
file_history=Verlauf
|
||||||
file_view_source=Quelltext anzeigen
|
file_view_source=Quelltext anzeigen
|
||||||
@ -1206,12 +1266,16 @@ file_view_rendered=Ansicht rendern
|
|||||||
file_view_raw=Originalformat anzeigen
|
file_view_raw=Originalformat anzeigen
|
||||||
file_permalink=Permalink
|
file_permalink=Permalink
|
||||||
file_too_large=Die Datei ist zu groß zum Anzeigen.
|
file_too_large=Die Datei ist zu groß zum Anzeigen.
|
||||||
|
file_is_empty=Die Datei ist leer.
|
||||||
|
code_preview_line_from_to=Zeilen %[1]d bis %[2]d in %[3]s
|
||||||
|
code_preview_line_in=Zeile %[1]d in %[2]s
|
||||||
invisible_runes_header=`Diese Datei enthält unsichtbare Unicode-Zeichen`
|
invisible_runes_header=`Diese Datei enthält unsichtbare Unicode-Zeichen`
|
||||||
invisible_runes_description=`Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.`
|
invisible_runes_description=`Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.`
|
||||||
ambiguous_runes_header=`Diese Datei enthält mehrdeutige Unicode-Zeichen`
|
ambiguous_runes_header=`Diese Datei enthält mehrdeutige Unicode-Zeichen`
|
||||||
ambiguous_runes_description=`Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.`
|
ambiguous_runes_description=`Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.`
|
||||||
invisible_runes_line=`Diese Zeile enthält unsichtbare Unicode-Zeichen`
|
invisible_runes_line=`Diese Zeile enthält unsichtbare Unicode-Zeichen`
|
||||||
ambiguous_runes_line=`Diese Zeile enthält mehrdeutige Unicode-Zeichen`
|
ambiguous_runes_line=`Diese Zeile enthält mehrdeutige Unicode-Zeichen`
|
||||||
|
ambiguous_character=`%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden`
|
||||||
|
|
||||||
escape_control_characters=Escapen
|
escape_control_characters=Escapen
|
||||||
unescape_control_characters=Unescapen
|
unescape_control_characters=Unescapen
|
||||||
@ -1259,6 +1323,7 @@ editor.or=oder
|
|||||||
editor.cancel_lower=Abbrechen
|
editor.cancel_lower=Abbrechen
|
||||||
editor.commit_signed_changes=Committe signierte Änderungen
|
editor.commit_signed_changes=Committe signierte Änderungen
|
||||||
editor.commit_changes=Änderungen committen
|
editor.commit_changes=Änderungen committen
|
||||||
|
editor.add_tmpl='{filename}' hinzufügen
|
||||||
editor.add=%s hinzugefügt
|
editor.add=%s hinzugefügt
|
||||||
editor.update=%s aktualisiert
|
editor.update=%s aktualisiert
|
||||||
editor.delete=%s gelöscht
|
editor.delete=%s gelöscht
|
||||||
@ -1342,6 +1407,7 @@ commitstatus.success=Erfolg
|
|||||||
ext_issues=Zugriff auf Externe Issues
|
ext_issues=Zugriff auf Externe Issues
|
||||||
ext_issues.desc=Link zu externem Issuetracker.
|
ext_issues.desc=Link zu externem Issuetracker.
|
||||||
|
|
||||||
|
projects.desc=Verwalte Issues und Pull-Requests in Projekten.
|
||||||
projects.description=Beschreibung (optional)
|
projects.description=Beschreibung (optional)
|
||||||
projects.description_placeholder=Beschreibung
|
projects.description_placeholder=Beschreibung
|
||||||
projects.create=Projekt erstellen
|
projects.create=Projekt erstellen
|
||||||
@ -1401,7 +1467,9 @@ issues.new.clear_milestone=Meilenstein entfernen
|
|||||||
issues.new.assignees=Zuständig
|
issues.new.assignees=Zuständig
|
||||||
issues.new.clear_assignees=Zuständige entfernen
|
issues.new.clear_assignees=Zuständige entfernen
|
||||||
issues.new.no_assignees=Niemand zuständig
|
issues.new.no_assignees=Niemand zuständig
|
||||||
|
issues.new.no_reviewers=Keine Reviewer
|
||||||
issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||||
|
issues.edit.already_changed=Änderungen zum Issue konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diese erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden
|
||||||
issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
|
issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||||
issues.choose.get_started=Los geht's
|
issues.choose.get_started=Los geht's
|
||||||
issues.choose.open_external_link=Öffnen
|
issues.choose.open_external_link=Öffnen
|
||||||
@ -1428,6 +1496,7 @@ issues.remove_labels=hat die Labels %s %s entfernt
|
|||||||
issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt
|
issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt
|
||||||
issues.add_milestone_at=`hat diesen Issue %[2]s zum <b>%[1]s</b> Meilenstein hinzugefügt`
|
issues.add_milestone_at=`hat diesen Issue %[2]s zum <b>%[1]s</b> Meilenstein hinzugefügt`
|
||||||
issues.add_project_at=`hat dieses zum <b>%s</b> projekt %s hinzugefügt`
|
issues.add_project_at=`hat dieses zum <b>%s</b> projekt %s hinzugefügt`
|
||||||
|
issues.move_to_column_of_project=`hat dies zu %s in %s %s verschoben`
|
||||||
issues.change_milestone_at=`hat den Meilenstein %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`
|
issues.change_milestone_at=`hat den Meilenstein %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`
|
||||||
issues.change_project_at=`hat das Projekt %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`
|
issues.change_project_at=`hat das Projekt %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`
|
||||||
issues.remove_milestone_at=`hat dieses Issue %[2]s vom <b>%[1]s</b> Meilenstein entfernt`
|
issues.remove_milestone_at=`hat dieses Issue %[2]s vom <b>%[1]s</b> Meilenstein entfernt`
|
||||||
@ -1459,6 +1528,8 @@ issues.filter_assignee=Zuständig
|
|||||||
issues.filter_assginee_no_select=Alle Zuständigen
|
issues.filter_assginee_no_select=Alle Zuständigen
|
||||||
issues.filter_assginee_no_assignee=Niemand zuständig
|
issues.filter_assginee_no_assignee=Niemand zuständig
|
||||||
issues.filter_poster=Autor
|
issues.filter_poster=Autor
|
||||||
|
issues.filter_user_placeholder=Benutzer suchen
|
||||||
|
issues.filter_user_no_select=Alle Benutzer
|
||||||
issues.filter_type=Typ
|
issues.filter_type=Typ
|
||||||
issues.filter_type.all_issues=Alle Issues
|
issues.filter_type.all_issues=Alle Issues
|
||||||
issues.filter_type.assigned_to_you=Dir zugewiesen
|
issues.filter_type.assigned_to_you=Dir zugewiesen
|
||||||
@ -1512,7 +1583,9 @@ issues.no_content=Keine Beschreibung angegeben.
|
|||||||
issues.close=Issue schließen
|
issues.close=Issue schließen
|
||||||
issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged
|
issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged
|
||||||
issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged
|
issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged
|
||||||
|
issues.close_comment_issue=Kommentieren und schließen
|
||||||
issues.reopen_issue=Wieder öffnen
|
issues.reopen_issue=Wieder öffnen
|
||||||
|
issues.reopen_comment_issue=Kommentieren und wieder öffnen
|
||||||
issues.create_comment=Kommentieren
|
issues.create_comment=Kommentieren
|
||||||
issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
|
issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||||
issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
|
issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
|
||||||
@ -1601,12 +1674,25 @@ issues.delete.title=Dieses Issue löschen?
|
|||||||
issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst)
|
issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst)
|
||||||
|
|
||||||
issues.tracker=Zeiterfassung
|
issues.tracker=Zeiterfassung
|
||||||
|
issues.timetracker_timer_start=Timer starten
|
||||||
|
issues.timetracker_timer_stop=Timer stoppen
|
||||||
|
issues.timetracker_timer_discard=Timer verwerfen
|
||||||
|
issues.timetracker_timer_manually_add=Zeit hinzufügen
|
||||||
|
|
||||||
|
issues.time_estimate_set=Geschätzte Zeit festlegen
|
||||||
|
issues.time_estimate_display=Schätzung: %s
|
||||||
|
issues.change_time_estimate_at=Zeitschätzung geändert zu <b>%s</b> %s
|
||||||
|
issues.remove_time_estimate_at=Zeitschätzung %s entfernt
|
||||||
|
issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig
|
||||||
|
issues.start_tracking_history=hat die Zeiterfassung %s gestartet
|
||||||
issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird
|
issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird
|
||||||
issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!`
|
issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!`
|
||||||
|
issues.stop_tracking_history=hat für <b>%s</b> gearbeitet %s
|
||||||
issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
|
issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
|
||||||
issues.del_time=Diese Zeiterfassung löschen
|
issues.del_time=Diese Zeiterfassung löschen
|
||||||
|
issues.add_time_history=hat <b>%s</b> gearbeitete Zeit hinzugefügt %s
|
||||||
issues.del_time_history=`hat %s gearbeitete Zeit gelöscht`
|
issues.del_time_history=`hat %s gearbeitete Zeit gelöscht`
|
||||||
|
issues.add_time_manually=Zeit manuell hinzufügen
|
||||||
issues.add_time_hours=Stunden
|
issues.add_time_hours=Stunden
|
||||||
issues.add_time_minutes=Minuten
|
issues.add_time_minutes=Minuten
|
||||||
issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben.
|
issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben.
|
||||||
@ -1666,6 +1752,7 @@ issues.dependency.add_error_dep_not_same_repo=Beide Issues müssen sich im selbe
|
|||||||
issues.review.self.approval=Du kannst nicht dein eigenen Pull-Request genehmigen.
|
issues.review.self.approval=Du kannst nicht dein eigenen Pull-Request genehmigen.
|
||||||
issues.review.self.rejection=Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen.
|
issues.review.self.rejection=Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen.
|
||||||
issues.review.approve=hat die Änderungen %s genehmigt
|
issues.review.approve=hat die Änderungen %s genehmigt
|
||||||
|
issues.review.comment=hat %s überprüft
|
||||||
issues.review.dismissed=verwarf %ss Review %s
|
issues.review.dismissed=verwarf %ss Review %s
|
||||||
issues.review.dismissed_label=Verworfen
|
issues.review.dismissed_label=Verworfen
|
||||||
issues.review.left_comment=hat einen Kommentar hinterlassen
|
issues.review.left_comment=hat einen Kommentar hinterlassen
|
||||||
@ -1691,6 +1778,11 @@ issues.review.resolve_conversation=Diskussion als "erledigt" markieren
|
|||||||
issues.review.un_resolve_conversation=Diskussion als "nicht-erledigt" markieren
|
issues.review.un_resolve_conversation=Diskussion als "nicht-erledigt" markieren
|
||||||
issues.review.resolved_by=markierte diese Unterhaltung als gelöst
|
issues.review.resolved_by=markierte diese Unterhaltung als gelöst
|
||||||
issues.review.commented=Kommentieren
|
issues.review.commented=Kommentieren
|
||||||
|
issues.review.official=Genehmigt
|
||||||
|
issues.review.requested=Prüfung ausstehend
|
||||||
|
issues.review.rejected=Änderungen angefordert
|
||||||
|
issues.review.stale=Aktualisiert seit der Genehmigung
|
||||||
|
issues.review.unofficial=Ungezählte Genehmigung
|
||||||
issues.assignee.error=Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden.
|
issues.assignee.error=Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden.
|
||||||
issues.reference_issue.body=Beschreibung
|
issues.reference_issue.body=Beschreibung
|
||||||
issues.content_history.deleted=gelöscht
|
issues.content_history.deleted=gelöscht
|
||||||
@ -1707,6 +1799,8 @@ compare.compare_head=vergleichen
|
|||||||
pulls.desc=Pull-Requests und Code-Reviews aktivieren.
|
pulls.desc=Pull-Requests und Code-Reviews aktivieren.
|
||||||
pulls.new=Neuer Pull-Request
|
pulls.new=Neuer Pull-Request
|
||||||
pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||||
|
pulls.new.must_collaborator=Du musst Mitarbeiter sein, um Pull-Requests zu erstellen.
|
||||||
|
pulls.edit.already_changed=Änderungen zum Pull-Request konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisieren die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden
|
||||||
pulls.view=Pull-Request ansehen
|
pulls.view=Pull-Request ansehen
|
||||||
pulls.compare_changes=Neuer Pull-Request
|
pulls.compare_changes=Neuer Pull-Request
|
||||||
pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben
|
pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben
|
||||||
@ -1762,6 +1856,8 @@ pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch.
|
|||||||
pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich.
|
pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich.
|
||||||
pulls.required_status_check_missing=Einige erforderliche Prüfungen fehlen.
|
pulls.required_status_check_missing=Einige erforderliche Prüfungen fehlen.
|
||||||
pulls.required_status_check_administrator=Als Administrator kannst du diesen Pull-Request weiterhin mergen.
|
pulls.required_status_check_administrator=Als Administrator kannst du diesen Pull-Request weiterhin mergen.
|
||||||
|
pulls.blocked_by_approvals=Dieser Pull-Request hat noch nicht genügend Genehmigungen. %d von %d Genehmigungen erteilt.
|
||||||
|
pulls.blocked_by_approvals_whitelisted=Dieser Pull-Request hat noch nicht genug erforderliche Genehmigungen. %d von %d Genehmigungen von Benutzern oder Teams auf der Berechtigungsliste.
|
||||||
pulls.blocked_by_rejection=Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden.
|
pulls.blocked_by_rejection=Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden.
|
||||||
pulls.blocked_by_official_review_requests=Dieser Pull Request hat offizielle Review-Anfragen.
|
pulls.blocked_by_official_review_requests=Dieser Pull Request hat offizielle Review-Anfragen.
|
||||||
pulls.blocked_by_outdated_branch=Dieser Pull Request ist blockiert, da er veraltet ist.
|
pulls.blocked_by_outdated_branch=Dieser Pull Request ist blockiert, da er veraltet ist.
|
||||||
@ -1803,7 +1899,9 @@ pulls.unrelated_histories=Merge fehlgeschlagen: Der Head des Merges und die Basi
|
|||||||
pulls.merge_out_of_date=Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut.
|
pulls.merge_out_of_date=Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut.
|
||||||
pulls.head_out_of_date=Mergen fehlgeschlagen: Der Head wurde aktualisiert während der Merge erstellt wurde. Tipp: Versuche es erneut.
|
pulls.head_out_of_date=Mergen fehlgeschlagen: Der Head wurde aktualisiert während der Merge erstellt wurde. Tipp: Versuche es erneut.
|
||||||
pulls.has_merged=Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern.
|
pulls.has_merged=Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern.
|
||||||
|
pulls.push_rejected=Push fehlgeschlagen: Der Push wurde abgelehnt. Überprüfe die Git Hooks für dieses Repository.
|
||||||
pulls.push_rejected_summary=Vollständige Ablehnungsmeldung
|
pulls.push_rejected_summary=Vollständige Ablehnungsmeldung
|
||||||
|
pulls.push_rejected_no_message=Push fehlgeschlagen: Der Push wurde abgelehnt, aber es gab keine Fehlermeldung. Überprüfe die Git Hooks für dieses Repository
|
||||||
pulls.open_unmerged_pull_exists=`Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.`
|
pulls.open_unmerged_pull_exists=`Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.`
|
||||||
pulls.status_checking=Einige Prüfungen sind noch ausstehend
|
pulls.status_checking=Einige Prüfungen sind noch ausstehend
|
||||||
pulls.status_checks_success=Alle Prüfungen waren erfolgreich
|
pulls.status_checks_success=Alle Prüfungen waren erfolgreich
|
||||||
@ -1827,6 +1925,7 @@ pulls.cmd_instruction_checkout_title=Checkout
|
|||||||
pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
|
pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
|
||||||
pulls.cmd_instruction_merge_title=Mergen
|
pulls.cmd_instruction_merge_title=Mergen
|
||||||
pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren.
|
pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren.
|
||||||
|
pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde
|
||||||
pulls.clear_merge_message=Merge-Nachricht löschen
|
pulls.clear_merge_message=Merge-Nachricht löschen
|
||||||
pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten.
|
pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten.
|
||||||
|
|
||||||
@ -1846,9 +1945,15 @@ pulls.delete.title=Diesen Pull-Request löschen?
|
|||||||
pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren)
|
pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren)
|
||||||
|
|
||||||
pulls.recently_pushed_new_branches=Du hast auf den Branch <strong>%[1]s</strong> %[2]s gepusht
|
pulls.recently_pushed_new_branches=Du hast auf den Branch <strong>%[1]s</strong> %[2]s gepusht
|
||||||
|
pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen
|
||||||
|
pulls.upstream_diverging_merge=Fork synchronisieren
|
||||||
|
|
||||||
pull.deleted_branch=(gelöscht):%s
|
pull.deleted_branch=(gelöscht):%s
|
||||||
|
pull.agit_documentation=Dokumentation zu AGit durchschauen
|
||||||
|
|
||||||
|
comments.edit.already_changed=Änderungen zum Kommentar konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden
|
||||||
|
|
||||||
milestones.new=Neuer Meilenstein
|
milestones.new=Neuer Meilenstein
|
||||||
milestones.closed=Geschlossen %s
|
milestones.closed=Geschlossen %s
|
||||||
@ -1857,6 +1962,7 @@ milestones.no_due_date=Kein Fälligkeitsdatum
|
|||||||
milestones.open=Öffnen
|
milestones.open=Öffnen
|
||||||
milestones.close=Schließen
|
milestones.close=Schließen
|
||||||
milestones.new_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen.
|
milestones.new_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen.
|
||||||
|
milestones.completeness=<strong>%d%%</strong> abgeschlossen
|
||||||
milestones.create=Meilenstein erstellen
|
milestones.create=Meilenstein erstellen
|
||||||
milestones.title=Titel
|
milestones.title=Titel
|
||||||
milestones.desc=Beschreibung
|
milestones.desc=Beschreibung
|
||||||
@ -2041,12 +2147,14 @@ settings.push_mirror_sync_in_progress=Aktuell werden Änderungen auf %s gepusht.
|
|||||||
settings.site=Webseite
|
settings.site=Webseite
|
||||||
settings.update_settings=Einstellungen speichern
|
settings.update_settings=Einstellungen speichern
|
||||||
settings.update_mirror_settings=Mirror-Einstellungen aktualisieren
|
settings.update_mirror_settings=Mirror-Einstellungen aktualisieren
|
||||||
|
settings.branches.switch_default_branch=Standardbranch wechseln
|
||||||
settings.branches.update_default_branch=Standardbranch aktualisieren
|
settings.branches.update_default_branch=Standardbranch aktualisieren
|
||||||
settings.branches.add_new_rule=Neue Regel hinzufügen
|
settings.branches.add_new_rule=Neue Regel hinzufügen
|
||||||
settings.advanced_settings=Erweiterte Einstellungen
|
settings.advanced_settings=Erweiterte Einstellungen
|
||||||
settings.wiki_desc=Repository-Wiki aktivieren
|
settings.wiki_desc=Repository-Wiki aktivieren
|
||||||
settings.use_internal_wiki=Eingebautes Wiki verwenden
|
settings.use_internal_wiki=Eingebautes Wiki verwenden
|
||||||
settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch
|
settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch
|
||||||
|
settings.default_wiki_everyone_access=Standard-Zugriffsberechtigung für angemeldete Benutzer:
|
||||||
settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen.
|
settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen.
|
||||||
settings.use_external_wiki=Externes Wiki verwenden
|
settings.use_external_wiki=Externes Wiki verwenden
|
||||||
settings.external_wiki_url=Externe Wiki-URL
|
settings.external_wiki_url=Externe Wiki-URL
|
||||||
@ -2077,6 +2185,7 @@ settings.pulls.default_delete_branch_after_merge=Standardmäßig bei Pull-Reques
|
|||||||
settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern standardmäßig erlauben
|
settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern standardmäßig erlauben
|
||||||
settings.releases_desc=Repository-Releases aktivieren
|
settings.releases_desc=Repository-Releases aktivieren
|
||||||
settings.packages_desc=Repository Packages Registry aktivieren
|
settings.packages_desc=Repository Packages Registry aktivieren
|
||||||
|
settings.projects_desc=Projekte aktivieren
|
||||||
settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen)
|
settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen)
|
||||||
settings.projects_mode_repo=Nur Repo-Projekte
|
settings.projects_mode_repo=Nur Repo-Projekte
|
||||||
settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte
|
settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte
|
||||||
@ -2116,6 +2225,7 @@ settings.transfer_in_progress=Es gibt derzeit eine laufende Übertragung. Bitte
|
|||||||
settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist.
|
settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist.
|
||||||
settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist.
|
settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist.
|
||||||
settings.transfer_notices_3=- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert).
|
settings.transfer_notices_3=- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert).
|
||||||
|
settings.transfer_notices_4=- Wenn das Repository einer Organisation gehört und du es an eine andere Organisation oder eine andere Person überträgst, verlierst du die Verlinkungen zwischen den Issues des Repositorys und dem Projektboard der Organisation.
|
||||||
settings.transfer_owner=Neuer Besitzer
|
settings.transfer_owner=Neuer Besitzer
|
||||||
settings.transfer_perform=Übertragung durchführen
|
settings.transfer_perform=Übertragung durchführen
|
||||||
settings.transfer_started=`Für dieses Repository wurde eine Übertragung eingeleitet und wartet nun auf die Bestätigung von "%s"`
|
settings.transfer_started=`Für dieses Repository wurde eine Übertragung eingeleitet und wartet nun auf die Bestätigung von "%s"`
|
||||||
@ -2215,6 +2325,7 @@ settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelösc
|
|||||||
settings.event_release=Release
|
settings.event_release=Release
|
||||||
settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht.
|
settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht.
|
||||||
settings.event_push=Push
|
settings.event_push=Push
|
||||||
|
settings.event_force_push=Force Push
|
||||||
settings.event_push_desc=Git push in ein Repository.
|
settings.event_push_desc=Git push in ein Repository.
|
||||||
settings.event_repository=Repository
|
settings.event_repository=Repository
|
||||||
settings.event_repository_desc=Repository erstellt oder gelöscht.
|
settings.event_repository_desc=Repository erstellt oder gelöscht.
|
||||||
@ -2251,6 +2362,7 @@ settings.event_pull_request_merge=Pull-Request-Merge
|
|||||||
settings.event_package=Paket
|
settings.event_package=Paket
|
||||||
settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht.
|
settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht.
|
||||||
settings.branch_filter=Branch-Filter
|
settings.branch_filter=Branch-Filter
|
||||||
|
settings.branch_filter_desc=Whitelist für Branches für Push-, Erzeugungs- und Löschevents, als Glob-Pattern beschrieben. Es werden Events für alle Branches gemeldet, falls das Pattern <code>*</code> ist, oder falls es leer ist. Siehe die <a href="%[1]s">%[2]s</a> Dokumentation für die Syntax (Englisch). Beispiele: <code>master</code>, <code>{master,release*}</code>.
|
||||||
settings.authorization_header=Authorization-Header
|
settings.authorization_header=Authorization-Header
|
||||||
settings.authorization_header_desc=Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s.
|
settings.authorization_header_desc=Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s.
|
||||||
settings.active=Aktiv
|
settings.active=Aktiv
|
||||||
@ -2299,22 +2411,50 @@ settings.branches=Branches
|
|||||||
settings.protected_branch=Branch-Schutz
|
settings.protected_branch=Branch-Schutz
|
||||||
settings.protected_branch.save_rule=Regel speichern
|
settings.protected_branch.save_rule=Regel speichern
|
||||||
settings.protected_branch.delete_rule=Regel löschen
|
settings.protected_branch.delete_rule=Regel löschen
|
||||||
|
settings.protected_branch_can_push=Push erlauben?
|
||||||
|
settings.protected_branch_can_push_yes=Du kannst pushen
|
||||||
|
settings.protected_branch_can_push_no=Du kannst nicht pushen
|
||||||
|
settings.branch_protection=Branch-Schutz für Branch '<b>%s</b>'
|
||||||
settings.protect_this_branch=Branch-Schutz aktivieren
|
settings.protect_this_branch=Branch-Schutz aktivieren
|
||||||
settings.protect_this_branch_desc=Verhindert das Löschen und schränkt Git auf Push- und Merge-Änderungen auf dem Branch ein.
|
settings.protect_this_branch_desc=Verhindert das Löschen und schränkt Git auf Push- und Merge-Änderungen auf dem Branch ein.
|
||||||
settings.protect_disable_push=Push deaktivieren
|
settings.protect_disable_push=Push deaktivieren
|
||||||
settings.protect_disable_push_desc=Kein Push auf diesen Branch erlauben.
|
settings.protect_disable_push_desc=Kein Push auf diesen Branch erlauben.
|
||||||
|
settings.protect_disable_force_push=Force-Push deaktivieren
|
||||||
|
settings.protect_disable_force_push_desc=Force-Push auf diesen Branch nicht erlauben.
|
||||||
settings.protect_enable_push=Push aktivieren
|
settings.protect_enable_push=Push aktivieren
|
||||||
settings.protect_enable_push_desc=Jeder, der Schreibzugriff hat, darf in diesen Branch Pushen (aber kein Force-Push).
|
settings.protect_enable_push_desc=Jeder, der Schreibzugriff hat, darf in diesen Branch Pushen (aber kein Force-Push).
|
||||||
|
settings.protect_enable_force_push_all=Force-Push aktivieren
|
||||||
|
settings.protect_enable_force_push_all_desc=Jeder mit Push-Zugriff wird in diesen Branch force-pushen können.
|
||||||
|
settings.protect_enable_force_push_allowlist=Force-Push beschränkt auf Genehmigungsliste
|
||||||
|
settings.protect_enable_force_push_allowlist_desc=Nur Benutzer oder Teams auf der Genehmigungsliste mit Push-Zugriff werden in diesen Branch force-pushen können.
|
||||||
settings.protect_enable_merge=Merge aktivieren
|
settings.protect_enable_merge=Merge aktivieren
|
||||||
settings.protect_enable_merge_desc=Jeder mit Schreibzugriff darf die Pull-Requests in diesen Branch mergen.
|
settings.protect_enable_merge_desc=Jeder mit Schreibzugriff darf die Pull-Requests in diesen Branch mergen.
|
||||||
|
settings.protect_whitelist_committers=Genehmigungsliste für eingeschränkten Push
|
||||||
|
settings.protect_whitelist_committers_desc=Jeder, der auf der Genehmigungsliste steht, darf in diesen Branch pushen (aber kein Force-Push).
|
||||||
|
settings.protect_whitelist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Pushen.
|
||||||
|
settings.protect_whitelist_users=Nutzer, die pushen dürfen:
|
||||||
|
settings.protect_whitelist_teams=Teams, die pushen dürfen:
|
||||||
|
settings.protect_force_push_allowlist_users=Erlaubte Benutzer für Force-Push:
|
||||||
|
settings.protect_force_push_allowlist_teams=Erlaubte Teams für Force-Push:
|
||||||
|
settings.protect_force_push_allowlist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Force-Push.
|
||||||
|
settings.protect_merge_whitelist_committers=Merge-Genehmigungsliste aktivieren
|
||||||
|
settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Genehmigungsliste Pull-Requests in diesen Branch zu mergen.
|
||||||
|
settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen:
|
||||||
|
settings.protect_merge_whitelist_teams=Teams, die mergen dürfen:
|
||||||
settings.protect_check_status_contexts=Statusprüfungen aktivieren
|
settings.protect_check_status_contexts=Statusprüfungen aktivieren
|
||||||
settings.protect_status_check_patterns=Statuscheck-Muster:
|
settings.protect_status_check_patterns=Statuscheck-Muster:
|
||||||
settings.protect_status_check_patterns_desc=Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein.
|
settings.protect_status_check_patterns_desc=Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein.
|
||||||
|
settings.protect_check_status_contexts_desc=Vor dem Mergen müssen Statusprüfungen bestanden werden. Wähle aus, welche Statusprüfungen erfolgreich durchgeführt werden müssen, bevor Branches in einen anderen gemergt werden können, der dieser Regel entspricht. Wenn aktiviert, müssen Commits zuerst auf einen anderen Branch gepusht werden, dann nach bestandener Statusprüfung gemergt oder direkt auf einen Branch gepusht werden, der dieser Regel entspricht. Wenn kein Kontext ausgewählt ist, muss der letzte Commit unabhängig vom Kontext erfolgreich sein.
|
||||||
settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden
|
settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden
|
||||||
settings.protect_status_check_matched=Übereinstimmung
|
settings.protect_status_check_matched=Übereinstimmung
|
||||||
settings.protect_invalid_status_check_pattern=Ungültiges Muster: "%s".
|
settings.protect_invalid_status_check_pattern=Ungültiges Muster: "%s".
|
||||||
settings.protect_no_valid_status_check_patterns=Keine gültigen Statuscheck-Muster.
|
settings.protect_no_valid_status_check_patterns=Keine gültigen Statuscheck-Muster.
|
||||||
settings.protect_required_approvals=Erforderliche Zustimmungen:
|
settings.protect_required_approvals=Erforderliche Zustimmungen:
|
||||||
|
settings.protect_required_approvals_desc=Erlaube das Mergen des Pull-Requests nur mit genügend Genehmigungen.
|
||||||
|
settings.protect_approvals_whitelist_enabled=Genehmigungen auf Benutzer oder Teams auf der Genehmigungsliste beschränken
|
||||||
|
settings.protect_approvals_whitelist_enabled_desc=Nur Bewertungen von Benutzern auf der Genehmigungsliste oder Teams zählen zu den erforderlichen Genehmigungen. Gibt es keine Genehmigungsliste, so zählen Reviews von jedem mit Schreibzugriff zu den erforderlichen Genehmigungen.
|
||||||
|
settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer:
|
||||||
|
settings.protect_approvals_whitelist_teams=Freigeschaltete Teams:
|
||||||
settings.dismiss_stale_approvals=Entferne alte Genehmigungen
|
settings.dismiss_stale_approvals=Entferne alte Genehmigungen
|
||||||
settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt.
|
settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt.
|
||||||
settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren
|
settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren
|
||||||
@ -2322,12 +2462,18 @@ settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits ert
|
|||||||
settings.require_signed_commits=Signierte Commits erforderlich
|
settings.require_signed_commits=Signierte Commits erforderlich
|
||||||
settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind.
|
settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind.
|
||||||
settings.protect_branch_name_pattern=Muster für geschützte Branchnamen
|
settings.protect_branch_name_pattern=Muster für geschützte Branchnamen
|
||||||
|
settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe <a href="%s">die Dokumentation</a> für die Pattern-Syntax. Beispiele: main, release/**
|
||||||
settings.protect_patterns=Muster
|
settings.protect_patterns=Muster
|
||||||
settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt):
|
settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt):
|
||||||
|
settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='%[1]s'>%[2]s</a> Dokumentation zur Pattern-Syntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||||
settings.protect_unprotected_file_patterns=Ungeschützte Dateimuster (durch Semikolon ';' getrennt):
|
settings.protect_unprotected_file_patterns=Ungeschützte Dateimuster (durch Semikolon ';' getrennt):
|
||||||
|
settings.protect_unprotected_file_patterns_desc=Ungeschützte Dateien, die direkt geändert werden dürfen, wenn der Benutzer Schreibzugriff hat, können die Push-Beschränkung umgehen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='%[1]s'>%[2]s</a> Dokumentation zur Mustersyntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||||
|
settings.add_protected_branch=Schutz aktivieren
|
||||||
|
settings.delete_protected_branch=Schutz deaktivieren
|
||||||
settings.update_protect_branch_success=Branchschutzregel "%s" wurde geändert.
|
settings.update_protect_branch_success=Branchschutzregel "%s" wurde geändert.
|
||||||
settings.remove_protected_branch_success=Branchschutzregel "%s" wurde deaktiviert.
|
settings.remove_protected_branch_success=Branchschutzregel "%s" wurde deaktiviert.
|
||||||
settings.remove_protected_branch_failed=Entfernen der Branchschutzregel "%s" fehlgeschlagen.
|
settings.remove_protected_branch_failed=Entfernen der Branchschutzregel "%s" fehlgeschlagen.
|
||||||
|
settings.protected_branch_deletion=Branch-Schutz deaktivieren
|
||||||
settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren?
|
settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren?
|
||||||
settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren
|
settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren
|
||||||
settings.block_rejected_reviews_desc=Mergen ist nicht möglich, wenn Änderungen durch offizielle Reviewer angefragt werden, auch wenn es genügend Zustimmungen gibt.
|
settings.block_rejected_reviews_desc=Mergen ist nicht möglich, wenn Änderungen durch offizielle Reviewer angefragt werden, auch wenn es genügend Zustimmungen gibt.
|
||||||
@ -2335,8 +2481,11 @@ settings.block_on_official_review_requests=Mergen bei offiziellen Review-Anfrage
|
|||||||
settings.block_on_official_review_requests_desc=Mergen ist nicht möglich wenn offizielle Review-Anfrangen vorliegen, selbst wenn es genügend Zustimmungen gibt.
|
settings.block_on_official_review_requests_desc=Mergen ist nicht möglich wenn offizielle Review-Anfrangen vorliegen, selbst wenn es genügend Zustimmungen gibt.
|
||||||
settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist
|
settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist
|
||||||
settings.block_outdated_branch_desc=Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist.
|
settings.block_outdated_branch_desc=Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist.
|
||||||
|
settings.block_admin_merge_override=Administratoren müssen die Schutzregeln für Branches befolgen
|
||||||
|
settings.block_admin_merge_override_desc=Administratoren müssen die Schutzregeln für Branches befolgen und können sie nicht umgehen.
|
||||||
settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits:
|
settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits:
|
||||||
settings.merge_style_desc=Merge-Styles
|
settings.merge_style_desc=Merge-Styles
|
||||||
|
settings.default_merge_style_desc=Standard-Mergeverhalten für Pull-Requests
|
||||||
settings.choose_branch=Branch wählen…
|
settings.choose_branch=Branch wählen…
|
||||||
settings.no_protected_branch=Es gibt keine geschützten Branches.
|
settings.no_protected_branch=Es gibt keine geschützten Branches.
|
||||||
settings.edit_protected_branch=Bearbeiten
|
settings.edit_protected_branch=Bearbeiten
|
||||||
@ -2352,12 +2501,25 @@ settings.tags.protection.allowed.teams=Erlaubte Teams
|
|||||||
settings.tags.protection.allowed.noone=Niemand
|
settings.tags.protection.allowed.noone=Niemand
|
||||||
settings.tags.protection.create=Tag schützen
|
settings.tags.protection.create=Tag schützen
|
||||||
settings.tags.protection.none=Es gibt keine geschützten Tags.
|
settings.tags.protection.none=Es gibt keine geschützten Tags.
|
||||||
|
settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im <a target="_blank" rel="noopener" href="%s">Guide für geschützte Tags (Englisch)</a>.
|
||||||
settings.bot_token=Bot-Token
|
settings.bot_token=Bot-Token
|
||||||
settings.chat_id=Chat-ID
|
settings.chat_id=Chat-ID
|
||||||
settings.thread_id=Thread-ID
|
settings.thread_id=Thread-ID
|
||||||
settings.matrix.homeserver_url=Homeserver-URL
|
settings.matrix.homeserver_url=Homeserver-URL
|
||||||
settings.matrix.room_id=Raum-ID
|
settings.matrix.room_id=Raum-ID
|
||||||
settings.matrix.message_type=Nachrichtentyp
|
settings.matrix.message_type=Nachrichtentyp
|
||||||
|
settings.visibility.private.button=Auf privat setzen
|
||||||
|
settings.visibility.private.text=Das Ändern der Sichtbarkeit auf privat wird das Repository nicht nur für erlaubte Mitglieder sichtbar machen, sondern kann auch die Beziehung zwischen ihm und Forks, Beobachtern und Sternen entfernen.
|
||||||
|
settings.visibility.private.bullet_title=<strong>Das Ändern der Sichtbarkeit auf privat wird:</strong>
|
||||||
|
settings.visibility.private.bullet_one=Das Repository nur für zugelassene Mitglieder sichtbar machen.
|
||||||
|
settings.visibility.private.bullet_two=Kann die Beziehung zwischen ihm und <strong>Forks</strong>, <strong>Beobachtern</strong>und <strong>Sternen</strong> entfernen.
|
||||||
|
settings.visibility.public.button=Auf öffentlich setzen
|
||||||
|
settings.visibility.public.text=Das Ändern der Sichtbarkeit auf öffentlich macht das Repository für jeden sichtbar.
|
||||||
|
settings.visibility.public.bullet_title=<strong>Das Ändern der Sichtbarkeit auf öffentlich wird:</strong>
|
||||||
|
settings.visibility.public.bullet_one=Das Repository für jeden sichtbar machen.
|
||||||
|
settings.visibility.success=Die Sichtbarkeit des Repositorys wurde geändert.
|
||||||
|
settings.visibility.error=Beim Versuch, die Sichtbarkeit des Repositorys zu ändern, ist ein Fehler aufgetreten.
|
||||||
|
settings.visibility.fork_error=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar.
|
||||||
settings.archive.button=Repo archivieren
|
settings.archive.button=Repo archivieren
|
||||||
settings.archive.header=Dieses Repo archivieren
|
settings.archive.header=Dieses Repo archivieren
|
||||||
settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird vom Dashboard versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen.
|
settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird vom Dashboard versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen.
|
||||||
@ -2469,6 +2631,7 @@ release.new_release=Neues Release
|
|||||||
release.draft=Entwurf
|
release.draft=Entwurf
|
||||||
release.prerelease=Pre-Release
|
release.prerelease=Pre-Release
|
||||||
release.stable=Stabil
|
release.stable=Stabil
|
||||||
|
release.latest=Aktuell
|
||||||
release.compare=Vergleichen
|
release.compare=Vergleichen
|
||||||
release.edit=bearbeiten
|
release.edit=bearbeiten
|
||||||
release.ahead.commits=<strong>%d</strong> Commits
|
release.ahead.commits=<strong>%d</strong> Commits
|
||||||
@ -2553,6 +2716,7 @@ tag.create_success=Tag "%s" wurde erstellt.
|
|||||||
|
|
||||||
topic.manage_topics=Themen verwalten
|
topic.manage_topics=Themen verwalten
|
||||||
topic.done=Fertig
|
topic.done=Fertig
|
||||||
|
topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen
|
||||||
topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig.
|
topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig.
|
||||||
|
|
||||||
find_file.go_to_file=Datei suchen
|
find_file.go_to_file=Datei suchen
|
||||||
@ -2650,6 +2814,7 @@ teams.leave.detail=%s verlassen?
|
|||||||
teams.can_create_org_repo=Repositories erstellen
|
teams.can_create_org_repo=Repositories erstellen
|
||||||
teams.can_create_org_repo_helper=Mitglieder können neue Repositories in der Organisation erstellen. Der Ersteller erhält Administrator-Zugriff auf das neue Repository.
|
teams.can_create_org_repo_helper=Mitglieder können neue Repositories in der Organisation erstellen. Der Ersteller erhält Administrator-Zugriff auf das neue Repository.
|
||||||
teams.none_access=Kein Zugriff
|
teams.none_access=Kein Zugriff
|
||||||
|
teams.none_access_helper=Mitglieder können keine anderen Aktionen für diese Einheit anzeigen oder durchführen. Dies hat keine Wirkung auf öffentliche Repositories.
|
||||||
teams.general_access=Allgemeiner Zugriff
|
teams.general_access=Allgemeiner Zugriff
|
||||||
teams.general_access_helper=Mitgliederberechtigungen werden durch folgende Berechtigungstabelle festgelegt.
|
teams.general_access_helper=Mitgliederberechtigungen werden durch folgende Berechtigungstabelle festgelegt.
|
||||||
teams.read_access=Lesen
|
teams.read_access=Lesen
|
||||||
@ -2695,7 +2860,9 @@ teams.invite.title=Du wurdest eingeladen, dem Team <strong>%s</strong> in der Or
|
|||||||
teams.invite.by=Von %s eingeladen
|
teams.invite.by=Von %s eingeladen
|
||||||
teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.
|
teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
|
maintenance=Wartung
|
||||||
dashboard=Dashboard
|
dashboard=Dashboard
|
||||||
self_check=Selbstprüfung
|
self_check=Selbstprüfung
|
||||||
identity_access=Identität & Zugriff
|
identity_access=Identität & Zugriff
|
||||||
@ -2717,7 +2884,9 @@ last_page=Letzte
|
|||||||
total=Gesamt: %d
|
total=Gesamt: %d
|
||||||
settings=Administratoreinstellungen
|
settings=Administratoreinstellungen
|
||||||
|
|
||||||
|
dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im <a target="_blank" rel="noreferrer" href="%s">Blog</a>.
|
||||||
dashboard.statistic=Übersicht
|
dashboard.statistic=Übersicht
|
||||||
|
dashboard.maintenance_operations=Wartungsoperationen
|
||||||
dashboard.system_status=System-Status
|
dashboard.system_status=System-Status
|
||||||
dashboard.operation_name=Name der Operation
|
dashboard.operation_name=Name der Operation
|
||||||
dashboard.operation_switch=Wechseln
|
dashboard.operation_switch=Wechseln
|
||||||
@ -2758,6 +2927,7 @@ dashboard.reinit_missing_repos=Alle Git-Repositories neu einlesen, für die Eint
|
|||||||
dashboard.sync_external_users=Externe Benutzerdaten synchronisieren
|
dashboard.sync_external_users=Externe Benutzerdaten synchronisieren
|
||||||
dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen
|
dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen
|
||||||
dashboard.cleanup_packages=Veraltete Pakete löschen
|
dashboard.cleanup_packages=Veraltete Pakete löschen
|
||||||
|
dashboard.cleanup_actions=Abgelaufene Ressourcen von Actions bereinigen
|
||||||
dashboard.server_uptime=Server-Uptime
|
dashboard.server_uptime=Server-Uptime
|
||||||
dashboard.current_goroutine=Aktuelle Goroutinen
|
dashboard.current_goroutine=Aktuelle Goroutinen
|
||||||
dashboard.current_memory_usage=Aktuelle Speichernutzung
|
dashboard.current_memory_usage=Aktuelle Speichernutzung
|
||||||
@ -2787,12 +2957,19 @@ dashboard.total_gc_time=Gesamte GC-Pause
|
|||||||
dashboard.total_gc_pause=Gesamte GC-Pause
|
dashboard.total_gc_pause=Gesamte GC-Pause
|
||||||
dashboard.last_gc_pause=Letzte GC-Pause
|
dashboard.last_gc_pause=Letzte GC-Pause
|
||||||
dashboard.gc_times=Anzahl GC
|
dashboard.gc_times=Anzahl GC
|
||||||
|
dashboard.delete_old_actions=Alle alten Aktionen aus der Datenbank löschen
|
||||||
|
dashboard.delete_old_actions.started=Löschen aller alten Aktionen in der Datenbank gestartet.
|
||||||
dashboard.update_checker=Update-Checker
|
dashboard.update_checker=Update-Checker
|
||||||
dashboard.delete_old_system_notices=Alle alten Systemmeldungen aus der Datenbank löschen
|
dashboard.delete_old_system_notices=Alle alten Systemmeldungen aus der Datenbank löschen
|
||||||
dashboard.gc_lfs=Garbage-Collection für LFS Meta-Objekte ausführen
|
dashboard.gc_lfs=Garbage-Collection für LFS Meta-Objekte ausführen
|
||||||
|
dashboard.stop_zombie_tasks=Zombie-Aufgaben stoppen
|
||||||
|
dashboard.stop_endless_tasks=Endlose Aktionen stoppen
|
||||||
|
dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen
|
||||||
|
dashboard.start_schedule_tasks=Terminierte Aufgaben starten
|
||||||
dashboard.sync_branch.started=Synchronisierung der Branches gestartet
|
dashboard.sync_branch.started=Synchronisierung der Branches gestartet
|
||||||
dashboard.sync_tag.started=Tag-Synchronisierung gestartet
|
dashboard.sync_tag.started=Tag-Synchronisierung gestartet
|
||||||
dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen
|
dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen
|
||||||
|
dashboard.sync_repo_licenses=Repo-Lizenzen synchronisieren
|
||||||
|
|
||||||
users.user_manage_panel=Benutzerkontenverwaltung
|
users.user_manage_panel=Benutzerkontenverwaltung
|
||||||
users.new_account=Benutzerkonto erstellen
|
users.new_account=Benutzerkonto erstellen
|
||||||
@ -2864,6 +3041,10 @@ emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: %
|
|||||||
emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet.
|
emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet.
|
||||||
emails.change_email_header=E-Mail-Eigenschaften aktualisieren
|
emails.change_email_header=E-Mail-Eigenschaften aktualisieren
|
||||||
emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest?
|
emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest?
|
||||||
|
emails.delete=E-Mail löschen
|
||||||
|
emails.delete_desc=Willst du diese E-Mail-Adresse wirklich löschen?
|
||||||
|
emails.deletion_success=Die E-Mail-Adresse wurde gelöscht.
|
||||||
|
emails.delete_primary_email_error=Du kannst die primäre E-Mail-Adresse nicht löschen.
|
||||||
|
|
||||||
orgs.org_manage_panel=Organisationsverwaltung
|
orgs.org_manage_panel=Organisationsverwaltung
|
||||||
orgs.name=Name
|
orgs.name=Name
|
||||||
@ -2896,10 +3077,12 @@ packages.size=Größe
|
|||||||
packages.published=Veröffentlicht
|
packages.published=Veröffentlicht
|
||||||
|
|
||||||
defaulthooks=Standard-Webhooks
|
defaulthooks=Standard-Webhooks
|
||||||
|
defaulthooks.desc=Webhooks senden automatisch eine HTTP-POST-Anfrage an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks sind die Standardwerte, die in alle neuen Repositories kopiert werden. Mehr Infos findest du in der <a target="_blank" rel="noopener" href="%s">Webhooks-Anleitung</a> (auf Englisch).
|
||||||
defaulthooks.add_webhook=Standard-Webhook hinzufügen
|
defaulthooks.add_webhook=Standard-Webhook hinzufügen
|
||||||
defaulthooks.update_webhook=Standard-Webhook aktualisieren
|
defaulthooks.update_webhook=Standard-Webhook aktualisieren
|
||||||
|
|
||||||
systemhooks=System-Webhooks
|
systemhooks=System-Webhooks
|
||||||
|
systemhooks.desc=Webhooks senden automatisch HTTP-POST-Anfragen an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks werden auf alle Repositories des Systems übertragen, beachte daher mögliche Performance-Einbrüche. Mehr Infos findest du in der <a target="_blank" rel="noopener" href="%s">Webhooks-Anleitung</a> (auf Englisch).
|
||||||
systemhooks.add_webhook=System-Webhook hinzufügen
|
systemhooks.add_webhook=System-Webhook hinzufügen
|
||||||
systemhooks.update_webhook=System-Webhook aktualisieren
|
systemhooks.update_webhook=System-Webhook aktualisieren
|
||||||
|
|
||||||
@ -2994,7 +3177,18 @@ auths.tips=Tipps
|
|||||||
auths.tips.oauth2.general=OAuth2-Authentifizierung
|
auths.tips.oauth2.general=OAuth2-Authentifizierung
|
||||||
auths.tips.oauth2.general.tip=Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten:
|
auths.tips.oauth2.general.tip=Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten:
|
||||||
auths.tip.oauth2_provider=OAuth2-Anbieter
|
auths.tip.oauth2_provider=OAuth2-Anbieter
|
||||||
|
auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu
|
||||||
auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz
|
auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz
|
||||||
|
auths.tip.dropbox=Erstelle eine neue App auf %s
|
||||||
|
auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt "Facebook Login“ hinzu
|
||||||
|
auths.tip.github=Erstelle unter %s eine neue OAuth-Anwendung
|
||||||
|
auths.tip.gitlab_new=Erstelle eine neue Anwendung unter %s
|
||||||
|
auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter %s
|
||||||
|
auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL "https://{server}/.well-known/openid-configuration", um die Endpunkte zu spezifizieren
|
||||||
|
auths.tip.twitter=Gehe zu %s, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist
|
||||||
|
auths.tip.discord=Erstelle unter %s eine neue Anwendung
|
||||||
|
auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s
|
||||||
|
auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem "Yandex.Passport API" Bereich: "Zugriff auf E-Mail-Adresse", "Zugriff auf Benutzeravatar" und "Zugriff auf Benutzername, Vor- und Nachname, Geschlecht"`
|
||||||
auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige)
|
auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige)
|
||||||
auths.edit=Authentifikationsquelle bearbeiten
|
auths.edit=Authentifikationsquelle bearbeiten
|
||||||
auths.activated=Diese Authentifikationsquelle ist aktiviert
|
auths.activated=Diese Authentifikationsquelle ist aktiviert
|
||||||
@ -3110,6 +3304,10 @@ config.cache_adapter=Cache-Adapter
|
|||||||
config.cache_interval=Cache-Intervall
|
config.cache_interval=Cache-Intervall
|
||||||
config.cache_conn=Cache-Anbindung
|
config.cache_conn=Cache-Anbindung
|
||||||
config.cache_item_ttl=Cache Item-TTL
|
config.cache_item_ttl=Cache Item-TTL
|
||||||
|
config.cache_test=Cache testen
|
||||||
|
config.cache_test_failed=Fehler beim Prüfen des Caches: %v.
|
||||||
|
config.cache_test_slow=Cache-Test erfolgreich, aber die Antwortzeit ist langsam: %s.
|
||||||
|
config.cache_test_succeeded=Cache-Test erfolgreich, Antwort in %s erhalten.
|
||||||
|
|
||||||
config.session_config=Session-Konfiguration
|
config.session_config=Session-Konfiguration
|
||||||
config.session_provider=Session-Provider
|
config.session_provider=Session-Provider
|
||||||
@ -3156,6 +3354,7 @@ monitor.next=Nächste Ausführung
|
|||||||
monitor.previous=Letzte Ausführung
|
monitor.previous=Letzte Ausführung
|
||||||
monitor.execute_times=Ausführungen
|
monitor.execute_times=Ausführungen
|
||||||
monitor.process=Laufende Prozesse
|
monitor.process=Laufende Prozesse
|
||||||
|
monitor.stacktrace=Stacktraces
|
||||||
monitor.processes_count=%d Prozesse
|
monitor.processes_count=%d Prozesse
|
||||||
monitor.download_diagnosis_report=Diagnosebericht herunterladen
|
monitor.download_diagnosis_report=Diagnosebericht herunterladen
|
||||||
monitor.desc=Beschreibung
|
monitor.desc=Beschreibung
|
||||||
@ -3163,6 +3362,8 @@ monitor.start=Startzeit
|
|||||||
monitor.execute_time=Ausführungszeit
|
monitor.execute_time=Ausführungszeit
|
||||||
monitor.last_execution_result=Ergebnis
|
monitor.last_execution_result=Ergebnis
|
||||||
monitor.process.cancel=Prozess abbrechen
|
monitor.process.cancel=Prozess abbrechen
|
||||||
|
monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen
|
||||||
|
monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>?
|
||||||
monitor.process.children=Subprozesse
|
monitor.process.children=Subprozesse
|
||||||
|
|
||||||
monitor.queues=Warteschlangen
|
monitor.queues=Warteschlangen
|
||||||
@ -3201,11 +3402,13 @@ notices.op=Aktion
|
|||||||
notices.delete_success=Diese Systemmeldung wurde gelöscht.
|
notices.delete_success=Diese Systemmeldung wurde gelöscht.
|
||||||
|
|
||||||
self_check.no_problem_found=Bisher wurde kein Problem festgestellt.
|
self_check.no_problem_found=Bisher wurde kein Problem festgestellt.
|
||||||
|
self_check.startup_warnings=Warnungen beim Start:
|
||||||
self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s
|
self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s
|
||||||
self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren.
|
self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren.
|
||||||
self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen.
|
self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen.
|
||||||
self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben.
|
self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben.
|
||||||
self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden.
|
self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden.
|
||||||
|
self_check.location_origin_mismatch=Aktuelle URL (%[1]s) stimmt nicht mit der URL überein, die Gitea (%[2]s) sieht. Wenn du einen Reverse-Proxy verwendest, stelle bitte sicher, dass die Header "Host" und "X-Forwarded-Proto" korrekt gesetzt sind.
|
||||||
|
|
||||||
[action]
|
[action]
|
||||||
create_repo=hat das Repository <a href="%s">%s</a> erstellt
|
create_repo=hat das Repository <a href="%s">%s</a> erstellt
|
||||||
@ -3233,6 +3436,7 @@ mirror_sync_create=neue Referenz <a href="%[2]s">%[3]s</a> bei <a href="%[1]s">%
|
|||||||
mirror_sync_delete=hat die Referenz des Mirrors <code>%[2]s</code> in <a href="%[1]s">%[3]s</a> synchronisiert und gelöscht
|
mirror_sync_delete=hat die Referenz des Mirrors <code>%[2]s</code> in <a href="%[1]s">%[3]s</a> synchronisiert und gelöscht
|
||||||
approve_pull_request=`hat <a href="%[1]s">%[3]s#%[2]s</a> approved`
|
approve_pull_request=`hat <a href="%[1]s">%[3]s#%[2]s</a> approved`
|
||||||
reject_pull_request=`schlug Änderungen für <a href="%[1]s">%[3]s#%[2]s</a> vor`
|
reject_pull_request=`schlug Änderungen für <a href="%[1]s">%[3]s#%[2]s</a> vor`
|
||||||
|
publish_release=`veröffentlichte Release <a href="%[2]s"> "%[4]s" </a> in <a href="%[1]s">%[3]s</a>`
|
||||||
review_dismissed=`verwarf das Review von <b>%[4]s</b> in <a href="%[1]s">%[3]s#%[2]s</a>`
|
review_dismissed=`verwarf das Review von <b>%[4]s</b> in <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||||
review_dismissed_reason=Grund:
|
review_dismissed_reason=Grund:
|
||||||
create_branch=legte den Branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a> an
|
create_branch=legte den Branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a> an
|
||||||
@ -3261,6 +3465,8 @@ raw_minutes=Minuten
|
|||||||
|
|
||||||
[dropzone]
|
[dropzone]
|
||||||
default_message=Zum Hochladen hier klicken oder Datei ablegen.
|
default_message=Zum Hochladen hier klicken oder Datei ablegen.
|
||||||
|
invalid_input_type=Dateien dieses Dateityps können nicht hochgeladen werden.
|
||||||
|
file_too_big=Dateigröße ({{filesize}} MB) überschreitet die Maximalgröße ({{maxFilesize}} MB).
|
||||||
remove_file=Datei entfernen
|
remove_file=Datei entfernen
|
||||||
|
|
||||||
[notification]
|
[notification]
|
||||||
@ -3297,6 +3503,7 @@ error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bere
|
|||||||
title=Pakete
|
title=Pakete
|
||||||
desc=Repository-Pakete verwalten.
|
desc=Repository-Pakete verwalten.
|
||||||
empty=Noch keine Pakete vorhanden.
|
empty=Noch keine Pakete vorhanden.
|
||||||
|
no_metadata=Keine Metadaten.
|
||||||
empty.documentation=Weitere Informationen zur Paket-Registry findest Du in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a>.
|
empty.documentation=Weitere Informationen zur Paket-Registry findest Du in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a>.
|
||||||
empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den <a href="%[1]s">Paketeinstellungen</a> und verlinke es mit diesem Repo.
|
empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den <a href="%[1]s">Paketeinstellungen</a> und verlinke es mit diesem Repo.
|
||||||
registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a> nach.
|
registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a> nach.
|
||||||
@ -3331,6 +3538,8 @@ alpine.repository=Repository-Informationen
|
|||||||
alpine.repository.branches=Branches
|
alpine.repository.branches=Branches
|
||||||
alpine.repository.repositories=Repositories
|
alpine.repository.repositories=Repositories
|
||||||
alpine.repository.architectures=Architekturen
|
alpine.repository.architectures=Architekturen
|
||||||
|
arch.registry=Server mit gebrauchtem Repository und Architektur zu <code>/etc/pacman.conf</code> hinzufügen:
|
||||||
|
arch.install=Paket mit pacman synchronisieren:
|
||||||
arch.repository=Repository-Informationen
|
arch.repository=Repository-Informationen
|
||||||
arch.repository.repositories=Repositories
|
arch.repository.repositories=Repositories
|
||||||
arch.repository.architectures=Architekturen
|
arch.repository.architectures=Architekturen
|
||||||
@ -3381,6 +3590,7 @@ npm.install=Um das Paket mit npm zu installieren, führe den folgenden Befehl au
|
|||||||
npm.install2=oder füge es zur package.json-Datei hinzu:
|
npm.install2=oder füge es zur package.json-Datei hinzu:
|
||||||
npm.dependencies=Abhängigkeiten
|
npm.dependencies=Abhängigkeiten
|
||||||
npm.dependencies.development=Entwicklungsabhängigkeiten
|
npm.dependencies.development=Entwicklungsabhängigkeiten
|
||||||
|
npm.dependencies.bundle=Gebündelte Abhängigkeiten
|
||||||
npm.dependencies.peer=Peer Abhängigkeiten
|
npm.dependencies.peer=Peer Abhängigkeiten
|
||||||
npm.dependencies.optional=Optionale Abhängigkeiten
|
npm.dependencies.optional=Optionale Abhängigkeiten
|
||||||
npm.details.tag=Tag
|
npm.details.tag=Tag
|
||||||
@ -3512,6 +3722,7 @@ runners.status.active=Aktiv
|
|||||||
runners.status.offline=Offline
|
runners.status.offline=Offline
|
||||||
runners.version=Version
|
runners.version=Version
|
||||||
runners.reset_registration_token=Registrierungs-Token zurücksetzen
|
runners.reset_registration_token=Registrierungs-Token zurücksetzen
|
||||||
|
runners.reset_registration_token_confirm=Möchtest du den aktuellen Token invalidieren und einen neuen generieren?
|
||||||
runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt
|
runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt
|
||||||
|
|
||||||
runs.all_workflows=Alle Workflows
|
runs.all_workflows=Alle Workflows
|
||||||
@ -3521,6 +3732,7 @@ runs.pushed_by=gepusht von
|
|||||||
runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s
|
runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s
|
||||||
runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s
|
runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s
|
||||||
runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.
|
runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.
|
||||||
|
runs.no_job=Der Workflow muss mindestens einen Job enthalten
|
||||||
runs.actor=Initiator
|
runs.actor=Initiator
|
||||||
runs.status=Status
|
runs.status=Status
|
||||||
runs.actors_no_select=Alle Initiatoren
|
runs.actors_no_select=Alle Initiatoren
|
||||||
@ -3531,12 +3743,18 @@ runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst
|
|||||||
runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>.
|
runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>.
|
||||||
runs.no_runs=Der Workflow hat noch keine Ausführungen.
|
runs.no_runs=Der Workflow hat noch keine Ausführungen.
|
||||||
runs.empty_commit_message=(leere Commit-Nachricht)
|
runs.empty_commit_message=(leere Commit-Nachricht)
|
||||||
|
runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren.
|
||||||
|
|
||||||
workflow.disable=Workflow deaktivieren
|
workflow.disable=Workflow deaktivieren
|
||||||
workflow.disable_success=Workflow '%s' erfolgreich deaktiviert.
|
workflow.disable_success=Workflow '%s' erfolgreich deaktiviert.
|
||||||
workflow.enable=Workflow aktivieren
|
workflow.enable=Workflow aktivieren
|
||||||
workflow.enable_success=Workflow '%s' erfolgreich aktiviert.
|
workflow.enable_success=Workflow '%s' erfolgreich aktiviert.
|
||||||
workflow.disabled=Workflow ist deaktiviert.
|
workflow.disabled=Workflow ist deaktiviert.
|
||||||
|
workflow.run=Workflow ausführen
|
||||||
|
workflow.not_found=Workflow '%s' wurde nicht gefunden.
|
||||||
|
workflow.run_success=Workflow '%s' erfolgreich ausgeführt.
|
||||||
|
workflow.from_ref=Nutze Workflow von
|
||||||
|
workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger.
|
||||||
|
|
||||||
need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich.
|
need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich.
|
||||||
|
|
||||||
@ -3556,8 +3774,11 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt.
|
|||||||
variables.update.failed=Fehler beim Bearbeiten der Variable.
|
variables.update.failed=Fehler beim Bearbeiten der Variable.
|
||||||
variables.update.success=Die Variable wurde bearbeitet.
|
variables.update.success=Die Variable wurde bearbeitet.
|
||||||
|
|
||||||
|
logs.always_auto_scroll=Autoscroll für Logs immer aktivieren
|
||||||
|
logs.always_expand_running=Laufende Logs immer erweitern
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
|
deleted.display_name=Gelöschtes Projekt
|
||||||
type-1.display_name=Individuelles Projekt
|
type-1.display_name=Individuelles Projekt
|
||||||
type-2.display_name=Repository-Projekt
|
type-2.display_name=Repository-Projekt
|
||||||
type-3.display_name=Organisationsprojekt
|
type-3.display_name=Organisationsprojekt
|
||||||
|
@ -580,6 +580,7 @@ joined_on=Εγγράφηκε την %s
|
|||||||
repositories=Αποθετήρια
|
repositories=Αποθετήρια
|
||||||
activity=Δημόσια Δραστηριότητα
|
activity=Δημόσια Δραστηριότητα
|
||||||
followers=Ακόλουθοι
|
followers=Ακόλουθοι
|
||||||
|
show_more=Εμφάνιση Περισσότερων
|
||||||
starred=Αγαπημένα Αποθετήρια
|
starred=Αγαπημένα Αποθετήρια
|
||||||
watched=Ακολουθούμενα Αποθετήρια
|
watched=Ακολουθούμενα Αποθετήρια
|
||||||
code=Κώδικας
|
code=Κώδικας
|
||||||
@ -907,7 +908,6 @@ new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχ
|
|||||||
owner=Ιδιοκτήτης
|
owner=Ιδιοκτήτης
|
||||||
owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων.
|
owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων.
|
||||||
repo_name=Όνομα αποθετηρίου
|
repo_name=Όνομα αποθετηρίου
|
||||||
repo_name_helper=Τα καλά ονόματα αποθετηρίων χρησιμοποιούν σύντομες, αξέχαστες και μοναδικές λέξεις-κλειδιά.
|
|
||||||
repo_size=Μέγεθος Αποθετηρίου
|
repo_size=Μέγεθος Αποθετηρίου
|
||||||
template=Πρότυπο
|
template=Πρότυπο
|
||||||
template_select=Επιλέξτε πρότυπο.
|
template_select=Επιλέξτε πρότυπο.
|
||||||
@ -2592,6 +2592,7 @@ teams.invite.title=Έχετε προσκληθεί να συμμετάσχετε
|
|||||||
teams.invite.by=Προσκλήθηκε από %s
|
teams.invite.by=Προσκλήθηκε από %s
|
||||||
teams.invite.description=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα.
|
teams.invite.description=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Πίνακας Ελέγχου
|
dashboard=Πίνακας Ελέγχου
|
||||||
identity_access=Ταυτότητα & Πρόσβαση
|
identity_access=Ταυτότητα & Πρόσβαση
|
||||||
|
@ -244,6 +244,7 @@ license_desc = Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">
|
|||||||
|
|
||||||
[install]
|
[install]
|
||||||
install = Installation
|
install = Installation
|
||||||
|
installing_desc = Installing now, please wait...
|
||||||
title = Initial Configuration
|
title = Initial Configuration
|
||||||
docker_helper = If you run Gitea inside Docker, please read the <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> before changing any settings.
|
docker_helper = If you run Gitea inside Docker, please read the <a target="_blank" rel="noopener noreferrer" href="%s">documentation</a> before changing any settings.
|
||||||
require_db_desc = Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
require_db_desc = Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||||
@ -1015,7 +1016,9 @@ new_repo_helper = A repository contains all project files, including revision hi
|
|||||||
owner = Owner
|
owner = Owner
|
||||||
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
||||||
repo_name = Repository Name
|
repo_name = Repository Name
|
||||||
repo_name_helper = Good repository names use short, memorable and unique keywords.
|
repo_name_profile_public_hint= .profile is a special repository that you can use to add README.md to your public organization profile, visible to anyone. Make sure it’s public and initialize it with a README in the profile directory to get started.
|
||||||
|
repo_name_profile_private_hint = .profile-private is a special repository that you can use to add a README.md to your organization member profile, visible only to organization members. Make sure it’s private and initialize it with a README in the profile directory to get started.
|
||||||
|
repo_name_helper = Good repository names use short, memorable and unique keywords. A repository named ".profile" or ".profile-private" could be used to add a README.md for the user/organization profile.
|
||||||
repo_size = Repository Size
|
repo_size = Repository Size
|
||||||
template = Template
|
template = Template
|
||||||
template_select = Select a template.
|
template_select = Select a template.
|
||||||
@ -2862,6 +2865,10 @@ teams.invite.title = You have been invited to join team <strong>%s</strong> in o
|
|||||||
teams.invite.by = Invited by %s
|
teams.invite.by = Invited by %s
|
||||||
teams.invite.description = Please click the button below to join the team.
|
teams.invite.description = Please click the button below to join the team.
|
||||||
|
|
||||||
|
view_as_role = View as: %s
|
||||||
|
view_as_public_hint = You are viewing the README as a public user.
|
||||||
|
view_as_member_hint = You are viewing the README as a member of this organization.
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
maintenance = Maintenance
|
maintenance = Maintenance
|
||||||
dashboard = Dashboard
|
dashboard = Dashboard
|
||||||
@ -3531,6 +3538,7 @@ versions = Versions
|
|||||||
versions.view_all = View all
|
versions.view_all = View all
|
||||||
dependency.id = ID
|
dependency.id = ID
|
||||||
dependency.version = Version
|
dependency.version = Version
|
||||||
|
search_in_external_registry = Search in %s
|
||||||
alpine.registry = Setup this registry by adding the url in your <code>/etc/apk/repositories</code> file:
|
alpine.registry = Setup this registry by adding the url in your <code>/etc/apk/repositories</code> file:
|
||||||
alpine.registry.key = Download the registry public RSA key into the <code>/etc/apk/keys/</code> folder to verify the index signature:
|
alpine.registry.key = Download the registry public RSA key into the <code>/etc/apk/keys/</code> folder to verify the index signature:
|
||||||
alpine.registry.info = Choose $branch and $repository from the list below.
|
alpine.registry.info = Choose $branch and $repository from the list below.
|
||||||
|
@ -577,6 +577,7 @@ joined_on=Se unió el %s
|
|||||||
repositories=Repositorios
|
repositories=Repositorios
|
||||||
activity=Actividad pública
|
activity=Actividad pública
|
||||||
followers=Seguidores
|
followers=Seguidores
|
||||||
|
show_more=Ver más
|
||||||
starred=Repositorios Favoritos
|
starred=Repositorios Favoritos
|
||||||
watched=Repositorios seguidos
|
watched=Repositorios seguidos
|
||||||
code=Código
|
code=Código
|
||||||
@ -897,7 +898,6 @@ visibility.private_tooltip=Visible sólo para los miembros de organizaciones a l
|
|||||||
owner=Propietario
|
owner=Propietario
|
||||||
owner_helper=Algunas organizaciones pueden no aparecer en el menú desplegable debido a un límite máximo de recuento de repositorios.
|
owner_helper=Algunas organizaciones pueden no aparecer en el menú desplegable debido a un límite máximo de recuento de repositorios.
|
||||||
repo_name=Nombre del repositorio
|
repo_name=Nombre del repositorio
|
||||||
repo_name_helper=Un buen nombre de repositorio está compuesto por palabras clave cortas, memorables y únicas.
|
|
||||||
repo_size=Tamaño del repositorio
|
repo_size=Tamaño del repositorio
|
||||||
template=Plantilla
|
template=Plantilla
|
||||||
template_select=Seleccionar una plantilla.
|
template_select=Seleccionar una plantilla.
|
||||||
@ -2573,6 +2573,7 @@ teams.invite.title=Has sido invitado a unirte al equipo <strong>%s</strong> en l
|
|||||||
teams.invite.by=Invitado por %s
|
teams.invite.by=Invitado por %s
|
||||||
teams.invite.description=Por favor, haga clic en el botón de abajo para unirse al equipo.
|
teams.invite.description=Por favor, haga clic en el botón de abajo para unirse al equipo.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Panel de control
|
dashboard=Panel de control
|
||||||
identity_access=Identidad y acceso
|
identity_access=Identidad y acceso
|
||||||
|
@ -463,6 +463,7 @@ change_avatar=تغییر آواتار…
|
|||||||
repositories=مخازن
|
repositories=مخازن
|
||||||
activity=فعالیت های عمومی
|
activity=فعالیت های عمومی
|
||||||
followers=دنبال کنندگان
|
followers=دنبال کنندگان
|
||||||
|
show_more=نمایش بیشتر
|
||||||
starred=مخان ستاره دار
|
starred=مخان ستاره دار
|
||||||
watched=مخازنی که دنبال میشوند
|
watched=مخازنی که دنبال میشوند
|
||||||
projects=پروژهها
|
projects=پروژهها
|
||||||
@ -703,7 +704,6 @@ visibility.private=خصوصی
|
|||||||
owner=مالک
|
owner=مالک
|
||||||
owner_helper=بخاطر بیشینه تعداد مخزن، ممکن است برخی از سازمانها در لیست کشویی دیده نشود.
|
owner_helper=بخاطر بیشینه تعداد مخزن، ممکن است برخی از سازمانها در لیست کشویی دیده نشود.
|
||||||
repo_name=نام مخزن
|
repo_name=نام مخزن
|
||||||
repo_name_helper=نام خوب مخزن معمولا از کلمات کلیدی کوتاه و به یاد ماندنی و منحصر به فرد تشکیل شده است.
|
|
||||||
repo_size=اندازه مخزن
|
repo_size=اندازه مخزن
|
||||||
template=قالب / الگو
|
template=قالب / الگو
|
||||||
template_select=انتخاب یک قالب/ الگو.
|
template_select=انتخاب یک قالب/ الگو.
|
||||||
@ -1992,6 +1992,7 @@ teams.all_repositories_read_permission_desc=این تیم دسترسی<strong>
|
|||||||
teams.all_repositories_write_permission_desc=این تیم دسترسی<strong> نوشتن </strong> <strong> مخازن همه</strong> را می بخشد: اعضا می توانند مخازن را مشاهده و درج کنند.
|
teams.all_repositories_write_permission_desc=این تیم دسترسی<strong> نوشتن </strong> <strong> مخازن همه</strong> را می بخشد: اعضا می توانند مخازن را مشاهده و درج کنند.
|
||||||
teams.all_repositories_admin_permission_desc=این تیم دسترسی<strong> مدیر </strong> به <strong> مخازن همه</strong> را می بخشد: اعضا می توانند مخازن را بخواند، همکار و مخزن اضافه کنند.
|
teams.all_repositories_admin_permission_desc=این تیم دسترسی<strong> مدیر </strong> به <strong> مخازن همه</strong> را می بخشد: اعضا می توانند مخازن را بخواند، همکار و مخزن اضافه کنند.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=پیشخوان
|
dashboard=پیشخوان
|
||||||
users=حساب کاربران
|
users=حساب کاربران
|
||||||
|
@ -635,7 +635,6 @@ visibility.private=Yksityinen
|
|||||||
owner=Omistaja
|
owner=Omistaja
|
||||||
owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska repojen maksimimäärää on rajoitettu.
|
owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska repojen maksimimäärää on rajoitettu.
|
||||||
repo_name=Repon nimi
|
repo_name=Repon nimi
|
||||||
repo_name_helper=Hyvä repon nimi on lyhyt, mieleenpainuva ja yksilöllinen.
|
|
||||||
repo_size=Repon koko
|
repo_size=Repon koko
|
||||||
template=Malli
|
template=Malli
|
||||||
template_select=Valitse malli.
|
template_select=Valitse malli.
|
||||||
@ -1361,6 +1360,7 @@ teams.repositories=Tiimin repot
|
|||||||
teams.members.none=Ei jäseniä tässä tiimissä.
|
teams.members.none=Ei jäseniä tässä tiimissä.
|
||||||
teams.all_repositories=Kaikki repot
|
teams.all_repositories=Kaikki repot
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Kojelauta
|
dashboard=Kojelauta
|
||||||
users=Käyttäjätilit
|
users=Käyttäjätilit
|
||||||
|
@ -649,6 +649,7 @@ joined_on=Inscrit le %s
|
|||||||
repositories=Dépôts
|
repositories=Dépôts
|
||||||
activity=Activité publique
|
activity=Activité publique
|
||||||
followers=abonnés
|
followers=abonnés
|
||||||
|
show_more=Voir plus
|
||||||
starred=Dépôts favoris
|
starred=Dépôts favoris
|
||||||
watched=Dépôts surveillés
|
watched=Dépôts surveillés
|
||||||
code=Code
|
code=Code
|
||||||
@ -1014,7 +1015,6 @@ new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l
|
|||||||
owner=Propriétaire
|
owner=Propriétaire
|
||||||
owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts.
|
owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts.
|
||||||
repo_name=Nom du dépôt
|
repo_name=Nom du dépôt
|
||||||
repo_name_helper=Idéalement, le nom d'un dépôt devrait être court, mémorisable et unique.
|
|
||||||
repo_size=Taille du dépôt
|
repo_size=Taille du dépôt
|
||||||
template=Modèle
|
template=Modèle
|
||||||
template_select=Répliquer un modèle
|
template_select=Répliquer un modèle
|
||||||
@ -2860,6 +2860,7 @@ teams.invite.title=Vous avez été invité à rejoindre l'équipe <strong>%s</st
|
|||||||
teams.invite.by=Invité par %s
|
teams.invite.by=Invité par %s
|
||||||
teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe.
|
teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
maintenance=Maintenance
|
maintenance=Maintenance
|
||||||
dashboard=Tableau de bord
|
dashboard=Tableau de bord
|
||||||
|
@ -649,6 +649,7 @@ joined_on=Cláraigh ar %s
|
|||||||
repositories=Stórais
|
repositories=Stórais
|
||||||
activity=Gníomhaíocht Phoiblí
|
activity=Gníomhaíocht Phoiblí
|
||||||
followers=Leantóirí
|
followers=Leantóirí
|
||||||
|
show_more=Taispeáin Tuilleadh
|
||||||
starred=Stórais Réaltaithe
|
starred=Stórais Réaltaithe
|
||||||
watched=Stórais Breathnaithe
|
watched=Stórais Breathnaithe
|
||||||
code=Cód
|
code=Cód
|
||||||
@ -1014,7 +1015,6 @@ new_repo_helper=Tá gach comhad tionscadail i stór, lena n-áirítear stair ath
|
|||||||
owner=Úinéir
|
owner=Úinéir
|
||||||
owner_helper=B'fhéidir nach dtaispeánfar roinnt eagraíochtaí sa anuas mar gheall ar theorainn uasta comhaireamh stórais.
|
owner_helper=B'fhéidir nach dtaispeánfar roinnt eagraíochtaí sa anuas mar gheall ar theorainn uasta comhaireamh stórais.
|
||||||
repo_name=Ainm Stórais
|
repo_name=Ainm Stórais
|
||||||
repo_name_helper=Úsáideann dea-ainmneacha stórtha eochairfhocail ghearr, i gcuimhne agus uathúla.
|
|
||||||
repo_size=Méid an Stóras
|
repo_size=Méid an Stóras
|
||||||
template=Teimpléad
|
template=Teimpléad
|
||||||
template_select=Roghnaigh teimpléad.
|
template_select=Roghnaigh teimpléad.
|
||||||
@ -1945,6 +1945,8 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo?
|
|||||||
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
|
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
|
||||||
|
|
||||||
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
|
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %[1]d tiomantas taobh thiar de %[2]s
|
||||||
|
pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %[1]d geallta taobh thiar de %[2]s
|
||||||
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
|
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
|
||||||
pulls.upstream_diverging_merge=Forc sionc
|
pulls.upstream_diverging_merge=Forc sionc
|
||||||
|
|
||||||
@ -2858,6 +2860,7 @@ teams.invite.title=Tugadh cuireadh duit dul isteach i bhfoireann <strong>%s</str
|
|||||||
teams.invite.by=Ar cuireadh ó %s
|
teams.invite.by=Ar cuireadh ó %s
|
||||||
teams.invite.description=Cliceáil ar an gcnaipe thíos le do thoil chun dul isteach san fhoireann.
|
teams.invite.description=Cliceáil ar an gcnaipe thíos le do thoil chun dul isteach san fhoireann.
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
maintenance=Cothabháil
|
maintenance=Cothabháil
|
||||||
dashboard=Deais
|
dashboard=Deais
|
||||||
@ -3719,6 +3722,7 @@ runners.status.active=Gníomhach
|
|||||||
runners.status.offline=As líne
|
runners.status.offline=As líne
|
||||||
runners.version=Leagan
|
runners.version=Leagan
|
||||||
runners.reset_registration_token=Athshocraigh comhartha clár
|
runners.reset_registration_token=Athshocraigh comhartha clár
|
||||||
|
runners.reset_registration_token_confirm=Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint?
|
||||||
runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit
|
runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit
|
||||||
|
|
||||||
runs.all_workflows=Gach Sreafaí Oibre
|
runs.all_workflows=Gach Sreafaí Oibre
|
||||||
@ -3770,6 +3774,8 @@ variables.creation.success=Tá an athróg "%s" curtha leis.
|
|||||||
variables.update.failed=Theip ar athróg a chur in eagar.
|
variables.update.failed=Theip ar athróg a chur in eagar.
|
||||||
variables.update.success=Tá an t-athróg curtha in eagar.
|
variables.update.success=Tá an t-athróg curtha in eagar.
|
||||||
|
|
||||||
|
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
|
||||||
|
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
deleted.display_name=Tionscadal scriosta
|
deleted.display_name=Tionscadal scriosta
|
||||||
|
@ -563,7 +563,6 @@ visibility.private=Privát
|
|||||||
[repo]
|
[repo]
|
||||||
owner=Tulajdonos
|
owner=Tulajdonos
|
||||||
repo_name=Tároló neve
|
repo_name=Tároló neve
|
||||||
repo_name_helper=A jó tárolónév általában rövid, megjegyezhető és egyedi kulcsszavakból tevődik össze.
|
|
||||||
repo_size=Repozitórium mérete
|
repo_size=Repozitórium mérete
|
||||||
template=Sablon
|
template=Sablon
|
||||||
template_select=Válasszon sablont.
|
template_select=Válasszon sablont.
|
||||||
@ -1229,6 +1228,7 @@ teams.members.none=Ennek a csapatnak nincsenek tagjai.
|
|||||||
teams.specific_repositories=Meghatározott tárolók
|
teams.specific_repositories=Meghatározott tárolók
|
||||||
teams.all_repositories=Minden tároló
|
teams.all_repositories=Minden tároló
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Műszerfal
|
dashboard=Műszerfal
|
||||||
users=Felhasználói fiókok
|
users=Felhasználói fiókok
|
||||||
|
@ -585,7 +585,6 @@ visibility.private=Pribadi
|
|||||||
[repo]
|
[repo]
|
||||||
owner=Pemilik
|
owner=Pemilik
|
||||||
repo_name=Nama Repositori
|
repo_name=Nama Repositori
|
||||||
repo_name_helper=Nama repositori yang baik menggunakan kata kunci yang pendek, unik, dan bisa diingat.
|
|
||||||
repo_size=Ukuran Repositori
|
repo_size=Ukuran Repositori
|
||||||
template=Templat
|
template=Templat
|
||||||
template_select=Pilih template.
|
template_select=Pilih template.
|
||||||
@ -1083,6 +1082,7 @@ teams.add_team_member=Tambahkan Anggota Tim
|
|||||||
teams.delete_team_success=Tim sudah di hapus.
|
teams.delete_team_success=Tim sudah di hapus.
|
||||||
teams.repositories=Tim repositori
|
teams.repositories=Tim repositori
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
dashboard=Dasbor
|
dashboard=Dasbor
|
||||||
organizations=Organisasi
|
organizations=Organisasi
|
||||||
|
@ -1136,6 +1136,7 @@ teams.settings=Stillingar
|
|||||||
teams.update_settings=Uppfæra Stillingar
|
teams.update_settings=Uppfæra Stillingar
|
||||||
teams.all_repositories=Öll hugbúnaðarsöfn
|
teams.all_repositories=Öll hugbúnaðarsöfn
|
||||||
|
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
repositories=Hugbúnaðarsöfn
|
repositories=Hugbúnaðarsöfn
|
||||||
config=Stilling
|
config=Stilling
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user