mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into lunny/add_comment_move_issue_column
This commit is contained in:
commit
1c478e6e8b
|
@ -3,6 +3,7 @@ reportUnusedDisableDirectives: true
|
|||
|
||||
ignorePatterns:
|
||||
- /web_src/js/vendor
|
||||
- /web_src/fomantic
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
|
@ -536,7 +537,7 @@ rules:
|
|||
no-underscore-dangle: [0]
|
||||
no-unexpected-multiline: [2]
|
||||
no-unmodified-loop-condition: [2]
|
||||
no-unneeded-ternary: [0]
|
||||
no-unneeded-ternary: [2]
|
||||
no-unreachable-loop: [2]
|
||||
no-unreachable: [2]
|
||||
no-unsafe-finally: [2]
|
||||
|
@ -715,12 +716,14 @@ rules:
|
|||
unicorn/import-style: [0]
|
||||
unicorn/new-for-builtins: [2]
|
||||
unicorn/no-abusive-eslint-disable: [0]
|
||||
unicorn/no-anonymous-default-export: [0]
|
||||
unicorn/no-array-callback-reference: [0]
|
||||
unicorn/no-array-for-each: [2]
|
||||
unicorn/no-array-method-this-argument: [2]
|
||||
unicorn/no-array-push-push: [2]
|
||||
unicorn/no-array-reduce: [2]
|
||||
unicorn/no-await-expression-member: [0]
|
||||
unicorn/no-await-in-promise-methods: [2]
|
||||
unicorn/no-console-spaces: [0]
|
||||
unicorn/no-document-cookie: [2]
|
||||
unicorn/no-empty-file: [2]
|
||||
|
@ -737,6 +740,7 @@ rules:
|
|||
unicorn/no-null: [0]
|
||||
unicorn/no-object-as-default-parameter: [0]
|
||||
unicorn/no-process-exit: [0]
|
||||
unicorn/no-single-promise-in-promise-methods: [2]
|
||||
unicorn/no-static-only-class: [2]
|
||||
unicorn/no-thenable: [2]
|
||||
unicorn/no-this-assignment: [2]
|
||||
|
|
|
@ -4,13 +4,6 @@ modifies/docs:
|
|||
- "**/*.md"
|
||||
- "docs/**"
|
||||
|
||||
modifies/frontend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "web_src/**"
|
||||
- "tailwind.config.js"
|
||||
- "webpack.config.js"
|
||||
|
||||
modifies/templates:
|
||||
- changed-files:
|
||||
- all-globs-to-any-file:
|
||||
|
|
4
.ignore
4
.ignore
|
@ -4,6 +4,8 @@
|
|||
/modules/options/bindata.go
|
||||
/modules/public/bindata.go
|
||||
/modules/templates/bindata.go
|
||||
/vendor
|
||||
/options/gitignore
|
||||
/options/license
|
||||
/public/assets
|
||||
/vendor
|
||||
node_modules
|
||||
|
|
296
cmd/dump.go
296
cmd/dump.go
|
@ -6,14 +6,13 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/dump"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -25,89 +24,17 @@ import (
|
|||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
|
||||
if verbose {
|
||||
log.Info("Adding file %s", customName)
|
||||
}
|
||||
|
||||
return w.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: customName,
|
||||
},
|
||||
ReadCloser: r,
|
||||
})
|
||||
}
|
||||
|
||||
func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
|
||||
file, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, file, fileInfo, filePath, verbose)
|
||||
}
|
||||
|
||||
func isSubdir(upper, lower string) (bool, error) {
|
||||
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
||||
return false, err
|
||||
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type outputType struct {
|
||||
Enum []string
|
||||
Default string
|
||||
selected string
|
||||
}
|
||||
|
||||
func (o outputType) Join() string {
|
||||
return strings.Join(o.Enum, ", ")
|
||||
}
|
||||
|
||||
func (o *outputType) Set(value string) error {
|
||||
for _, enum := range o.Enum {
|
||||
if enum == value {
|
||||
o.selected = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("allowed values are %s", o.Join())
|
||||
}
|
||||
|
||||
func (o outputType) String() string {
|
||||
if o.selected == "" {
|
||||
return o.Default
|
||||
}
|
||||
return o.selected
|
||||
}
|
||||
|
||||
var outputTypeEnum = &outputType{
|
||||
Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
|
||||
Default: "zip",
|
||||
}
|
||||
|
||||
// CmdDump represents the available dump sub-command.
|
||||
var CmdDump = &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
Description: `Dump compresses all related files and database into zip file.
|
||||
It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Action: runDump,
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
|
||||
Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
|
||||
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
|
@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
|
|||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.GenericFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Value: outputTypeEnum,
|
||||
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
|
||||
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
log.Fatal(format, args...)
|
||||
}
|
||||
|
||||
func runDump(ctx *cli.Context) error {
|
||||
var file *os.File
|
||||
fileName := ctx.String("file")
|
||||
outType := ctx.String("type")
|
||||
if fileName == "-" {
|
||||
file = os.Stdout
|
||||
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
|
||||
} else {
|
||||
for _, suffix := range outputTypeEnum.Enum {
|
||||
if strings.HasSuffix(fileName, "."+suffix) {
|
||||
fileName = strings.TrimSuffix(fileName, "."+suffix)
|
||||
break
|
||||
}
|
||||
}
|
||||
fileName += "." + outType
|
||||
}
|
||||
setting.MustInstalled()
|
||||
|
||||
// make sure we are logging to the console no matter what the configuration tells us do to
|
||||
// FIXME: don't use CfgProvider directly
|
||||
if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil {
|
||||
fatal("Setting logging mode to console failed: %v", err)
|
||||
}
|
||||
if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
|
||||
fatal("Setting console logger to stderr failed: %v", err)
|
||||
}
|
||||
|
||||
// Set loglevel to Warn if quiet-mode is requested
|
||||
if ctx.Bool("quiet") {
|
||||
if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil {
|
||||
fatal("Setting console log-level failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !setting.InstallLock {
|
||||
log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
|
||||
return fmt.Errorf("gitea is not initialized")
|
||||
}
|
||||
setting.LoadSettings() // cannot access session settings otherwise
|
||||
|
||||
quite := ctx.Bool("quiet")
|
||||
verbose := ctx.Bool("verbose")
|
||||
if verbose && ctx.Bool("quiet") {
|
||||
return fmt.Errorf("--quiet and --verbose cannot both be set")
|
||||
if verbose && quite {
|
||||
fatal("Option --quiet and --verbose cannot both be set")
|
||||
}
|
||||
|
||||
// outFileName is either "-" or a file name (will be made absolute)
|
||||
outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
|
||||
if outType == "" {
|
||||
fatal("Invalid output type")
|
||||
}
|
||||
|
||||
outFile := os.Stdout
|
||||
if outFileName != "-" {
|
||||
var err error
|
||||
if outFileName, err = filepath.Abs(outFileName); err != nil {
|
||||
fatal("Unable to get absolute path of dump file: %v", err)
|
||||
}
|
||||
if exist, _ := util.IsExist(outFileName); exist {
|
||||
fatal("Dump file %q exists", outFileName)
|
||||
}
|
||||
if outFile, err = os.Create(outFileName); err != nil {
|
||||
fatal("Unable to create dump file %q: %v", outFileName, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
}
|
||||
|
||||
setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
|
||||
|
||||
setting.DisableLoggerInit()
|
||||
setting.LoadSettings() // cannot access session settings otherwise
|
||||
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
|
@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
if err = storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
file, err = os.Create(fileName)
|
||||
if err != nil {
|
||||
fatal("Unable to open %s: %v", fileName, err)
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
absFileName, err := filepath.Abs(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var iface any
|
||||
if fileName == "-" {
|
||||
iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
|
||||
} else {
|
||||
iface, err = archiver.ByExtension(fileName)
|
||||
}
|
||||
archiverGeneric, err := archiver.ByExtension("." + outType)
|
||||
if err != nil {
|
||||
fatal("Unable to get archiver for extension: %v", err)
|
||||
}
|
||||
|
||||
w, _ := iface.(archiver.Writer)
|
||||
if err := w.Create(file); err != nil {
|
||||
archiverWriter := archiverGeneric.(archiver.Writer)
|
||||
if err := archiverWriter.Create(outFile); err != nil {
|
||||
fatal("Creating archiver.Writer failed: %v", err)
|
||||
}
|
||||
defer w.Close()
|
||||
defer archiverWriter.Close()
|
||||
|
||||
dumper := &dump.Dumper{
|
||||
Writer: archiverWriter,
|
||||
Verbose: verbose,
|
||||
}
|
||||
dumper.GlobalExcludeAbsPath(outFileName)
|
||||
|
||||
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
|
||||
log.Info("Skip dumping local repositories")
|
||||
} else {
|
||||
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
|
||||
if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
|
||||
fatal("Failed to include repositories: %v", err)
|
||||
}
|
||||
|
||||
|
@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump LFS objects: %v", err)
|
||||
}
|
||||
|
@ -310,15 +212,13 @@ func runDump(ctx *cli.Context) error {
|
|||
fatal("Failed to dump database: %v", err)
|
||||
}
|
||||
|
||||
if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
|
||||
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
|
||||
fatal("Failed to include gitea-db.sql: %v", err)
|
||||
}
|
||||
|
||||
if len(setting.CustomConf) > 0 {
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
|
||||
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
||||
|
@ -326,8 +226,8 @@ func runDump(ctx *cli.Context) error {
|
|||
} else {
|
||||
customDir, err := os.Stat(setting.CustomPath)
|
||||
if err == nil && customDir.IsDir() {
|
||||
if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||
if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
|
||||
if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||
if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
|
||||
fatal("Failed to include custom: %v", err)
|
||||
}
|
||||
} else {
|
||||
|
@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error {
|
|||
excludes = append(excludes, setting.Attachment.Storage.Path)
|
||||
excludes = append(excludes, setting.Packages.Storage.Path)
|
||||
excludes = append(excludes, setting.Log.RootPath)
|
||||
excludes = append(excludes, absFileName)
|
||||
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
|
||||
fatal("Failed to include data directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump attachments: %v", err)
|
||||
}
|
||||
|
@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump packages: %v", err)
|
||||
}
|
||||
|
@ -409,80 +306,23 @@ func runDump(ctx *cli.Context) error {
|
|||
log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
|
||||
}
|
||||
if isExist {
|
||||
if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
|
||||
fatal("Failed to include log: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileName != "-" {
|
||||
if err = w.Close(); err != nil {
|
||||
_ = util.Remove(fileName)
|
||||
fatal("Failed to save %s: %v", fileName, err)
|
||||
if outFileName == "-" {
|
||||
log.Info("Finish dumping to stdout")
|
||||
} else {
|
||||
if err = archiverWriter.Close(); err != nil {
|
||||
_ = os.Remove(outFileName)
|
||||
fatal("Failed to save %q: %v", outFileName, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(fileName, 0o600); err != nil {
|
||||
if err = os.Chmod(outFileName, 0o600); err != nil {
|
||||
log.Info("Can't change file access permissions mask to 0600: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if fileName != "-" {
|
||||
log.Info("Finish dumping in file %s", fileName)
|
||||
} else {
|
||||
log.Info("Finish dumping to stdout")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
|
||||
func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
|
||||
absPath, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
currentAbsPath := filepath.Join(absPath, file.Name())
|
||||
currentInsidePath := path.Join(insidePath, file.Name())
|
||||
if file.IsDir() {
|
||||
if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
|
||||
if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
||||
shouldAdd := file.Mode().IsRegular()
|
||||
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
target, err := filepath.EvalSymlinks(currentAbsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetStat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldAdd = targetStat.Mode().IsRegular()
|
||||
}
|
||||
if shouldAdd {
|
||||
if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("Finish dumping in file %s", outFileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ func showWebStartupMessage(msg string) {
|
|||
log.Info("* WorkPath: %s", setting.AppWorkPath)
|
||||
log.Info("* CustomPath: %s", setting.CustomPath)
|
||||
log.Info("* ConfigFile: %s", setting.CustomConf)
|
||||
log.Info("%s", msg)
|
||||
log.Info("%s", msg) // show startup message
|
||||
}
|
||||
|
||||
func serveInstall(ctx *cli.Context) error {
|
||||
|
|
|
@ -2315,6 +2315,8 @@ LEVEL = Info
|
|||
;SHOW_FOOTER_VERSION = true
|
||||
;; Show template execution time in the footer
|
||||
;SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
|
||||
;; Show the "powered by" text in the footer
|
||||
;SHOW_FOOTER_POWERED_BY = true
|
||||
;; Generate sitemap. Defaults to `true`.
|
||||
;ENABLE_SITEMAP = true
|
||||
;; Enable/Disable RSS/Atom feed
|
||||
|
|
|
@ -1429,5 +1429,6 @@ Like `uses: https://gitea.com/actions/checkout@v4` or `uses: http://your-git-ser
|
|||
|
||||
- `SHOW_FOOTER_VERSION`: **true**: Show Gitea and Go version information in the footer.
|
||||
- `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: Show time of template execution in the footer.
|
||||
- `SHOW_FOOTER_POWERED_BY`: **true**: Show the "powered by" text in the footer.
|
||||
- `ENABLE_SITEMAP`: **true**: Generate sitemap.
|
||||
- `ENABLE_FEED`: **true**: Enable/Disable RSS/Atom feed.
|
||||
|
|
|
@ -1353,5 +1353,6 @@ PROXY_HOSTS = *.github.com
|
|||
|
||||
- `SHOW_FOOTER_VERSION`: **true**: 在页面底部显示Gitea的版本。
|
||||
- `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: 在页脚显示模板执行的时间。
|
||||
- `SHOW_FOOTER_POWERED_BY`: **true**: 在页脚显示“由...提供动力”的文本。
|
||||
- `ENABLE_SITEMAP`: **true**: 生成sitemap.
|
||||
- `ENABLE_FEED`: **true**: 是否启用RSS/Atom
|
||||
|
|
|
@ -304,7 +304,8 @@ services:
|
|||
- GITEA__mailer__ENABLED=true
|
||||
- GITEA__mailer__FROM=${GITEA__mailer__FROM:?GITEA__mailer__FROM not set}
|
||||
- GITEA__mailer__PROTOCOL=smtps
|
||||
- GITEA__mailer__HOST=${GITEA__mailer__HOST:?GITEA__mailer__HOST not set}
|
||||
- GITEA__mailer__SMTP_ADDR=${GITEA__mailer__SMTP_ADDR:?GITEA__mailer__SMTP_ADDR not set}
|
||||
- GITEA__mailer__SMTP_PORT=${GITEA__mailer__SMTP_PORT:?GITEA__mailer__SMTP_PORT not set}
|
||||
- GITEA__mailer__USER=${GITEA__mailer__USER:-apikey}
|
||||
- GITEA__mailer__PASSWD="""${GITEA__mailer__PASSWD:?GITEA__mailer__PASSWD not set}"""
|
||||
```
|
||||
|
@ -545,7 +546,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in
|
|||
```bash
|
||||
cat <<"EOF" | sudo tee /home/git/docker-shell
|
||||
#!/bin/sh
|
||||
/usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
|
||||
/usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
|
||||
EOF
|
||||
sudo chmod +x /home/git/docker-shell
|
||||
sudo usermod -s /home/git/docker-shell git
|
||||
|
@ -560,7 +561,7 @@ Add the following block to `/etc/ssh/sshd_config`, on the host:
|
|||
```bash
|
||||
Match User git
|
||||
AuthorizedKeysCommandUser git
|
||||
AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
|
||||
AuthorizedKeysCommand /usr/bin/docker exec -i -u git gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
|
||||
```
|
||||
|
||||
(From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.)
|
||||
|
|
6
go.mod
6
go.mod
|
@ -105,11 +105,11 @@ require (
|
|||
github.com/yuin/goldmark v1.7.0
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/tools v0.19.0
|
||||
google.golang.org/grpc v1.62.1
|
||||
|
|
16
go.sum
16
go.sum
|
@ -846,8 +846,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
|
|||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
|
@ -881,8 +881,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -932,8 +932,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
|
@ -943,8 +943,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
|||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
|
@ -139,13 +139,7 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
|
|||
}
|
||||
}
|
||||
|
||||
keyID := ""
|
||||
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
|
||||
keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
|
||||
}
|
||||
if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
|
||||
keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
|
||||
}
|
||||
keyID := tryGetKeyIDFromSignature(sig)
|
||||
defaultReason := NoKeyFound
|
||||
|
||||
// First check if the sig has a keyID and if so just look at that
|
||||
|
|
|
@ -134,3 +134,13 @@ func extractSignature(s string) (*packet.Signature, error) {
|
|||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func tryGetKeyIDFromSignature(sig *packet.Signature) string {
|
||||
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
|
||||
return fmt.Sprintf("%016X", *sig.IssuerKeyId)
|
||||
}
|
||||
if sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
|
||||
return fmt.Sprintf("%016X", sig.IssuerFingerprint[12:20])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -391,3 +393,13 @@ epiDVQ==
|
|||
assert.Equal(t, time.Unix(1586105389, 0), expire)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryGetKeyIDFromSignature(t *testing.T) {
|
||||
assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{}))
|
||||
assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
|
||||
IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)),
|
||||
}))
|
||||
assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{
|
||||
IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -76,23 +76,14 @@ func calcFingerprintNative(publicKeyContent string) (string, error) {
|
|||
// CalcFingerprint calculate public key's fingerprint
|
||||
func CalcFingerprint(publicKeyContent string) (string, error) {
|
||||
// Call the method based on configuration
|
||||
var (
|
||||
fnName, fp string
|
||||
err error
|
||||
)
|
||||
if len(setting.SSH.KeygenPath) == 0 {
|
||||
fnName = "calcFingerprintNative"
|
||||
fp, err = calcFingerprintNative(publicKeyContent)
|
||||
} else {
|
||||
fnName = "calcFingerprintSSHKeygen"
|
||||
fp, err = calcFingerprintSSHKeygen(publicKeyContent)
|
||||
}
|
||||
useNative := setting.SSH.KeygenPath == ""
|
||||
calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen)
|
||||
fp, err := calcFn(publicKeyContent)
|
||||
if err != nil {
|
||||
if IsErrKeyUnableVerify(err) {
|
||||
log.Info("%s", publicKeyContent)
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("%s: %w", fnName, err)
|
||||
return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err)
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
|
|
@ -10,21 +10,21 @@ import (
|
|||
)
|
||||
|
||||
// CountOrphanedObjects count subjects with have no existing refobject anymore
|
||||
func CountOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) (int64, error) {
|
||||
func CountOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) (int64, error) {
|
||||
return GetEngine(ctx).
|
||||
Table("`"+subject+"`").
|
||||
Join("LEFT", "`"+refobject+"`", joinCond).
|
||||
Where(builder.IsNull{"`" + refobject + "`.id"}).
|
||||
Join("LEFT", "`"+refObject+"`", joinCond).
|
||||
Where(builder.IsNull{"`" + refObject + "`.id"}).
|
||||
Select("COUNT(`" + subject + "`.`id`)").
|
||||
Count()
|
||||
}
|
||||
|
||||
// DeleteOrphanedObjects delete subjects with have no existing refobject anymore
|
||||
func DeleteOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) error {
|
||||
func DeleteOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) error {
|
||||
subQuery := builder.Select("`"+subject+"`.id").
|
||||
From("`"+subject+"`").
|
||||
Join("LEFT", "`"+refobject+"`", joinCond).
|
||||
Where(builder.IsNull{"`" + refobject + "`.id"})
|
||||
Join("LEFT", "`"+refObject+"`", joinCond).
|
||||
Where(builder.IsNull{"`" + refObject + "`.id"})
|
||||
b := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`")
|
||||
_, err := GetEngine(ctx).Exec(b)
|
||||
return err
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
-
|
||||
id: 1
|
||||
project_id: 1
|
||||
issue_id: 1
|
||||
|
||||
-
|
||||
id: 2
|
||||
project_id: 1
|
||||
issue_id: 1
|
|
@ -577,6 +577,8 @@ var migrations = []Migration{
|
|||
// Gitea 1.22.0 ends at 294
|
||||
|
||||
// v294 -> v295
|
||||
NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
|
||||
// v295 -> v296
|
||||
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
base.MainTest(m)
|
||||
}
|
|
@ -1,22 +1,53 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
|
||||
type CommentMetaData struct {
|
||||
ProjectColumnID int64 `json:"project_column_id"`
|
||||
ProjectColumnName string `json:"project_column_name"`
|
||||
ProjectName string `json:"project_name"`
|
||||
}
|
||||
|
||||
func AddCommentMetaDataColumn(x *xorm.Engine) error {
|
||||
type Comment struct {
|
||||
CommentMetaData CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
|
||||
// AddUniqueIndexForProjectIssue adds unique indexes for project issue table
|
||||
func AddUniqueIndexForProjectIssue(x *xorm.Engine) error {
|
||||
// remove possible duplicated records in table project_issue
|
||||
type result struct {
|
||||
IssueID int64
|
||||
ProjectID int64
|
||||
Cnt int
|
||||
}
|
||||
var results []result
|
||||
if err := x.Select("issue_id, project_id, count(*) as cnt").
|
||||
Table("project_issue").
|
||||
GroupBy("issue_id, project_id").
|
||||
Having("count(*) > 1").
|
||||
Find(&results); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range results {
|
||||
if x.Dialect().URI().DBType == schemas.MSSQL {
|
||||
if _, err := x.Exec(fmt.Sprintf("delete from project_issue where id in (SELECT top %d id FROM project_issue WHERE issue_id = ? and project_id = ?)", r.Cnt-1), r.IssueID, r.ProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var ids []int64
|
||||
if err := x.SQL("SELECT id FROM project_issue WHERE issue_id = ? and project_id = ? limit ?", r.IssueID, r.ProjectID, r.Cnt-1).Find(&ids); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := x.Table("project_issue").In("id", ids).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x.Sync(new(Comment))
|
||||
// add unique index for project_issue table
|
||||
type ProjectIssue struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX unique(s)"`
|
||||
ProjectID int64 `xorm:"INDEX unique(s)"`
|
||||
}
|
||||
|
||||
return x.Sync(new(ProjectIssue))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func Test_AddUniqueIndexForProjectIssue(t *testing.T) {
|
||||
type ProjectIssue struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
cnt, err := x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, cnt)
|
||||
|
||||
assert.NoError(t, AddUniqueIndexForProjectIssue(x))
|
||||
|
||||
cnt, err = x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, cnt)
|
||||
|
||||
tables, err := x.DBMetas()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, len(tables))
|
||||
found := false
|
||||
for _, index := range tables[0].Indexes {
|
||||
if index.Type == schemas.UniqueType {
|
||||
found = true
|
||||
slices.Equal(index.Cols, []string{"project_id", "issue_id"})
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
|
||||
type CommentMetaData struct {
|
||||
ProjectColumnID int64 `json:"project_column_id"`
|
||||
ProjectColumnName string `json:"project_column_name"`
|
||||
ProjectTitle string `json:"project_title"`
|
||||
}
|
||||
|
||||
func AddCommentMetaDataColumn(x *xorm.Engine) error {
|
||||
type Comment struct {
|
||||
CommentMetaData CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
|
||||
}
|
||||
|
||||
return x.Sync(new(Comment))
|
||||
}
|
|
@ -53,7 +53,7 @@ func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
|
|||
var u *RepoUnit
|
||||
var err error
|
||||
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
|
||||
log.Trace("%s", err)
|
||||
log.Trace("IsDependenciesEnabled: %v", err)
|
||||
return setting.Service.DefaultEnableDependencies
|
||||
}
|
||||
return u.IssuesConfig().EnableDependencies
|
||||
|
|
|
@ -256,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) {
|
|||
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
|
||||
}
|
||||
|
||||
// DeleteInactiveEmailAddresses deletes inactive email addresses
|
||||
func DeleteInactiveEmailAddresses(ctx context.Context) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("is_activated = ?", false).
|
||||
Delete(new(EmailAddress))
|
||||
return err
|
||||
}
|
||||
|
||||
// ActivateEmail activates the email address to given user.
|
||||
func ActivateEmail(ctx context.Context, email *EmailAddress) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package charset
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -156,13 +157,16 @@ func TestEscapeControlReader(t *testing.T) {
|
|||
tests = append(tests, test)
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := &strings.Builder{}
|
||||
status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.status, *status)
|
||||
assert.Equal(t, tt.result, output.String())
|
||||
outStr := output.String()
|
||||
outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character")
|
||||
assert.Equal(t, tt.result, outStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -561,14 +561,14 @@ func TestFormatError(t *testing.T) {
|
|||
err: &csv.ParseError{
|
||||
Err: csv.ErrFieldCount,
|
||||
},
|
||||
expectedMessage: "repo.error.csv.invalid_field_count",
|
||||
expectedMessage: "repo.error.csv.invalid_field_count:0",
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
err: &csv.ParseError{
|
||||
Err: csv.ErrBareQuote,
|
||||
},
|
||||
expectedMessage: "repo.error.csv.unexpected",
|
||||
expectedMessage: "repo.error.csv.unexpected:0,0",
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
||||
var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
|
||||
|
||||
// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
|
||||
func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
|
||||
if argFile == "" && argType == "" {
|
||||
outType = SupportedOutputTypes[0]
|
||||
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||
} else if argFile == "" {
|
||||
outType = argType
|
||||
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||
} else if argType == "" {
|
||||
if filepath.Ext(outFileName) == "" {
|
||||
outType = SupportedOutputTypes[0]
|
||||
outFileName = argFile
|
||||
} else {
|
||||
for _, t := range SupportedOutputTypes {
|
||||
if strings.HasSuffix(argFile, "."+t) {
|
||||
outFileName = argFile
|
||||
outType = t
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outFileName, outType = argFile, argType
|
||||
}
|
||||
if !slices.Contains(SupportedOutputTypes, outType) {
|
||||
return "", ""
|
||||
}
|
||||
return outFileName, outType
|
||||
}
|
||||
|
||||
func IsSubdir(upper, lower string) (bool, error) {
|
||||
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
||||
return false, err
|
||||
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type Dumper struct {
|
||||
Writer archiver.Writer
|
||||
Verbose bool
|
||||
|
||||
globalExcludeAbsPaths []string
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
|
||||
if dumper.Verbose {
|
||||
log.Info("Adding file %s", customName)
|
||||
}
|
||||
|
||||
return dumper.Writer.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: customName,
|
||||
},
|
||||
ReadCloser: r,
|
||||
})
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddFile(filePath, absPath string) error {
|
||||
file, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dumper.AddReader(file, fileInfo, filePath)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) normalizeFilePath(absPath string) string {
|
||||
absPath = filepath.Clean(absPath)
|
||||
if setting.IsWindows {
|
||||
absPath = strings.ToLower(absPath)
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
|
||||
for _, absPath := range absPaths {
|
||||
dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
|
||||
}
|
||||
}
|
||||
|
||||
func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
|
||||
norm := dumper.normalizeFilePath(absPath)
|
||||
return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
|
||||
excludes = slices.Clone(excludes)
|
||||
for i := range excludes {
|
||||
excludes[i] = dumper.normalizeFilePath(excludes[i])
|
||||
}
|
||||
return dumper.addFileOrDir(insidePath, absPath, excludes)
|
||||
}
|
||||
|
||||
func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
|
||||
absPath, err := filepath.Abs(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, err := dir.Readdir(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
currentAbsPath := filepath.Join(absPath, file.Name())
|
||||
if dumper.shouldExclude(currentAbsPath, excludes) {
|
||||
continue
|
||||
}
|
||||
|
||||
currentInsidePath := path.Join(insidePath, file.Name())
|
||||
if file.IsDir() {
|
||||
if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
||||
shouldAdd := file.Mode().IsRegular()
|
||||
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
target, err := filepath.EvalSymlinks(currentAbsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetStat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldAdd = targetStat.Mode().IsRegular()
|
||||
}
|
||||
if shouldAdd {
|
||||
if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrepareFileNameAndType(t *testing.T) {
|
||||
defer timeutil.MockSet(time.Unix(1234, 0))()
|
||||
test := func(argFile, argType, expFile, expType string) {
|
||||
outFile, outType := PrepareFileNameAndType(argFile, argType)
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
|
||||
fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
|
||||
fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
|
||||
)
|
||||
}
|
||||
|
||||
test("", "", "gitea-dump-1234.zip", "zip")
|
||||
test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz")
|
||||
test("", "no-such", "", "")
|
||||
|
||||
test("-", "", "-", "zip")
|
||||
test("-", "tar.gz", "-", "tar.gz")
|
||||
test("-", "no-such", "", "")
|
||||
|
||||
test("a", "", "a", "zip")
|
||||
test("a", "tar.gz", "a", "tar.gz")
|
||||
test("a", "no-such", "", "")
|
||||
|
||||
test("a.zip", "", "a.zip", "zip")
|
||||
test("a.zip", "tar.gz", "a.zip", "tar.gz")
|
||||
test("a.zip", "no-such", "", "")
|
||||
|
||||
test("a.tar.gz", "", "a.tar.gz", "zip")
|
||||
test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz")
|
||||
test("a.tar.gz", "no-such", "", "")
|
||||
}
|
||||
|
||||
func TestIsSubDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||
|
||||
isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isSub)
|
||||
|
||||
isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isSub)
|
||||
|
||||
isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include"))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isSub)
|
||||
}
|
||||
|
||||
type testWriter struct {
|
||||
added []string
|
||||
}
|
||||
|
||||
func (t *testWriter) Create(out io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testWriter) Write(f archiver.File) error {
|
||||
t.added = append(t.added, f.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDumper(t *testing.T) {
|
||||
sortStrings := func(s []string) []string {
|
||||
sort.Strings(s)
|
||||
return s
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644)
|
||||
|
||||
tw := &testWriter{}
|
||||
d := &Dumper{Writer: tw}
|
||||
d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1"))
|
||||
err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added))
|
||||
|
||||
tw = &testWriter{}
|
||||
d = &Dumper{Writer: tw}
|
||||
err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added))
|
||||
}
|
|
@ -26,14 +26,14 @@ type Commit struct {
|
|||
Author *Signature
|
||||
Committer *Signature
|
||||
CommitMessage string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *CommitSignature
|
||||
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache
|
||||
}
|
||||
|
||||
// CommitGPGSignature represents a git commit signature part.
|
||||
type CommitGPGSignature struct {
|
||||
// CommitSignature represents a git commit signature part.
|
||||
type CommitSignature struct {
|
||||
Signature string
|
||||
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
func convertPGPSignature(c *object.Commit) *CommitSignature {
|
||||
if c.PGPSignature == "" {
|
||||
return nil
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
|||
return nil
|
||||
}
|
||||
|
||||
return &CommitGPGSignature{
|
||||
return &CommitSignature{
|
||||
Signature: c.PGPSignature,
|
||||
Payload: w.String(),
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ readLoop:
|
|||
}
|
||||
}
|
||||
commit.CommitMessage = messageSB.String()
|
||||
commit.Signature = &CommitGPGSignature{
|
||||
commit.Signature = &CommitSignature{
|
||||
Signature: signatureSB.String(),
|
||||
Payload: payloadSB.String(),
|
||||
}
|
||||
|
|
|
@ -185,17 +185,15 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
|
|||
|
||||
tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
|
||||
tag.Message = ref["contents"]
|
||||
// strip PGP signature if present in contents field
|
||||
pgpStart := strings.Index(tag.Message, beginpgp)
|
||||
if pgpStart >= 0 {
|
||||
tag.Message = tag.Message[0:pgpStart]
|
||||
}
|
||||
|
||||
// strip any signature if present in contents field
|
||||
_, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0)
|
||||
|
||||
// annotated tag with GPG signature
|
||||
if tag.Type == "tag" && ref["contents:signature"] != "" {
|
||||
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
|
||||
tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
|
||||
tag.Signature = &CommitGPGSignature{
|
||||
tag.Signature = &CommitSignature{
|
||||
Signature: ref["contents:signature"],
|
||||
Payload: payload,
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ qbHDASXl
|
|||
Type: "tag",
|
||||
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
|
||||
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
|
||||
Signature: &CommitGPGSignature{
|
||||
Signature: &CommitSignature{
|
||||
Signature: `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3
|
||||
|
|
|
@ -6,16 +6,10 @@ package git
|
|||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
|
||||
endpgp = "\n-----END PGP SIGNATURE-----"
|
||||
)
|
||||
|
||||
// Tag represents a Git tag.
|
||||
type Tag struct {
|
||||
Name string
|
||||
|
@ -24,7 +18,7 @@ type Tag struct {
|
|||
Type string
|
||||
Tagger *Signature
|
||||
Message string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *CommitSignature
|
||||
}
|
||||
|
||||
// Commit return the commit of the tag reference
|
||||
|
@ -32,6 +26,36 @@ func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) {
|
|||
return gitRepo.getCommit(tag.Object)
|
||||
}
|
||||
|
||||
func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) {
|
||||
pos := messageStart
|
||||
signStart, signEnd := -1, -1
|
||||
for {
|
||||
eol := bytes.IndexByte(data[pos:], '\n')
|
||||
if eol < 0 {
|
||||
break
|
||||
}
|
||||
line := data[pos : pos+eol]
|
||||
signType, hasPrefix := bytes.CutPrefix(line, []byte("-----BEGIN "))
|
||||
signType, hasSuffix := bytes.CutSuffix(signType, []byte(" SIGNATURE-----"))
|
||||
if hasPrefix && hasSuffix {
|
||||
signEndBytes := append([]byte("\n-----END "), signType...)
|
||||
signEndBytes = append(signEndBytes, []byte(" SIGNATURE-----")...)
|
||||
signEnd = bytes.Index(data[pos:], signEndBytes)
|
||||
if signEnd != -1 {
|
||||
signStart = pos
|
||||
signEnd = pos + signEnd + len(signEndBytes)
|
||||
}
|
||||
}
|
||||
pos += eol + 1
|
||||
}
|
||||
|
||||
if signStart != -1 && signEnd != -1 {
|
||||
msgEnd := max(messageStart, signStart-1)
|
||||
return string(data[:msgEnd]), string(data[messageStart:msgEnd]), string(data[signStart:signEnd])
|
||||
}
|
||||
return string(data), string(data[messageStart:]), ""
|
||||
}
|
||||
|
||||
// Parse commit information from the (uncompressed) raw
|
||||
// data from the commit object.
|
||||
// \n\n separate headers from message
|
||||
|
@ -40,47 +64,37 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) {
|
|||
tag.ID = objectFormat.EmptyObjectID()
|
||||
tag.Object = objectFormat.EmptyObjectID()
|
||||
tag.Tagger = &Signature{}
|
||||
// we now have the contents of the commit object. Let's investigate...
|
||||
nextline := 0
|
||||
l:
|
||||
|
||||
pos := 0
|
||||
for {
|
||||
eol := bytes.IndexByte(data[nextline:], '\n')
|
||||
switch {
|
||||
case eol > 0:
|
||||
line := data[nextline : nextline+eol]
|
||||
spacepos := bytes.IndexByte(line, ' ')
|
||||
reftype := line[:spacepos]
|
||||
switch string(reftype) {
|
||||
case "object":
|
||||
id, err := NewIDFromString(string(line[spacepos+1:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag.Object = id
|
||||
case "type":
|
||||
// A commit can have one or more parents
|
||||
tag.Type = string(line[spacepos+1:])
|
||||
case "tagger":
|
||||
tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:]))
|
||||
}
|
||||
nextline += eol + 1
|
||||
case eol == 0:
|
||||
tag.Message = string(data[nextline+1:])
|
||||
break l
|
||||
default:
|
||||
break l
|
||||
eol := bytes.IndexByte(data[pos:], '\n')
|
||||
if eol == -1 {
|
||||
break // shouldn't happen, but could just tolerate it
|
||||
}
|
||||
if eol == 0 {
|
||||
pos++
|
||||
break // end of headers
|
||||
}
|
||||
line := data[pos : pos+eol]
|
||||
key, val, _ := bytes.Cut(line, []byte(" "))
|
||||
switch string(key) {
|
||||
case "object":
|
||||
id, err := NewIDFromString(string(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag.Object = id
|
||||
case "type":
|
||||
tag.Type = string(val) // A commit can have one or more parents
|
||||
case "tagger":
|
||||
tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(val))
|
||||
}
|
||||
pos += eol + 1
|
||||
}
|
||||
idx := strings.LastIndex(tag.Message, beginpgp)
|
||||
if idx > 0 {
|
||||
endSigIdx := strings.Index(tag.Message[idx:], endpgp)
|
||||
if endSigIdx > 0 {
|
||||
tag.Signature = &CommitGPGSignature{
|
||||
Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
|
||||
Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
|
||||
}
|
||||
tag.Message = tag.Message[:idx+1]
|
||||
}
|
||||
payload, msg, sign := parsePayloadSignature(data, pos)
|
||||
tag.Message = msg
|
||||
if len(sign) > 0 {
|
||||
tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
|
|
@ -12,24 +12,28 @@ import (
|
|||
|
||||
func Test_parseTagData(t *testing.T) {
|
||||
testData := []struct {
|
||||
data []byte
|
||||
tag Tag
|
||||
data string
|
||||
expected Tag
|
||||
}{
|
||||
{data: []byte(`object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
|
||||
{
|
||||
data: `object 3b114ab800c6432ad42387ccf6bc8d4388a2885a
|
||||
type commit
|
||||
tag 1.22.0
|
||||
tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100
|
||||
|
||||
`), tag: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: &Sha1Hash{0x3b, 0x11, 0x4a, 0xb8, 0x0, 0xc6, 0x43, 0x2a, 0xd4, 0x23, 0x87, 0xcc, 0xf6, 0xbc, 0x8d, 0x43, 0x88, 0xa2, 0x88, 0x5a},
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0)},
|
||||
Message: "",
|
||||
Signature: nil,
|
||||
}},
|
||||
{data: []byte(`object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
|
||||
`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "",
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc
|
||||
type commit
|
||||
tag 1.22.1
|
||||
tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
|
||||
|
@ -37,37 +41,57 @@ tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100
|
|||
test message
|
||||
o
|
||||
|
||||
ono`), tag: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: &Sha1Hash{0x7c, 0xdf, 0x42, 0xc0, 0xb1, 0xcc, 0x76, 0x3a, 0xb7, 0xe4, 0xc3, 0x3c, 0x47, 0xa2, 0x4e, 0x27, 0xc6, 0x6b, 0xfc, 0xcc},
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0)},
|
||||
Message: "test message\no\n\nono",
|
||||
Signature: nil,
|
||||
}},
|
||||
ono`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "test message\no\n\nono",
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
|
||||
type commit
|
||||
tag v0
|
||||
tagger dummy user <dummy-email@example.com> 1484491741 +0100
|
||||
|
||||
dummy message
|
||||
-----BEGIN SSH SIGNATURE-----
|
||||
dummy signature
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
expected: Tag{
|
||||
Name: "",
|
||||
ID: Sha1ObjectFormat.EmptyObjectID(),
|
||||
Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"),
|
||||
Type: "commit",
|
||||
Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))},
|
||||
Message: "dummy message",
|
||||
Signature: &CommitSignature{
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
dummy signature
|
||||
-----END SSH SIGNATURE-----`,
|
||||
Payload: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa
|
||||
type commit
|
||||
tag v0
|
||||
tagger dummy user <dummy-email@example.com> 1484491741 +0100
|
||||
|
||||
dummy message`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
tag, err := parseTagData(Sha1ObjectFormat, test.data)
|
||||
tag, err := parseTagData(Sha1ObjectFormat, []byte(test.data))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, test.tag.ID, tag.ID)
|
||||
assert.EqualValues(t, test.tag.Object, tag.Object)
|
||||
assert.EqualValues(t, test.tag.Name, tag.Name)
|
||||
assert.EqualValues(t, test.tag.Message, tag.Message)
|
||||
assert.EqualValues(t, test.tag.Type, tag.Type)
|
||||
if test.tag.Signature != nil && assert.NotNil(t, tag.Signature) {
|
||||
assert.EqualValues(t, test.tag.Signature.Signature, tag.Signature.Signature)
|
||||
assert.EqualValues(t, test.tag.Signature.Payload, tag.Signature.Payload)
|
||||
} else {
|
||||
assert.Nil(t, tag.Signature)
|
||||
}
|
||||
if test.tag.Tagger != nil && assert.NotNil(t, tag.Tagger) {
|
||||
assert.EqualValues(t, test.tag.Tagger.Name, tag.Tagger.Name)
|
||||
assert.EqualValues(t, test.tag.Tagger.Email, tag.Tagger.Email)
|
||||
assert.EqualValues(t, test.tag.Tagger.When.Unix(), tag.Tagger.When.Unix())
|
||||
} else {
|
||||
assert.Nil(t, tag.Tagger)
|
||||
}
|
||||
assert.Equal(t, test.expected, *tag)
|
||||
}
|
||||
|
||||
tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted..."))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ type Result struct {
|
|||
UpdatedUnix timeutil.TimeStamp
|
||||
Language string
|
||||
Color string
|
||||
Lines []ResultLine
|
||||
Lines []*ResultLine
|
||||
}
|
||||
|
||||
type ResultLine struct {
|
||||
|
@ -70,16 +70,18 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
|
||||
func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine {
|
||||
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
|
||||
hl, _ := highlight.Code(filename, "", code)
|
||||
hl, _ := highlight.Code(filename, language, code)
|
||||
highlightedLines := strings.Split(string(hl), "\n")
|
||||
|
||||
// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
|
||||
lines := make([]ResultLine, min(len(highlightedLines), len(lineNums)))
|
||||
lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums)))
|
||||
for i := 0; i < len(lines); i++ {
|
||||
lines[i].Num = lineNums[i]
|
||||
lines[i].FormattedContent = template.HTML(highlightedLines[i])
|
||||
lines[i] = &ResultLine{
|
||||
Num: lineNums[i],
|
||||
FormattedContent: template.HTML(highlightedLines[i]),
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
@ -122,7 +124,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
|
|||
UpdatedUnix: result.UpdatedUnix,
|
||||
Language: result.Language,
|
||||
Color: result.Color,
|
||||
Lines: HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
|
||||
Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ func (c *FilesystemClient) Download(ctx context.Context, objects []Pointer, call
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
if err := callback(p, f, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ func (c *FilesystemClient) Upload(ctx context.Context, objects []Pointer, callba
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, content)
|
||||
|
||||
return err
|
||||
|
|
|
@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
|
|||
var defaultProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
codePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
||||
var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
||||
|
||||
type RenderCodePreviewOptions struct {
|
||||
FullURL string
|
||||
OwnerName string
|
||||
RepoName string
|
||||
CommitID string
|
||||
FilePath string
|
||||
|
||||
LineStart, LineStop int
|
||||
}
|
||||
|
||||
func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
|
||||
m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
|
||||
opts := RenderCodePreviewOptions{
|
||||
FullURL: node.Data[m[0]:m[1]],
|
||||
OwnerName: node.Data[m[2]:m[3]],
|
||||
RepoName: node.Data[m[4]:m[5]],
|
||||
CommitID: node.Data[m[6]:m[7]],
|
||||
FilePath: node.Data[m[8]:m[9]],
|
||||
}
|
||||
if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
u, err := url.Parse(opts.FilePath)
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
opts.FilePath = strings.TrimPrefix(u.Path, "/")
|
||||
|
||||
lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
|
||||
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
|
||||
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
|
||||
opts.LineStart, opts.LineStop = lineStart, lineStop
|
||||
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
|
||||
return m[0], m[1], h, err
|
||||
}
|
||||
|
||||
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
for node != nil {
|
||||
if node.Type != html.TextNode {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
|
||||
if err != nil || h == "" {
|
||||
if err != nil {
|
||||
log.Error("Unable to render code preview: %v", err)
|
||||
}
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
next := node.NextSibling
|
||||
textBefore := node.Data[:urlPosStart]
|
||||
textAfter := node.Data[urlPosEnd:]
|
||||
// "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here.
|
||||
// However, the empty node can't be simply removed, because:
|
||||
// 1. the following processors will still try to access it (need to double-check undefined behaviors)
|
||||
// 2. the new node is inserted as "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li")
|
||||
// then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
|
||||
// so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
|
||||
node.Data = textBefore
|
||||
node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
|
||||
if textAfter != "" {
|
||||
node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
|
||||
}
|
||||
node = next
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderCodePreview(t *testing.T) {
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
|
||||
return "<div>code preview</div>", nil
|
||||
},
|
||||
})
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Type: "markdown",
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
|
||||
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
|
||||
}
|
|
@ -436,6 +436,10 @@ func TestColorPreview(t *testing.T) {
|
|||
testcase string
|
||||
expected string
|
||||
}{
|
||||
{ // do not render color names
|
||||
"The CSS class `red` is there",
|
||||
"<p>The CSS class <code>red</code> is there</p>\n",
|
||||
},
|
||||
{ // hex
|
||||
"`#FF0000`",
|
||||
`<p><code>#FF0000<span class="color-preview" style="background-color: #FF0000"></span></code></p>` + nl,
|
||||
|
@ -445,8 +449,8 @@ func TestColorPreview(t *testing.T) {
|
|||
`<p><code>rgb(16, 32, 64)<span class="color-preview" style="background-color: rgb(16, 32, 64)"></span></code></p>` + nl,
|
||||
},
|
||||
{ // short hex
|
||||
"This is the color white `#000`",
|
||||
`<p>This is the color white <code>#000<span class="color-preview" style="background-color: #000"></span></code></p>` + nl,
|
||||
"This is the color white `#0a0`",
|
||||
`<p>This is the color white <code>#0a0<span class="color-preview" style="background-color: #0a0"></span></code></p>` + nl,
|
||||
},
|
||||
{ // hsl
|
||||
"HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.",
|
||||
|
@ -507,9 +511,17 @@ func TestMathBlock(t *testing.T) {
|
|||
`\(a\) \(b\)`,
|
||||
`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl,
|
||||
},
|
||||
{
|
||||
`$a$.`,
|
||||
`<p><code class="language-math is-loading">a</code>.</p>` + nl,
|
||||
},
|
||||
{
|
||||
`.$a$`,
|
||||
`<p>.$a$</p>` + nl,
|
||||
},
|
||||
{
|
||||
`$a a$b b$`,
|
||||
`<p><code class="language-math is-loading">a a$b b</code></p>` + nl,
|
||||
`<p>$a a$b b$</p>` + nl,
|
||||
},
|
||||
{
|
||||
`a a$b b`,
|
||||
|
@ -517,7 +529,15 @@ func TestMathBlock(t *testing.T) {
|
|||
},
|
||||
{
|
||||
`a$b $a a$b b$`,
|
||||
`<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl,
|
||||
`<p>a$b $a a$b b$</p>` + nl,
|
||||
},
|
||||
{
|
||||
"a$x$",
|
||||
`<p>a$x$</p>` + nl,
|
||||
},
|
||||
{
|
||||
"$x$a",
|
||||
`<p>$x$a</p>` + nl,
|
||||
},
|
||||
{
|
||||
"$$a$$",
|
||||
|
|
|
@ -41,9 +41,12 @@ func (parser *inlineParser) Trigger() []byte {
|
|||
return parser.start[0:1]
|
||||
}
|
||||
|
||||
func isPunctuation(b byte) bool {
|
||||
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
|
||||
}
|
||||
|
||||
func isAlphanumeric(b byte) bool {
|
||||
// Github only cares about 0-9A-Za-z
|
||||
return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
// Parse parses the current line and returns a result of parsing.
|
||||
|
@ -56,7 +59,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
|||
}
|
||||
|
||||
precedingCharacter := block.PrecendingCharacter()
|
||||
if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
|
||||
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
|
||||
// need to exclude things like `a$` from being considered a start
|
||||
return nil
|
||||
}
|
||||
|
@ -75,14 +78,19 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
|||
ender += pos
|
||||
|
||||
// Now we want to check the character at the end of our parser section
|
||||
// that is ender + len(parser.end)
|
||||
// that is ender + len(parser.end) and check if char before ender is '\'
|
||||
pos = ender + len(parser.end)
|
||||
if len(line) <= pos {
|
||||
break
|
||||
}
|
||||
if !isAlphanumeric(line[pos]) {
|
||||
suceedingCharacter := line[pos]
|
||||
if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') {
|
||||
return nil
|
||||
}
|
||||
if line[ender-1] != '\\' {
|
||||
break
|
||||
}
|
||||
|
||||
// move the pointer onwards
|
||||
ender += len(parser.end)
|
||||
}
|
||||
|
|
|
@ -49,9 +49,28 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
|
|||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// cssColorHandler checks if a string is a render-able CSS color value.
|
||||
// The code is from "github.com/microcosm-cc/bluemonday/css.ColorHandler", except that it doesn't handle color words like "red".
|
||||
func cssColorHandler(value string) bool {
|
||||
value = strings.ToLower(value)
|
||||
if css.HexRGB.MatchString(value) {
|
||||
return true
|
||||
}
|
||||
if css.RGB.MatchString(value) {
|
||||
return true
|
||||
}
|
||||
if css.RGBA.MatchString(value) {
|
||||
return true
|
||||
}
|
||||
if css.HSL.MatchString(value) {
|
||||
return true
|
||||
}
|
||||
return css.HSLA.MatchString(value)
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
|
||||
colorContent := v.Text(reader.Source())
|
||||
if css.ColorHandler(strings.ToLower(string(colorContent))) {
|
||||
if cssColorHandler(string(colorContent)) {
|
||||
v.AppendChild(v, NewColorPreview(colorContent))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
@ -33,6 +34,8 @@ type ProcessorHelper struct {
|
|||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
|
||||
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||
|
||||
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
||||
}
|
||||
|
||||
var DefaultProcessorHelper ProcessorHelper
|
||||
|
|
|
@ -60,6 +60,21 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ type Package struct {
|
|||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
ReleaseNotes string `json:"release_notes,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
Authors string `json:"authors,omitempty"`
|
||||
ProjectURL string `json:"project_url,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
|
@ -71,6 +72,7 @@ type Dependency struct {
|
|||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/nuget/reference/nuspec
|
||||
type nuspecPackage struct {
|
||||
Metadata struct {
|
||||
ID string `xml:"id"`
|
||||
|
@ -80,6 +82,7 @@ type nuspecPackage struct {
|
|||
ProjectURL string `xml:"projectUrl"`
|
||||
Description string `xml:"description"`
|
||||
ReleaseNotes string `xml:"releaseNotes"`
|
||||
Readme string `xml:"readme"`
|
||||
PackageTypes struct {
|
||||
PackageType []struct {
|
||||
Name string `xml:"name,attr"`
|
||||
|
@ -89,6 +92,11 @@ type nuspecPackage struct {
|
|||
URL string `xml:"url,attr"`
|
||||
} `xml:"repository"`
|
||||
Dependencies struct {
|
||||
Dependency []struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
Exclude string `xml:"exclude,attr"`
|
||||
} `xml:"dependency"`
|
||||
Group []struct {
|
||||
TargetFramework string `xml:"targetFramework,attr"`
|
||||
Dependency []struct {
|
||||
|
@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
return ParseNuspecMetaData(f)
|
||||
return ParseNuspecMetaData(archive, f)
|
||||
}
|
||||
}
|
||||
return nil, ErrMissingNuspecFile
|
||||
}
|
||||
|
||||
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
|
||||
func ParseNuspecMetaData(r io.Reader) (*Package, error) {
|
||||
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||
var p nuspecPackage
|
||||
if err := xml.NewDecoder(r).Decode(&p); err != nil {
|
||||
return nil, err
|
||||
|
@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
|
|||
Dependencies: make(map[string][]Dependency),
|
||||
}
|
||||
|
||||
if p.Metadata.Readme != "" {
|
||||
f, err := archive.Open(p.Metadata.Readme)
|
||||
if err == nil {
|
||||
buf, _ := io.ReadAll(f)
|
||||
m.Readme = string(buf)
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.Metadata.Dependencies.Dependency) > 0 {
|
||||
deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
|
||||
for _, dep := range p.Metadata.Dependencies.Dependency {
|
||||
if dep.ID == "" || dep.Version == "" {
|
||||
continue
|
||||
}
|
||||
deps = append(deps, Dependency{
|
||||
ID: dep.ID,
|
||||
Version: dep.Version,
|
||||
})
|
||||
}
|
||||
m.Dependencies[""] = deps
|
||||
}
|
||||
for _, group := range p.Metadata.Dependencies.Group {
|
||||
deps := make([]Dependency, 0, len(group.Dependency))
|
||||
for _, dep := range group.Dependency {
|
||||
|
|
|
@ -6,7 +6,6 @@ package nuget
|
|||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -19,6 +18,7 @@ const (
|
|||
projectURL = "https://gitea.io"
|
||||
description = "Package Description"
|
||||
releaseNotes = "Package Release Notes"
|
||||
readme = "Readme"
|
||||
repositoryURL = "https://gitea.io/gitea/gitea"
|
||||
targetFramework = ".NETStandard2.1"
|
||||
dependencyID = "System.Text.Json"
|
||||
|
@ -36,6 +36,7 @@ const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
|
|||
<description>` + description + `</description>
|
||||
<releaseNotes>` + releaseNotes + `</releaseNotes>
|
||||
<repository url="` + repositoryURL + `" />
|
||||
<readme>README.md</readme>
|
||||
<dependencies>
|
||||
<group targetFramework="` + targetFramework + `">
|
||||
<dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
|
||||
|
@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
|
|||
</package>`
|
||||
|
||||
func TestParsePackageMetaData(t *testing.T) {
|
||||
createArchive := func(name, content string) []byte {
|
||||
createArchive := func(files map[string]string) []byte {
|
||||
var buf bytes.Buffer
|
||||
archive := zip.NewWriter(&buf)
|
||||
w, _ := archive.Create(name)
|
||||
w.Write([]byte(content))
|
||||
for name, content := range files {
|
||||
w, _ := archive.Create(name)
|
||||
w.Write([]byte(content))
|
||||
}
|
||||
archive.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
t.Run("MissingNuspecFile", func(t *testing.T) {
|
||||
data := createArchive("dummy.txt", "")
|
||||
data := createArchive(map[string]string{"dummy.txt": ""})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
|
||||
data := createArchive("sub/package.nuspec", "")
|
||||
data := createArchive(map[string]string{"sub/package.nuspec": ""})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidNuspecFile", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", "")
|
||||
data := createArchive(map[string]string{"package.nuspec": ""})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidPackageId", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
|
||||
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata></metadata>
|
||||
</package>`)
|
||||
</package>`})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
|
||||
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>`+id+`</id>
|
||||
<id>` + id + `</id>
|
||||
</metadata>
|
||||
</package>`)
|
||||
</package>`})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", nuspecContent)
|
||||
t.Run("MissingReadme", func(t *testing.T) {
|
||||
data := createArchive(map[string]string{"package.nuspec": nuspecContent})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Empty(t, np.Metadata.Readme)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseNuspecMetaData(t *testing.T) {
|
||||
t.Run("Dependency Package", func(t *testing.T) {
|
||||
np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
|
||||
data := createArchive(map[string]string{
|
||||
"package.nuspec": nuspecContent,
|
||||
"README.md": readme,
|
||||
})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Equal(t, DependencyPackage, np.PackageType)
|
||||
|
@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
|
|||
assert.Equal(t, projectURL, np.Metadata.ProjectURL)
|
||||
assert.Equal(t, description, np.Metadata.Description)
|
||||
assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
|
||||
assert.Equal(t, readme, np.Metadata.Readme)
|
||||
assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
|
||||
assert.Len(t, np.Metadata.Dependencies, 1)
|
||||
assert.Contains(t, np.Metadata.Dependencies, targetFramework)
|
||||
|
@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
|
|||
assert.Equal(t, dependencyVersion, deps[0].Version)
|
||||
|
||||
t.Run("NormalizedVersion", func(t *testing.T) {
|
||||
np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>test</id>
|
||||
<version>1.04.5.2.5-rc.1+metadata</version>
|
||||
</metadata>
|
||||
</package>`))
|
||||
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>test</id>
|
||||
<version>1.04.5.2.5-rc.1+metadata</version>
|
||||
</metadata>
|
||||
</package>`})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Equal(t, "1.4.5.2-rc.1", np.Version)
|
||||
|
@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Symbols Package", func(t *testing.T) {
|
||||
np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
|
||||
data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Equal(t, SymbolsPackage, np.PackageType)
|
||||
|
|
|
@ -315,21 +315,25 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
|
|||
}
|
||||
}
|
||||
|
||||
// DeprecatedWarnings contains the warning message for various deprecations, including: setting option, file/folder, etc
|
||||
var DeprecatedWarnings []string
|
||||
// StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc
|
||||
var StartupProblems []string
|
||||
|
||||
func logStartupProblem(skip int, level log.Level, format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Log(skip+1, level, "%s", msg)
|
||||
StartupProblems = append(StartupProblems, msg)
|
||||
}
|
||||
|
||||
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
msg := fmt.Sprintf("Deprecated config option `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
log.Error("%v", msg)
|
||||
DeprecatedWarnings = append(DeprecatedWarnings, msg)
|
||||
logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
}
|
||||
}
|
||||
|
||||
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
|
||||
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
|
||||
logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
|
|||
if !filepath.IsAbs(Indexer.IssuePath) {
|
||||
Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
|
||||
}
|
||||
checkOverlappedPath("indexer.ISSUE_INDEXER_PATH", Indexer.IssuePath)
|
||||
checkOverlappedPath("[indexer].ISSUE_INDEXER_PATH", Indexer.IssuePath)
|
||||
} else {
|
||||
Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
|
||||
if Indexer.IssueType == "meilisearch" {
|
||||
|
|
|
@ -185,8 +185,13 @@ func InitLoggersForTest() {
|
|||
initAllLoggers()
|
||||
}
|
||||
|
||||
var initLoggerDisabled bool
|
||||
|
||||
// initAllLoggers creates all the log services
|
||||
func initAllLoggers() {
|
||||
if initLoggerDisabled {
|
||||
return
|
||||
}
|
||||
initManagedLoggers(log.GetManager(), CfgProvider)
|
||||
|
||||
golog.SetFlags(0)
|
||||
|
@ -194,6 +199,10 @@ func initAllLoggers() {
|
|||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||
}
|
||||
|
||||
func DisableLoggerInit() {
|
||||
initLoggerDisabled = true
|
||||
}
|
||||
|
||||
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
|
||||
loadLogGlobalFrom(cfg)
|
||||
prepareLoggerConfig(cfg)
|
||||
|
|
|
@ -118,6 +118,10 @@ func loadOAuth2From(rootCfg ConfigProvider) {
|
|||
return
|
||||
}
|
||||
|
||||
if sec.HasKey("DEFAULT_APPLICATIONS") && sec.Key("DEFAULT_APPLICATIONS").String() == "" {
|
||||
OAuth2.DefaultApplications = nil
|
||||
}
|
||||
|
||||
// Handle the rename of ENABLE to ENABLED
|
||||
deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
|
||||
if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
|
||||
|
@ -168,7 +172,7 @@ func GetGeneralTokenSigningSecret() []byte {
|
|||
}
|
||||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
|
||||
// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
|
||||
log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
|
||||
logStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
|
||||
return jwtSecret
|
||||
}
|
||||
return *generalSigningSecret.Load()
|
||||
|
|
|
@ -32,3 +32,21 @@ JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
|||
assert.Len(t, actual, 32)
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestOauth2DefaultApplications(t *testing.T) {
|
||||
cfg, _ := NewConfigProviderFromData(``)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea"}, OAuth2.DefaultApplications)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||
DEFAULT_APPLICATIONS = tea
|
||||
`)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Equal(t, []string{"tea"}, OAuth2.DefaultApplications)
|
||||
|
||||
cfg, _ = NewConfigProviderFromData(`[oauth2]
|
||||
DEFAULT_APPLICATIONS =
|
||||
`)
|
||||
loadOAuth2From(cfg)
|
||||
assert.Nil(t, nil, OAuth2.DefaultApplications)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import "code.gitea.io/gitea/modules/log"
|
|||
type OtherConfig struct {
|
||||
ShowFooterVersion bool
|
||||
ShowFooterTemplateLoadTime bool
|
||||
ShowFooterPoweredBy bool
|
||||
EnableFeed bool
|
||||
EnableSitemap bool
|
||||
}
|
||||
|
@ -15,6 +16,7 @@ type OtherConfig struct {
|
|||
var Other = OtherConfig{
|
||||
ShowFooterVersion: true,
|
||||
ShowFooterTemplateLoadTime: true,
|
||||
ShowFooterPoweredBy: true,
|
||||
EnableSitemap: true,
|
||||
EnableFeed: true,
|
||||
}
|
||||
|
|
|
@ -286,7 +286,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
|||
RepoRootPath = filepath.Clean(RepoRootPath)
|
||||
}
|
||||
|
||||
checkOverlappedPath("repository.ROOT", RepoRootPath)
|
||||
checkOverlappedPath("[repository].ROOT", RepoRootPath)
|
||||
|
||||
defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
|
||||
for _, charset := range Repository.DetectedCharsetsOrder {
|
||||
|
|
|
@ -331,7 +331,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||
if !filepath.IsAbs(PprofDataPath) {
|
||||
PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
|
||||
}
|
||||
checkOverlappedPath("server.PPROF_DATA_PATH", PprofDataPath)
|
||||
checkOverlappedPath("[server].PPROF_DATA_PATH", PprofDataPath)
|
||||
|
||||
landingPage := sec.Key("LANDING_PAGE").MustString("home")
|
||||
switch landingPage {
|
||||
|
|
|
@ -46,7 +46,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
|||
SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
|
||||
if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
|
||||
SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
|
||||
checkOverlappedPath("session.PROVIDER_CONFIG", SessionConfig.ProviderConfig)
|
||||
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
|
||||
}
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||
SessionConfig.CookiePath = AppSubURL
|
||||
|
|
|
@ -235,9 +235,7 @@ var configuredPaths = make(map[string]string)
|
|||
func checkOverlappedPath(name, path string) {
|
||||
// TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path)
|
||||
if targetName, ok := configuredPaths[path]; ok && targetName != name {
|
||||
msg := fmt.Sprintf("Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
|
||||
log.Error("%s", msg)
|
||||
DeprecatedWarnings = append(DeprecatedWarnings, msg)
|
||||
logStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
|
||||
}
|
||||
configuredPaths[path] = name
|
||||
}
|
||||
|
|
|
@ -240,7 +240,7 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
|
|||
}
|
||||
}
|
||||
|
||||
checkOverlappedPath("storage."+name+".PATH", storage.Path)
|
||||
checkOverlappedPath("[storage."+name+"].PATH", storage.Path)
|
||||
|
||||
return &storage, nil
|
||||
}
|
||||
|
|
|
@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap {
|
|||
"JsonUtils": NewJsonUtils,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// svg / avatar / icon
|
||||
// svg / avatar / icon / color
|
||||
"svg": svg.RenderHTML,
|
||||
"EntryIcon": base.EntryIcon,
|
||||
"MigrationIcon": MigrationIcon,
|
||||
"ActionIcon": ActionIcon,
|
||||
|
||||
"SortArrow": SortArrow,
|
||||
"SortArrow": SortArrow,
|
||||
"ContrastColor": util.ContrastColor,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
|
@ -106,6 +106,9 @@ func NewFuncMap() template.FuncMap {
|
|||
"ShowFooterTemplateLoadTime": func() bool {
|
||||
return setting.Other.ShowFooterTemplateLoadTime
|
||||
},
|
||||
"ShowFooterPoweredBy": func() bool {
|
||||
return setting.Other.ShowFooterPoweredBy
|
||||
},
|
||||
"AllowedReactions": func() []string {
|
||||
return setting.UI.Reactions
|
||||
},
|
||||
|
|
|
@ -123,16 +123,10 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
|
|||
func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
|
||||
var (
|
||||
archivedCSSClass string
|
||||
textColor = "#111"
|
||||
textColor = util.ContrastColor(label.Color)
|
||||
labelScope = label.ExclusiveScope()
|
||||
)
|
||||
|
||||
r, g, b := util.HexToRBGColor(label.Color)
|
||||
// Determine if label text should be light or dark to be readable on background color
|
||||
if util.UseLightTextOnBackground(r, g, b) {
|
||||
textColor = "#eee"
|
||||
}
|
||||
|
||||
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
||||
|
||||
if label.IsArchived() {
|
||||
|
@ -153,7 +147,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
|
||||
// Make scope and item background colors slightly darker and lighter respectively.
|
||||
// More contrast needed with higher luminance, empirically tweaked.
|
||||
luminance := util.GetLuminance(r, g, b)
|
||||
luminance := util.GetRelativeLuminance(label.Color)
|
||||
contrast := 0.01 + luminance*0.03
|
||||
// Ensure we add the same amount of contrast also near 0 and 1.
|
||||
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
|
||||
|
@ -162,6 +156,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
|
||||
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
|
||||
|
||||
r, g, b := util.HexToRBGColor(label.Color)
|
||||
scopeBytes := []byte{
|
||||
uint8(math.Min(math.Round(r*darkenFactor), 255)),
|
||||
uint8(math.Min(math.Round(g*darkenFactor), 255)),
|
||||
|
|
|
@ -21,8 +21,9 @@ var (
|
|||
)
|
||||
|
||||
// MockSet sets the time to a mocked time.Time
|
||||
func MockSet(now time.Time) {
|
||||
func MockSet(now time.Time) func() {
|
||||
mockNow = now
|
||||
return MockUnset
|
||||
}
|
||||
|
||||
// MockUnset will unset the mocked time.Time
|
||||
|
|
|
@ -6,6 +6,7 @@ package translation
|
|||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MockLocale provides a mocked locale without any translations
|
||||
|
@ -19,18 +20,25 @@ func (l MockLocale) Language() string {
|
|||
return "en"
|
||||
}
|
||||
|
||||
func (l MockLocale) TrString(s string, _ ...any) string {
|
||||
return s
|
||||
func (l MockLocale) TrString(s string, args ...any) string {
|
||||
return sprintAny(s, args...)
|
||||
}
|
||||
|
||||
func (l MockLocale) Tr(s string, a ...any) template.HTML {
|
||||
return template.HTML(s)
|
||||
func (l MockLocale) Tr(s string, args ...any) template.HTML {
|
||||
return template.HTML(sprintAny(s, args...))
|
||||
}
|
||||
|
||||
func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
||||
return template.HTML(key1)
|
||||
return template.HTML(sprintAny(key1, args...))
|
||||
}
|
||||
|
||||
func (l MockLocale) PrettyNumber(v any) string {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
func sprintAny(s string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return s
|
||||
}
|
||||
return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...)
|
||||
}
|
||||
|
|
|
@ -4,22 +4,10 @@ package util
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Check similar implementation in web_src/js/utils/color.js and keep synchronization
|
||||
|
||||
// Return R, G, B values defined in reletive luminance
|
||||
func getLuminanceRGB(channel float64) float64 {
|
||||
sRGB := channel / 255
|
||||
if sRGB <= 0.03928 {
|
||||
return sRGB / 12.92
|
||||
}
|
||||
return math.Pow((sRGB+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
// Get color as RGB values in 0..255 range from the hex color string (with or without #)
|
||||
func HexToRBGColor(colorString string) (float64, float64, float64) {
|
||||
hexString := colorString
|
||||
|
@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
|
|||
return r, g, b
|
||||
}
|
||||
|
||||
// return luminance given RGB channels
|
||||
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
|
||||
func GetLuminance(r, g, b float64) float64 {
|
||||
R := getLuminanceRGB(r)
|
||||
G := getLuminanceRGB(g)
|
||||
B := getLuminanceRGB(b)
|
||||
luminance := 0.2126*R + 0.7152*G + 0.0722*B
|
||||
return luminance
|
||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||
// Keep this in sync with web_src/js/utils/color.js
|
||||
func GetRelativeLuminance(color string) float64 {
|
||||
r, g, b := HexToRBGColor(color)
|
||||
return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255
|
||||
}
|
||||
|
||||
// Reference from: https://firsching.ch/github_labels.html
|
||||
// In the future WCAG 3 APCA may be a better solution.
|
||||
// Check if text should use light color based on RGB of background
|
||||
func UseLightTextOnBackground(r, g, b float64) bool {
|
||||
return GetLuminance(r, g, b) < 0.453
|
||||
func UseLightText(backgroundColor string) bool {
|
||||
return GetRelativeLuminance(backgroundColor) < 0.453
|
||||
}
|
||||
|
||||
// Given a background color, returns a black or white foreground color that the highest
|
||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||
func ContrastColor(backgroundColor string) string {
|
||||
if UseLightText(backgroundColor) {
|
||||
return "#fff"
|
||||
}
|
||||
return "#000"
|
||||
}
|
||||
|
|
|
@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_UseLightTextOnBackground(t *testing.T) {
|
||||
func Test_UseLightText(t *testing.T) {
|
||||
cases := []struct {
|
||||
r float64
|
||||
g float64
|
||||
b float64
|
||||
expected bool
|
||||
color string
|
||||
expected string
|
||||
}{
|
||||
{215, 58, 74, true},
|
||||
{0, 117, 202, true},
|
||||
{207, 211, 215, false},
|
||||
{162, 238, 239, false},
|
||||
{112, 87, 255, true},
|
||||
{0, 134, 114, true},
|
||||
{228, 230, 105, false},
|
||||
{216, 118, 227, true},
|
||||
{255, 255, 255, false},
|
||||
{43, 134, 133, true},
|
||||
{43, 135, 134, true},
|
||||
{44, 135, 134, true},
|
||||
{59, 182, 179, true},
|
||||
{124, 114, 104, true},
|
||||
{126, 113, 108, true},
|
||||
{129, 112, 109, true},
|
||||
{128, 112, 112, true},
|
||||
{"#d73a4a", "#fff"},
|
||||
{"#0075ca", "#fff"},
|
||||
{"#cfd3d7", "#000"},
|
||||
{"#a2eeef", "#000"},
|
||||
{"#7057ff", "#fff"},
|
||||
{"#008672", "#fff"},
|
||||
{"#e4e669", "#000"},
|
||||
{"#d876e3", "#000"},
|
||||
{"#ffffff", "#000"},
|
||||
{"#2b8684", "#fff"},
|
||||
{"#2b8786", "#fff"},
|
||||
{"#2c8786", "#000"},
|
||||
{"#3bb6b3", "#000"},
|
||||
{"#7c7268", "#fff"},
|
||||
{"#7e716c", "#fff"},
|
||||
{"#81706d", "#fff"},
|
||||
{"#807070", "#fff"},
|
||||
{"#84b6eb", "#000"},
|
||||
}
|
||||
for n, c := range cases {
|
||||
result := UseLightTextOnBackground(c.r, c.g, c.b)
|
||||
assert.Equal(t, c.expected, result, "case %d: error should match", n)
|
||||
assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -213,6 +213,14 @@ func ToPointer[T any](val T) *T {
|
|||
return &val
|
||||
}
|
||||
|
||||
// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
|
||||
func Iif[T any](condition bool, trueVal, falseVal T) T {
|
||||
if condition {
|
||||
return trueVal
|
||||
}
|
||||
return falseVal
|
||||
}
|
||||
|
||||
// IfZero returns "def" if "v" is a zero value, otherwise "v"
|
||||
func IfZero[T comparable](v, def T) T {
|
||||
var zero T
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Copyright 1989, 1990 Advanced Micro Devices, Inc.
|
||||
|
||||
This software is the property of Advanced Micro Devices, Inc (AMD) which
|
||||
specifically grants the user the right to modify, use and distribute this
|
||||
software provided this notice is not removed or altered. All other rights
|
||||
are reserved by AMD.
|
||||
|
||||
AMD MAKES NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THIS
|
||||
SOFTWARE. IN NO EVENT SHALL AMD BE LIABLE FOR INCIDENTAL OR CONSEQUENTIAL
|
||||
DAMAGES IN CONNECTION WITH OR ARISING FROM THE FURNISHING, PERFORMANCE, OR
|
||||
USE OF THIS SOFTWARE.
|
|
@ -210,21 +210,21 @@ PART 1: INITIAL CONTRIBUTOR AND DESIGNATED WEB SITE
|
|||
|
||||
The Initial Contributor is:
|
||||
____________________________________________________
|
||||
|
||||
|
||||
[Enter full name of Initial Contributor]
|
||||
|
||||
Address of Initial Contributor:
|
||||
________________________________________________
|
||||
|
||||
|
||||
________________________________________________
|
||||
|
||||
|
||||
________________________________________________
|
||||
|
||||
|
||||
[Enter address above]
|
||||
|
||||
The Designated Web Site is:
|
||||
__________________________________________________
|
||||
|
||||
|
||||
[Enter URL for Designated Web Site of Initial Contributor]
|
||||
|
||||
NOTE: The Initial Contributor is to complete this Part 1, along with Parts 2, 3, and 5, and, if applicable, Parts 4 and 6.
|
||||
|
@ -237,27 +237,27 @@ The date on which the Initial Work was first available under this License: _____
|
|||
|
||||
PART 3: GOVERNING JURISDICTION
|
||||
|
||||
For the purposes of this License, the Governing Jurisdiction is _________________________________________________.
[Initial Contributor to Enter Governing Jurisdiction here]
|
||||
For the purposes of this License, the Governing Jurisdiction is _________________________________________________. [Initial Contributor to Enter Governing Jurisdiction here]
|
||||
|
||||
PART 4: THIRD PARTIES
|
||||
|
||||
For the purposes of this License, "Third Party" has the definition set forth below in the ONE paragraph selected by the Initial Contributor from paragraphs A, B, C, D and E when the Initial Work is distributed or otherwise made available by the Initial Contributor. To select one of the following paragraphs, the Initial Contributor must place an "X" or "x" in the selection box alongside the one respective paragraph selected.
|
||||
SELECTION
|
||||
|
||||
|
||||
BOX PARAGRAPH
|
||||
[ ] A. "THIRD PARTY" means any third party.
|
||||
|
||||
|
||||
[ ] B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
|
||||
|
||||
|
||||
[ ] C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
|
||||
|
||||
|
||||
[ ] D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
|
||||
|
||||
|
||||
[ ] E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
|
||||
[ ] A. "THIRD PARTY" means any third party.
|
||||
|
||||
|
||||
[ ] B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
|
||||
|
||||
|
||||
[ ] C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
|
||||
|
||||
|
||||
[ ] D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
|
||||
|
||||
|
||||
[ ] E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
|
||||
The default definition of "THIRD PARTY" is the definition set forth in paragraph A, if NONE OR MORE THAN ONE of paragraphs A, B, C, D or E in this Part 4 are selected by the Initial Contributor.
|
||||
|
||||
PART 5: NOTICE
|
||||
|
@ -271,8 +271,8 @@ PART 6: PATENT LICENSING TERMS
|
|||
For the purposes of this License, paragraphs A, B, C, D and E of this Part 6 of Exhibit A are only incorporated and form part of the terms of the License if the Initial Contributor places an "X" or "x" in the selection box alongside the YES answer to the question immediately below.
|
||||
|
||||
Is this a Patents-Included License pursuant to Section 2.2 of the License?
|
||||
YES [ ]
|
||||
NO [ ]
|
||||
YES [ ]
|
||||
NO [ ]
|
||||
|
||||
By default, if YES is not selected by the Initial Contributor, the answer is NO.
|
||||
|
||||
|
|
|
@ -4,5 +4,5 @@ Any user of this software should understand that IBM cannot provide technical su
|
|||
|
||||
Any person who transfers this source code or any derivative work must include the IBM copyright notice, this paragraph, and the preceding two paragraphs in the transferred software.
|
||||
|
||||
COPYRIGHT I B M CORPORATION 2002
|
||||
LICENSED MATERIAL - PROGRAM PROPERTY OF I B M
|
||||
COPYRIGHT I B M CORPORATION 2002
|
||||
LICENSED MATERIAL - PROGRAM PROPERTY OF I B M
|
||||
|
|
|
@ -12,15 +12,15 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
|
|||
This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
|
||||
|
||||
You are free to:
|
||||
copy, publish, distribute and transmit the Information;
|
||||
copy, publish, distribute and transmit the Information;
|
||||
adapt the Information;
|
||||
exploit the Information for Non-Commercial purposes for example, by combining it with other information in your own product or application.
|
||||
|
||||
You are not permitted to:
|
||||
exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
|
||||
exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
|
||||
|
||||
You must, where you do any of the above:
|
||||
acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
|
||||
acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
|
||||
|
||||
If the Information Provider does not provide a specific attribution statement, you must use the following:
|
||||
Contains information licensed under the Non-Commercial Government Licence v2.0.
|
||||
|
|
|
@ -2,7 +2,7 @@ Netscape Public LIcense version 1.1
|
|||
|
||||
AMENDMENTS
|
||||
|
||||
The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License. Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
|
||||
The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License. Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
|
||||
|
||||
Additional Terms applicable to the Netscape Public License.
|
||||
|
||||
|
@ -28,7 +28,7 @@ Additional Terms applicable to the Netscape Public License.
|
|||
Notwithstanding the limitations of Section 11 above, the provisions regarding litigation in Section 11(a), (b) and (c) of the License shall apply to all disputes relating to this License.
|
||||
|
||||
EXHIBIT A-Netscape Public License.
|
||||
|
||||
|
||||
"The contents of this file are subject to the Netscape Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/NPL/
|
||||
|
||||
Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
|
||||
|
@ -37,8 +37,8 @@ The Original Code is Mozilla Communicator client code, released March 31, 1998.
|
|||
|
||||
The Initial Developer of the Original Code is Netscape Communications Corporation. Portions created by Netscape are Copyright (C) 1998-1999 Netscape Communications Corporation. All Rights Reserved.
|
||||
Contributor(s): ______________________________________.
|
||||
|
||||
Alternatively, the contents of this file may be used under the terms of the _____ license (the "[___] License"), in which case the provisions of [______] License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the [___] License. If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
|
||||
|
||||
Alternatively, the contents of this file may be used under the terms of the _____ license (the "[___] License"), in which case the provisions of [______] License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the [___] License. If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
|
||||
|
||||
|
||||
Mozilla Public License Version 1.1
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
COPYRIGHT (c) 1989-2013, 2015.
|
||||
On-Line Applications Research Corporation (OAR).
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose without fee is hereby granted, provided that this entire notice
|
||||
is included in all copies of any software which is or includes a copy
|
||||
or modification of this software.
|
||||
|
||||
THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
|
||||
WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION
|
||||
OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS
|
||||
SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
|
|
@ -6,7 +6,7 @@ OPEN CASCADE releases and makes publicly available the source code of the softwa
|
|||
It is not the purpose of this license to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this license has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||
|
||||
Please read this license carefully and completely before downloading this software. By downloading, using, modifying, distributing and sublicensing this software, you indicate your acceptance to be bound by the terms and conditions of this license. If you do not want to accept or cannot accept for any reasons the terms and conditions of this license, please do not download or use in any manner this software.
|
||||
|
||||
|
||||
1. Definitions
|
||||
|
||||
Unless there is something in the subject matter or in the context inconsistent therewith, the capitalized terms used in this License shall have the following meaning.
|
||||
|
@ -26,13 +26,13 @@ Unless there is something in the subject matter or in the context inconsistent t
|
|||
"Software": means the Original Code, the Modifications, the combination of Original Code and any Modifications or any respective portions thereof.
|
||||
|
||||
"You" or "Your": means an individual or a legal entity exercising rights under this License
|
||||
|
||||
|
||||
2. Acceptance of license
|
||||
By using, reproducing, modifying, distributing or sublicensing the Software or any portion thereof, You expressly indicate Your acceptance of the terms and conditions of this License and undertake to act in accordance with all the provisions of this License applicable to You.
|
||||
|
||||
|
||||
3. Scope and purpose
|
||||
This License applies to the Software and You may not use, reproduce, modify, distribute, sublicense or circulate the Software, or any portion thereof, except as expressly provided under this License. Any attempt to otherwise use, reproduce, modify, distribute or sublicense the Software is void and will automatically terminate Your rights under this License.
|
||||
|
||||
|
||||
4. Contributor license
|
||||
Subject to the terms and conditions of this License, the Initial Developer and each of the Contributors hereby grant You a world-wide, royalty-free, irrevocable and non-exclusive license under the Applicable Intellectual Property Rights they own or control, to use, reproduce, modify, distribute and sublicense the Software provided that:
|
||||
|
||||
|
|
|
@ -10,20 +10,20 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
|
|||
This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
|
||||
|
||||
You are free to:
|
||||
copy, publish, distribute and transmit the Information;
|
||||
copy, publish, distribute and transmit the Information;
|
||||
adapt the Information;
|
||||
exploit the Information commercially for example, by combining it with other Information, or by including it in your own product or application.
|
||||
|
||||
You must, where you do any of the above:
|
||||
acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
|
||||
If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following:
Contains public sector information licensed under the Open Government Licence v1.0.
|
||||
acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
|
||||
If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following: Contains public sector information licensed under the Open Government Licence v1.0.
|
||||
ensure that you do not use the Information in a way that suggests any official status or that the Information Provider endorses you or your use of the Information;
|
||||
ensure that you do not mislead others or misrepresent the Information or its source;
|
||||
ensure that your use of the Information does not breach the Data Protection Act 1998 or the Privacy and Electronic Communications (EC Directive) Regulations 2003.
|
||||
|
||||
These are important conditions of this licence and if you fail to comply with them the rights granted to you under this licence, or any similar licence granted by the Licensor, will end automatically.
|
||||
|
||||
Exemptions
|
||||
Exemptions
|
||||
|
||||
This licence does not cover the use of:
|
||||
- personal data in the Information;
|
||||
|
@ -48,22 +48,22 @@ Definitions
|
|||
|
||||
In this licence, the terms below have the following meanings:
|
||||
|
||||
‘Information’
means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
|
||||
‘Information’ means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
|
||||
|
||||
‘Information Provider’
means the person or organisation providing the Information under this licence.
|
||||
‘Information Provider’ means the person or organisation providing the Information under this licence.
|
||||
|
||||
‘Licensor’
means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
|
||||
‘Licensor’ means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
|
||||
|
||||
‘Use’
as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
|
||||
‘Use’ as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
|
||||
|
||||
‘You’
means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
|
||||
‘You’ means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
|
||||
|
||||
About the Open Government Licence
|
||||
The Controller of Her Majesty’s Stationery Office (HMSO) has developed this licence as a tool to enable Information Providers in the public sector to license the use and re-use of their Information under a common open licence. The Controller invites public sector bodies owning their own copyright and database rights to permit the use of their Information under this licence.
|
||||
|
||||
The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
|
||||
The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
|
||||
|
||||
This is version 1.0 of the Open Government Licence. The Controller of HMSO may, from time to time, issue new versions of the Open Government Licence. However, you may continue to use Information licensed under this version should you wish to do so.
|
||||
These terms have been aligned to be interoperable with any Creative Commons Attribution Licence, which covers copyright, and Open Data Commons Attribution License, which covers database rights and applicable copyrights.
|
||||
|
||||
Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
|
||||
Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
|
||||
|
|
|
@ -100,7 +100,8 @@ If it is impossible for You to comply with any of the terms of this License with
|
|||
5.1 Failure to Comply
|
||||
The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60-days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30-days after Your receipt of the notice.
|
||||
|
||||
5.2 Patent Infringement Claims
If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
|
||||
5.2 Patent Infringement Claims
|
||||
If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
|
||||
|
||||
5.3 Additional Compliance Terms
|
||||
Notwithstanding the foregoing in this Section 5, for purposes of this Section, if You breach Section 3.1 (Distribution of Source Form), Section 3.2 (Distribution of Executable Form), Section 3.3 (Distribution of a Larger Work), or Section 3.4 (Notices), then becoming compliant as described in Section 5.1 must also include, no later than 30 days after receipt by You of notice of such violation by a Contributor, making the Covered Software available in Source Code Form as required by this License on a publicly available computer network for a period of no less than three (3) years.
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
# Solderpad Hardware Licence Version 2.0
|
||||
|
||||
This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
|
||||
This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
|
||||
|
||||
1. The definitions set out in the Apache License are modified as follows:
|
||||
|
||||
Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
|
||||
Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
|
||||
|
||||
Contribution also includes any design, as well as any work of authorship.
|
||||
Contribution also includes any design, as well as any work of authorship.
|
||||
|
||||
Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
|
||||
Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
|
||||
|
||||
Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
|
||||
Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
|
||||
|
||||
Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
|
||||
Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
|
||||
|
||||
Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
|
||||
Work also includes a design or work of authorship, whether in Source form or other Object form.
|
||||
Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
|
||||
Work also includes a design or work of authorship, whether in Source form or other Object form.
|
||||
|
||||
2. Grant of Licence
|
||||
|
||||
2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
|
||||
2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
|
||||
|
|
|
@ -19,7 +19,7 @@ The following definitions shall replace the corresponding definitions in the Apa
|
|||
"License" shall mean this Solderpad Hardware License version 2.1, being the terms and conditions for use, manufacture, instantiation, adaptation, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the Rights owner or entity authorized by the Rights owner that is granting the License.
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship or design. For the purposes of this License, Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the Work and Derivative Works thereof.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object or material and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
|
||||
|
|
|
@ -36,13 +36,13 @@ Sun Industry Standards Source License - Version 1.1
|
|||
|
||||
2.0 SOURCE CODE LICENSE
|
||||
|
||||
2.1 The Initial Developer Grant The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
|
||||
2.1 The Initial Developer Grant The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and
|
||||
|
||||
(b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof).
|
||||
(c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License.
|
||||
(d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications.
|
||||
(d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications.
|
||||
|
||||
3.0 DISTRIBUTION OBLIGATIONS
|
||||
|
||||
|
@ -92,14 +92,14 @@ This License represents the complete agreement concerning subject matter hereof.
|
|||
|
||||
EXHIBIT A - Sun Standards License
|
||||
|
||||
"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
|
||||
"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
|
||||
|
||||
Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
|
||||
Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
|
||||
express or implied. See the License for the specific language governing rights and limitations under the License.
|
||||
|
||||
The Original Code is ______________________________________.
|
||||
|
||||
The Initial Developer of the Original Code is:
|
||||
The Initial Developer of the Original Code is:
|
||||
Sun Microsystems, Inc..
|
||||
|
||||
Portions created by: _______________________________________
|
||||
|
|
|
@ -4,7 +4,7 @@ Copyright (c) 1994-2002 World Wide Web Consortium, (Massachusetts Institute of T
|
|||
|
||||
This W3C work (including software, documents, or other related items) is being provided by the copyright holders under the following license. By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
|
||||
Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
|
||||
|
||||
1. The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
Copyright Itai Nahshon 1995, 1996.
|
||||
This program is distributed with no warranty.
|
||||
|
||||
Source files for this program may be distributed freely.
|
||||
Modifications to this file are okay as long as:
|
||||
a. This copyright notice and comment are preserved and
|
||||
left at the top of the file.
|
||||
b. The man page is fixed to reflect the change.
|
||||
c. The author of this change adds his name and change
|
||||
description to the list of changes below.
|
||||
Executable files may be distributed with sources, or with
|
||||
exact location where the source code can be obtained.
|
|
@ -2790,7 +2790,6 @@ settings=Nastavení správce
|
|||
|
||||
dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací.
|
||||
dashboard.statistic=Souhrn
|
||||
dashboard.operations=Operace údržby
|
||||
dashboard.system_status=Status systému
|
||||
dashboard.operation_name=Název operace
|
||||
dashboard.operation_switch=Přepnout
|
||||
|
|
|
@ -2798,7 +2798,6 @@ 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="https://blog.gitea.io">Blog</a>.
|
||||
dashboard.statistic=Übersicht
|
||||
dashboard.operations=Wartungsoperationen
|
||||
dashboard.system_status=System-Status
|
||||
dashboard.operation_name=Name der Operation
|
||||
dashboard.operation_switch=Wechseln
|
||||
|
|
|
@ -2687,7 +2687,6 @@ settings=Ρυθμίσεις Διαχειριστή
|
|||
|
||||
dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">στο blog</a> για περισσότερες λεπτομέρειες.
|
||||
dashboard.statistic=Περίληψη
|
||||
dashboard.operations=Λειτουργίες Συντήρησης
|
||||
dashboard.system_status=Κατάσταση Συστήματος
|
||||
dashboard.operation_name=Όνομα Λειτουργίας
|
||||
dashboard.operation_switch=Αλλαγή
|
||||
|
|
|
@ -1233,6 +1233,8 @@ file_view_rendered = View Rendered
|
|||
file_view_raw = View Raw
|
||||
file_permalink = Permalink
|
||||
file_too_large = The file is too large to be shown.
|
||||
code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s
|
||||
code_preview_line_in = Line %[1]d in %[2]s
|
||||
invisible_runes_header = `This file contains invisible Unicode characters`
|
||||
invisible_runes_description = `This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.`
|
||||
ambiguous_runes_header = `This file contains ambiguous Unicode characters`
|
||||
|
|
|
@ -2672,7 +2672,6 @@ settings=Configuración de Admin
|
|||
|
||||
dashboard.new_version_hint=Gitea %s ya está disponible, estás ejecutando %s. Revisa <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">el blog</a> para más detalles.
|
||||
dashboard.statistic=Resumen
|
||||
dashboard.operations=Operaciones de mantenimiento
|
||||
dashboard.system_status=Estado del sistema
|
||||
dashboard.operation_name=Nombre de la operación
|
||||
dashboard.operation_switch=Interruptor
|
||||
|
|
|
@ -2064,7 +2064,6 @@ last_page=واپسین
|
|||
total=مجموع: %d
|
||||
|
||||
dashboard.statistic=چکیده
|
||||
dashboard.operations=عملیاتهای نگهداری
|
||||
dashboard.system_status=وضعیت سامانه
|
||||
dashboard.operation_name=نام عملیات
|
||||
dashboard.operation_switch=تعویض
|
||||
|
|
|
@ -1407,7 +1407,6 @@ last_page=Viimeisin
|
|||
total=Yhteensä: %d
|
||||
|
||||
dashboard.statistic=Yhteenveto
|
||||
dashboard.operations=Huoltotoimet
|
||||
dashboard.system_status=Järjestelmän tila
|
||||
dashboard.operation_name=Toiminnon nimi
|
||||
dashboard.operation_switch=Vaihda
|
||||
|
|
|
@ -2712,7 +2712,6 @@ settings=Paramètres administrateur
|
|||
|
||||
dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails.
|
||||
dashboard.statistic=Résumé
|
||||
dashboard.operations=Opérations de maintenance
|
||||
dashboard.system_status=État du système
|
||||
dashboard.operation_name=Nom de l'Opération
|
||||
dashboard.operation_switch=Basculer
|
||||
|
|
|
@ -1266,7 +1266,6 @@ last_page=Utolsó
|
|||
total=Összesen: %d
|
||||
|
||||
dashboard.statistic=Összefoglaló
|
||||
dashboard.operations=Karbantartási műveletek
|
||||
dashboard.system_status=Rendszer Állapota
|
||||
dashboard.operation_name=Művelet Neve
|
||||
dashboard.operation_switch=Váltás
|
||||
|
|
|
@ -2233,7 +2233,6 @@ last_page=Ultima
|
|||
total=Totale: %d
|
||||
|
||||
dashboard.statistic=Riepilogo
|
||||
dashboard.operations=Operazioni di manutenzione
|
||||
dashboard.system_status=Stato del sistema
|
||||
dashboard.operation_name=Nome Operazione
|
||||
dashboard.operation_switch=Cambia
|
||||
|
|
|
@ -113,6 +113,7 @@ loading=読み込み中…
|
|||
error=エラー
|
||||
error404=アクセスしようとしたページは<strong>存在しない</strong>か、閲覧が<strong>許可されていません</strong>。
|
||||
go_back=戻る
|
||||
invalid_data=無効なデータ: %v
|
||||
|
||||
never=無し
|
||||
unknown=不明
|
||||
|
@ -143,13 +144,41 @@ name=名称
|
|||
value=値
|
||||
|
||||
filter=フィルター
|
||||
filter.clear=フィルターをクリア
|
||||
filter.is_archived=アーカイブ
|
||||
filter.not_archived=非アーカイブ
|
||||
filter.is_fork=フォーク
|
||||
filter.not_fork=非フォーク
|
||||
filter.is_mirror=ミラー
|
||||
filter.not_mirror=非ミラー
|
||||
filter.is_template=テンプレート
|
||||
filter.not_template=非テンプレート
|
||||
filter.public=公開
|
||||
filter.private=プライベート
|
||||
|
||||
no_results_found=見つかりません。
|
||||
|
||||
[search]
|
||||
search=検索…
|
||||
type_tooltip=検索タイプ
|
||||
fuzzy=あいまい
|
||||
fuzzy_tooltip=検索ワードに近い結果も含めます
|
||||
match=一致
|
||||
match_tooltip=検索ワードと完全に一致する結果のみ含めます
|
||||
repo_kind=リポジトリを検索...
|
||||
user_kind=ユーザーを検索...
|
||||
org_kind=組織を検索...
|
||||
team_kind=チームを検索…
|
||||
code_kind=コードを検索...
|
||||
code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
|
||||
code_search_by_git_grep=現在のコード検索結果は "git grep" で提供されています。 サイト管理者がリポジトリインデクサーを有効にすると、より良い結果が得られるかもしれません。
|
||||
package_kind=パッケージを検索...
|
||||
project_kind=プロジェクトを検索...
|
||||
branch_kind=ブランチを検索...
|
||||
commit_kind=コミットを検索...
|
||||
runner_kind=ランナーを検索...
|
||||
no_results=一致する結果が見つかりませんでした
|
||||
keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
|
||||
|
||||
[aria]
|
||||
navbar=ナビゲーションバー
|
||||
|
@ -256,6 +285,7 @@ email_title=メール設定
|
|||
smtp_addr=SMTPホスト
|
||||
smtp_port=SMTPポート
|
||||
smtp_from=メール送信者
|
||||
smtp_from_invalid=「メール送信者」のアドレスが無効です
|
||||
smtp_from_helper=Giteaが使用するメールアドレス。 メールアドレスのみ、または、 "名前" <email@example.com> の形式で入力してください。
|
||||
mailer_user=SMTPユーザー名
|
||||
mailer_password=SMTPパスワード
|
||||
|
@ -315,6 +345,7 @@ env_config_keys=環境設定
|
|||
env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます:
|
||||
|
||||
[home]
|
||||
nav_menu=ナビゲーションメニュー
|
||||
uname_holder=ユーザー名またはメールアドレス
|
||||
password_holder=パスワード
|
||||
switch_dashboard_context=ダッシュボードのコンテキスト切替
|
||||
|
@ -618,6 +649,23 @@ block.block.org=組織向けにユーザーをブロック
|
|||
block.block.failure=ユーザーのブロックに失敗しました: %s
|
||||
block.unblock=ブロックを解除
|
||||
block.unblock.failure=ユーザーのブロック解除に失敗しました: %s
|
||||
block.blocked=あなたはこのユーザーをブロックしています。
|
||||
block.title=ユーザーをブロックする
|
||||
block.info=ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。
|
||||
block.info_1=ユーザーをブロックすることで、あなたのアカウントとあなたのリポジトリに対する以下の行為を阻止します:
|
||||
block.info_2=あなたのアカウントのフォロー
|
||||
block.info_3=あなたのユーザー名で@メンションして通知を送ること
|
||||
block.info_4=そのユーザーのリポジトリに、あなたを共同作業者として招待すること
|
||||
block.info_5=リポジトリへの、スター、フォーク、ウォッチ
|
||||
block.info_6=イシューやプルリクエストの作成、コメント投稿
|
||||
block.info_7=イシューやプルリクエストでの、あなたのコメントに対するリアクションの送信
|
||||
block.user_to_block=ブロックするユーザー
|
||||
block.note=メモ
|
||||
block.note.title=メモ(任意):
|
||||
block.note.info=メモはブロックされるユーザーには表示されません。
|
||||
block.note.edit=メモを編集
|
||||
block.list=ブロックしたユーザー
|
||||
block.list.none=ブロックしているユーザーはいません。
|
||||
|
||||
[settings]
|
||||
profile=プロフィール
|
||||
|
@ -661,8 +709,8 @@ language=言語
|
|||
ui=テーマ
|
||||
hidden_comment_types=非表示にするコメントの種類
|
||||
hidden_comment_types_description=ここでチェックを入れたコメントの種類は、イシューのページには表示されません。 たとえば「ラベル」にチェックを入れると、「<ユーザー> が <ラベル> を追加/削除」といったコメントはすべて除去されます。
|
||||
hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照されたというコメント
|
||||
hidden_comment_types.issue_ref_tooltip=このイシューに関連付けるブランチやタグをユーザーが変更したというコメント
|
||||
hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照された、というコメント
|
||||
hidden_comment_types.issue_ref_tooltip=このイシューのブランチやタグへの関連付けをユーザーが変更した、というコメント
|
||||
comment_type_group_reference=参照
|
||||
comment_type_group_label=ラベル
|
||||
comment_type_group_milestone=マイルストーン
|
||||
|
@ -732,7 +780,7 @@ add_email_success=新しいメールアドレスを追加しました。
|
|||
email_preference_set_success=メール設定を保存しました。
|
||||
add_openid_success=新しいOpenIDアドレスを追加しました。
|
||||
keep_email_private=メールアドレスを隠す
|
||||
keep_email_private_popup=これによりプロフィールでメールアドレスが隠され、Webインターフェースでのプルリクエスト作成やファイル編集でもメールアドレスが隠されます。 プッシュ済みのコミットは変更されません。
|
||||
keep_email_private_popup=あなたのプロフィールからメールアドレスが隠され、Webインターフェースを使ったプルリクエスト作成やファイル編集でも、メールアドレスが隠されます。 プッシュ済みのコミットは変更されません。 コミットであなたのアカウントに関連付ける場合は %s を使用してください。
|
||||
openid_desc=OpenIDを使うと外部プロバイダーに認証を委任することができます。
|
||||
|
||||
manage_ssh_keys=SSHキーの管理
|
||||
|
@ -956,6 +1004,7 @@ fork_branch=フォークにクローンされるブランチ
|
|||
all_branches=すべてのブランチ
|
||||
fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
|
||||
use_template=このテンプレートを使用
|
||||
open_with_editor=%s で開く
|
||||
download_zip=ZIPファイルをダウンロード
|
||||
download_tar=TAR.GZファイルをダウンロード
|
||||
download_bundle=バンドルをダウンロード
|
||||
|
@ -1008,6 +1057,7 @@ watchers=ウォッチャー
|
|||
stargazers=スターゲイザー
|
||||
stars_remove_warning=これを指定すると、このリポジトリのスターはすべて削除されます。
|
||||
forks=フォーク
|
||||
stars=スター
|
||||
reactions_more=さらに %d 件
|
||||
unit_disabled=サイト管理者がこのリポジトリセクションを無効にしています。
|
||||
language_other=その他
|
||||
|
@ -1039,7 +1089,7 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま
|
|||
desc.private=プライベート
|
||||
desc.public=公開
|
||||
desc.template=テンプレート
|
||||
desc.internal=組織内
|
||||
desc.internal=内部
|
||||
desc.archived=アーカイブ
|
||||
desc.sha256=SHA256
|
||||
|
||||
|
@ -1257,6 +1307,8 @@ editor.file_editing_no_longer_exists=編集中のファイル "%s" が、もう
|
|||
editor.file_deleting_no_longer_exists=削除しようとしたファイル "%s" が、すでにリポジトリ内にありません。
|
||||
editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 <a target="_blank" rel="noopener noreferrer" href="%s">ここをクリック</a>して何が変更されたか確認するか、<strong>もう一度"変更をコミット"をクリック</strong>して上書きします。
|
||||
editor.file_already_exists=ファイル "%s" は、このリポジトリに既に存在します。
|
||||
editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。
|
||||
editor.push_out_of_date=このプッシュは最新ではないようです。
|
||||
editor.commit_empty_file_header=空ファイルのコミット
|
||||
editor.commit_empty_file_text=コミットしようとしているファイルは空です。 続行しますか?
|
||||
editor.no_changes_to_show=表示する変更箇所はありません。
|
||||
|
@ -1281,6 +1333,7 @@ commits.commits=コミット
|
|||
commits.no_commits=共通のコミットはありません。 "%s" と "%s" の履歴はすべて異なっています。
|
||||
commits.nothing_to_compare=二つのブランチは同じ内容です。
|
||||
commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"`
|
||||
commits.search_branch=このブランチ
|
||||
commits.search_all=すべてのブランチ
|
||||
commits.author=作成者
|
||||
commits.message=メッセージ
|
||||
|
@ -1339,6 +1392,7 @@ projects.column.new=新しい列
|
|||
projects.column.set_default=デフォルトに設定
|
||||
projects.column.set_default_desc=この列を未分類のイシューやプルリクエストが入るデフォルトの列にします
|
||||
projects.column.delete=列を削除
|
||||
projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?
|
||||
projects.column.color=カラー
|
||||
projects.open=オープン
|
||||
projects.close=クローズ
|
||||
|
@ -1738,7 +1792,7 @@ pulls.is_checking=マージのコンフリクトを確認中です。 少し待
|
|||
pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
|
||||
pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
|
||||
pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
|
||||
pulls.required_status_check_missing=必要なステータスチェックが見つかりません。
|
||||
pulls.required_status_check_missing=必要なチェックがいくつか抜けています。
|
||||
pulls.required_status_check_administrator=管理者であるため、このプルリクエストをマージすることは可能です。
|
||||
pulls.blocked_by_approvals=このプルリクエストはまだ承認数が足りません。 %[1]d/%[2]dの承認を得ています。
|
||||
pulls.blocked_by_rejection=このプルリクエストは公式レビューアにより変更要請されています。
|
||||
|
@ -1907,6 +1961,7 @@ wiki.original_git_entry_tooltip=フレンドリーリンクを使用する代わ
|
|||
activity=アクティビティ
|
||||
activity.navbar.pulse=Pulse
|
||||
activity.navbar.contributors=貢献者
|
||||
activity.navbar.recent_commits=最近のコミット
|
||||
activity.period.filter_label=期間:
|
||||
activity.period.daily=1日
|
||||
activity.period.halfweekly=3日
|
||||
|
@ -2571,6 +2626,7 @@ component_loading_failed=%sを読み込めませんでした
|
|||
component_loading_info=少し時間がかかるかもしれません…
|
||||
component_failed_to_load=予期しないエラーが発生しました。
|
||||
contributors.what=実績
|
||||
recent_commits.what=最近のコミット
|
||||
|
||||
[org]
|
||||
org_name_holder=組織名
|
||||
|
@ -2684,6 +2740,7 @@ teams.add_nonexistent_repo=追加しようとしているリポジトリは存
|
|||
teams.add_duplicate_users=ユーザーは既にチームのメンバーです。
|
||||
teams.repos.none=このチームがアクセスできるリポジトリはありません。
|
||||
teams.members.none=このチームにはメンバーがいません。
|
||||
teams.members.blocked_user=組織によってブロックされているため、ユーザーを追加できません。
|
||||
teams.specific_repositories=指定したリポジトリ
|
||||
teams.specific_repositories_helper=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでに<i>すべてのリポジトリ</i>で追加されたリポジトリは自動的に除去<strong>されません</strong>。
|
||||
teams.all_repositories=すべてのリポジトリ
|
||||
|
@ -2719,7 +2776,6 @@ settings=管理設定
|
|||
|
||||
dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。
|
||||
dashboard.statistic=サマリー
|
||||
dashboard.operations=メンテナンス操作
|
||||
dashboard.system_status=システム状況
|
||||
dashboard.operation_name=操作の名称
|
||||
dashboard.operation_switch=切り替え
|
||||
|
@ -2905,12 +2961,12 @@ packages.size=サイズ
|
|||
packages.published=配布
|
||||
|
||||
defaulthooks=デフォルトWebhook
|
||||
defaulthooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義されたWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
|
||||
defaulthooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
|
||||
defaulthooks.add_webhook=デフォルトWebhookの追加
|
||||
defaulthooks.update_webhook=デフォルトWebhookの更新
|
||||
|
||||
systemhooks=システムWebhook
|
||||
systemhooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義したWebhookはシステム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
|
||||
systemhooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookは、システム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。
|
||||
systemhooks.add_webhook=システムWebhookを追加
|
||||
systemhooks.update_webhook=システムWebhookを更新
|
||||
|
||||
|
@ -3010,6 +3066,7 @@ auths.tip.nextcloud=新しいOAuthコンシューマーを、インスタンス
|
|||
auths.tip.dropbox=新しいアプリケーションを https://www.dropbox.com/developers/apps から登録してください。
|
||||
auths.tip.facebook=新しいアプリケーションを https://developers.facebook.com/apps で登録し、"Facebook Login"を追加してください。
|
||||
auths.tip.github=新しいOAuthアプリケーションを https://github.com/settings/applications/new から登録してください。
|
||||
auths.tip.gitlab_new=新しいアプリケーションを https://gitlab.com/-/profile/applications から登録してください。
|
||||
auths.tip.google_plus=OAuth2クライアント資格情報を、Google APIコンソール https://console.developers.google.com/ から取得してください。
|
||||
auths.tip.openid_connect=OpenID Connect DiscoveryのURL (<server>/.well-known/openid-configuration) をエンドポイントとして指定してください
|
||||
auths.tip.twitter=https://dev.twitter.com/apps へアクセスしてアプリケーションを作成し、“Allow this application to be used to Sign in with Twitter”オプションを有効にしてください。
|
||||
|
@ -3145,6 +3202,7 @@ config.picture_config=画像とアバターの設定
|
|||
config.picture_service=画像サービス
|
||||
config.disable_gravatar=Gravatarが無効
|
||||
config.enable_federated_avatar=フェデレーテッド・アバター有効
|
||||
config.open_with_editor_app_help=クローンメニューの「~で開く」に表示するエディタ。 空白のままにするとデフォルトが使用されます。 展開するとデフォルトを確認できます。
|
||||
|
||||
config.git_config=Git設定
|
||||
config.git_disable_diff_highlight=Diffのシンタックスハイライトが無効
|
||||
|
@ -3284,9 +3342,9 @@ raw_seconds=秒
|
|||
raw_minutes=分
|
||||
|
||||
[dropzone]
|
||||
default_message=ここにファイルをドロップまたはクリックしてアップロードします。
|
||||
default_message=ファイルをここにドロップ、またはここをクリックしてアップロード
|
||||
invalid_input_type=この種類のファイルはアップロードできません。
|
||||
file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) が最大サイズ ({{maxFilesize}} MB) を超えています。
|
||||
file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) は、最大サイズ ({{maxFilesize}} MB) を超えています。
|
||||
remove_file=ファイル削除
|
||||
|
||||
[notification]
|
||||
|
@ -3311,7 +3369,7 @@ error.no_committer_account=コミッターのメールアドレスに対応す
|
|||
error.no_gpg_keys_found=この署名に対応する既知のキーがデータベースに存在しません
|
||||
error.not_signed_commit=署名されたコミットではありません
|
||||
error.failed_retrieval_gpg_keys=コミッターのアカウントに登録されたキーを取得できませんでした
|
||||
error.probable_bad_signature=警告! このIDの鍵はデータベースに登録されていますが、その鍵でコミットの検証が通りません! これは疑わしいコミットです。
|
||||
error.probable_bad_signature=警告! このIDに該当する鍵がデータベースにありますが、コミットの検証が通りません! これは疑わしいコミットです。
|
||||
error.probable_bad_default_signature=警告! これはデフォルト鍵のIDですが、デフォルト鍵ではコミットの検証が通りません! これは疑わしいコミットです。
|
||||
|
||||
[units]
|
||||
|
@ -3324,7 +3382,7 @@ title=パッケージ
|
|||
desc=リポジトリ パッケージを管理します。
|
||||
empty=パッケージはまだありません。
|
||||
empty.documentation=パッケージレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
|
||||
empty.repo=パッケージはアップロードしたけども、ここに表示されない? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
|
||||
empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
|
||||
registry.documentation=%sレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
|
||||
filter.type=タイプ
|
||||
filter.type.all=すべて
|
||||
|
@ -3543,6 +3601,7 @@ runs.scheduled=スケジュール済み
|
|||
runs.pushed_by=pushed by
|
||||
runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s
|
||||
runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s
|
||||
runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。
|
||||
runs.actor=アクター
|
||||
runs.status=ステータス
|
||||
runs.actors_no_select=すべてのアクター
|
||||
|
|
|
@ -2693,7 +2693,6 @@ settings=Administratora iestatījumi
|
|||
|
||||
dashboard.new_version_hint=Ir pieejama Gitea versija %s, pašreizējā versija %s. Papildus informācija par jauno versiju ir pieejama <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">mājas lapā</a>.
|
||||
dashboard.statistic=Kopsavilkums
|
||||
dashboard.operations=Uzturēšanas darbības
|
||||
dashboard.system_status=Sistēmas statuss
|
||||
dashboard.operation_name=Darbības nosaukums
|
||||
dashboard.operation_switch=Pārslēgt
|
||||
|
|
|
@ -2135,7 +2135,6 @@ last_page=Laatste
|
|||
total=Totaal: %d
|
||||
|
||||
dashboard.statistic=Overzicht
|
||||
dashboard.operations=Onderhoudswerkzaamheden
|
||||
dashboard.system_status=Systeemtatus
|
||||
dashboard.operation_name=Bewerking naam
|
||||
dashboard.operation_switch=Omschakelen
|
||||
|
|
|
@ -2010,7 +2010,6 @@ last_page=Ostatnia
|
|||
total=Ogółem: %d
|
||||
|
||||
dashboard.statistic=Podsumowanie
|
||||
dashboard.operations=Operacje konserwacji
|
||||
dashboard.system_status=Status strony
|
||||
dashboard.operation_name=Nazwa operacji
|
||||
dashboard.operation_switch=Przełącz
|
||||
|
|
|
@ -2648,7 +2648,6 @@ settings=Configurações de Administrador
|
|||
|
||||
dashboard.new_version_hint=Uma nova versão está disponível: %s. Versão atual: %s. Visite <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">o blog</a> para mais informações.
|
||||
dashboard.statistic=Resumo
|
||||
dashboard.operations=Operações de manutenção
|
||||
dashboard.system_status=Status do sistema
|
||||
dashboard.operation_name=Nome da operação
|
||||
dashboard.operation_switch=Trocar
|
||||
|
|
|
@ -1233,6 +1233,8 @@ file_view_rendered=Ver resultado processado
|
|||
file_view_raw=Ver em bruto
|
||||
file_permalink=Ligação permanente
|
||||
file_too_large=O ficheiro é demasiado grande para ser apresentado.
|
||||
code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s
|
||||
code_preview_line_in=Linha %[1]d em %[2]s
|
||||
invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis`
|
||||
invisible_runes_description=`Este ficheiro contém caracteres Unicode indistinguíveis para humanos mas que podem ser processados de forma diferente por um computador. Se acha que é intencional, pode ignorar este aviso com segurança. Use o botão Revelar para os mostrar.`
|
||||
ambiguous_runes_header=`Este ficheiro contém caracteres Unicode ambíguos`
|
||||
|
@ -2775,6 +2777,7 @@ teams.invite.by=Convidado(a) por %s
|
|||
teams.invite.description=Clique no botão abaixo para se juntar à equipa.
|
||||
|
||||
[admin]
|
||||
maintenance=Manutenção
|
||||
dashboard=Painel de controlo
|
||||
self_check=Auto-verificação
|
||||
identity_access=Identidade e acesso
|
||||
|
@ -2798,7 +2801,7 @@ settings=Configurações de administração
|
|||
|
||||
dashboard.new_version_hint=O Gitea %s está disponível, você está a correr a versão %s. Verifique o <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a> para mais detalhes.
|
||||
dashboard.statistic=Resumo
|
||||
dashboard.operations=Operações de manutenção
|
||||
dashboard.maintenance_operations=Operações de manutenção
|
||||
dashboard.system_status=Estado do sistema
|
||||
dashboard.operation_name=Nome da operação
|
||||
dashboard.operation_switch=Comutar
|
||||
|
@ -3305,6 +3308,7 @@ notices.op=Op.
|
|||
notices.delete_success=As notificações do sistema foram eliminadas.
|
||||
|
||||
self_check.no_problem_found=Nenhum problema encontrado até agora.
|
||||
self_check.startup_warnings=Alertas do arranque:
|
||||
self_check.database_collation_mismatch=Supor que a base de dados usa a colação: %s
|
||||
self_check.database_collation_case_insensitive=A base de dados está a usar a colação %s, que é insensível à diferença entre maiúsculas e minúsculas. Embora o Gitea possa trabalhar com ela, pode haver alguns casos raros que não funcionem como esperado.
|
||||
self_check.database_inconsistent_collation_columns=A base de dados está a usar a colação %s, mas estas colunas estão a usar colações diferentes. Isso poderá causar alguns problemas inesperados.
|
||||
|
|
|
@ -2634,7 +2634,6 @@ total=Всего: %d
|
|||
|
||||
dashboard.new_version_hint=Доступна новая версия Gitea %s, вы используете %s. Более подробную информацию читайте в <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">блоге</a>.
|
||||
dashboard.statistic=Статистика
|
||||
dashboard.operations=Операции
|
||||
dashboard.system_status=Состояние системы
|
||||
dashboard.operation_name=Имя операции
|
||||
dashboard.operation_switch=Переключить
|
||||
|
|
|
@ -2024,7 +2024,6 @@ last_page=පසුගිය
|
|||
total=මුළු: %d
|
||||
|
||||
dashboard.statistic=සාරාංශය
|
||||
dashboard.operations=නඩත්තු මෙහෙයුම්
|
||||
dashboard.system_status=පද්ධතියේ තත්වය
|
||||
dashboard.operation_name=මෙහෙයුමේ නම
|
||||
dashboard.operation_switch=මාරුවන්න
|
||||
|
|
|
@ -1647,7 +1647,6 @@ last_page=Sista
|
|||
total=Totalt: %d
|
||||
|
||||
dashboard.statistic=Översikt
|
||||
dashboard.operations=Operationer för underhåll
|
||||
dashboard.system_status=Status
|
||||
dashboard.operation_name=Operationsnamn
|
||||
dashboard.operation_switch=Byt till
|
||||
|
|
|
@ -2687,7 +2687,6 @@ settings=Yönetici Ayarları
|
|||
|
||||
dashboard.new_version_hint=Gitea %s şimdi hazır, %s çalıştırıyorsunuz. Ayrıntılar için <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a>'a bakabilirsiniz.
|
||||
dashboard.statistic=Özet
|
||||
dashboard.operations=Bakım İşlemleri
|
||||
dashboard.system_status=Sistem Durumu
|
||||
dashboard.operation_name=İşlem Adı
|
||||
dashboard.operation_switch=Geç
|
||||
|
|
|
@ -2074,7 +2074,6 @@ last_page=Остання
|
|||
total=Разом: %d
|
||||
|
||||
dashboard.statistic=Підсумок
|
||||
dashboard.operations=Технічне обслуговування
|
||||
dashboard.system_status=Статус системи
|
||||
dashboard.operation_name=Назва операції
|
||||
dashboard.operation_switch=Перемкнути
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue