diff --git a/.ignore b/.ignore
index 5c945ab9810ee..5b96dabd38aa2 100644
--- a/.ignore
+++ b/.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
diff --git a/cmd/dump.go b/cmd/dump.go
index 69ecdcec1244d..da0a51d9ce934 100644
--- a/cmd/dump.go
+++ b/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,63 +87,51 @@ 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)
+ quite := ctx.Bool("quiet")
+ verbose := ctx.Bool("verbose")
+ if verbose && quite {
+ fatal("Option --quiet and --verbose cannot both be set")
}
- if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
- fatal("Setting console logger to stderr failed: %v", err)
+
+ // 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")
}
- // 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)
+ 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()
}
- 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
+ setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
- verbose := ctx.Bool("verbose")
- if verbose && ctx.Bool("quiet") {
- return fmt.Errorf("--quiet and --verbose cannot both be set")
- }
+ 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
}
diff --git a/docs/content/installation/with-docker.en-us.md b/docs/content/installation/with-docker.en-us.md
index e67f5bccb2a41..e8a80f7c9694a 100644
--- a/docs/content/installation/with-docker.en-us.md
+++ b/docs/content/installation/with-docker.en-us.md
@@ -545,7 +545,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 +560,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.)
diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 83fbab5d36c8a..06ac31bc6f325 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -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
diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go
index b02be2851a75e..9c015582f1bdb 100644
--- a/models/asymkey/gpg_key_common.go
+++ b/models/asymkey/gpg_key_common.go
@@ -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 ""
+}
diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go
index dee74bc281d0c..d3fbb01d82b1d 100644
--- a/models/asymkey/gpg_key_test.go
+++ b/models/asymkey/gpg_key_test.go
@@ -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},
+ }))
+}
diff --git a/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
new file mode 100644
index 0000000000000..6feaeb39f0598
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml
@@ -0,0 +1,9 @@
+-
+ id: 1
+ project_id: 1
+ issue_id: 1
+
+-
+ id: 2
+ project_id: 1
+ issue_id: 1
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 0daa799ff6754..387cd96a53471 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_22"
+ "code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@@ -572,6 +573,10 @@ var migrations = []Migration{
NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
// v293 -> v294
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
+
+ // Gitea 1.22.0 ends at 294
+
+ NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go
new file mode 100644
index 0000000000000..b7948bd4dd248
--- /dev/null
+++ b/models/migrations/v1_23/main_test.go
@@ -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)
+}
diff --git a/models/migrations/v1_23/v294.go b/models/migrations/v1_23/v294.go
new file mode 100644
index 0000000000000..f2a54f6d23ecc
--- /dev/null
+++ b/models/migrations/v1_23/v294.go
@@ -0,0 +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"
+)
+
+// 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
+ }
+ }
+ }
+
+ // 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))
+}
diff --git a/models/migrations/v1_23/v294_test.go b/models/migrations/v1_23/v294_test.go
new file mode 100644
index 0000000000000..d9a44ad866bb1
--- /dev/null
+++ b/models/migrations/v1_23/v294_test.go
@@ -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)
+}
diff --git a/models/user/email_address.go b/models/user/email_address.go
index d26549f383db4..08771efe99d15 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -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)
diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go
index a353ced63169f..9d796a0c1832e 100644
--- a/modules/charset/escape_test.go
+++ b/modules/charset/escape_test.go
@@ -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)
})
}
}
diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go
index f6e782a5a460d..3ddb47acbbe77 100644
--- a/modules/csv/csv_test.go
+++ b/modules/csv/csv_test.go
@@ -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,
},
{
diff --git a/modules/dump/dumper.go b/modules/dump/dumper.go
new file mode 100644
index 0000000000000..47730851fb369
--- /dev/null
+++ b/modules/dump/dumper.go
@@ -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
+}
diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go
new file mode 100644
index 0000000000000..b444fa2de538e
--- /dev/null
+++ b/modules/dump/dumper_test.go
@@ -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))
+}
diff --git a/modules/git/commit.go b/modules/git/commit.go
index ef2676762c174..5f442b0e1aaca 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -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
}
diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go
index 33ef2f4487020..d7b945ed6b8c3 100644
--- a/modules/git/commit_convert_gogit.go
+++ b/modules/git/commit_convert_gogit.go
@@ -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(),
}
diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go
index 56c41dc473b1d..f1f4a0e588dec 100644
--- a/modules/git/commit_reader.go
+++ b/modules/git/commit_reader.go
@@ -99,7 +99,7 @@ readLoop:
}
}
commit.CommitMessage = messageSB.String()
- commit.Signature = &CommitGPGSignature{
+ commit.Signature = &CommitSignature{
Signature: signatureSB.String(),
Payload: payloadSB.String(),
}
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index e8c5ce6fb85b4..2026a4c9f5c1a 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -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,
}
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index 785c3442a79c5..0117cb902d4a6 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -315,7 +315,7 @@ qbHDASXl
Type: "tag",
Tagger: parseSignatureFromCommitLine("Foo Bar 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
diff --git a/modules/git/tag.go b/modules/git/tag.go
index 94e5cd7c63b78..f7666aa89b126 100644
--- a/modules/git/tag.go
+++ b/modules/git/tag.go
@@ -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
}
- }
- 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]),
+ 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.Message = tag.Message[:idx+1]
+ 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
+ }
+ payload, msg, sign := parsePayloadSignature(data, pos)
+ tag.Message = msg
+ if len(sign) > 0 {
+ tag.Signature = &CommitSignature{Signature: sign, Payload: payload}
}
return tag, nil
}
diff --git a/modules/git/tag_test.go b/modules/git/tag_test.go
index f980b0c560c4f..ba02c28946de2 100644
--- a/modules/git/tag_test.go
+++ b/modules/git/tag_test.go
@@ -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 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 1484553735 +0100
@@ -37,37 +41,57 @@ tagger Lucas Michot 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 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 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)
}
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 5f35e8073b6cb..74c957dde65e4 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -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
}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 21bd6206e0eb7..56aa1cb49cf9c 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
var defaultProcessors = []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
+ codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor,
linkProcessor,
diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go
new file mode 100644
index 0000000000000..d9da24ea34495
--- /dev/null
+++ b/modules/markup/html_codepreview.go
@@ -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 "{TextBefore}
{TextAfter}
" (the parent could also be "li")
+ // then it is resolved as: "{TextBefore}
{TextAfter}
",
+ // 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
+ }
+}
diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go
new file mode 100644
index 0000000000000..d33630d0401b4
--- /dev/null
+++ b/modules/markup/html_codepreview_test.go
@@ -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 "code preview
", 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", "code preview
")
+ test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`)
+}
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index c664758a277a0..a9c9024982871 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -511,9 +511,17 @@ func TestMathBlock(t *testing.T) {
`\(a\) \(b\)`,
`a
b
` + nl,
},
+ {
+ `$a$.`,
+ `a
.
` + nl,
+ },
+ {
+ `.$a$`,
+ `.$a$
` + nl,
+ },
{
`$a a$b b$`,
- `a a$b b
` + nl,
+ `$a a$b b$
` + nl,
},
{
`a a$b b`,
@@ -521,7 +529,15 @@ func TestMathBlock(t *testing.T) {
},
{
`a$b $a a$b b$`,
- `a$b a a$b b
` + nl,
+ `a$b $a a$b b$
` + nl,
+ },
+ {
+ "a$x$",
+ `a$x$
` + nl,
+ },
+ {
+ "$x$a",
+ `$x$a
` + nl,
},
{
"$$a$$",
diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go
index 0ac25c2b2ac86..862234e69bb74 100644
--- a/modules/markup/markdown/math/inline_parser.go
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -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)
}
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 0f0bf557403e4..005fcc278b973 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -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
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 79a2ba0dfb8d2..77fbdf4520fc6 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -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("code")
+
+ // 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")
diff --git a/modules/setting/log.go b/modules/setting/log.go
index e404074b72f08..50c57799945e5 100644
--- a/modules/setting/log.go
+++ b/modules/setting/log.go
@@ -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)
diff --git a/modules/timeutil/timestamp.go b/modules/timeutil/timestamp.go
index 27a80b668238b..e77652b24f230 100644
--- a/modules/timeutil/timestamp.go
+++ b/modules/timeutil/timestamp.go
@@ -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
diff --git a/modules/translation/mock.go b/modules/translation/mock.go
index 18fbc1044add5..f457271ea5478 100644
--- a/modules/translation/mock.go
+++ b/modules/translation/mock.go
@@ -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...)
+}
diff --git a/modules/util/util.go b/modules/util/util.go
index b6e730eb54bbd..3921002e2a5c2 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -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 comparable](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
diff --git a/options/license/AMD-newlib b/options/license/AMD-newlib
new file mode 100644
index 0000000000000..1b2f1abd6fd0d
--- /dev/null
+++ b/options/license/AMD-newlib
@@ -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.
diff --git a/options/license/OAR b/options/license/OAR
new file mode 100644
index 0000000000000..ca5c4b9617f43
--- /dev/null
+++ b/options/license/OAR
@@ -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.
diff --git a/options/license/xzoom b/options/license/xzoom
new file mode 100644
index 0000000000000..f312dedbc2ce8
--- /dev/null
+++ b/options/license/xzoom
@@ -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.
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 4abf8137253cb..82a8fe5d45628 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -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 blogu 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
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 4d446db86ffb5..9a09c2922e3b0 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -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 Blog.
dashboard.statistic=Übersicht
-dashboard.operations=Wartungsoperationen
dashboard.system_status=System-Status
dashboard.operation_name=Name der Operation
dashboard.operation_switch=Wechseln
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 1199d84581875..6ce5ae1ce9605 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -2687,7 +2687,6 @@ settings=Ρυθμίσεις Διαχειριστή
dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε στο blog για περισσότερες λεπτομέρειες.
dashboard.statistic=Περίληψη
-dashboard.operations=Λειτουργίες Συντήρησης
dashboard.system_status=Κατάσταση Συστήματος
dashboard.operation_name=Όνομα Λειτουργίας
dashboard.operation_switch=Αλλαγή
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 39b9855186145..0a3d12d7a40ff 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -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`
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index ce50b71ec418d..fc78e1d4397f2 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -2672,7 +2672,6 @@ settings=Configuración de Admin
dashboard.new_version_hint=Gitea %s ya está disponible, estás ejecutando %s. Revisa el blog 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
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 31122841a7e5f..d19eb356d2829 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -2064,7 +2064,6 @@ last_page=واپسین
total=مجموع: %d
dashboard.statistic=چکیده
-dashboard.operations=عملیاتهای نگهداری
dashboard.system_status=وضعیت سامانه
dashboard.operation_name=نام عملیات
dashboard.operation_switch=تعویض
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index 00581f49fce67..f283209908c69 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -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
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 062c818bd4bbc..dc664029014d7 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -2712,7 +2712,6 @@ settings=Paramètres administrateur
dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez le blog 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
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 93e3b42115153..fb229090d4eaf 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -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
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index cc379e8109f11..9a22995dfba4c 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -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
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index d5c2885f00405..eddad3507308f 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -2719,7 +2719,6 @@ settings=管理設定
dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は ブログ を確認してください。
dashboard.statistic=サマリー
-dashboard.operations=メンテナンス操作
dashboard.system_status=システム状況
dashboard.operation_name=操作の名称
dashboard.operation_switch=切り替え
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 0a2729980b860..9a1509001292b 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -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 mājas lapā.
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
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 255a3db9fa2ca..6b5122a86f616 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -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
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 1496877fd59e3..a1d7e95842feb 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -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
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 0d1614df3f037..45f1c3b3f8f68 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -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 o blog 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
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index ea80cd7abb63a..09b9d4e3ce6c3 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -2775,6 +2775,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 +2799,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 blog 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 +3306,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.
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 74c4c9c9350d0..818dad1147dcc 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -2634,7 +2634,6 @@ total=Всего: %d
dashboard.new_version_hint=Доступна новая версия Gitea %s, вы используете %s. Более подробную информацию читайте в блоге.
dashboard.statistic=Статистика
-dashboard.operations=Операции
dashboard.system_status=Состояние системы
dashboard.operation_name=Имя операции
dashboard.operation_switch=Переключить
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 7e82cfe3d62d5..99559802c521e 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -2024,7 +2024,6 @@ last_page=පසුගිය
total=මුළු: %d
dashboard.statistic=සාරාංශය
-dashboard.operations=නඩත්තු මෙහෙයුම්
dashboard.system_status=පද්ධතියේ තත්වය
dashboard.operation_name=මෙහෙයුමේ නම
dashboard.operation_switch=මාරුවන්න
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index e48d84ff78292..9234e9aa58998 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -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
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 5a5036f87d13a..119e1ef150107 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -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 blog'a bakabilirsiniz.
dashboard.statistic=Özet
-dashboard.operations=Bakım İşlemleri
dashboard.system_status=Sistem Durumu
dashboard.operation_name=İşlem Adı
dashboard.operation_switch=Geç
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 09561a79028ea..e8a3acedda967 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -2074,7 +2074,6 @@ last_page=Остання
total=Разом: %d
dashboard.statistic=Підсумок
-dashboard.operations=Технічне обслуговування
dashboard.system_status=Статус системи
dashboard.operation_name=Назва операції
dashboard.operation_switch=Перемкнути
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 406e9ac8f2956..01058d48d2db6 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -2711,7 +2711,6 @@ settings=管理设置
dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 博客 了解详情。
dashboard.statistic=摘要
-dashboard.operations=维护操作
dashboard.system_status=系统状态
dashboard.operation_name=操作名称
dashboard.operation_switch=开关
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 0511fa44aeb2c..0447a7d8b765f 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -2439,7 +2439,6 @@ total=總計:%d
dashboard.new_version_hint=現已推出 Gitea %s,您正在執行 %s。詳情請參閱部落格的說明。
dashboard.statistic=摘要
-dashboard.operations=維護作業
dashboard.system_status=系統狀態
dashboard.operation_name=作業名稱
dashboard.operation_switch=開關
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 80504b9c330d4..822e368fa84ae 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -12,6 +12,7 @@ import (
"strings"
"time"
+ actions_model "code.gitea.io/gitea/models/actions"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
@@ -31,6 +32,7 @@ import (
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/issue"
@@ -1035,6 +1037,9 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
return err
}
+ if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+ log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
} else {
if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
@@ -1042,6 +1047,11 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
return err
}
+ if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+ if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+ log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
+ }
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
}
}
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 935e6d78fc412..1887e4d95da3d 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -87,9 +88,16 @@ func RefBlame(ctx *context.Context) {
ctx.Data["IsBlame"] = true
- ctx.Data["FileSize"] = blob.Size()
+ fileSize := blob.Size()
+ ctx.Data["FileSize"] = fileSize
ctx.Data["FileName"] = blob.Name()
+ if fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["IsFileTooLarge"] = true
+ ctx.HTML(http.StatusOK, tplRepoHome)
+ return
+ }
+
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
ctx.Data["NumLinesSet"] = true
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 9d65427b8f348..46f0208453585 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -81,7 +81,7 @@ func Search(ctx *context.Context) {
// UpdatedUnix: not supported yet
// Language: not supported yet
// Color: not supported yet
- Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")),
+ Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
})
}
}
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 32049cf0a423c..6dddade066e8e 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -287,22 +287,19 @@ func LFSFileGet(ctx *context.Context) {
st := typesniffer.DetectContentType(buf)
ctx.Data["IsTextFile"] = st.IsText()
- isRepresentableAsText := st.IsRepresentableAsText()
-
- fileSize := meta.Size
ctx.Data["FileSize"] = meta.Size
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
switch {
- case isRepresentableAsText:
- if st.IsSvgImage() {
- ctx.Data["IsImageFile"] = true
- }
-
- if fileSize >= setting.UI.MaxDisplayFileSize {
+ case st.IsRepresentableAsText():
+ if meta.Size >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
+ if st.IsSvgImage() {
+ ctx.Data["IsImageFile"] = true
+ }
+
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
// Building code view blocks with line number on server side.
@@ -338,6 +335,8 @@ func LFSFileGet(ctx *context.Context) {
ctx.Data["IsAudioFile"] = true
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
+ default:
+ // TODO: the logic is not the same as "renderFile" in "view.go"
}
ctx.HTML(http.StatusOK, tplSettingsLFSFile)
}
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index e045e3b8dcc09..00a5282f34e77 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -13,6 +13,7 @@ import (
"time"
"code.gitea.io/gitea/models"
+ actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
@@ -29,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
+ actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@@ -897,6 +899,10 @@ func SettingsPost(ctx *context.Context) {
return
}
+ if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+ log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
+
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@@ -915,6 +921,12 @@ func SettingsPost(ctx *context.Context) {
return
}
+ if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+ if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+ log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
+ }
+
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 93e0f5bcbdbdf..8aa9dbb1be333 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -482,17 +482,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
switch {
case isRepresentableAsText:
+ if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["IsFileTooLarge"] = true
+ break
+ }
+
if fInfo.st.IsSvgImage() {
ctx.Data["IsImageFile"] = true
ctx.Data["CanCopyContent"] = true
ctx.Data["HasSourceRenderedToggle"] = true
}
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
shouldRenderSource := ctx.FormString("display") == "source"
@@ -606,6 +606,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
break
}
+ // TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+ // maybe for this case, the file is a binary file, and shouldn't be rendered?
if markupType := markup.Type(blob.Name()); markupType != "" {
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
ctx.Data["IsMarkup"] = true
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 52e216e6a0ac2..8b5207f9d9503 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -145,7 +145,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
})
NewWikiPost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
- assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
+ assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 66a19844c20c7..8c98f56af53ef 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,7 +117,7 @@ func notify(ctx context.Context, input *notifyInput) error {
log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
return nil
}
- if input.Repo.IsEmpty {
+ if input.Repo.IsEmpty || input.Repo.IsArchived {
return nil
}
if unit_model.TypeActions.UnitGlobalDisabled() {
@@ -501,7 +501,7 @@ func handleSchedules(
// DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
- if repo.IsEmpty {
+ if repo.IsEmpty || repo.IsArchived {
return nil
}
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 59862fd0d8db2..e4e56e5122968 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -66,6 +66,11 @@ func startTasks(ctx context.Context) error {
}
}
+ if row.Repo.IsArchived {
+ // Skip if the repo is archived
+ continue
+ }
+
cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
if err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index d3e6de7efe4f9..3064c56590d45 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -63,6 +63,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
base.Locale = &translation.MockLocale{}
ctx := context.NewWebContext(base, opt.Render, nil)
+ ctx.AppendContextValue(context.WebContextKey, ctx)
ctx.PageData = map[string]any{}
ctx.Data["PageStartTime"] = time.Now()
chiCtx := chi.NewRouteContext()
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 89fe3e7e3461a..5553ebc058948 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -11,6 +11,6 @@ import (
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
- FixtureFiles: []string{"user.yml"},
+ FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
})
}
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index a4378678a08b5..68487fb8dbb57 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -14,6 +14,8 @@ import (
func ProcessorHelper() *markup.ProcessorHelper {
return &markup.ProcessorHelper{
ElementDir: "auto", // set dir="auto" for necessary (eg: , , etc) tags
+
+ RenderRepoFileCodePreview: renderRepoFileCodePreview,
IsUsernameMentionable: func(ctx context.Context, username string) bool {
mentionedUser, err := user.GetUserByName(ctx, username)
if err != nil {
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/processorhelper_codepreview.go
new file mode 100644
index 0000000000000..ef95046128d08
--- /dev/null
+++ b/services/markup/processorhelper_codepreview.go
@@ -0,0 +1,117 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "html/template"
+ "strings"
+
+ "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/indexer/code"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+ gitea_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+ opts.LineStop = max(opts.LineStop, opts.LineStart)
+ lineCount := opts.LineStop - opts.LineStart + 1
+ if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
+ lineCount = 10
+ opts.LineStop = opts.LineStart + lineCount
+ }
+
+ dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
+ if err != nil {
+ return "", err
+ }
+
+ webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
+ if !ok {
+ return "", fmt.Errorf("context is not a web context")
+ }
+ doer := webCtx.Doer
+
+ perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
+ if err != nil {
+ return "", err
+ }
+ if !perms.CanRead(unit.TypeCode) {
+ return "", fmt.Errorf("no permission")
+ }
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
+ if err != nil {
+ return "", err
+ }
+ defer gitRepo.Close()
+
+ commit, err := gitRepo.GetCommit(opts.CommitID)
+ if err != nil {
+ return "", err
+ }
+
+ language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
+ blob, err := commit.GetBlobByPath(opts.FilePath)
+ if err != nil {
+ return "", err
+ }
+
+ if blob.Size() > setting.UI.MaxDisplayFileSize {
+ return "", fmt.Errorf("file is too large")
+ }
+
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ return "", err
+ }
+ defer dataRc.Close()
+
+ reader := bufio.NewReader(dataRc)
+ for i := 1; i < opts.LineStart; i++ {
+ if _, err = reader.ReadBytes('\n'); err != nil {
+ return "", err
+ }
+ }
+
+ lineNums := make([]int, 0, lineCount)
+ lineCodes := make([]string, 0, lineCount)
+ for i := opts.LineStart; i <= opts.LineStop; i++ {
+ if line, err := reader.ReadString('\n'); err != nil && line == "" {
+ break
+ } else {
+ lineNums = append(lineNums, i)
+ lineCodes = append(lineCodes, line)
+ }
+ }
+ realLineStop := max(opts.LineStart, opts.LineStart+len(lineNums)-1)
+ highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
+
+ escapeStatus := &charset.EscapeStatus{}
+ lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
+ for i, hl := range highlightLines {
+ lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.RuneNBSP)
+ escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
+ }
+
+ return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
+ "FullURL": opts.FullURL,
+ "FilePath": opts.FilePath,
+ "LineStart": opts.LineStart,
+ "LineStop": realLineStop,
+ "RepoLink": dbRepo.Link(),
+ "CommitID": opts.CommitID,
+ "HighlightLines": highlightLines,
+ "EscapeStatus": escapeStatus,
+ "LineEscapeStatus": lineEscapeStatus,
+ })
+}
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go
new file mode 100644
index 0000000000000..01db792925e30
--- /dev/null
+++ b/services/markup/processorhelper_codepreview_test.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProcessorHelperCodePreview(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user2",
+ RepoName: "repo1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ LineStop: 2,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `
+`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ htm, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user2",
+ RepoName: "repo1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `
+`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ _, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user15",
+ RepoName: "big_test_private_1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ LineStop: 10,
+ })
+ assert.ErrorContains(t, err, "no permission")
+}
diff --git a/services/user/user.go b/services/user/user.go
index 4fcb81581d0d8..2287e36c716ac 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -126,7 +126,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
return fmt.Errorf("%s is an organization not a user", u.Name)
}
- if user_model.IsLastAdminUser(ctx, u) {
+ if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
return models.ErrDeleteLastAdminUser{UID: u.ID}
}
@@ -250,7 +250,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
if err := committer.Commit(); err != nil {
return err
}
- committer.Close()
+ _ = committer.Close()
if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return err
@@ -259,50 +259,45 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
return err
}
- // Note: There are something just cannot be roll back,
- // so just keep error logs of those operations.
+ // Note: There are something just cannot be roll back, so just keep error logs of those operations.
path := user_model.UserPath(u.Name)
- if err := util.RemoveAll(path); err != nil {
- err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
+ if err = util.RemoveAll(path); err != nil {
+ err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
- return err
}
if u.Avatar != "" {
avatarPath := u.CustomAvatarRelativePath()
- if err := storage.Avatars.Delete(avatarPath); err != nil {
- err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
+ if err = storage.Avatars.Delete(avatarPath); err != nil {
+ err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
- return err
}
}
return nil
}
-// DeleteInactiveUsers deletes all inactive users and email addresses.
+// DeleteInactiveUsers deletes all inactive users and their email addresses.
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
- users, err := user_model.GetInactiveUsers(ctx, olderThan)
+ inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
if err != nil {
return err
}
// FIXME: should only update authorized_keys file once after all deletions.
- for _, u := range users {
- select {
- case <-ctx.Done():
- return db.ErrCancelledf("Before delete inactive user %s", u.Name)
- default:
- }
- if err := DeleteUser(ctx, u, false); err != nil {
- // Ignore users that were set inactive by admin.
- if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
- models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
+ for _, u := range inactiveUsers {
+ if err = DeleteUser(ctx, u, false); err != nil {
+ // Ignore inactive users that were ever active but then were set inactive by admin
+ if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
continue
}
- return err
+ select {
+ case <-ctx.Done():
+ return db.ErrCancelledf("when deleting inactive user %q", u.Name)
+ default:
+ return err
+ }
}
}
-
- return user_model.DeleteInactiveEmailAddresses(ctx)
+ return nil // TODO: there could be still inactive users left, and the number would increase gradually
}
diff --git a/services/user/user_test.go b/services/user/user_test.go
index f110bd26d0886..bd6019a14fdcf 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
"testing"
+ "time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/auth"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
)
@@ -185,3 +187,26 @@ func TestCreateUser_Issue5882(t *testing.T) {
assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
}
}
+
+func TestDeleteInactiveUsers(t *testing.T) {
+ addUser := func(name, email string, createdUnix timeutil.TimeStamp, active bool) {
+ inactiveUser := &user_model.User{Name: name, LowerName: strings.ToLower(name), Email: email, CreatedUnix: createdUnix, IsActive: active}
+ _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(inactiveUser)
+ assert.NoError(t, err)
+ inactiveUserEmail := &user_model.EmailAddress{UID: inactiveUser.ID, IsPrimary: true, Email: email, LowerEmail: strings.ToLower(email), IsActivated: active}
+ err = db.Insert(db.DefaultContext, inactiveUserEmail)
+ assert.NoError(t, err)
+ }
+ addUser("user-inactive-10", "user-inactive-10@test.com", timeutil.TimeStampNow().Add(-600), false)
+ addUser("user-inactive-5", "user-inactive-5@test.com", timeutil.TimeStampNow().Add(-300), false)
+ addUser("user-active-10", "user-active-10@test.com", timeutil.TimeStampNow().Add(-600), true)
+ addUser("user-active-5", "user-active-5@test.com", timeutil.TimeStampNow().Add(-300), true)
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-10"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+ assert.NoError(t, DeleteInactiveUsers(db.DefaultContext, 8*time.Minute))
+ unittest.AssertNotExistsBean(t, &user_model.User{Name: "user-inactive-10"})
+ unittest.AssertNotExistsBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-5"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-10"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-5"})
+}
diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl
new file mode 100644
index 0000000000000..c65ab284065c7
--- /dev/null
+++ b/templates/base/markup_codepreview.tmpl
@@ -0,0 +1,25 @@
+
+
+
+
+ {{- range $idx, $line := .HighlightLines -}}
+
+ |
+ {{- if $.EscapeStatus.Escaped -}}
+ {{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}}
+ {{if $lineEscapeStatus.Escaped}}{{end}} |
+ {{- end}}
+ {{$line.FormattedContent}} |
+
+ {{- end -}}
+
+
+
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 1a148a2d1cc29..30d1a3d78d9e8 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -30,6 +30,9 @@
+ {{if .IsFileTooLarge}}
+ {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+ {{else}}
{{range $row := .BlameRows}}
@@ -75,6 +78,7 @@
{{end}}
+ {{end}}{{/* end if .IsFileTooLarge */}}