diff --git a/cmd/main.go b/cmd/main.go index e44f9382b72f2..feda41e68b24a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "os" - "reflect" "strings" "code.gitea.io/gitea/modules/log" @@ -58,7 +57,6 @@ func appGlobalFlags() []cli.Flag { return []cli.Flag{ // make the builtin flags at the top helpFlag, - cli.VersionFlag, // shared configuration flags, they are for global and for each sub-command at the same time // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed @@ -120,38 +118,12 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) } } -func reflectGet(v any, fieldName string) any { - e := reflect.ValueOf(v).Elem() - return e.FieldByName(fieldName).Interface() -} - -// https://cli.urfave.org/migrate-v1-to-v2/#flag-aliases-are-done-differently -// Sadly v2 doesn't warn you if a comma is in the name. (https://github.com/urfave/cli/issues/1103) -func checkCommandFlags(c any) bool { - var cmds []*cli.Command - if app, ok := c.(*cli.App); ok { - cmds = app.Commands - } else { - cmds = c.(*cli.Command).Subcommands - } - ok := true - for _, cmd := range cmds { - for _, flag := range cmd.Flags { - flagName := reflectGet(flag, "Name").(string) - if strings.Contains(flagName, ",") { - ok = false - log.Error("cli.Flag can't have comma in its Name: %q, use Aliases instead", flagName) - } - } - if !checkCommandFlags(cmd) { - ok = false - } - } - return ok -} - -func NewMainApp() *cli.App { +func NewMainApp(version, versionExtra string) *cli.App { app := cli.NewApp() + app.Name = "Gitea" + app.Usage = "A painless self-hosted Git service" + app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` + app.Version = version + versionExtra app.EnableBashCompletion = true // these sub-commands need to use config file @@ -187,6 +159,7 @@ func NewMainApp() *cli.App { app.DefaultCommand = CmdWeb.Name globalFlags := appGlobalFlags() + app.Flags = append(app.Flags, cli.VersionFlag) app.Flags = append(app.Flags, globalFlags...) app.HideHelp = true // use our own help action to show helps (with more information like default config) app.Before = PrepareConsoleLoggerLevel(log.INFO) @@ -196,8 +169,20 @@ func NewMainApp() *cli.App { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) - if !checkCommandFlags(app) { - panic("some flags are incorrect") // this is a runtime check to help developers - } return app } + +func RunMainApp(app *cli.App, args ...string) error { + err := app.Run(args) + if err == nil { + return nil + } + if strings.HasPrefix(err.Error(), "flag provided but not defined:") { + // the cli package should already have output the error message, so just exit + cli.OsExiter(1) + return err + } + _, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err) + cli.OsExiter(1) + return err +} diff --git a/cmd/main_test.go b/cmd/main_test.go index e9204d09cce3a..0e32dce564872 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -5,6 +5,7 @@ package cmd import ( "fmt" + "io" "os" "path/filepath" "strings" @@ -12,6 +13,7 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v2" @@ -27,21 +29,38 @@ func makePathOutput(workPath, customPath, customConf string) string { return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) } -func newTestApp() *cli.App { - app := NewMainApp() - testCmd := &cli.Command{ - Name: "test-cmd", - Action: func(ctx *cli.Context) error { - _, _ = fmt.Fprint(app.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) - return nil - }, - } +func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App { + app := NewMainApp("version", "version-extra") + testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction} prepareSubcommandWithConfig(testCmd, appGlobalFlags()) app.Commands = append(app.Commands, testCmd) app.DefaultCommand = testCmd.Name return app } +type runResult struct { + Stdout string + Stderr string + ExitCode int +} + +func runTestApp(app *cli.App, args ...string) (runResult, error) { + outBuf := new(strings.Builder) + errBuf := new(strings.Builder) + app.Writer = outBuf + app.ErrWriter = errBuf + exitCode := -1 + defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)() + defer test.MockVariableValue(&cli.OsExiter, func(code int) { + if exitCode == -1 { + exitCode = code // save the exit code once and then reset the writer (to simulate the exit) + app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard + } + })() + err := RunMainApp(app, args...) + return runResult{outBuf.String(), errBuf.String(), exitCode}, err +} + func TestCliCmd(t *testing.T) { defaultWorkPath := filepath.Dir(setting.AppPath) defaultCustomPath := filepath.Join(defaultWorkPath, "custom") @@ -92,7 +111,10 @@ func TestCliCmd(t *testing.T) { }, } - app := newTestApp() + app := newTestApp(func(ctx *cli.Context) error { + _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) + return nil + }) var envBackup []string for _, s := range os.Environ() { if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { @@ -120,12 +142,39 @@ func TestCliCmd(t *testing.T) { _ = os.Setenv(k, v) } args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - out := new(strings.Builder) - app.Writer = out - err := app.Run(args) + r, err := runTestApp(app, args...) assert.NoError(t, err, c.cmd) assert.NotEmpty(t, c.exp, c.cmd) - outStr := out.String() - assert.Contains(t, outStr, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) } } + +func TestCliCmdError(t *testing.T) { + app := newTestApp(func(ctx *cli.Context) error { return fmt.Errorf("normal error") }) + r, err := runTestApp(app, "./gitea", "test-cmd") + assert.Error(t, err) + assert.Equal(t, 1, r.ExitCode) + assert.Equal(t, "", r.Stdout) + assert.Equal(t, "Command error: normal error\n", r.Stderr) + + app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) }) + r, err = runTestApp(app, "./gitea", "test-cmd") + assert.Error(t, err) + assert.Equal(t, 2, r.ExitCode) + assert.Equal(t, "", r.Stdout) + assert.Equal(t, "exit error\n", r.Stderr) + + app = newTestApp(func(ctx *cli.Context) error { return nil }) + r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") + assert.Error(t, err) + assert.Equal(t, 1, r.ExitCode) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout) + assert.Equal(t, "", r.Stderr) // the cli package's strange behavior, the error message is not in stderr .... + + app = newTestApp(func(ctx *cli.Context) error { return nil }) + r, err = runTestApp(app, "./gitea", "test-cmd") + assert.NoError(t, err) + assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called + assert.Equal(t, "", r.Stdout) + assert.Equal(t, "", r.Stderr) +} diff --git a/cmd/web.go b/cmd/web.go index dfe2091d064b6..b69769ec433bf 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -107,13 +107,18 @@ func createPIDFile(pidPath string) { } } -func serveInstall(ctx *cli.Context) error { +func showWebStartupMessage(msg string) { log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith) - log.Info("App path: %s", setting.AppPath) - log.Info("Work path: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Config file: %s", setting.CustomConf) - log.Info("Prepare to run install page") + log.Info("* RunMode: %s", setting.RunMode) + log.Info("* AppPath: %s", setting.AppPath) + log.Info("* WorkPath: %s", setting.AppWorkPath) + log.Info("* CustomPath: %s", setting.CustomPath) + log.Info("* ConfigFile: %s", setting.CustomConf) + log.Info("%s", msg) +} + +func serveInstall(ctx *cli.Context) error { + showWebStartupMessage("Prepare to run install page") routers.InitWebInstallPage(graceful.GetManager().HammerContext()) @@ -150,29 +155,24 @@ func serveInstalled(ctx *cli.Context) error { setting.LoadCommonSettings() setting.MustInstalled() - log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith) - log.Info("App path: %s", setting.AppPath) - log.Info("Work path: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Config file: %s", setting.CustomConf) - log.Info("Run mode: %s", setting.RunMode) - log.Info("Prepare to run web server") + showWebStartupMessage("Prepare to run web server") if setting.AppWorkPathMismatch { log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+ - "Only WORK_PATH in config should be set and used. Please remove the other outdated work paths from environment variables and command arguments", setting.CustomConf) + "Only WORK_PATH in config should be set and used. Please make sure the path in config file is correct, "+ + "remove the other outdated work paths from environment variables and command arguments", setting.CustomConf) } rootCfg := setting.CfgProvider if rootCfg.Section("").Key("WORK_PATH").String() == "" { saveCfg, err := rootCfg.PrepareSaving() if err != nil { - log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) + log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou should set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) } else { rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) if err = saveCfg.Save(); err != nil { - log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) + log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou should set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) } } } diff --git a/docs/content/usage/actions/act-runner.en-us.md b/docs/content/usage/actions/act-runner.en-us.md index 05ed83c2c4882..1be81c5c78d15 100644 --- a/docs/content/usage/actions/act-runner.en-us.md +++ b/docs/content/usage/actions/act-runner.en-us.md @@ -245,8 +245,8 @@ You can find more useful images on [act images](https://github.com/nektos/act/bl If you want to run jobs in the host directly, you can change it to `ubuntu-22.04:host` or just `ubuntu-22.04`, the `:host` is optional. However, we suggest you to use a special name like `linux_amd64:host` or `windows:host` to avoid misusing it. -One more thing is that it is recommended to register the runner if you want to change the labels. -It may be annoying to do this, so we may provide a better way to do it in the future. +Starting with Gitea 1.21, you can change labels by modifying `container.labels` in the runner configuration file (if you don't have a configuration file, please refer to [configuration tutorials](#configuration)). +The runner will use these new labels as soon as you restart it, i.e., by calling `./act_runner daemon --config config.yaml`. ## Running @@ -261,3 +261,34 @@ After you have registered the runner, you can run it by running the following co The runner will fetch jobs from the Gitea instance and run them automatically. Since act runner is still in development, it is recommended to check the latest version and upgrade it regularly. + +## Configuration variable + +You can create configuration variables on the user, organization and repository level. +The level of the variable depends on where you created it. + +### Naming conventions + +The following rules apply to variable names: + +- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed. + +- Variable names must not start with the `GITHUB_` and `GITEA_` prefix. + +- Variable names must not start with a number. + +- Variable names are case-insensitive. + +- Variable names must be unique at the level they are created at. + +- Variable names must not be `CI`. + +### Using variable + +After creating configuration variables, they will be automatically filled in the `vars` context. +They can be accessed through expressions like `{{ vars.VARIABLE_NAME }}` in the workflow. + +### Precedence + +If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence: +A repository variable will always be chosen over an organization/user variable. diff --git a/docs/content/usage/actions/act-runner.zh-cn.md b/docs/content/usage/actions/act-runner.zh-cn.md index c3978f6361358..f1404bf0b4db4 100644 --- a/docs/content/usage/actions/act-runner.zh-cn.md +++ b/docs/content/usage/actions/act-runner.zh-cn.md @@ -241,8 +241,7 @@ Runner的标签用于确定Runner可以运行哪些Job以及如何运行它们 如果您想直接在主机上运行Job,您可以将其更改为`ubuntu-22.04:host`或仅`ubuntu-22.04`,`:host`是可选的。 然而,我们建议您使用类似`linux_amd64:host`或`windows:host`的特殊名称,以避免误用。 -还有一点需要注意的是,建议在更改标签时注册Runner。 -这可能会有些麻烦,所以我们可能会在将来提供更好的方法来处理。 +从 Gitea 1.21 开始,您可以通过修改 runner 的配置文件中的 `container.labels` 来更改标签(如果没有配置文件,请参考 [配置教程](#配置)),通过执行 `./act_runner daemon --config config.yaml` 命令重启 runner 之后,这些新定义的标签就会生效。 ## 运行 @@ -257,3 +256,32 @@ Runner的标签用于确定Runner可以运行哪些Job以及如何运行它们 Runner将从Gitea实例获取Job并自动运行它们。 由于Act Runner仍处于开发中,建议定期检查最新版本并进行升级。 + +## 变量 + +您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。 + +### 命名规则 + +以下规则适用于变量名: + +- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。 + +- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。 + +- 变量名称不能以数字开头。 + +- 变量名称不区分大小写。 + +- 变量名称在创建它们的级别上必须是唯一的。 + +- 变量名称不能为 “CI”。 + +### 使用 + +创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `{{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。 + +### 优先级 + +如果同名变量存在于多个级别,则级别最低的变量优先。 +仓库级别的变量总是比组织或者用户级别的变量优先被选中。 diff --git a/docs/content/usage/actions/quickstart.zh-cn.md b/docs/content/usage/actions/quickstart.zh-cn.md index 510d4a904fd15..c5b67ce1014db 100644 --- a/docs/content/usage/actions/quickstart.zh-cn.md +++ b/docs/content/usage/actions/quickstart.zh-cn.md @@ -127,7 +127,7 @@ jobs: 请注意,演示文件中包含一些表情符号。 请确保您的数据库支持它们,特别是在使用MySQL时。 -如果字符集不是`utf8mb4,将出现错误,例如`Error 1366 (HY000): Incorrect string value: '\\xF0\\x9F\\x8E\\x89 T...' for column 'name' at row 1`。 +如果字符集不是`utf8mb4`,将出现错误,例如`Error 1366 (HY000): Incorrect string value: '\\xF0\\x9F\\x8E\\x89 T...' for column 'name' at row 1`。 有关更多信息,请参阅[数据库准备工作](installation/database-preparation.md#mysql)。 或者,您可以从演示文件中删除所有表情符号,然后再尝试一次。 diff --git a/docs/content/usage/agit-support.en-us.md b/docs/content/usage/agit-support.en-us.md index 25523efe6a62c..b1643d27b3ef7 100644 --- a/docs/content/usage/agit-support.en-us.md +++ b/docs/content/usage/agit-support.en-us.md @@ -21,8 +21,8 @@ In Gitea `1.13`, support for [agit](https://git-repo.info/en/2020/03/agit-flow-a ## Creating PRs with Agit -Agit allows to create PRs while pushing code to the remote repo. \ -This can be done by pushing to the branch followed by a specific refspec (a location identifier known to git). \ +Agit allows to create PRs while pushing code to the remote repo. +This can be done by pushing to the branch followed by a specific refspec (a location identifier known to git). The following example illustrates this: ```shell diff --git a/main.go b/main.go index 652a71195e58d..775c729c569ea 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ package main import ( - "fmt" "os" "runtime" "strings" @@ -21,6 +20,8 @@ import ( _ "code.gitea.io/gitea/modules/markup/csv" _ "code.gitea.io/gitea/modules/markup/markdown" _ "code.gitea.io/gitea/modules/markup/orgmode" + + "github.com/urfave/cli/v2" ) // these flags will be set by the build flags @@ -37,17 +38,12 @@ func init() { } func main() { - app := cmd.NewMainApp() - app.Name = "Gitea" - app.Usage = "A painless self-hosted Git service" - app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` - app.Version = Version + formatBuiltWith() - - err := app.Run(os.Args) - if err != nil { - _, _ = fmt.Fprintf(app.Writer, "\nFailed to run with %s: %v\n", os.Args, err) + cli.OsExiter = func(code int) { + log.GetManager().Close() + os.Exit(code) } - + app := cmd.NewMainApp(Version, formatBuiltWith()) + _ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp log.GetManager().Close() } diff --git a/models/actions/task.go b/models/actions/task.go index 9cc0fd0df83db..b31afb2126be6 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -278,7 +278,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask if gots, err := jobparser.Parse(job.WorkflowPayload); err != nil { return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) } else if len(gots) != 1 { - return nil, false, fmt.Errorf("workflow of job %d: not signle workflow", job.ID) + return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) } else { _, workflowJob = gots[0].Job() } diff --git a/models/activities/notification.go b/models/activities/notification.go index e0af2ee8bbf56..ef263ef735c3d 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -310,7 +310,7 @@ func createIssueNotification(ctx context.Context, userID int64, issue *issues_mo } func updateIssueNotification(ctx context.Context, userID, issueID, commentID, updatedByID int64) error { - notification, err := getIssueNotification(ctx, userID, issueID) + notification, err := GetIssueNotification(ctx, userID, issueID) if err != nil { return err } @@ -331,7 +331,8 @@ func updateIssueNotification(ctx context.Context, userID, issueID, commentID, up return err } -func getIssueNotification(ctx context.Context, userID, issueID int64) (*Notification, error) { +// GetIssueNotification return the notification about an issue +func GetIssueNotification(ctx context.Context, userID, issueID int64) (*Notification, error) { notification := new(Notification) _, err := db.GetEngine(ctx). Where("user_id = ?", userID). @@ -742,7 +743,7 @@ func GetUIDsAndNotificationCounts(since, until timeutil.TimeStamp) ([]UserIDCoun // SetIssueReadBy sets issue to be read by given user. func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { - if err := issues_model.UpdateIssueUserByRead(userID, issueID); err != nil { + if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil { return err } @@ -750,7 +751,7 @@ func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { } func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID int64) error { - notification, err := getIssueNotification(ctx, userID, issueID) + notification, err := GetIssueNotification(ctx, userID, issueID) // ignore if not exists if err != nil { return nil @@ -762,7 +763,7 @@ func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID notification.Status = NotificationStatusRead - _, err = db.GetEngine(ctx).ID(notification.ID).Update(notification) + _, err = db.GetEngine(ctx).ID(notification.ID).Cols("status").Update(notification) return err } diff --git a/models/activities/notification_test.go b/models/activities/notification_test.go index 36b63b266b1e6..2d4c369a99b04 100644 --- a/models/activities/notification_test.go +++ b/models/activities/notification_test.go @@ -4,6 +4,7 @@ package activities_test import ( + "context" "testing" activities_model "code.gitea.io/gitea/models/activities" @@ -109,3 +110,16 @@ func TestUpdateNotificationStatuses(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notfPinned.ID, Status: activities_model.NotificationStatusPinned}) } + +func TestSetIssueReadBy(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + assert.NoError(t, db.WithTx(db.DefaultContext, func(ctx context.Context) error { + return activities_model.SetIssueReadBy(ctx, issue.ID, user.ID) + })) + + nt, err := activities_model.GetIssueNotification(db.DefaultContext, user.ID, issue.ID) + assert.NoError(t, err) + assert.EqualValues(t, activities_model.NotificationStatusRead, nt.Status) +} diff --git a/models/issues/comment.go b/models/issues/comment.go index be020b2e1fb2d..e78193126183b 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -777,6 +777,12 @@ func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { // CreateComment creates comment with context func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + e := db.GetEngine(ctx) var LabelID int64 if opts.Label != nil { @@ -832,7 +838,9 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil { return nil, err } - + if err = committer.Commit(); err != nil { + return nil, err + } return comment, nil } diff --git a/models/issues/issue_user.go b/models/issues/issue_user.go index 47da5c62c4d4a..d053b1d54350b 100644 --- a/models/issues/issue_user.go +++ b/models/issues/issue_user.go @@ -55,8 +55,8 @@ func NewIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issu } // UpdateIssueUserByRead updates issue-user relation for reading. -func UpdateIssueUserByRead(uid, issueID int64) error { - _, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID) +func UpdateIssueUserByRead(ctx context.Context, uid, issueID int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID) return err } diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go index 0daace6c9b19c..ce47adb53a0b5 100644 --- a/models/issues/issue_user_test.go +++ b/models/issues/issue_user_test.go @@ -40,13 +40,13 @@ func TestUpdateIssueUserByRead(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID)) + assert.NoError(t, issues_model.UpdateIssueUserByRead(db.DefaultContext, 4, issue.ID)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") - assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID)) + assert.NoError(t, issues_model.UpdateIssueUserByRead(db.DefaultContext, 4, issue.ID)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") - assert.NoError(t, issues_model.UpdateIssueUserByRead(unittest.NonexistentID, unittest.NonexistentID)) + assert.NoError(t, issues_model.UpdateIssueUserByRead(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) } func TestUpdateIssueUsersByMentions(t *testing.T) { diff --git a/models/issues/review.go b/models/issues/review.go index b2736044fce0b..cae3ef1d395e5 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -192,6 +192,9 @@ func (r *Review) LoadAttributes(ctx context.Context) (err error) { func (r *Review) HTMLTypeColorName() string { switch r.Type { case ReviewTypeApprove: + if r.Stale { + return "yellow" + } return "green" case ReviewTypeComment: return "grey" diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 2c7cec5591de3..de340a74ec9ca 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -95,7 +95,7 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { return events, nil } -func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) ([]*DetectedWorkflow, error) { +func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) ([]*DetectedWorkflow, error) { entries, err := ListWorkflows(commit) if err != nil { return nil, err @@ -114,7 +114,7 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy } for _, evt := range events { log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) - if detectMatched(commit, triggedEvent, payload, evt) { + if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { dwf := &DetectedWorkflow{ EntryName: entry.Name(), TriggerEvent: evt.Name, @@ -128,7 +128,7 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy return workflows, nil } -func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { +func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { if !canGithubEventMatch(evt.Name, triggedEvent) { return false } @@ -168,7 +168,7 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel: - return matchPullRequestEvent(commit, payload.(*api.PullRequestPayload), evt) + return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt) case // pull_request_review webhook_module.HookEventPullRequestReviewApproved, @@ -331,7 +331,7 @@ func matchIssuesEvent(commit *git.Commit, issuePayload *api.IssuePayload, evt *j return matchTimes == len(evt.Acts()) } -func matchPullRequestEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { +func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { acts := evt.Acts() activityTypeMatched := false matchTimes := 0 @@ -370,6 +370,18 @@ func matchPullRequestEvent(commit *git.Commit, prPayload *api.PullRequestPayload } } + var ( + headCommit = commit + err error + ) + if evt.Name == GithubEventPullRequestTarget && (len(acts["paths"]) > 0 || len(acts["paths-ignore"]) > 0) { + headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha) + if err != nil { + log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err) + return false + } + } + // all acts conditions should be satisfied for cond, vals := range acts { switch cond { @@ -392,9 +404,9 @@ func matchPullRequestEvent(commit *git.Commit, prPayload *api.PullRequestPayload matchTimes++ } case "paths": - filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) + filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) if err != nil { - log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) + log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) } else { patterns, err := workflowpattern.CompilePatterns(vals...) if err != nil { @@ -405,9 +417,9 @@ func matchPullRequestEvent(commit *git.Commit, prPayload *api.PullRequestPayload } } case "paths-ignore": - filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) + filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) if err != nil { - log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) + log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) } else { patterns, err := workflowpattern.CompilePatterns(vals...) if err != nil { diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index ef553c4a57267..2d57f19488e4d 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -125,7 +125,7 @@ func TestDetectMatched(t *testing.T) { evts, err := GetEventsFromContent([]byte(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) - assert.Equal(t, tc.expected, detectMatched(tc.commit, tc.triggedEvent, tc.payload, evts[0])) + assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) }) } } diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 891e8a2384c11..7a44e6295c4e2 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -74,6 +74,8 @@ func CatFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, Stdin: batchStdinReader, Stdout: batchStdoutWriter, Stderr: &stderr, + + UseContextTimeout: true, }) if err != nil { _ = batchStdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String())) @@ -124,6 +126,8 @@ func CatFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi Stdin: batchStdinReader, Stdout: batchStdoutWriter, Stderr: &stderr, + + UseContextTimeout: true, }) if err != nil { _ = batchStdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String())) diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index e706275856856..aad725fa9db89 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -280,8 +280,16 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error { } // GetFilesChangedBetween returns a list of all files that have been changed between the given commits +// If base is undefined empty SHA (zeros), it only returns the files changed in the head commit +// If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(base + ".." + head).RunStdString(&RunOpts{Dir: repo.Path}) + cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") + if base == EmptySHA { + cmd.AddDynamicArguments(head) + } else { + cmd.AddDynamicArguments(base, head) + } + stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 5b50bc82adeae..603aabde42a09 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -119,3 +119,42 @@ func TestReadWritePullHead(t *testing.T) { err = repo.RemoveReference(PullPrefix + "1/head") assert.NoError(t, err) } + +func TestGetCommitFilesChanged(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + repo, err := openRepositoryWithDefaultContext(bareRepo1Path) + assert.NoError(t, err) + defer repo.Close() + + testCases := []struct { + base, head string + files []string + }{ + { + EmptySHA, + "95bb4d39648ee7e325106df01a621c530863a653", + []string{"file1.txt"}, + }, + { + EmptySHA, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + []string{"file2.txt"}, + }, + { + "95bb4d39648ee7e325106df01a621c530863a653", + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + []string{"file2.txt"}, + }, + { + EmptyTreeSHA, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + []string{"file1.txt", "file2.txt"}, + }, + } + + for _, tc := range testCases { + changedFiles, err := repo.GetFilesChangedBetween(tc.base, tc.head) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.files, changedFiles) + } +} diff --git a/modules/git/sha1.go b/modules/git/sha1.go index 7d9d9776da9d4..8d6403e8657fb 100644 --- a/modules/git/sha1.go +++ b/modules/git/sha1.go @@ -11,10 +11,10 @@ import ( "strings" ) -// EmptySHA defines empty git SHA +// EmptySHA defines empty git SHA (undefined, non-existent) const EmptySHA = "0000000000000000000000000000000000000000" -// EmptyTreeSHA is the SHA of an empty tree +// EmptyTreeSHA is the SHA of an empty tree, the root of all git repositories const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" // SHAFullLength is the full length of a git SHA diff --git a/modules/html/html.go b/modules/html/html.go index 6cb6b847ef19c..b1ebd584c6b6d 100644 --- a/modules/html/html.go +++ b/modules/html/html.go @@ -6,28 +6,20 @@ package html // ParseSizeAndClass get size and class from string with default values // If present, "others" expects the new size first and then the classes to use func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) { - if len(others) == 0 { - return defaultSize, defaultClass - } - size := defaultSize - _size, ok := others[0].(int) - if ok && _size != 0 { - size = _size - } - - if len(others) == 1 { - return size, defaultClass + if len(others) >= 1 { + if v, ok := others[0].(int); ok && v != 0 { + size = v + } } - class := defaultClass - if _class, ok := others[1].(string); ok && _class != "" { - if defaultClass == "" { - class = _class - } else { - class = defaultClass + " " + _class + if len(others) >= 2 { + if v, ok := others[1].(string); ok && v != "" { + if class != "" { + class += " " + } + class += v } } - return size, class } diff --git a/modules/packages/debian/metadata.go b/modules/packages/debian/metadata.go index bb77f7524bf63..32460a84ae2e1 100644 --- a/modules/packages/debian/metadata.go +++ b/modules/packages/debian/metadata.go @@ -172,19 +172,10 @@ func ParseControlFile(r io.Reader) (*Package, error) { value := strings.TrimSpace(parts[1]) switch key { case "Package": - if !namePattern.MatchString(value) { - return nil, ErrInvalidName - } p.Name = value case "Version": - if !versionPattern.MatchString(value) { - return nil, ErrInvalidVersion - } p.Version = value case "Architecture": - if value == "" { - return nil, ErrInvalidArchitecture - } p.Architecture = value case "Maintainer": a, err := mail.ParseAddress(value) @@ -208,13 +199,23 @@ func ParseControlFile(r io.Reader) (*Package, error) { return nil, err } + if !namePattern.MatchString(p.Name) { + return nil, ErrInvalidName + } + if !versionPattern.MatchString(p.Version) { + return nil, ErrInvalidVersion + } + if p.Architecture == "" { + return nil, ErrInvalidArchitecture + } + dependencies := strings.Split(depends.String(), ",") for i := range dependencies { dependencies[i] = strings.TrimSpace(dependencies[i]) } p.Metadata.Dependencies = dependencies - p.Control = control.String() + p.Control = strings.TrimSpace(control.String()) return p, nil } diff --git a/modules/setting/storage.go b/modules/setting/storage.go index ed804a910a461..c28f2be4ed714 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -84,12 +84,15 @@ func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection { return storageSec } +// getStorage will find target section and extra special section first and then read override +// items from extra section func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*Storage, error) { if name == "" { return nil, errors.New("no name for storage") } var targetSec ConfigSection + // check typ first if typ != "" { var err error targetSec, err = rootCfg.GetSection(storageSectionName + "." + typ) @@ -111,24 +114,40 @@ func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*S } } - storageNameSec, _ := rootCfg.GetSection(storageSectionName + "." + name) - - if targetSec == nil { - targetSec = sec + if targetSec == nil && sec != nil { + secTyp := sec.Key("STORAGE_TYPE").String() + if IsValidStorageType(StorageType(secTyp)) { + targetSec = sec + } else if secTyp != "" { + targetSec, _ = rootCfg.GetSection(storageSectionName + "." + secTyp) + } } + + targetSecIsStoragename := false + storageNameSec, _ := rootCfg.GetSection(storageSectionName + "." + name) if targetSec == nil { targetSec = storageNameSec + targetSecIsStoragename = storageNameSec != nil } + if targetSec == nil { targetSec = getDefaultStorageSection(rootCfg) } else { targetType := targetSec.Key("STORAGE_TYPE").String() switch { case targetType == "": - if targetSec.Key("PATH").String() == "" { - targetSec = getDefaultStorageSection(rootCfg) + if targetSec != storageNameSec && storageNameSec != nil { + targetSec = storageNameSec + targetSecIsStoragename = true + if targetSec.Key("STORAGE_TYPE").String() == "" { + return nil, fmt.Errorf("storage section %s.%s has no STORAGE_TYPE", storageSectionName, name) + } } else { - targetSec.Key("STORAGE_TYPE").SetValue("local") + if targetSec.Key("PATH").String() == "" { + targetSec = getDefaultStorageSection(rootCfg) + } else { + targetSec.Key("STORAGE_TYPE").SetValue("local") + } } default: newTargetSec, _ := rootCfg.GetSection(storageSectionName + "." + targetType) @@ -153,27 +172,47 @@ func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*S return nil, fmt.Errorf("invalid storage type %q", targetType) } + // extra config section will be read SERVE_DIRECT, PATH, MINIO_BASE_PATH, MINIO_BUCKET to override the targetsec when possible + extraConfigSec := sec + if extraConfigSec == nil { + extraConfigSec = storageNameSec + } + var storage Storage storage.Type = StorageType(targetType) switch targetType { case string(LocalStorageType): - storage.Path = ConfigSectionKeyString(targetSec, "PATH", filepath.Join(AppDataPath, name)) - if !filepath.IsAbs(storage.Path) { - storage.Path = filepath.Join(AppWorkPath, storage.Path) + targetPath := ConfigSectionKeyString(targetSec, "PATH", "") + if targetPath == "" { + targetPath = AppDataPath + } else if !filepath.IsAbs(targetPath) { + targetPath = filepath.Join(AppDataPath, targetPath) } - case string(MinioStorageType): - storage.MinioConfig.BasePath = name + "/" - if err := targetSec.MapTo(&storage.MinioConfig); err != nil { - return nil, fmt.Errorf("map minio config failed: %v", err) + var fallbackPath string + if targetSecIsStoragename { + fallbackPath = targetPath + } else { + fallbackPath = filepath.Join(targetPath, name) } - // extra config section will be read SERVE_DIRECT, PATH, MINIO_BASE_PATH to override the targetsec - extraConfigSec := sec + if extraConfigSec == nil { - extraConfigSec = storageNameSec + storage.Path = fallbackPath + } else { + storage.Path = ConfigSectionKeyString(extraConfigSec, "PATH", fallbackPath) + if !filepath.IsAbs(storage.Path) { + storage.Path = filepath.Join(targetPath, storage.Path) + } + } + + case string(MinioStorageType): + if err := targetSec.MapTo(&storage.MinioConfig); err != nil { + return nil, fmt.Errorf("map minio config failed: %v", err) } + storage.MinioConfig.BasePath = name + "/" + if extraConfigSec != nil { storage.MinioConfig.ServeDirect = ConfigSectionKeyBool(extraConfigSec, "SERVE_DIRECT", storage.MinioConfig.ServeDirect) storage.MinioConfig.BasePath = ConfigSectionKeyString(extraConfigSec, "MINIO_BASE_PATH", storage.MinioConfig.BasePath) diff --git a/modules/setting/storage_test.go b/modules/setting/storage_test.go index 4eda7646e81d0..9a83f31d918b4 100644 --- a/modules/setting/storage_test.go +++ b/modules/setting/storage_test.go @@ -4,6 +4,7 @@ package setting import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -90,3 +91,161 @@ STORAGE_TYPE = minio assert.EqualValues(t, "gitea", RepoAvatar.Storage.MinioConfig.Bucket) assert.EqualValues(t, "repo-avatars/", RepoAvatar.Storage.MinioConfig.BasePath) } + +type testLocalStoragePathCase struct { + loader func(rootCfg ConfigProvider) error + storagePtr **Storage + expectedPath string +} + +func testLocalStoragePath(t *testing.T, appDataPath, iniStr string, cases []testLocalStoragePathCase) { + cfg, err := NewConfigProviderFromData(iniStr) + assert.NoError(t, err) + AppDataPath = appDataPath + for _, c := range cases { + assert.NoError(t, c.loader(cfg)) + storage := *c.storagePtr + + assert.EqualValues(t, "local", storage.Type) + assert.True(t, filepath.IsAbs(storage.Path)) + assert.EqualValues(t, filepath.Clean(c.expectedPath), filepath.Clean(storage.Path)) + } +} + +func Test_getStorageInheritStorageTypeLocal(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage] +STORAGE_TYPE = local +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/appdata/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/repo-archive"}, + {loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalPath(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage] +STORAGE_TYPE = local +PATH = /data/gitea +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/repo-archive"}, + {loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalRelativePath(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage] +STORAGE_TYPE = local +PATH = storages +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/appdata/storages/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/appdata/storages/repo-archive"}, + {loadActionsFrom, &Actions.LogStorage, "/appdata/storages/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/appdata/storages/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/storages/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalPathOverride(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage] +STORAGE_TYPE = local +PATH = /data/gitea + +[repo-archive] +PATH = /data/gitea/the-archives-dir +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/the-archives-dir"}, + {loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalPathOverrideEmpty(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage] +STORAGE_TYPE = local +PATH = /data/gitea + +[repo-archive] +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/repo-archive"}, + {loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalRelativePathOverride(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage] +STORAGE_TYPE = local +PATH = /data/gitea + +[repo-archive] +PATH = the-archives-dir +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/data/gitea/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/the-archives-dir"}, + {loadActionsFrom, &Actions.LogStorage, "/data/gitea/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/data/gitea/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/data/gitea/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalPathOverride3(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage.repo-archive] +STORAGE_TYPE = local +PATH = /data/gitea/archives +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/appdata/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/archives"}, + {loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalPathOverride4(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage.repo-archive] +STORAGE_TYPE = local +PATH = /data/gitea/archives + +[repo-archive] +PATH = /tmp/gitea/archives +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/appdata/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/tmp/gitea/archives"}, + {loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"}, + }) +} + +func Test_getStorageInheritStorageTypeLocalPathOverride5(t *testing.T) { + testLocalStoragePath(t, "/appdata", ` +[storage.repo-archive] +STORAGE_TYPE = local +PATH = /data/gitea/archives + +[repo-archive] +`, []testLocalStoragePathCase{ + {loadPackagesFrom, &Packages.Storage, "/appdata/packages"}, + {loadRepoArchiveFrom, &RepoArchive.Storage, "/data/gitea/archives"}, + {loadActionsFrom, &Actions.LogStorage, "/appdata/actions_log"}, + {loadAvatarsFrom, &Avatar.Storage, "/appdata/avatars"}, + {loadRepoAvatarFrom, &RepoAvatar.Storage, "/appdata/repo-avatars"}, + }) +} diff --git a/modules/svg/processor.go b/modules/svg/processor.go new file mode 100644 index 0000000000000..82248fb0c1216 --- /dev/null +++ b/modules/svg/processor.go @@ -0,0 +1,59 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package svg + +import ( + "bytes" + "fmt" + "regexp" + "sync" +) + +type normalizeVarsStruct struct { + reXMLDoc, + reComment, + reAttrXMLNs, + reAttrSize, + reAttrClassPrefix *regexp.Regexp +} + +var ( + normalizeVars *normalizeVarsStruct + normalizeVarsOnce sync.Once +) + +// Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes +// It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed. +func Normalize(data []byte, size int) []byte { + normalizeVarsOnce.Do(func() { + normalizeVars = &normalizeVarsStruct{ + reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), + reComment: regexp.MustCompile(`(?s)`), + + reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), + reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), + reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), + } + }) + data = normalizeVars.reXMLDoc.ReplaceAll(data, nil) + data = normalizeVars.reComment.ReplaceAll(data, nil) + + data = bytes.TrimSpace(data) + svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) + if !ok || !bytes.HasPrefix(svgTag, []byte(`') + normalized = append(normalized, svgRemaining...) + return normalized +} diff --git a/modules/svg/processor_test.go b/modules/svg/processor_test.go new file mode 100644 index 0000000000000..a0286666ed3f7 --- /dev/null +++ b/modules/svg/processor_test.go @@ -0,0 +1,29 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package svg + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalize(t *testing.T) { + res := Normalize([]byte("foo"), 1) + assert.Equal(t, "foo", string(res)) + + res = Normalize([]byte(` + +content`), 1) + assert.Equal(t, `content`, string(res)) + + res = Normalize([]byte(`content`), 16) + + assert.Equal(t, `content`, string(res)) +} diff --git a/modules/svg/svg.go b/modules/svg/svg.go index fc96ea8e6abdc..016e1dc08bb34 100644 --- a/modules/svg/svg.go +++ b/modules/svg/svg.go @@ -7,42 +7,35 @@ import ( "fmt" "html/template" "path" - "regexp" "strings" - "code.gitea.io/gitea/modules/html" + gitea_html "code.gitea.io/gitea/modules/html" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" ) -var ( - // SVGs contains discovered SVGs - SVGs = map[string]string{} - - widthRe = regexp.MustCompile(`width="[0-9]+?"`) - heightRe = regexp.MustCompile(`height="[0-9]+?"`) -) +var svgIcons map[string]string const defaultSize = 16 -// Init discovers SVGs and populates the `SVGs` variable +// Init discovers SVG icons and populates the `svgIcons` variable func Init() error { - files, err := public.AssetFS().ListFiles("assets/img/svg") + const svgAssetsPath = "assets/img/svg" + files, err := public.AssetFS().ListFiles(svgAssetsPath) if err != nil { return err } - // Remove `xmlns` because inline SVG does not need it - reXmlns := regexp.MustCompile(`(]*?)\s+xmlns="[^"]*"`) + svgIcons = make(map[string]string, len(files)) for _, file := range files { if path.Ext(file) != ".svg" { continue } - bs, err := public.AssetFS().ReadFile("assets/img/svg", file) + bs, err := public.AssetFS().ReadFile(svgAssetsPath, file) if err != nil { log.Error("Failed to read SVG file %s: %v", file, err) } else { - SVGs[file[:len(file)-4]] = reXmlns.ReplaceAllString(string(bs), "$1") + svgIcons[file[:len(file)-4]] = string(Normalize(bs, defaultSize)) } } return nil @@ -50,12 +43,12 @@ func Init() error { // RenderHTML renders icons - arguments icon name (string), size (int), class (string) func RenderHTML(icon string, others ...any) template.HTML { - size, class := html.ParseSizeAndClass(defaultSize, "", others...) - - if svgStr, ok := SVGs[icon]; ok { + size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...) + if svgStr, ok := svgIcons[icon]; ok { + // the code is somewhat hacky, but it just works, because the SVG contents are all normalized if size != defaultSize { - svgStr = widthRe.ReplaceAllString(svgStr, fmt.Sprintf(`width="%d"`, size)) - svgStr = heightRe.ReplaceAllString(svgStr, fmt.Sprintf(`height="%d"`, size)) + svgStr = strings.Replace(svgStr, fmt.Sprintf(`width="%d"`, defaultSize), fmt.Sprintf(`width="%d"`, size), 1) + svgStr = strings.Replace(svgStr, fmt.Sprintf(`height="%d"`, defaultSize), fmt.Sprintf(`height="%d"`, size), 1) } if class != "" { svgStr = strings.Replace(svgStr, `class="`, fmt.Sprintf(`class="%s `, class), 1) diff --git a/modules/test/utils.go b/modules/test/utils.go index 2917741c45a69..4a0c2f1b3b298 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -33,3 +33,9 @@ func RedirectURL(resp http.ResponseWriter) string { func IsNormalPageCompleted(s string) bool { return strings.Contains(s, `