diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6288e4d367c6..31e42d0ab30d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,8 @@ // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { "version":"20" - } + }, + "ghcr.io/devcontainers/features/git-lfs:1.1.0": {} }, "customizations": { "vscode": { @@ -20,7 +21,7 @@ "Vue.volar", "ms-azuretools.vscode-docker", "zixuanchen.vitest-explorer", - "alexcvzz.vscode-sqlite" + "qwtel.sqlite-viewer" ] } }, diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 71ec6dae841e..0e94f5217cb9 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -55,6 +55,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + # fetch all commits instead of only the last as some branches are long lived and could have many between versions + # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 + - run: git fetch --unshallow --quiet --tags --force - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - name: Get cleaned branch name @@ -75,12 +78,14 @@ jobs: - name: build rootful docker image uses: docker/build-push-action@v4 with: + context: . platforms: linux/amd64,linux/arm64 push: true tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} - name: build rootless docker image uses: docker/build-push-action@v4 with: + context: . platforms: linux/amd64,linux/arm64 push: true file: Dockerfile.rootless diff --git a/.gitignore b/.gitignore index 581417df6102..6851be742c64 100644 --- a/.gitignore +++ b/.gitignore @@ -53,8 +53,6 @@ cpu.out /bin /dist /custom/* -!/custom/conf -/custom/conf/* !/custom/conf/app.example.ini /data /indexers diff --git a/.gitpod.yml b/.gitpod.yml index f1b2fb195712..000f534e8539 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -34,7 +34,7 @@ vscode: - Vue.volar - ms-azuretools.vscode-docker - zixuanchen.vitest-explorer - - alexcvzz.vscode-sqlite + - qwtel.sqlite-viewer ports: - name: Gitea diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66ee37bfa277..3710aa5fe2f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -557,7 +557,7 @@ be reviewed by two maintainers and must pass the automatic tests. - And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) - If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. - Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. -- Verify all release assets were correctly published through CI on dl.gitea.io and GitHub releases. Once ACKed: - - bump the version of https://dl.gitea.io/gitea/version.json +- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: + - bump the version of https://dl.gitea.com/gitea/version.json - merge the blog post PR - announce the release in discord `#announcements` diff --git a/Makefile b/Makefile index f6d9b9d04fe8..08d439f422a7 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,9 @@ endif STORED_VERSION_FILE := VERSION HUGO_VERSION ?= 0.111.3 +GITHUB_REF_TYPE ?= branch +GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) + ifneq ($(GITHUB_REF_TYPE),branch) VERSION ?= $(subst v,,$(GITHUB_REF_NAME)) GITEA_VERSION ?= $(GITHUB_REF_NAME) @@ -1011,9 +1014,5 @@ docker: docker build --disable-content-trust=false -t $(DOCKER_REF) . # support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . -.PHONY: docker-build -docker-build: - docker run -ti --rm -v "$(CURDIR):/srv/app/src/code.gitea.io/gitea" -w /srv/app/src/code.gitea.io/gitea -e TAGS="bindata $(TAGS)" LDFLAGS="$(LDFLAGS)" CGO_EXTRA_CFLAGS="$(CGO_EXTRA_CFLAGS)" webhippie/golang:edge make clean build - # This endif closes the if at the top of the file endif diff --git a/README.md b/README.md index 0ee177228620..41793d3d9295 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,8 @@ for the full license text. Looking for an overview of the interface? Check it out! -|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)| +|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| -|![Branches](https://dl.gitea.io/screenshots/branches.png)|![Web Editor](https://dl.gitea.io/screenshots/web_editor.png)|![Activity](https://dl.gitea.io/screenshots/activity.png)| -|![New Migration](https://dl.gitea.io/screenshots/migration.png)|![Migrating](https://dl.gitea.io/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) -![Pull Request Dark](https://dl.gitea.io/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.io/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.io/screenshots/diff_dark.png)| +|![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| +|![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) +![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| diff --git a/README_ZH.md b/README_ZH.md index 285a81403798..48eee9214d2f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -91,8 +91,8 @@ Fork -> Patch -> Push -> Pull Request ## 截图 -|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)| +|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| -|![Branches](https://dl.gitea.io/screenshots/branches.png)|![Web Editor](https://dl.gitea.io/screenshots/web_editor.png)|![Activity](https://dl.gitea.io/screenshots/activity.png)| -|![New Migration](https://dl.gitea.io/screenshots/migration.png)|![Migrating](https://dl.gitea.io/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) -![Pull Request Dark](https://dl.gitea.io/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.io/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.io/screenshots/diff_dark.png)| +|![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| +|![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) +![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| diff --git a/cmd/actions.go b/cmd/actions.go index 346de5b21a6f..f52a91bd5514 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -42,7 +42,7 @@ func runGenerateActionsRunnerToken(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() scope := c.String("scope") diff --git a/cmd/cmd.go b/cmd/cmd.go index b148007fbe3b..8076acecaa25 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -58,7 +58,7 @@ func confirm() (bool, error) { } func initDB(ctx context.Context) error { - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() setting.InitSQLLoggersForCli(log.INFO) diff --git a/cmd/doctor.go b/cmd/doctor.go index b596e9ac0cb7..b79436fc0a9f 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -91,7 +91,7 @@ func runRecreateTable(ctx *cli.Context) error { golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) debug := ctx.Bool("debug") - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() if debug { diff --git a/cmd/dump.go b/cmd/dump.go index 7dda7fd2b32d..0b7c1d32c5b0 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -182,7 +182,7 @@ func runDump(ctx *cli.Context) error { } fileName += "." + outType } - setting.Init(&setting.Options{}) + 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 diff --git a/cmd/embedded.go b/cmd/embedded.go index e51f8477b445..204a623cf704 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -99,11 +99,6 @@ type assetFile struct { func initEmbeddedExtractor(c *cli.Context) error { setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) - // Read configuration file - setting.Init(&setting.Options{ - AllowEmpty: true, - }) - patterns, err := compileCollectPatterns(c.Args()) if err != nil { return err diff --git a/cmd/mailer.go b/cmd/mailer.go index 74bae1ab68c7..eaa5a1afe1c8 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -16,7 +16,7 @@ func runSendMail(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() if err := argsSet(c, "title"); err != nil { return err diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 5a7ede493975..c19e28f13df7 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -51,7 +51,7 @@ func runRestoreRepository(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() var units []string if s := c.String("units"); s != "" { units = strings.Split(s, ",") diff --git a/cmd/serv.go b/cmd/serv.go index 87bf1cce20e4..01102d3800c0 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -61,7 +61,7 @@ func setup(ctx context.Context, debug bool) { } else { setupConsoleLogger(log.FATAL, false, os.Stderr) } - setting.Init(&setting.Options{}) + setting.MustInstalled() if debug { setting.RunMode = "dev" } diff --git a/cmd/web.go b/cmd/web.go index 3a46b909114d..7a257a62a277 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -101,6 +101,110 @@ func createPIDFile(pidPath string) { } } +func serveInstall(ctx *cli.Context) error { + 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") + + routers.InitWebInstallPage(graceful.GetManager().HammerContext()) + + // Flag for port number in case first time run conflict + if ctx.IsSet("port") { + if err := setPort(ctx.String("port")); err != nil { + return err + } + } + if ctx.IsSet("install-port") { + if err := setPort(ctx.String("install-port")); err != nil { + return err + } + } + c := install.Routes() + err := listen(c, false) + if err != nil { + log.Critical("Unable to open listener for installer. Is Gitea already running?") + graceful.GetManager().DoGracefulShutdown() + } + select { + case <-graceful.GetManager().IsShutdown(): + <-graceful.GetManager().Done() + log.Info("PID: %d Gitea Web Finished", os.Getpid()) + log.GetManager().Close() + return err + default: + } + return nil +} + +func serveInstalled(ctx *cli.Context) error { + setting.InitCfgProvider(setting.CustomConf) + 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") + + 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) + } + + 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) + } 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) + } + } + } + + routers.InitWebInstalled(graceful.GetManager().HammerContext()) + + // We check that AppDataPath exists here (it should have been created during installation) + // We can't check it in `InitWebInstalled`, because some integration tests + // use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests. + if _, err := os.Stat(setting.AppDataPath); err != nil { + log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath) + } + + // Override the provided port number within the configuration + if ctx.IsSet("port") { + if err := setPort(ctx.String("port")); err != nil { + return err + } + } + + // Set up Chi routes + c := routers.NormalRoutes() + err := listen(c, true) + <-graceful.GetManager().Done() + log.Info("PID: %d Gitea Web Finished", os.Getpid()) + log.GetManager().Close() + return err +} + +func servePprof() { + http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) + _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) + // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. + log.Info("Starting pprof server on localhost:6060") + log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) + finished() +} + func runWeb(ctx *cli.Context) error { if ctx.Bool("verbose") { setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout) @@ -128,75 +232,19 @@ func runWeb(ctx *cli.Context) error { createPIDFile(ctx.String("pid")) } - // Perform pre-initialization - needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext()) - if needsInstall { - // Flag for port number in case first time run conflict - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { - return err - } - } - if ctx.IsSet("install-port") { - if err := setPort(ctx.String("install-port")); err != nil { - return err - } - } - c := install.Routes() - err := listen(c, false) - if err != nil { - log.Critical("Unable to open listener for installer. Is Gitea already running?") - graceful.GetManager().DoGracefulShutdown() - } - select { - case <-graceful.GetManager().IsShutdown(): - <-graceful.GetManager().Done() - log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() + if !setting.InstallLock { + if err := serveInstall(ctx); err != nil { return err - default: } } else { NoInstallListener() } if setting.EnablePprof { - go func() { - http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) - _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) - // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. - log.Info("Starting pprof server on localhost:6060") - log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) - finished() - }() - } - - log.Info("Global init") - // Perform global initialization - setting.Init(&setting.Options{}) - routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) - - // We check that AppDataPath exists here (it should have been created during installation) - // We can't check it in `GlobalInitInstalled`, because some integration tests - // use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests. - if _, err := os.Stat(setting.AppDataPath); err != nil { - log.Fatal("Can not find APP_DATA_PATH '%s'", setting.AppDataPath) - } - - // Override the provided port number within the configuration - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { - return err - } + go servePprof() } - // Set up Chi routes - c := routers.NormalRoutes() - err := listen(c, true) - <-graceful.GetManager().Done() - log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() - return err + return serveInstalled(ctx) } func setPort(port string) error { @@ -217,9 +265,15 @@ func setPort(port string) error { defaultLocalURL += ":" + setting.HTTPPort + "/" // Save LOCAL_ROOT_URL if port changed - setting.CfgProvider.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) - if err := setting.CfgProvider.Save(); err != nil { - return fmt.Errorf("Failed to save config file: %v", err) + rootCfg := setting.CfgProvider + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + return fmt.Errorf("failed to save config file: %v", err) + } + rootCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) + saveCfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) + if err = saveCfg.Save(); err != nil { + return fmt.Errorf("failed to save config file: %v", err) } } return nil diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index 3405d7d429b4..2cdf4e3943b4 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -81,8 +81,6 @@ func main() { }, } app.Action = runEnvironmentToIni - setting.SetCustomPathAndConf("", "", "") - err := app.Run(os.Args) if err != nil { log.Fatal("Failed to run app with %s: %v", os.Args, err) @@ -90,12 +88,13 @@ func main() { } func runEnvironmentToIni(c *cli.Context) error { - providedCustom := c.String("custom-path") - providedConf := c.String("config") - providedWorkPath := c.String("work-path") - setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) + setting.InitWorkPathAndCommonConfig(os.Getenv, setting.ArgWorkPathAndCustomConf{ + WorkPath: c.String("work-path"), + CustomPath: c.String("custom-path"), + CustomConf: c.String("config"), + }) - cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f53d9ee08900..e51f0558831a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2159,7 +2159,7 @@ LEVEL = Info ;RUN_AT_START = false ;ENABLE_SUCCESS_NOTICE = false ;SCHEDULE = @every 168h -;HTTP_ENDPOINT = https://dl.gitea.io/gitea/version.json +;HTTP_ENDPOINT = https://dl.gitea.com/gitea/version.json ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 7b94c7a4882e..64a356555478 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -1013,7 +1013,7 @@ Default templates for project boards: - `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED). - `ENABLE_SUCCESS_NOTICE`: **true**: Set to false to switch off success notices. - `SCHEDULE`: **@every 168h**: Cron syntax for scheduling a work, e.g. `@every 168h`. -- `HTTP_ENDPOINT`: **https://dl.gitea.io/gitea/version.json**: the endpoint that Gitea will check for newer versions +- `HTTP_ENDPOINT`: **https://dl.gitea.com/gitea/version.json**: the endpoint that Gitea will check for newer versions #### Cron - Delete all old system notices from database (`cron.delete_old_system_notices`) diff --git a/docs/content/doc/help/faq.zh-cn.md b/docs/content/doc/help/faq.zh-cn.md index 345853452423..6a63b4530e8c 100644 --- a/docs/content/doc/help/faq.zh-cn.md +++ b/docs/content/doc/help/faq.zh-cn.md @@ -31,7 +31,7 @@ menu: **注意:**此示例也适用于Docker镜像! -在我们的[下载页面](https://dl.gitea.io/gitea/)上,您会看到一个1.7目录,以及1.7.0、1.7.1、1.7.2、1.7.3、1.7.4、1.7.5和1.7.6的目录。 +在我们的[下载页面](https://dl.gitea.com/gitea/)上,您会看到一个1.7目录,以及1.7.0、1.7.1、1.7.2、1.7.3、1.7.4、1.7.5和1.7.6的目录。 1.7目录和1.7.0目录是**不同**的。1.7目录是在每个合并到[`release/v1.7`](https://github.com/go-gitea/gitea/tree/release/v1.7)分支的提交上构建的。 diff --git a/docs/content/doc/installation/from-binary.fr-fr.md b/docs/content/doc/installation/from-binary.fr-fr.md index f5273054bccf..f3d3110439df 100644 --- a/docs/content/doc/installation/from-binary.fr-fr.md +++ b/docs/content/doc/installation/from-binary.fr-fr.md @@ -17,10 +17,10 @@ menu: # Installation avec le binaire pré-compilé -Tous les binaires sont livrés avec le support de SQLite, MySQL et PostgreSQL, et sont construits avec les ressources incorporées. Gardez à l'esprit que cela peut être différent pour les versions antérieures. L'installation basée sur nos binaires est assez simple, il suffit de choisir le fichier correspondant à votre plateforme à partir de la [page de téléchargement](https://dl.gitea.io/gitea). Copiez l'URL et remplacer l'URL dans les commandes suivantes par la nouvelle: +Tous les binaires sont livrés avec le support de SQLite, MySQL et PostgreSQL, et sont construits avec les ressources incorporées. Gardez à l'esprit que cela peut être différent pour les versions antérieures. L'installation basée sur nos binaires est assez simple, il suffit de choisir le fichier correspondant à votre plateforme à partir de la [page de téléchargement](https://dl.gitea.com/gitea). Copiez l'URL et remplacer l'URL dans les commandes suivantes par la nouvelle: ``` -wget -O gitea https://dl.gitea.io/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 +wget -O gitea https://dl.gitea.com/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 chmod +x gitea ``` diff --git a/docs/content/doc/installation/from-binary.zh-tw.md b/docs/content/doc/installation/from-binary.zh-tw.md index 858cee2193de..78db79775d2e 100644 --- a/docs/content/doc/installation/from-binary.zh-tw.md +++ b/docs/content/doc/installation/from-binary.zh-tw.md @@ -17,10 +17,10 @@ menu: # 從執行檔安裝 -所有的執行檔皆支援 SQLite, MySQL and PostgreSQL,且所有檔案都已經包在執行檔內,這一點跟之前的版本有所不同。關於執行檔的安裝方式非常簡單,只要從[下載頁面](https://dl.gitea.io/gitea)選擇相對應平台,複製下載連結,使用底下指令就可以完成了: +所有的執行檔皆支援 SQLite, MySQL and PostgreSQL,且所有檔案都已經包在執行檔內,這一點跟之前的版本有所不同。關於執行檔的安裝方式非常簡單,只要從[下載頁面](https://dl.gitea.com/gitea)選擇相對應平台,複製下載連結,使用底下指令就可以完成了: ``` -wget -O gitea https://dl.gitea.io/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 +wget -O gitea https://dl.gitea.com/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64 chmod +x gitea ``` diff --git a/docs/content/doc/installation/on-kubernetes.en-us.md b/docs/content/doc/installation/on-kubernetes.en-us.md index b46a61df025a..0847a3b85225 100644 --- a/docs/content/doc/installation/on-kubernetes.en-us.md +++ b/docs/content/doc/installation/on-kubernetes.en-us.md @@ -22,7 +22,7 @@ Gitea provides a Helm Chart to allow for installation on kubernetes. A non-customized install can be done with: ``` -helm repo add gitea-charts https://dl.gitea.io/charts/ +helm repo add gitea-charts https://dl.gitea.com/charts/ helm install gitea gitea-charts/gitea ``` diff --git a/docs/content/doc/installation/on-kubernetes.zh-cn.md b/docs/content/doc/installation/on-kubernetes.zh-cn.md index f5e8f9f762cb..83647a2eab9a 100644 --- a/docs/content/doc/installation/on-kubernetes.zh-cn.md +++ b/docs/content/doc/installation/on-kubernetes.zh-cn.md @@ -22,7 +22,7 @@ Gitea 已经提供了便于在 Kubernetes 云原生环境中安装所需的 Helm 默认安装指令为: ```bash -helm repo add gitea https://dl.gitea.io/charts +helm repo add gitea https://dl.gitea.com/charts helm repo update helm install gitea gitea/gitea ``` diff --git a/docs/content/doc/installation/on-kubernetes.zh-tw.md b/docs/content/doc/installation/on-kubernetes.zh-tw.md index 51446911d599..28dfbda81d32 100644 --- a/docs/content/doc/installation/on-kubernetes.zh-tw.md +++ b/docs/content/doc/installation/on-kubernetes.zh-tw.md @@ -22,7 +22,7 @@ Gitea 提供 Helm Chart 用來安裝於 kubernetes。 非自訂安裝可使用下列指令: ``` -helm repo add gitea-charts https://dl.gitea.io/charts/ +helm repo add gitea-charts https://dl.gitea.com/charts/ helm install gitea gitea-charts/gitea ``` diff --git a/docs/content/doc/installation/upgrade-from-gogs.en-us.md b/docs/content/doc/installation/upgrade-from-gogs.en-us.md index 2e149c6a2bfe..fa545ee025f2 100644 --- a/docs/content/doc/installation/upgrade-from-gogs.en-us.md +++ b/docs/content/doc/installation/upgrade-from-gogs.en-us.md @@ -27,7 +27,7 @@ There are some basic steps to follow. On a Linux system run as the Gogs user: * Create a Gogs backup with `gogs backup`. This creates `gogs-backup-[timestamp].zip` file containing all important Gogs data. You would need it if you wanted to move to the `gogs` back later. -* Download the file matching the destination platform from the [downloads page](https://dl.gitea.io/gitea/). +* Download the file matching the destination platform from the [downloads page](https://dl.gitea.com/gitea/). It should be `1.0.x` version. Migrating from `gogs` to any other version is impossible. * Put the binary at the desired install location. * Copy `gogs/custom/conf/app.ini` to `gitea/custom/conf/app.ini`. @@ -79,11 +79,11 @@ There are some basic steps to follow. On a Linux system run as the Gogs user: After successful migration from `gogs` to `gitea 1.0.x`, it is possible to upgrade `gitea` to a modern version in a two steps process. -Upgrade to [`gitea 1.6.4`](https://dl.gitea.io/gitea/1.6.4/) first. Download the file matching -the destination platform from the [downloads page](https://dl.gitea.io/gitea/1.6.4/) and replace the binary. +Upgrade to [`gitea 1.6.4`](https://dl.gitea.com/gitea/1.6.4/) first. Download the file matching +the destination platform from the [downloads page](https://dl.gitea.com/gitea/1.6.4/) and replace the binary. Run Gitea at least once and check that everything works as expected. -Then repeat the procedure, but this time using the [latest release](https://dl.gitea.io/gitea/{{< version >}}/). +Then repeat the procedure, but this time using the [latest release](https://dl.gitea.com/gitea/{{< version >}}/). ## Upgrading from a more recent version of Gogs diff --git a/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md b/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md index 9a46562f066e..9d287d111dc5 100644 --- a/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md +++ b/docs/content/doc/installation/upgrade-from-gogs.fr-fr.md @@ -22,7 +22,7 @@ menu: Veuillez suivre les étapes ci-dessous. Sur Unix, toute les commandes s'exécutent en tant que l'utilisateur utilisé pour votre installation de Gogs : * Crééer une sauvegarde de Gogs avec la commande `gogs dump`. Le fichier nouvellement créé `gogs-dump-[timestamp].zip` contient toutes les données de votre instance de Gogs. -* Téléchargez le fichier correspondant à votre plateforme à partir de la [page de téléchargements](https://dl.gitea.io/gitea). +* Téléchargez le fichier correspondant à votre plateforme à partir de la [page de téléchargements](https://dl.gitea.com/gitea). * Mettez la binaire dans le répertoire d'installation souhaité. * Copiez le fichier `gogs/custom/conf/app.ini` vers `gitea/custom/conf/app.ini`. * Si vous avez personnalisé les répertoires `templates, public` dans `gogs/custom/`, copiez-les vers `gitea/custom/`. diff --git a/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md b/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md index 9812efaf946f..46442845e780 100644 --- a/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md +++ b/docs/content/doc/installation/upgrade-from-gogs.zh-tw.md @@ -27,7 +27,7 @@ menu: - 使用 `gogs backup` 建立 Gogs 的備份。這會建立檔案 `gogs-backup-[timestamp].zip` 包含所有重要的 Gogs 資料。 如果稍後您要恢復到 `gogs` 時會用到它。 -- 從[下載頁](https://dl.gitea.io/gitea/)下載對應您平臺的檔案。請下載 `1.0.x` 版,從 `gogs` 遷移到其它版本是不可行的。 +- 從[下載頁](https://dl.gitea.com/gitea/)下載對應您平臺的檔案。請下載 `1.0.x` 版,從 `gogs` 遷移到其它版本是不可行的。 - 將二進位檔放到適當的安裝位置。 - 複製 `gogs/custom/conf/app.ini` 到 `gitea/custom/conf/app.ini`。 - 從 `gogs/custom/` 複製自訂 `templates, public` 到 `gitea/custom/`。 @@ -77,10 +77,10 @@ menu: 成功從 `gogs` 升級到 `gitea 1.0.x` 後再用 2 個步驟即可升級到最新版的 `gitea`。 -請先升級到 [`gitea 1.6.4`](https://dl.gitea.io/gitea/1.6.4/),先從[下載頁](https://dl.gitea.io/gitea/1.6.4/)下載 +請先升級到 [`gitea 1.6.4`](https://dl.gitea.com/gitea/1.6.4/),先從[下載頁](https://dl.gitea.com/gitea/1.6.4/)下載 您平臺的二進位檔取代既有的。至少執行一次 Gitea 並確認一切符合預期。 -接著重複上述步驟,但這次請使用[最新發行版本](https://dl.gitea.io/gitea/{{< version >}}/)。 +接著重複上述步驟,但這次請使用[最新發行版本](https://dl.gitea.com/gitea/{{< version >}}/)。 ## 從更新版本的 Gogs 升級 diff --git a/docs/content/doc/usage/template-repositories.en-us.md b/docs/content/doc/usage/template-repositories.en-us.md index 0c278648b3ec..5687861b8ca4 100644 --- a/docs/content/doc/usage/template-repositories.en-us.md +++ b/docs/content/doc/usage/template-repositories.en-us.md @@ -51,6 +51,8 @@ a/b/c/d.json In any file matched by the above globs, certain variables will be expanded. +Matching filenames and paths can also be expanded, and are conservatively sanitized to support cross-platform filesystems. + All variables must be of the form `$VAR` or `${VAR}`. To escape an expansion, use a double `$$`, such as `$$VAR` or `$${VAR}` | Variable | Expands To | Transformable | diff --git a/main.go b/main.go index 49093eb8a703..1c87824c83ce 100644 --- a/main.go +++ b/main.go @@ -33,30 +33,58 @@ var ( Tags = "" // MakeVersion holds the current Make version if built with make MakeVersion = "" - - originalAppHelpTemplate = "" - originalCommandHelpTemplate = "" - originalSubcommandHelpTemplate = "" ) func init() { setting.AppVer = Version setting.AppBuiltWith = formatBuiltWith() setting.AppStartTime = time.Now().UTC() +} - // Grab the original help templates - originalAppHelpTemplate = cli.AppHelpTemplate - originalCommandHelpTemplate = cli.CommandHelpTemplate - originalSubcommandHelpTemplate = cli.SubcommandHelpTemplate +// cmdHelp is our own help subcommand with more information +// test cases: +// ./gitea help +// ./gitea -h +// ./gitea web help +// ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info) +// ./gitea admin help auth +// ./gitea -c /tmp/app.ini -h +// ./gitea -c /tmp/app.ini help +// ./gitea help -c /tmp/app.ini +// GITEA_WORK_DIR=/tmp ./gitea help +// GITEA_WORK_DIR=/tmp ./gitea help --work-path /tmp/other +// GITEA_WORK_DIR=/tmp ./gitea help --config /tmp/app-other.ini +var cmdHelp = cli.Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *cli.Context) (err error) { + args := c.Args() + if args.Present() { + err = cli.ShowCommandHelp(c, args.First()) + } else { + err = cli.ShowAppHelp(c) + } + _, _ = fmt.Fprintf(c.App.Writer, ` +DEFAULT CONFIGURATION: + AppPath: %s + WorkPath: %s + CustomPath: %s + ConfigFile: %s + +`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) + return err + }, } func main() { app := cli.NewApp() app.Name = "Gitea" app.Usage = "A painless self-hosted Git service" - app.Description = `By default, gitea will start serving using the webserver with no -arguments - which can alternatively be run by running the subcommand web.` + 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() + app.EnableBashCompletion = true app.Commands = []cli.Command{ cmd.CmdWeb, cmd.CmdServ, @@ -77,118 +105,83 @@ arguments - which can alternatively be run by running the subcommand web.` cmd.CmdRestoreRepository, cmd.CmdActions, } - // Now adjust these commands to add our global configuration options - - // First calculate the default paths and set the AppHelpTemplates in this context - setting.SetCustomPathAndConf("", "", "") - setAppHelpTemplates() // default configuration flags - defaultFlags := []cli.Flag{ + globalFlags := []cli.Flag{ + cli.HelpFlag, cli.StringFlag{ Name: "custom-path, C", - Value: setting.CustomPath, - Usage: "Custom path file path", + Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, cli.StringFlag{ Name: "config, c", Value: setting.CustomConf, - Usage: "Custom configuration file path", + Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", }, - cli.VersionFlag, cli.StringFlag{ Name: "work-path, w", - Value: setting.AppWorkPath, - Usage: "Set the gitea working path", + Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", }, } // Set the default to be equivalent to cmdWeb and add the default flags + app.Flags = append(app.Flags, globalFlags...) app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) - app.Flags = append(app.Flags, defaultFlags...) - app.Action = cmd.CmdWeb.Action - - // Add functions to set these paths and these flags to the commands - app.Before = establishCustomPath + app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action) + app.HideHelp = true // use our own help action to show helps (with more information like default config) + app.Commands = append(app.Commands, cmdHelp) for i := range app.Commands { - setFlagsAndBeforeOnSubcommands(&app.Commands[i], defaultFlags, establishCustomPath) + prepareSubcommands(&app.Commands[i], globalFlags) } - app.EnableBashCompletion = true - err := app.Run(os.Args) if err != nil { - log.Fatal("Failed to run app with %s: %v", os.Args, err) + _, _ = fmt.Fprintf(app.Writer, "\nFailed to run with %s: %v\n", os.Args, err) } log.GetManager().Close() } -func setFlagsAndBeforeOnSubcommands(command *cli.Command, defaultFlags []cli.Flag, before cli.BeforeFunc) { +func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) { command.Flags = append(command.Flags, defaultFlags...) - command.Before = establishCustomPath + command.Action = prepareWorkPathAndCustomConf(command.Action) + command.HideHelp = true + if command.Name != "help" { + command.Subcommands = append(command.Subcommands, cmdHelp) + } for i := range command.Subcommands { - setFlagsAndBeforeOnSubcommands(&command.Subcommands[i], defaultFlags, before) + prepareSubcommands(&command.Subcommands[i], defaultFlags) } } -func establishCustomPath(ctx *cli.Context) error { - var providedCustom string - var providedConf string - var providedWorkPath string - - currentCtx := ctx - for { - if len(providedCustom) != 0 && len(providedConf) != 0 && len(providedWorkPath) != 0 { - break - } - if currentCtx == nil { - break - } - if currentCtx.IsSet("custom-path") && len(providedCustom) == 0 { - providedCustom = currentCtx.String("custom-path") - } - if currentCtx.IsSet("config") && len(providedConf) == 0 { - providedConf = currentCtx.String("config") +// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config +// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times +func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error { + if a == nil { + return nil + } + action := a.(func(*cli.Context) error) + return func(ctx *cli.Context) error { + var args setting.ArgWorkPathAndCustomConf + curCtx := ctx + for curCtx != nil { + if curCtx.IsSet("work-path") && args.WorkPath == "" { + args.WorkPath = curCtx.String("work-path") + } + if curCtx.IsSet("custom-path") && args.CustomPath == "" { + args.CustomPath = curCtx.String("custom-path") + } + if curCtx.IsSet("config") && args.CustomConf == "" { + args.CustomConf = curCtx.String("config") + } + curCtx = curCtx.Parent() } - if currentCtx.IsSet("work-path") && len(providedWorkPath) == 0 { - providedWorkPath = currentCtx.String("work-path") + setting.InitWorkPathAndCommonConfig(os.Getenv, args) + if ctx.Bool("help") { + return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx) } - currentCtx = currentCtx.Parent() - - } - setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) - - setAppHelpTemplates() - - if ctx.IsSet("version") { - cli.ShowVersion(ctx) - os.Exit(0) - } - - return nil -} - -func setAppHelpTemplates() { - cli.AppHelpTemplate = adjustHelpTemplate(originalAppHelpTemplate) - cli.CommandHelpTemplate = adjustHelpTemplate(originalCommandHelpTemplate) - cli.SubcommandHelpTemplate = adjustHelpTemplate(originalSubcommandHelpTemplate) -} - -func adjustHelpTemplate(originalTemplate string) string { - overridden := "" - if _, ok := os.LookupEnv("GITEA_CUSTOM"); ok { - overridden = "(GITEA_CUSTOM)" + return action(ctx) } - - return fmt.Sprintf(`%s -DEFAULT CONFIGURATION: - CustomPath: %s %s - CustomConf: %s - AppPath: %s - AppWorkPath: %s - -`, originalTemplate, setting.CustomPath, overridden, setting.CustomConf, setting.AppPath, setting.AppWorkPath) } func formatBuiltWith() string { diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 56de8eb9169c..29ab193d57f3 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -71,6 +71,7 @@ type FindRunOptions struct { WorkflowFileName string TriggerUserID int64 Approved bool // not util.OptionalBool, it works only when it's true + Status Status } func (opts FindRunOptions) toConds() builder.Cond { @@ -90,6 +91,9 @@ func (opts FindRunOptions) toConds() builder.Cond { if opts.Approved { cond = cond.And(builder.Gt{"approved_by": 0}) } + if opts.Status > StatusUnknown { + cond = cond.And(builder.Eq{"status": opts.Status}) + } return cond } @@ -106,3 +110,34 @@ func FindRuns(ctx context.Context, opts FindRunOptions) (RunList, int64, error) func CountRuns(ctx context.Context, opts FindRunOptions) (int64, error) { return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionRun)) } + +type StatusInfo struct { + Status int + DisplayedStatus string +} + +// GetStatusInfoList returns a slice of StatusInfo +func GetStatusInfoList(ctx context.Context) []StatusInfo { + // same as those in aggregateJobStatus + allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning} + statusInfoList := make([]StatusInfo, 0, 4) + for _, s := range allStatus { + statusInfoList = append(statusInfoList, StatusInfo{ + Status: int(s), + DisplayedStatus: s.String(), + }) + } + return statusInfoList +} + +// GetActors returns a slice of Actors +func GetActors(ctx context.Context, repoID int64) ([]*user_model.User, error) { + actors := make([]*user_model.User, 0, 10) + + return actors, db.GetEngine(ctx).Where(builder.In("id", builder.Select("`action_run`.trigger_user_id").From("`action_run`"). + GroupBy("`action_run`.trigger_user_id"). + Where(builder.Eq{"`action_run`.repo_id": repoID}))). + Cols("id", "name", "full_name", "avatar", "avatar_email", "use_custom_avatar"). + OrderBy(user_model.GetOrderByName()). + Find(&actors) +} diff --git a/models/actions/variable.go b/models/actions/variable.go new file mode 100644 index 000000000000..e0bb59ccbe6e --- /dev/null +++ b/models/actions/variable.go @@ -0,0 +1,97 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +type ActionVariable struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"` + RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"` + Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` + Data string `xorm:"LONGTEXT NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +func init() { + db.RegisterModel(new(ActionVariable)) +} + +func (v *ActionVariable) Validate() error { + if v.OwnerID == 0 && v.RepoID == 0 { + return errors.New("the variable is not bound to any scope") + } + return nil +} + +func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) { + variable := &ActionVariable{ + OwnerID: ownerID, + RepoID: repoID, + Name: strings.ToUpper(name), + Data: data, + } + if err := variable.Validate(); err != nil { + return variable, err + } + return variable, db.Insert(ctx, variable) +} + +type FindVariablesOpts struct { + db.ListOptions + OwnerID int64 + RepoID int64 +} + +func (opts *FindVariablesOpts) toConds() builder.Cond { + cond := builder.NewCond() + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + return cond +} + +func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) { + var variables []*ActionVariable + sess := db.GetEngine(ctx) + if opts.PageSize != 0 { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + return variables, sess.Where(opts.toConds()).Find(&variables) +} + +func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) { + var variable ActionVariable + has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist) + } + return &variable, nil +} + +func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { + count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data"). + Update(&ActionVariable{ + Name: variable.Name, + Data: variable.Data, + }) + return count != 0, err +} diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index ac65d654f20a..698014afeba5 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -199,8 +199,8 @@ func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount in return tt, db.Insert(ctx, tt) } -// TotalTimes returns the spent time for each user by an issue -func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, error) { +// TotalTimes returns the spent time in seconds for each user by an issue +func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]int64, error) { trackedTimes, err := GetTrackedTimes(db.DefaultContext, options) if err != nil { return nil, err @@ -211,7 +211,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, totalTimesByUser[t.UserID] += t.Time } - totalTimes := make(map[*user_model.User]string) + totalTimes := make(map[*user_model.User]int64) // Fetching User and making time human readable for userID, total := range totalTimesByUser { user, err := user_model.GetUserByID(db.DefaultContext, userID) @@ -221,7 +221,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, } return nil, err } - totalTimes[user] = util.SecToTime(total) + totalTimes[user] = total } return totalTimes, nil } diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index 99b977cca526..baa170b20126 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -86,8 +86,8 @@ func TestTotalTimes(t *testing.T) { assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { - assert.Equal(t, int64(1), user.ID) - assert.Equal(t, "6 minutes 40 seconds", time) + assert.EqualValues(t, 1, user.ID) + assert.EqualValues(t, 400, time) } total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2}) @@ -95,9 +95,9 @@ func TestTotalTimes(t *testing.T) { assert.Len(t, total, 2) for user, time := range total { if user.ID == 2 { - assert.Equal(t, "1 hour 1 minute", time) + assert.EqualValues(t, 3662, time) } else if user.ID == 1 { - assert.Equal(t, "20 seconds", time) + assert.EqualValues(t, 20, time) } else { assert.Error(t, assert.AnError) } @@ -107,8 +107,8 @@ func TestTotalTimes(t *testing.T) { assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { - assert.Equal(t, int64(2), user.ID) - assert.Equal(t, "1 second", time) + assert.EqualValues(t, 2, user.ID) + assert.EqualValues(t, 1, time) } total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 4}) diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index dd99a1eda280..c3100ba66594 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -147,9 +147,9 @@ func MainTest(m *testing.M) { os.Exit(1) } + setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.AppDataPath = tmpDataPath - setting.SetCustomPathAndConf("", "", "") unittest.InitSettings() if err = git.InitFull(context.Background()); err != nil { fmt.Printf("Unable to InitFull: %v\n", err) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4eb512ab49f5..1d443b3d152c 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -503,6 +503,9 @@ var migrations = []Migration{ // v260 -> v261 NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner), + + // v261 -> v262 + NewMigration("Add variable table", v1_21.CreateVariableTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v261.go b/models/migrations/v1_21/v261.go new file mode 100644 index 000000000000..4ec1160d0b3e --- /dev/null +++ b/models/migrations/v1_21/v261.go @@ -0,0 +1,24 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func CreateVariableTable(x *xorm.Engine) error { + type ActionVariable struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"` + RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"` + Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` + Data string `xorm:"LONGTEXT NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync(new(ActionVariable)) +} diff --git a/models/secret/secret.go b/models/secret/secret.go index 8b23b6c35cf8..5a17cc37a5dc 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -5,38 +5,17 @@ package secret import ( "context" - "fmt" - "regexp" + "errors" "strings" "code.gitea.io/gitea/models/db" secret_module "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) -type ErrSecretInvalidValue struct { - Name *string - Data *string -} - -func (err ErrSecretInvalidValue) Error() string { - if err.Name != nil { - return fmt.Sprintf("secret name %q is invalid", *err.Name) - } - if err.Data != nil { - return fmt.Sprintf("secret data %q is invalid", *err.Data) - } - return util.ErrInvalidArgument.Error() -} - -func (err ErrSecretInvalidValue) Unwrap() error { - return util.ErrInvalidArgument -} - // Secret represents a secret type Secret struct { ID int64 @@ -74,24 +53,11 @@ func init() { db.RegisterModel(new(Secret)) } -var ( - secretNameReg = regexp.MustCompile("^[A-Z_][A-Z0-9_]*$") - forbiddenSecretPrefixReg = regexp.MustCompile("^GIT(EA|HUB)_") -) - -// Validate validates the required fields and formats. func (s *Secret) Validate() error { - switch { - case len(s.Name) == 0 || len(s.Name) > 50: - return ErrSecretInvalidValue{Name: &s.Name} - case len(s.Data) == 0: - return ErrSecretInvalidValue{Data: &s.Data} - case !secretNameReg.MatchString(s.Name) || - forbiddenSecretPrefixReg.MatchString(s.Name): - return ErrSecretInvalidValue{Name: &s.Name} - default: - return nil + if s.OwnerID == 0 && s.RepoID == 0 { + return errors.New("the secret is not bound to any scope") } + return nil } type FindSecretsOptions struct { diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 5351ff113998..f926a65538ef 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -42,12 +42,14 @@ func fatalTestError(fmtStr string, args ...interface{}) { os.Exit(1) } -// InitSettings initializes config provider and load common setttings for tests +// InitSettings initializes config provider and load common settings for tests func InitSettings(extraConfigs ...string) { - setting.Init(&setting.Options{ - AllowEmpty: true, - ExtraConfig: strings.Join(extraConfigs, "\n"), - }) + if setting.CustomConf == "" { + setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") + _ = os.Remove(setting.CustomConf) + } + setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n")) + setting.LoadCommonSettings() if err := setting.PrepareAppDataPath(); err != nil { log.Fatalf("Can not prepare APP_DATA_PATH: %v", err) @@ -69,7 +71,7 @@ type TestOptions struct { // MainTest a reusable TestMain(..) function for unit tests that need to use a // test database. Creates the test database, and sets necessary settings. func MainTest(m *testing.M, testOpts *TestOptions) { - setting.SetCustomPathAndConf("", "", "") + setting.CustomPath = filepath.Join(testOpts.GiteaRootPath, "custom") InitSettings() var err error diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go index 10838a751217..ceee32285218 100644 --- a/modules/doctor/doctor.go +++ b/modules/doctor/doctor.go @@ -28,7 +28,7 @@ type Check struct { } func initDBSkipLogger(ctx context.Context) error { - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() if err := db.InitEngine(ctx); err != nil { return fmt.Errorf("db.InitEngine: %w", err) diff --git a/modules/doctor/paths.go b/modules/doctor/paths.go index 957152349c25..3f62d587ab4c 100644 --- a/modules/doctor/paths.go +++ b/modules/doctor/paths.go @@ -66,7 +66,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo return err } - setting.Init(&setting.Options{}) + setting.MustInstalled() configurationFiles := []configurationFile{ {"Configuration File Path", setting.CustomConf, false, true, false}, diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 5e5e4fecbbb7..a8d7ba7948de 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -28,9 +29,7 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) + unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 4bd2ca8d4107..f2322b25544b 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -33,9 +34,7 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) + unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 31d5ebbb11f4..102c5af1c91f 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "regexp" "strings" "time" @@ -48,7 +49,7 @@ var defaultTransformers = []transformer{ {Name: "TITLE", Transform: util.ToTitleCase}, } -func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository) string { +func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string { expansions := []expansion{ {Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers}, {Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers}, @@ -74,6 +75,9 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi return os.Expand(src, func(key string) string { if expansion, ok := expansionMap[key]; ok { + if sanitizeFileName { + return fileNameSanitize(expansion) + } return expansion } return key @@ -191,10 +195,24 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r } if err := os.WriteFile(path, - []byte(generateExpansion(string(content), templateRepo, generateRepo)), + []byte(generateExpansion(string(content), templateRepo, generateRepo, false)), 0o644); err != nil { return err } + + substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, + generateExpansion(base, templateRepo, generateRepo, true))) + + // Create parent subdirectories if needed or continue silently if it exists + if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { + return err + } + + // Substitute filename variables + if err := os.Rename(path, substPath); err != nil { + return err + } + break } } @@ -353,3 +371,13 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ return generateRepo, nil } + +// Sanitize user input to valid OS filenames +// +// Based on https://github.com/sindresorhus/filename-reserved-regex +// Adds ".." to prevent directory traversal +func fileNameSanitize(s string) string { + re := regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`) + + return strings.TrimSpace(re.ReplaceAllString(s, "_")) +} diff --git a/modules/repository/generate_test.go b/modules/repository/generate_test.go index 1cb9a50f6731..b0f97d0ffb4e 100644 --- a/modules/repository/generate_test.go +++ b/modules/repository/generate_test.go @@ -54,3 +54,14 @@ func TestGiteaTemplate(t *testing.T) { }) } } + +func TestFileNameSanitize(t *testing.T) { + assert.Equal(t, "test_CON", fileNameSanitize("test_CON")) + assert.Equal(t, "test CON", fileNameSanitize("test CON ")) + assert.Equal(t, "__traverse__", fileNameSanitize("../traverse/..")) + assert.Equal(t, "http___localhost_3003_user_test.git", fileNameSanitize("http://localhost:3003/user/test.git")) + assert.Equal(t, "_", fileNameSanitize("CON")) + assert.Equal(t, "_", fileNameSanitize("con")) + assert.Equal(t, "_", fileNameSanitize("\u0000")) + assert.Equal(t, "目标", fileNameSanitize("目标")) +} diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 526d69bbdc7d..94dd989850ae 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -4,6 +4,7 @@ package setting import ( + "errors" "fmt" "os" "path/filepath" @@ -51,12 +52,18 @@ type ConfigProvider interface { GetSection(name string) (ConfigSection, error) Save() error SaveTo(filename string) error + + DisableSaving() + PrepareSaving() (ConfigProvider, error) + IsLoadedFromEmpty() bool } type iniConfigProvider struct { - opts *Options - ini *ini.File - newFile bool // whether the file has not existed previously + file string + ini *ini.File + + disableSaving bool // disable the "Save" method because the config options could be polluted + loadedFromEmpty bool // whether the file has not existed previously } type iniConfigSection struct { @@ -175,53 +182,43 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { } cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ - ini: cfg, - newFile: true, + ini: cfg, + loadedFromEmpty: true, }, nil } -type Options struct { - CustomConf string // the ini file path - AllowEmpty bool // whether not finding configuration files is allowed - ExtraConfig string - - DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()" -} - // NewConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. -func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) { - cfg := ini.Empty() - newFile := true +func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { + cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) + loadedFromEmpty := true - if opts.CustomConf != "" { - isFile, err := util.IsFile(opts.CustomConf) + if file != "" { + isFile, err := util.IsFile(file) if err != nil { - return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", opts.CustomConf, err) + return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err) } if isFile { - if err := cfg.Append(opts.CustomConf); err != nil { - return nil, fmt.Errorf("failed to load custom conf '%s': %v", opts.CustomConf, err) + if err = cfg.Append(file); err != nil { + return nil, fmt.Errorf("failed to load config file %q: %v", file, err) } - newFile = false + loadedFromEmpty = false } } - if newFile && !opts.AllowEmpty { - return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf) - } - - if opts.ExtraConfig != "" { - if err := cfg.Append([]byte(opts.ExtraConfig)); err != nil { - return nil, fmt.Errorf("unable to append more config: %v", err) + if len(extraConfigs) > 0 { + for _, s := range extraConfigs { + if err := cfg.Append([]byte(s)); err != nil { + return nil, fmt.Errorf("unable to append more config: %v", err) + } } } cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ - opts: opts, - ini: cfg, - newFile: newFile, + file: file, + ini: cfg, + loadedFromEmpty: loadedFromEmpty, }, nil } @@ -252,22 +249,24 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) { return &iniConfigSection{sec: sec}, nil } +var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save") + // Save saves the content into file func (p *iniConfigProvider) Save() error { - filename := p.opts.CustomConf + if p.disableSaving { + return errDisableSaving + } + filename := p.file if filename == "" { - if !p.opts.AllowEmpty { - return fmt.Errorf("custom config path must not be empty") - } - return nil + return fmt.Errorf("config file path must not be empty") } - if p.newFile { + if p.loadedFromEmpty { if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { - return fmt.Errorf("failed to create '%s': %v", filename, err) + return fmt.Errorf("failed to create %q: %v", filename, err) } } if err := p.ini.SaveTo(filename); err != nil { - return fmt.Errorf("failed to save '%s': %v", filename, err) + return fmt.Errorf("failed to save %q: %v", filename, err) } // Change permissions to be more restrictive @@ -285,9 +284,32 @@ func (p *iniConfigProvider) Save() error { } func (p *iniConfigProvider) SaveTo(filename string) error { + if p.disableSaving { + return errDisableSaving + } return p.ini.SaveTo(filename) } +// DisableSaving disables the saving function, use PrepareSaving to get clear config options. +func (p *iniConfigProvider) DisableSaving() { + p.disableSaving = true +} + +// PrepareSaving loads the ini from file again to get clear config options. +// Otherwise, the "MustXxx" calls would have polluted the current config provider, +// it makes the "Save" outputs a lot of garbage options +// After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped. +func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) { + if p.file == "" { + return nil, errors.New("no config file to save") + } + return NewConfigProviderFromFile(p.file) +} + +func (p *iniConfigProvider) IsLoadedFromEmpty() bool { + return p.loadedFromEmpty +} + func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { if err := rootCfg.Section(sectionName).MapTo(setting); err != nil { log.Fatal("Failed to map %s settings: %v", sectionName, err) @@ -324,8 +346,8 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro } iniFile.BlockMode = false return &iniConfigProvider{ - ini: iniFile, - newFile: true, + ini: iniFile, + loadedFromEmpty: true, }, nil } diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go index 17650edea404..7e7c6be2bb83 100644 --- a/modules/setting/config_provider_test.go +++ b/modules/setting/config_provider_test.go @@ -67,13 +67,14 @@ key = 123 } func TestNewConfigProviderFromFile(t *testing.T) { - _, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false}) - assert.ErrorContains(t, err, "unable to find configuration file") + cfg, err := NewConfigProviderFromFile("no-such.ini") + assert.NoError(t, err) + assert.True(t, cfg.IsLoadedFromEmpty()) // load non-existing file and save testFile := t.TempDir() + "/test.ini" testFile1 := t.TempDir() + "/test1.ini" - cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err = NewConfigProviderFromFile(testFile) assert.NoError(t, err) sec, _ := cfg.NewSection("foo") @@ -84,14 +85,14 @@ func TestNewConfigProviderFromFile(t *testing.T) { bs, err := os.ReadFile(testFile) assert.NoError(t, err) - assert.Equal(t, "[foo]\nk1=a\n", string(bs)) + assert.Equal(t, "[foo]\nk1 = a\n", string(bs)) bs, err = os.ReadFile(testFile1) assert.NoError(t, err) - assert.Equal(t, "[foo]\nk1=a\nk2=b\n", string(bs)) + assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs)) // load existing file and save - cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err = NewConfigProviderFromFile(testFile) assert.NoError(t, err) assert.Equal(t, "a", cfg.Section("foo").Key("k1").String()) sec, _ = cfg.NewSection("bar") @@ -99,7 +100,7 @@ func TestNewConfigProviderFromFile(t *testing.T) { assert.NoError(t, cfg.Save()) bs, err = os.ReadFile(testFile) assert.NoError(t, err) - assert.Equal(t, "[foo]\nk1=a\n\n[bar]\nk1=b\n", string(bs)) + assert.Equal(t, "[foo]\nk1 = a\n\n[bar]\nk1 = b\n", string(bs)) } func TestNewConfigProviderForLocale(t *testing.T) { @@ -119,3 +120,27 @@ func TestNewConfigProviderForLocale(t *testing.T) { assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) assert.Equal(t, "xxx", cfg.Section("").Key("k2").String()) } + +func TestDisableSaving(t *testing.T) { + testFile := t.TempDir() + "/test.ini" + _ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644) + cfg, err := NewConfigProviderFromFile(testFile) + assert.NoError(t, err) + + cfg.DisableSaving() + err = cfg.Save() + assert.ErrorIs(t, err, errDisableSaving) + + saveCfg, err := cfg.PrepareSaving() + assert.NoError(t, err) + + saveCfg.Section("").Key("k1").MustString("x") + saveCfg.Section("").Key("k2").SetValue("y") + saveCfg.Section("").Key("k3").SetValue("z") + err = saveCfg.Save() + assert.NoError(t, err) + + bs, err := os.ReadFile(testFile) + assert.NoError(t, err) + assert.Equal(t, "k1 = a\nk2 = y\nk3 = z\n", string(bs)) +} diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index d68349be8611..140a96f9eda8 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -59,13 +59,18 @@ func loadLFSFrom(rootCfg ConfigProvider) error { if err != nil || n != 32 { LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64() if err != nil { - return fmt.Errorf("Error generating JWT Secret for custom config: %v", err) + return fmt.Errorf("error generating JWT Secret for custom config: %v", err) } // Save secret - sec.Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) - if err := rootCfg.Save(); err != nil { - return fmt.Errorf("Error saving JWT Secret for custom config: %v", err) + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + return fmt.Errorf("error saving JWT Secret for custom config: %v", err) + } + rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + if err := saveCfg.Save(); err != nil { + return fmt.Errorf("error saving JWT Secret for custom config: %v", err) } } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 836a2bb25f48..83c607a416a9 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -130,8 +130,13 @@ func loadOAuth2From(rootCfg ConfigProvider) { } secretBase64 := base64.RawURLEncoding.EncodeToString(key) + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + log.Fatal("save oauth2.JWT_SECRET failed: %v", err) + } rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) - if err := rootCfg.Save(); err != nil { + saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) + if err := saveCfg.Save(); err != nil { log.Fatal("save oauth2.JWT_SECRET failed: %v", err) } } diff --git a/modules/setting/path.go b/modules/setting/path.go new file mode 100644 index 000000000000..91bb2e9bb777 --- /dev/null +++ b/modules/setting/path.go @@ -0,0 +1,191 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/log" +) + +var ( + // AppPath represents the path to the gitea binary + AppPath string + + // AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR. + // If that is not set it is the default set here by the linker or failing that the directory of AppPath. + // It is used as the base path for several other paths. + AppWorkPath string + CustomPath string // Custom directory path. Env: GITEA_CUSTOM + CustomConf string + + appWorkPathBuiltin string + customPathBuiltin string + customConfBuiltin string + + AppWorkPathMismatch bool +) + +func getAppPath() (string, error) { + var appPath string + var err error + if IsWindows && filepath.IsAbs(os.Args[0]) { + appPath = filepath.Clean(os.Args[0]) + } else { + appPath, err = exec.LookPath(os.Args[0]) + } + if err != nil { + if !errors.Is(err, exec.ErrDot) { + return "", err + } + appPath, err = filepath.Abs(os.Args[0]) + } + if err != nil { + return "", err + } + appPath, err = filepath.Abs(appPath) + if err != nil { + return "", err + } + // Note: (legacy code) we don't use path.Dir here because it does not handle case which path starts with two "/" in Windows: "//psf/Home/..." + return strings.ReplaceAll(appPath, "\\", "/"), err +} + +func init() { + var err error + if AppPath, err = getAppPath(); err != nil { + log.Fatal("Failed to get app path: %v", err) + } + + if AppWorkPath == "" { + AppWorkPath = filepath.Dir(AppPath) + } + + appWorkPathBuiltin = AppWorkPath + customPathBuiltin = CustomPath + customConfBuiltin = CustomConf +} + +type ArgWorkPathAndCustomConf struct { + WorkPath string + CustomPath string + CustomConf string +} + +type stringWithDefault struct { + Value string + IsSet bool +} + +func (s *stringWithDefault) Set(v string) { + s.Value = v + s.IsSet = true +} + +// InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings, +func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) { + tryAbsPath := func(paths ...string) string { + s := paths[len(paths)-1] + for i := len(paths) - 2; i >= 0; i-- { + if filepath.IsAbs(s) { + break + } + s = filepath.Join(paths[i], s) + } + return s + } + + var err error + tmpWorkPath := stringWithDefault{Value: appWorkPathBuiltin} + if tmpWorkPath.Value == "" { + tmpWorkPath.Value = filepath.Dir(AppPath) + } + tmpCustomPath := stringWithDefault{Value: customPathBuiltin} + if tmpCustomPath.Value == "" { + tmpCustomPath.Value = "custom" + } + tmpCustomConf := stringWithDefault{Value: customConfBuiltin} + if tmpCustomConf.Value == "" { + tmpCustomConf.Value = "conf/app.ini" + } + + readFromEnv := func() { + envWorkPath := getEnvFn("GITEA_WORK_DIR") + if envWorkPath != "" { + tmpWorkPath.Set(envWorkPath) + if !filepath.IsAbs(tmpWorkPath.Value) { + log.Fatal("GITEA_WORK_DIR (work path) must be absolute path") + } + } + + envCustomPath := getEnvFn("GITEA_CUSTOM") + if envCustomPath != "" { + tmpCustomPath.Set(envCustomPath) + if !filepath.IsAbs(tmpCustomPath.Value) { + log.Fatal("GITEA_CUSTOM (custom path) must be absolute path") + } + } + } + + readFromArgs := func() { + if args.WorkPath != "" { + tmpWorkPath.Set(args.WorkPath) + if !filepath.IsAbs(tmpWorkPath.Value) { + log.Fatal("--work-path must be absolute path") + } + } + if args.CustomPath != "" { + tmpCustomPath.Set(args.CustomPath) // if it is not abs, it will be based on work-path, it shouldn't happen + if !filepath.IsAbs(tmpCustomPath.Value) { + log.Error("--custom-path must be absolute path") + } + } + if args.CustomConf != "" { + tmpCustomConf.Set(args.CustomConf) + if !filepath.IsAbs(tmpCustomConf.Value) { + // the config path can be relative to the real current working path + if tmpCustomConf.Value, err = filepath.Abs(tmpCustomConf.Value); err != nil { + log.Fatal("Failed to get absolute path of config %q: %v", tmpCustomConf.Value, err) + } + } + } + } + + readFromEnv() + readFromArgs() + + if !tmpCustomConf.IsSet { + tmpCustomConf.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value, tmpCustomConf.Value)) + } + + // only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready + InitCfgProvider(tmpCustomConf.Value) + configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH") + if configWorkPath != "" { + if !filepath.IsAbs(configWorkPath) { + log.Fatal("WORK_PATH in %q must be absolute path", configWorkPath) + } + configWorkPath = filepath.Clean(configWorkPath) + if tmpWorkPath.Value != "" && (getEnvFn("GITEA_WORK_DIR") != "" || args.WorkPath != "") { + fi1, err1 := os.Stat(tmpWorkPath.Value) + fi2, err2 := os.Stat(configWorkPath) + if err1 != nil || err2 != nil || !os.SameFile(fi1, fi2) { + AppWorkPathMismatch = true + } + } + tmpWorkPath.Set(configWorkPath) + } + + tmpCustomPath.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value)) + + AppWorkPath = tmpWorkPath.Value + CustomPath = tmpCustomPath.Value + CustomConf = tmpCustomConf.Value + + LoadCommonSettings() +} diff --git a/modules/setting/path_test.go b/modules/setting/path_test.go new file mode 100644 index 000000000000..fc6a2116dc93 --- /dev/null +++ b/modules/setting/path_test.go @@ -0,0 +1,151 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +type envVars map[string]string + +func (e envVars) Getenv(key string) string { + return e[key] +} + +func TestInitWorkPathAndCommonConfig(t *testing.T) { + testInit := func(defaultWorkPath, defaultCustomPath, defaultCustomConf string) { + AppWorkPathMismatch = false + AppWorkPath = defaultWorkPath + appWorkPathBuiltin = defaultWorkPath + CustomPath = defaultCustomPath + customPathBuiltin = defaultCustomPath + CustomConf = defaultCustomConf + customConfBuiltin = defaultCustomConf + } + + fp := filepath.Join + + tmpDir := t.TempDir() + dirFoo := fp(tmpDir, "foo") + dirBar := fp(tmpDir, "bar") + dirXxx := fp(tmpDir, "xxx") + dirYyy := fp(tmpDir, "yyy") + + t.Run("Default", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("WorkDir(env)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirBar, AppWorkPath) + assert.Equal(t, fp(dirBar, "custom"), CustomPath) + assert.Equal(t, fp(dirBar, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("WorkDir(env,arg)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirXxx}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, fp(dirXxx, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("CustomPath(env)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirBar, "custom1"), CustomPath) + assert.Equal(t, fp(dirBar, "custom1/conf/app.ini"), CustomConf) + }) + + t.Run("CustomPath(env,arg)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{CustomPath: "custom2"}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom2"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom2/conf/app.ini"), CustomConf) + }) + + t.Run("CustomConf", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: "app1.ini"}) + assert.Equal(t, dirFoo, AppWorkPath) + cwd, _ := os.Getwd() + assert.Equal(t, fp(cwd, "app1.ini"), CustomConf) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: fp(dirBar, "app1.ini")}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirBar, "app1.ini"), CustomConf) + }) + + t.Run("CustomConfOverrideWorkPath", func(t *testing.T) { + iniWorkPath := fp(tmpDir, "app-workpath.ini") + _ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.False(t, AppWorkPathMismatch) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.True(t, AppWorkPathMismatch) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirBar, CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.True(t, AppWorkPathMismatch) + }) + + t.Run("Builtin", func(t *testing.T) { + testInit(dirFoo, dirBar, dirXxx) + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, dirBar, CustomPath) + assert.Equal(t, dirXxx, CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom1"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom1/cfg.ini"), CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirYyy}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirYyy, AppWorkPath) + assert.Equal(t, fp(dirYyy, "custom1"), CustomPath) + assert.Equal(t, fp(dirYyy, "custom1/cfg.ini"), CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": dirYyy}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, dirYyy, CustomPath) + assert.Equal(t, fp(dirYyy, "cfg.ini"), CustomConf) + + iniWorkPath := fp(tmpDir, "app-workpath.ini") + _ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644) + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom1"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + }) +} diff --git a/modules/setting/security.go b/modules/setting/security.go index ce2e7711f1e7..c39eb7f3ebd6 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -89,8 +89,13 @@ func generateSaveInternalToken(rootCfg ConfigProvider) { } InternalToken = token + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + log.Fatal("Error saving internal token: %v", err) + } rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) - if err := rootCfg.Save(); err != nil { + saveCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) + if err = saveCfg.Save(); err != nil { log.Fatal("Error saving internal token: %v", err) } } diff --git a/modules/setting/server.go b/modules/setting/server.go index d937faca1012..7c033bcc6ba7 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -61,6 +61,7 @@ var ( AssetVersion string // Server settings + Protocol Scheme UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` @@ -324,7 +325,6 @@ func loadServerFrom(rootCfg ConfigProvider) { StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) if !filepath.IsAbs(AppDataPath) { - log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath) AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 539eb4b19764..0d69847dbeab 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -5,12 +5,8 @@ package setting import ( - "errors" "fmt" "os" - "os/exec" - "path" - "path/filepath" "runtime" "strings" "time" @@ -28,19 +24,9 @@ var ( // AppStartTime store time gitea has started AppStartTime time.Time - // AppPath represents the path to the gitea binary - AppPath string - // AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR. - // If that is not set it is the default set here by the linker or failing that the directory of AppPath. - // - // AppWorkPath is used as the base path for several other paths. - AppWorkPath string - // Other global setting objects CfgProvider ConfigProvider - CustomPath string // Custom directory path - CustomConf string RunMode string RunUser string IsProd bool @@ -51,62 +37,6 @@ var ( IsInTesting = false ) -func getAppPath() (string, error) { - var appPath string - var err error - if IsWindows && filepath.IsAbs(os.Args[0]) { - appPath = filepath.Clean(os.Args[0]) - } else { - appPath, err = exec.LookPath(os.Args[0]) - } - - if err != nil { - if !errors.Is(err, exec.ErrDot) { - return "", err - } - appPath, err = filepath.Abs(os.Args[0]) - } - if err != nil { - return "", err - } - appPath, err = filepath.Abs(appPath) - if err != nil { - return "", err - } - // Note: we don't use path.Dir here because it does not handle case - // which path starts with two "/" in Windows: "//psf/Home/..." - return strings.ReplaceAll(appPath, "\\", "/"), err -} - -func getWorkPath(appPath string) string { - workPath := AppWorkPath - - if giteaWorkPath, ok := os.LookupEnv("GITEA_WORK_DIR"); ok { - workPath = giteaWorkPath - } - if len(workPath) == 0 { - i := strings.LastIndex(appPath, "/") - if i == -1 { - workPath = appPath - } else { - workPath = appPath[:i] - } - } - workPath = strings.ReplaceAll(workPath, "\\", "/") - if !filepath.IsAbs(workPath) { - log.Info("Provided work path %s is not absolute - will be made absolute against the current working directory", workPath) - - absPath, err := filepath.Abs(workPath) - if err != nil { - log.Error("Unable to absolute %s against the current working directory %v. Will absolute against the AppPath %s", workPath, err, appPath) - workPath = filepath.Join(appPath, workPath) - } else { - workPath = absPath - } - } - return strings.ReplaceAll(workPath, "\\", "/") -} - func init() { IsWindows = runtime.GOOS == "windows" if AppVer == "" { @@ -116,12 +46,6 @@ func init() { // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically // By default set this logger at Info - we'll change it later, but we need to start with something. log.SetConsoleLogger(log.DEFAULT, "console", log.INFO) - - var err error - if AppPath, err = getAppPath(); err != nil { - log.Fatal("Failed to get app path: %v", err) - } - AppWorkPath = getWorkPath(AppPath) } // IsRunUserMatchCurrentUser returns false if configured run user does not match @@ -137,36 +61,6 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) { return currentUser, runUser == currentUser } -// SetCustomPathAndConf will set CustomPath and CustomConf with reference to the -// GITEA_CUSTOM environment variable and with provided overrides before stepping -// back to the default -func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string) { - if len(providedWorkPath) != 0 { - AppWorkPath = filepath.ToSlash(providedWorkPath) - } - if giteaCustom, ok := os.LookupEnv("GITEA_CUSTOM"); ok { - CustomPath = giteaCustom - } - if len(providedCustom) != 0 { - CustomPath = providedCustom - } - if len(CustomPath) == 0 { - CustomPath = path.Join(AppWorkPath, "custom") - } else if !filepath.IsAbs(CustomPath) { - CustomPath = path.Join(AppWorkPath, CustomPath) - } - - if len(providedConf) != 0 { - CustomConf = providedConf - } - if len(CustomConf) == 0 { - CustomConf = path.Join(CustomPath, "conf/app.ini") - } else if !filepath.IsAbs(CustomConf) { - CustomConf = path.Join(CustomPath, CustomConf) - log.Warn("Using 'custom' directory as relative origin for configuration file: '%s'", CustomConf) - } -} - // PrepareAppDataPath creates app data directory if necessary func PrepareAppDataPath() error { // FIXME: There are too many calls to MkdirAll in old code. It is incorrect. @@ -196,25 +90,29 @@ func PrepareAppDataPath() error { return nil } -func Init(opts *Options) { - if opts.CustomConf == "" { - opts.CustomConf = CustomConf - } +func InitCfgProvider(file string, extraConfigs ...string) { var err error - CfgProvider, err = NewConfigProviderFromFile(opts) - if err != nil { - log.Fatal("newConfigProviderFromFile[%v]: %v", opts, err) + if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil { + log.Fatal("Unable to init config provider from %q: %v", file, err) } - if !opts.DisableLoadCommonSettings { - if err := loadCommonSettingsFrom(CfgProvider); err != nil { - log.Fatal("loadCommonSettingsFrom[%v]: %v", opts, err) - } + CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls +} + +func MustInstalled() { + if !InstallLock { + log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`) + } +} + +func LoadCommonSettings() { + if err := loadCommonSettingsFrom(CfgProvider); err != nil { + log.Fatal("Unable to load settings from config: %v", err) } } // loadCommonSettingsFrom loads common configurations from a configuration provider. func loadCommonSettingsFrom(cfg ConfigProvider) error { - // WARNNING: don't change the sequence except you know what you are doing. + // WARNING: don't change the sequence except you know what you are doing. loadRunModeFrom(cfg) loadLogGlobalFrom(cfg) loadServerFrom(cfg) diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 4bf57eafb721..923fa51d228a 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -68,7 +68,7 @@ func sessionHandler(session ssh.Session) { log.Trace("SSH: Payload: %v", command) - args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf} + args := []string{"--config=" + setting.CustomConf, "serv", "key-" + keyID} log.Trace("SSH: Arguments: %v", args) ctx, cancel := context.WithCancel(session.Context()) diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index b4275e600570..6a0cee4a2998 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -90,10 +90,11 @@ func (w *testLoggerWriterCloser) Reset() { // PrintCurrentTest prints the current test to os.Stdout func PrintCurrentTest(t testing.TB, skip ...int) func() { + t.Helper() start := time.Now() actualSkip := 1 if len(skip) > 0 { - actualSkip = skip[0] + actualSkip = skip[0] + 1 } _, filename, line, _ := runtime.Caller(actualSkip) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6cab7c0cbb09..234b898fc1d2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -132,6 +132,9 @@ show_full_screen = Show full screen confirm_delete_selected = Confirm to delete all selected items? +name = Name +value = Value + [aria] navbar = Navigation Bar footer = Footer @@ -3391,8 +3394,6 @@ owner.settings.chef.keypair.description = Generate a key pair used to authentica secrets = Secrets description = Secrets will be passed to certain actions and cannot be read otherwise. none = There are no secrets yet. -value = Value -name = Name creation = Add Secret creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. @@ -3459,9 +3460,31 @@ runs.commit = Commit runs.pushed_by = Pushed by runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.no_matching_runner_helper = No matching runner: %s +runs.actor = Actor +runs.status = Status +runs.actors_no_select = All actors +runs.status_no_select = All status +runs.no_results = No results matched. +runs.no_runs = The workflow has no runs yet. need_approval_desc = Need approval to run workflows for fork pull request. +variables = Variables +variables.management = Variables Management +variables.creation = Add Variable +variables.none = There are no variables yet. +variables.deletion = Remove variable +variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue? +variables.description = Variables will be passed to certain actions and cannot be read otherwise. +variables.id_not_exist = Variable with id %d not exists. +variables.edit = Edit Variable +variables.deletion.failed = Failed to remove variable. +variables.deletion.success = The variable has been removed. +variables.creation.failed = Failed to add variable. +variables.creation.success = The variable "%s" has been added. +variables.update.failed = Failed to edit variable. +variables.update.success = The variable has been edited. + [projects] type-1.display_name = Individual Project type-2.display_name = Repository Project diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index 9af51f2d7e66..cc9c06ab455c 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -36,6 +36,7 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv WorkflowPayload: t.Job.WorkflowPayload, Context: generateTaskContext(t), Secrets: getSecretsOfTask(ctx, t), + Vars: getVariablesOfTask(ctx, t), } if needs, err := findTaskNeeds(ctx, t); err != nil { @@ -88,6 +89,29 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s return secrets } +func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { + variables := map[string]string{} + + // Org / User level + ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID}) + if err != nil { + log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err) + } + + // Repo level + repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID}) + if err != nil { + log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err) + } + + // Level precedence: Repo > Org / User + for _, v := range append(ownerVariables, repoVariables...) { + variables[v.Name] = v.Data + } + + return variables +} + func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { event := map[string]interface{}{} _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) diff --git a/routers/init.go b/routers/init.go index 54e8d2b8b39e..ddbabcc39744 100644 --- a/routers/init.go +++ b/routers/init.go @@ -28,7 +28,6 @@ import ( "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" actions_router "code.gitea.io/gitea/routers/api/actions" packages_router "code.gitea.io/gitea/routers/api/packages" @@ -101,21 +100,16 @@ func syncAppConfForGit(ctx context.Context) error { return nil } -// GlobalInitInstalled is for global installed configuration. -func GlobalInitInstalled(ctx context.Context) { - if !setting.InstallLock { - log.Fatal("Gitea is not installed") - } +func InitWebInstallPage(ctx context.Context) { + translation.InitLocales(ctx) + setting.LoadSettingsForInstall() + mustInit(svg.Init) +} +// InitWebInstalled is for global installed configuration. +func InitWebInstalled(ctx context.Context) { mustInitCtx(ctx, git.InitFull) - log.Info("Gitea Version: %s%s", setting.AppVer, setting.AppBuiltWith) - log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) - log.Info("AppPath: %s", setting.AppPath) - log.Info("AppWorkPath: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.Log.RootPath) - log.Info("Configuration file: %s", setting.CustomConf) - log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode)) + log.Info("Git version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) // Setup i18n translation.InitLocales(ctx) diff --git a/routers/install/install.go b/routers/install/install.go index 16bb55b685b7..c94a30b89fcf 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/forms" "gitea.com/go-chi/session" @@ -370,11 +371,16 @@ func SubmitInstall(ctx *context.Context) { } // Save settings. - cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) } + cfg.Section("").Key("APP_NAME").SetValue(form.AppName) + cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) + cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + cfg.Section("").Key("RUN_MODE").SetValue("prod") + cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) @@ -386,9 +392,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("database").Key("PATH").SetValue(setting.Database.Path) cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful - cfg.Section("").Key("APP_NAME").SetValue(form.AppName) cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath) - cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort) @@ -450,8 +454,6 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress)) cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker)) - cfg.Section("").Key("RUN_MODE").SetValue("prod") - cfg.Section("session").Key("PROVIDER").SetValue("file") cfg.Section("log").Key("MODE").MustString("console") @@ -514,7 +516,13 @@ func SubmitInstall(ctx *context.Context) { // ---- All checks are passed // Reload settings (and re-initialize database connection) - reloadSettings(ctx) + setting.InitCfgProvider(setting.CustomConf) + setting.LoadCommonSettings() + setting.MustInstalled() + setting.LoadDBSetting() + if err := common.InitDBEngine(ctx); err != nil { + log.Fatal("ORM engine initialization failed: %v", err) + } // Create admin account if len(form.AdminName) > 0 { diff --git a/routers/install/setting.go b/routers/install/setting.go deleted file mode 100644 index c14843d8ee4d..000000000000 --- a/routers/install/setting.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package install - -import ( - "context" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/svg" - "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/routers/common" -) - -// PreloadSettings preloads the configuration to check if we need to run install -func PreloadSettings(ctx context.Context) bool { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) - if !setting.InstallLock { - log.Info("AppPath: %s", setting.AppPath) - log.Info("AppWorkPath: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.Log.RootPath) - log.Info("Configuration file: %s", setting.CustomConf) - log.Info("Prepare to run install page") - translation.InitLocales(ctx) - if setting.EnableSQLite3 { - log.Info("SQLite3 is supported") - } - - setting.LoadSettingsForInstall() - _ = svg.Init() - } - - return !setting.InstallLock -} - -// reloadSettings reloads the existing settings and starts up the database -func reloadSettings(ctx context.Context) { - setting.Init(&setting.Options{}) - setting.LoadDBSetting() - if setting.InstallLock { - if err := common.InitDBEngine(ctx); err == nil { - log.Info("ORM engine initialization successful!") - } else { - log.Fatal("ORM engine initialization failed: %v", err) - } - } -} diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index be662c22efdc..2c6989a71dbb 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strconv" "strings" @@ -167,20 +166,6 @@ func Config(ctx *context.Context) { ctx.Data["SessionConfig"] = sessionCfg ctx.Data["Git"] = setting.Git - - type envVar struct { - Name, Value string - } - - envVars := map[string]*envVar{} - if len(os.Getenv("GITEA_WORK_DIR")) > 0 { - envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} - } - if len(os.Getenv("GITEA_CUSTOM")) > 0 { - envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} - } - - ctx.Data["EnvVars"] = envVars ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate ctx.Data["LogSQL"] = setting.Database.LogSQL diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 10acb468542e..e1e07b5a7296 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -5,6 +5,7 @@ package actions import ( "bytes" + "fmt" "net/http" actions_model "code.gitea.io/gitea/models/actions" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/convert" "github.com/nektos/act/pkg/model" @@ -125,7 +127,16 @@ func List(ctx *context.Context) { } workflow := ctx.FormString("workflow") + actorID := ctx.FormInt64("actor") + status := ctx.FormInt("status") ctx.Data["CurWorkflow"] = workflow + // if status or actor query param is not given to frontend href, (href="//actions") + // they will be 0 by default, which indicates get all status or actors + ctx.Data["CurActor"] = actorID + ctx.Data["CurStatus"] = status + if actorID > 0 || status > int(actions_model.StatusUnknown) { + ctx.Data["IsFiltered"] = true + } opts := actions_model.FindRunOptions{ ListOptions: db.ListOptions{ @@ -134,6 +145,8 @@ func List(ctx *context.Context) { }, RepoID: ctx.Repo.Repository.ID, WorkflowFileName: workflow, + TriggerUserID: actorID, + Status: actions_model.Status(status), } runs, total, err := actions_model.FindRuns(ctx, opts) @@ -153,9 +166,20 @@ func List(ctx *context.Context) { ctx.Data["Runs"] = runs + actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx, actors) + + ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx) + pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) pager.SetDefaultParams(ctx) pager.AddParamString("workflow", workflow) + pager.AddParamString("actor", fmt.Sprint(actorID)) + pager.AddParamString("status", fmt.Sprint(status)) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplListActions) diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 7433a0a56b16..2fea8a9532b9 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -685,7 +685,11 @@ func UploadFilePost(ctx *context.Context) { message := strings.TrimSpace(form.CommitSummary) if len(message) == 0 { - message = ctx.Tr("repo.editor.upload_files_to_dir", form.TreePath) + dir := form.TreePath + if dir == "" { + dir = "/" + } + message = ctx.Tr("repo.editor.upload_files_to_dir", dir) } form.CommitMessage = strings.TrimSpace(form.CommitMessage) diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go index 6f9ca4874b00..fb5ada1bdb47 100644 --- a/routers/web/repo/helper.go +++ b/routers/web/repo/helper.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/context" ) -func makeSelfOnTop(ctx *context.Context, users []*user.User) []*user.User { +func MakeSelfOnTop(ctx *context.Context, users []*user.User) []*user.User { if ctx.Doer != nil { sort.Slice(users, func(i, j int) bool { if users[i].ID == users[j].ID { diff --git a/routers/web/repo/helper_test.go b/routers/web/repo/helper_test.go index e9ab44fe69f9..226e2e81f4dd 100644 --- a/routers/web/repo/helper_test.go +++ b/routers/web/repo/helper_test.go @@ -13,15 +13,15 @@ import ( ) func TestMakeSelfOnTop(t *testing.T) { - users := makeSelfOnTop(&context.Context{}, []*user.User{{ID: 2}, {ID: 1}}) + users := MakeSelfOnTop(&context.Context{}, []*user.User{{ID: 2}, {ID: 1}}) assert.Len(t, users, 2) assert.EqualValues(t, 2, users[0].ID) - users = makeSelfOnTop(&context.Context{Doer: &user.User{ID: 1}}, []*user.User{{ID: 2}, {ID: 1}}) + users = MakeSelfOnTop(&context.Context{Doer: &user.User{ID: 1}}, []*user.User{{ID: 2}, {ID: 1}}) assert.Len(t, users, 2) assert.EqualValues(t, 1, users[0].ID) - users = makeSelfOnTop(&context.Context{Doer: &user.User{ID: 2}}, []*user.User{{ID: 2}, {ID: 1}}) + users = MakeSelfOnTop(&context.Context{Doer: &user.User{ID: 2}}, []*user.User{{ID: 2}, {ID: 1}}) assert.Len(t, users, 2) assert.EqualValues(t, 2, users[0].ID) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 49ba753a7d34..a9ce1cc1e752 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -312,7 +312,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) handleTeamMentions(ctx) if ctx.Written() { @@ -508,7 +508,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) handleTeamMentions(ctx) } @@ -3487,7 +3487,7 @@ func IssuePosters(ctx *context.Context) { } } - posters = makeSelfOnTop(ctx, posters) + posters = MakeSelfOnTop(ctx, posters) resp := &userSearchResponse{} resp.Results = make([]*userSearchInfo, len(posters)) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 309e61cf6eba..ef9d5856da0e 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -809,7 +809,7 @@ func ViewPullFiles(ctx *context.Context) { ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) handleTeamMentions(ctx) if ctx.Written() { diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index afba1f18bfe6..5fddddb34408 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -349,7 +349,7 @@ func NewRelease(ctx *context.Context) { ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) upload.AddUploadContext(ctx, "release") ctx.HTML(http.StatusOK, tplReleaseNew) @@ -517,7 +517,7 @@ func EditRelease(ctx *context.Context) { ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = makeSelfOnTop(ctx, assigneeUsers) + ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers) ctx.HTML(http.StatusOK, tplReleaseNew) } diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index 444f16f86c19..3d7a0576027e 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -92,6 +92,12 @@ func SecretsPost(ctx *context.Context) { ctx.ServerError("getSecretsCtx", err) return } + + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + shared.PerformSecretsPost( ctx, sCtx.OwnerID, diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go new file mode 100644 index 000000000000..1005d1d9c610 --- /dev/null +++ b/routers/web/repo/setting/variables.go @@ -0,0 +1,119 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + shared "code.gitea.io/gitea/routers/web/shared/actions" +) + +const ( + tplRepoVariables base.TplName = "repo/settings/actions" + tplOrgVariables base.TplName = "org/settings/actions" + tplUserVariables base.TplName = "user/settings/actions" +) + +type variablesCtx struct { + OwnerID int64 + RepoID int64 + IsRepo bool + IsOrg bool + IsUser bool + VariablesTemplate base.TplName + RedirectLink string +} + +func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { + if ctx.Data["PageIsRepoSettings"] == true { + return &variablesCtx{ + RepoID: ctx.Repo.Repository.ID, + IsRepo: true, + VariablesTemplate: tplRepoVariables, + RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsOrgSettings"] == true { + return &variablesCtx{ + OwnerID: ctx.ContextUser.ID, + IsOrg: true, + VariablesTemplate: tplOrgVariables, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsUserSettings"] == true { + return &variablesCtx{ + OwnerID: ctx.Doer.ID, + IsUser: true, + VariablesTemplate: tplUserVariables, + RedirectLink: setting.AppSubURL + "/user/settings/actions/variables", + }, nil + } + + return nil, errors.New("unable to set Variables context") +} + +func Variables(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.variables") + ctx.Data["PageType"] = "variables" + ctx.Data["PageIsSharedSettingsVariables"] = true + + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) +} + +func VariableCreate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + + shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink) +} + +func VariableUpdate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + + shared.UpdateVariable(ctx, vCtx.RedirectLink) +} + +func VariableDelete(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + shared.DeleteVariable(ctx, vCtx.RedirectLink) +} diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go new file mode 100644 index 000000000000..8d1516c91ceb --- /dev/null +++ b/routers/web/shared/actions/variables.go @@ -0,0 +1,128 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "regexp" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) { + variables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{ + OwnerID: ownerID, + RepoID: repoID, + }) + if err != nil { + ctx.ServerError("FindVariables", err) + return + } + ctx.Data["Variables"] = variables +} + +// some regular expression of `variables` and `secrets` +// reference to: +// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables +// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets +var ( + nameRx = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") + forbiddenPrefixRx = regexp.MustCompile("(?i)^GIT(EA|HUB)_") + + forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI") +) + +func NameRegexMatch(name string) error { + if !nameRx.MatchString(name) || forbiddenPrefixRx.MatchString(name) { + log.Error("Name %s, regex match error", name) + return errors.New("name has invalid character") + } + return nil +} + +func envNameCIRegexMatch(name string) error { + if forbiddenEnvNameCIRx.MatchString(name) { + log.Error("Env Name cannot be ci") + return errors.New("env name cannot be ci") + } + return nil +} + +func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) { + form := web.GetForm(ctx).(*forms.EditVariableForm) + + if err := NameRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + if err := envNameCIRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data)) + if err != nil { + log.Error("InsertVariable error: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) + ctx.JSONRedirect(redirectURL) +} + +func UpdateVariable(ctx *context.Context, redirectURL string) { + id := ctx.ParamsInt64(":variable_id") + form := web.GetForm(ctx).(*forms.EditVariableForm) + + if err := NameRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + if err := envNameCIRegexMatch(form.Name); err != nil { + ctx.JSONError(err.Error()) + return + } + + ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ + ID: id, + Name: strings.ToUpper(form.Name), + Data: ReserveLineBreakForTextarea(form.Data), + }) + if err != nil || !ok { + log.Error("UpdateVariable error: %v", err) + ctx.JSONError(ctx.Tr("actions.variables.update.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) + ctx.JSONRedirect(redirectURL) +} + +func DeleteVariable(ctx *context.Context, redirectURL string) { + id := ctx.ParamsInt64(":variable_id") + + if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil { + log.Error("Delete variable [%d] failed: %v", id, err) + ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) + return + } + ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) + ctx.JSONRedirect(redirectURL) +} + +func ReserveLineBreakForTextarea(input string) string { + // Since the content is from a form which is a textarea, the line endings are \r\n. + // It's a standard behavior of HTML. + // But we want to store them as \n like what GitHub does. + // And users are unlikely to really need to keep the \r. + // Other than this, we should respect the original content, even leading or trailing spaces. + return strings.ReplaceAll(input, "\r\n", "\n") +} diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index a0d648f908fd..c09ce51499a4 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -4,14 +4,12 @@ package secrets import ( - "net/http" - "strings" - "code.gitea.io/gitea/models/db" secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/shared/actions" "code.gitea.io/gitea/services/forms" ) @@ -28,23 +26,20 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) { func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) { form := web.GetForm(ctx).(*forms.AddSecretForm) - content := form.Content - // Since the content is from a form which is a textarea, the line endings are \r\n. - // It's a standard behavior of HTML. - // But we want to store them as \n like what GitHub does. - // And users are unlikely to really need to keep the \r. - // Other than this, we should respect the original content, even leading or trailing spaces. - content = strings.ReplaceAll(content, "\r\n", "\n") + if err := actions.NameRegexMatch(form.Name); err != nil { + ctx.JSONError(ctx.Tr("secrets.creation.failed")) + return + } - s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Title, content) + s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data)) if err != nil { log.Error("InsertEncryptedSecret: %v", err) - ctx.Flash.Error(ctx.Tr("secrets.creation.failed")) - } else { - ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name)) + ctx.JSONError(ctx.Tr("secrets.creation.failed")) + return } - ctx.Redirect(redirectURL) + ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name)) + ctx.JSONRedirect(redirectURL) } func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) { @@ -52,12 +47,9 @@ func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectU if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id, OwnerID: ownerID, RepoID: repoID}); err != nil { log.Error("Delete secret %d failed: %v", id, err) - ctx.Flash.Error(ctx.Tr("secrets.deletion.failed")) - } else { - ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) + ctx.JSONError(ctx.Tr("secrets.deletion.failed")) + return } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": redirectURL, - }) + ctx.Flash.Success(ctx.Tr("secrets.deletion.success")) + ctx.JSONRedirect(redirectURL) } diff --git a/routers/web/web.go b/routers/web/web.go index 8ac01f174296..a7573b38f559 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -307,6 +307,15 @@ func registerRoutes(m *web.Route) { m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) } + addSettingVariablesRoutes := func() { + m.Group("/variables", func() { + m.Get("", repo_setting.Variables) + m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate) + m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate) + m.Post("/{variable_id}/delete", repo_setting.VariableDelete) + }) + } + addSettingsSecretsRoutes := func() { m.Group("/secrets", func() { m.Get("", repo_setting.Secrets) @@ -494,6 +503,7 @@ func registerRoutes(m *web.Route) { m.Get("", user_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.Get("/organization", user_setting.Organization) @@ -760,6 +770,7 @@ func registerRoutes(m *web.Route) { m.Get("", org_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.RouteMethods("/delete", "GET,POST", org.SettingsDelete) @@ -941,6 +952,7 @@ func registerRoutes(m *web.Route) { m.Get("", repo_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.Post("/migrate/cancel", repo.MigrateCancelPost) // this handler must be under "settings", otherwise this incomplete repo can't be accessed }, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer)) diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 4571ff6540c3..3e0f47a37e29 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -6,7 +6,6 @@ package ldap import ( "context" "fmt" - "sort" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -24,7 +23,6 @@ import ( func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name) - var existingUsers []int isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 var sshKeysNeedUpdate bool @@ -41,9 +39,14 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { default: } - sort.Slice(users, func(i, j int) bool { - return users[i].LowerName < users[j].LowerName - }) + usernameUsers := make(map[string]*user_model.User, len(users)) + mailUsers := make(map[string]*user_model.User, len(users)) + keepActiveUsers := make(map[int64]struct{}) + + for _, u := range users { + usernameUsers[u.LowerName] = u + mailUsers[strings.ToLower(u.Email)] = u + } sr, err := source.SearchEntries() if err != nil { @@ -59,11 +62,6 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings") } - sort.Slice(sr, func(i, j int) bool { - return sr[i].LowerName < sr[j].LowerName - }) - - userPos := 0 orgCache := make(map[string]*organization.Organization) teamCache := make(map[string]*organization.Team) @@ -86,21 +84,27 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name) default: } - if len(su.Username) == 0 { + if len(su.Username) == 0 && len(su.Mail) == 0 { continue } - if len(su.Mail) == 0 { - su.Mail = fmt.Sprintf("%s@localhost", su.Username) + var usr *user_model.User + if len(su.Username) > 0 { + usr = usernameUsers[su.LowerName] + } + if usr == nil && len(su.Mail) > 0 { + usr = mailUsers[strings.ToLower(su.Mail)] } - var usr *user_model.User - for userPos < len(users) && users[userPos].LowerName < su.LowerName { - userPos++ + if usr != nil { + keepActiveUsers[usr.ID] = struct{}{} + } else if len(su.Username) == 0 { + // we cannot create the user if su.Username is empty + continue } - if userPos < len(users) && users[userPos].LowerName == su.LowerName { - usr = users[userPos] - existingUsers = append(existingUsers, userPos) + + if len(su.Mail) == 0 { + su.Mail = fmt.Sprintf("%s@localhost", su.Username) } fullName := composeFullName(su.Name, su.Surname, su.Username) @@ -203,19 +207,17 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { // Deactivate users not present in LDAP if updateExisting { - existPos := 0 - for i, usr := range users { - for existPos < len(existingUsers) && i > existingUsers[existPos] { - existPos++ + for _, usr := range users { + if _, ok := keepActiveUsers[usr.ID]; ok { + continue } - if usr.IsActive && (existPos >= len(existingUsers) || i < existingUsers[existPos]) { - log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name) - usr.IsActive = false - err = user_model.UpdateUserCols(ctx, usr, "is_active") - if err != nil { - log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err) - } + log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name) + + usr.IsActive = false + err = user_model.UpdateUserCols(ctx, usr, "is_active") + if err != nil { + log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err) } } } diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index 3e0dbd132eb3..acf2d3373c8e 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -150,7 +150,7 @@ func registerUpdateGiteaChecker() { RunAtStart: false, Schedule: "@every 168h", }, - HTTPEndpoint: "https://dl.gitea.io/gitea/version.json", + HTTPEndpoint: "https://dl.gitea.com/gitea/version.json", }, func(ctx context.Context, _ *user_model.User, config Config) error { updateCheckerConfig := config.(*UpdateCheckerConfig) return updatechecker.GiteaUpdateChecker(updateCheckerConfig.HTTPEndpoint) diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 1315fb237b3b..0a4e2729e7b5 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -367,8 +367,8 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er // AddSecretForm for adding secrets type AddSecretForm struct { - Title string `binding:"Required;MaxSize(50)"` - Content string `binding:"Required"` + Name string `binding:"Required;MaxSize(255)"` + Data string `binding:"Required;MaxSize(65535)"` } // Validate validates the fields @@ -377,6 +377,16 @@ func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +type EditVariableForm struct { + Name string `binding:"Required;MaxSize(255)"` + Data string `binding:"Required;MaxSize(65535)"` +} + +func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + // NewAccessTokenForm form for creating access token type NewAccessTokenForm struct { Name string `binding:"Required;MaxSize(255)"` diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 2850cc8d370d..2ddc0c1ac673 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -46,15 +46,6 @@
{{.ScriptType}}
{{.locale.Tr "admin.config.reverse_auth_user"}}
{{.ReverseProxyAuthUser}}
- - {{if .EnvVars}} -
- {{range .EnvVars}} -
{{.Name}}
-
{{.Value}}
- {{end}} - {{end}} - diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 9007f4aa4b8a..3eb12ac99a98 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -3,62 +3,63 @@ {{$notificationUnreadCount = call .NotificationUnreadCount}} {{end}} -