diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6288e4d367..be34bfd02d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,10 @@ // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { "version":"20" - } + }, + "ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, + "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/devcontainers/features/python:1": {} }, "customizations": { "vscode": { @@ -20,7 +23,7 @@ "Vue.volar", "ms-azuretools.vscode-docker", "zixuanchen.vitest-explorer", - "alexcvzz.vscode-sqlite" + "qwtel.sqlite-viewer" ] } }, diff --git a/.eslintrc.yaml b/.eslintrc.yaml index ea85ab1298..71d8dc3814 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -25,10 +25,11 @@ env: es2022: true node: true -globals: - __webpack_public_path__: true - overrides: + - files: ["web_src/**/*"] + globals: + __webpack_public_path__: true + process: false # https://github.com/webpack/webpack/issues/15833 - files: ["web_src/**/*", "docs/**/*"] env: browser: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 398fb6eae3..0d43e191a3 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -20,7 +20,6 @@ jobs: detect: runs-on: ubuntu-latest timeout-minutes: 3 - # Map a step output to a job output outputs: backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} @@ -39,12 +38,14 @@ jobs: - "templates/**/*.tmpl" - "go.mod" - "go.sum" + - "Makefile" frontend: - "**/*.js" - "web_src/**" - "package.json" - "package-lock.json" + - "Makefile" docs: - "**/*.md" @@ -56,7 +57,9 @@ jobs: templates: - "templates/**/*.tmpl" - "poetry.lock" + docker: - "Dockerfile" - "Dockerfile.rootless" - "docker/**" + - "Makefile" diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 5e094f02c1..71e4fe02dc 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -96,6 +96,7 @@ jobs: - run: make deps-frontend - run: make lint-frontend - run: make checks-frontend + - run: make test-frontend - run: make frontend backend: @@ -110,7 +111,7 @@ jobs: check-latest: true # no frontend build here as backend should be able to build # even without any frontend files - - run: make deps-backend deps-tools + - run: make deps-backend - run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag - name: build-backend-arm64 run: make backend # test cross compile diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 71ec6dae84..0e94f5217c 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 581417df61..6851be742c 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 f1b2fb1957..000f534e85 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/.stylelintrc.yaml b/.stylelintrc.yaml index 8aeb706182..a96589f1e8 100644 --- a/.stylelintrc.yaml +++ b/.stylelintrc.yaml @@ -50,7 +50,7 @@ rules: declaration-no-important: null declaration-property-max-values: null declaration-property-unit-allowed-list: null - declaration-property-unit-disallowed-list: null + declaration-property-unit-disallowed-list: {line-height: [em]} declaration-property-value-allowed-list: null declaration-property-value-disallowed-list: null declaration-property-value-no-unknown: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66ee37bfa2..3710aa5fe2 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 f6d9b9d04f..d1989fbcb7 100644 --- a/Makefile +++ b/Makefile @@ -79,9 +79,12 @@ 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) + GITEA_VERSION ?= $(VERSION) else ifneq ($(GITHUB_REF_NAME),) VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME)) @@ -223,6 +226,8 @@ help: @echo " - test-frontend test frontend files" @echo " - test-backend test backend files" @echo " - test-e2e[\#TestSpecificName] test end to end using playwright" + @echo " - update-js update js dependencies" + @echo " - update-py update py dependencies" @echo " - webpack build webpack files" @echo " - svg build svg files" @echo " - fomantic build fomantic files" @@ -921,13 +926,20 @@ node_modules: package-lock.json poetry install @touch .venv -.PHONY: npm-update -npm-update: node-check | node_modules - npx updates -cu +.PHONY: update-js +update-js: node-check | node_modules + npx updates -u -f package.json rm -rf node_modules package-lock.json npm install --package-lock @touch node_modules +.PHONY: update-py +update-py: node-check | node_modules + npx updates -u -f pyproject.toml + rm -rf .venv poetry.lock + poetry install + @touch .venv + .PHONY: fomantic fomantic: rm -rf $(FOMANTIC_WORK_DIR)/build @@ -1011,9 +1023,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 0ee1772286..41793d3d92 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 285a814037..48eee9214d 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 346de5b21a..f52a91bd55 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 b148007fbe..4ed636a9b4 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) @@ -106,5 +106,21 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) { WriterOption: log.WriterConsoleOption{Stderr: out == os.Stderr}, } writer := log.NewEventWriterConsole("console-default", writeMode) - log.GetManager().GetLogger(log.DEFAULT).RemoveAllWriters().AddWriters(writer) + log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) +} + +// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout. +// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever. +func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error { + return func(c *cli.Context) error { + level := defaultLevel + if c.Bool("quiet") || c.GlobalBoolT("quiet") { + level = log.FATAL + } + if c.Bool("debug") || c.GlobalBool("debug") || c.Bool("verbose") || c.GlobalBool("verbose") { + level = log.TRACE + } + log.SetConsoleLogger(log.DEFAULT, "console-default", level) + return nil + } } diff --git a/cmd/doctor.go b/cmd/doctor.go index b596e9ac0c..cd5f125e20 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 { @@ -151,7 +151,7 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { log.FallbackErrorf("unable to create file log writer: %v", err) return } - log.GetManager().GetLogger(log.DEFAULT).RemoveAllWriters().AddWriters(writer) + log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) } } diff --git a/cmd/dump.go b/cmd/dump.go index 7dda7fd2b3..0b7c1d32c5 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 e51f8477b4..105acee26c 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -22,9 +22,9 @@ import ( "github.com/urfave/cli" ) -// Cmdembedded represents the available extract sub-command. +// CmdEmbedded represents the available extract sub-command. var ( - Cmdembedded = cli.Command{ + CmdEmbedded = cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", @@ -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/hook.go b/cmd/hook.go index 6453267832..ed6efc19ea 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -15,6 +15,7 @@ import ( "time" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -32,6 +33,7 @@ var ( Name: "hook", Usage: "Delegate commands to corresponding Git hooks", Description: "This should only be called by Git", + Before: PrepareConsoleLoggerLevel(log.FATAL), Subcommands: []cli.Command{ subcmdHookPreReceive, subcmdHookUpdate, diff --git a/cmd/keys.go b/cmd/keys.go index deb94fca5d..8aeee37527 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" "github.com/urfave/cli" @@ -17,6 +18,7 @@ import ( var CmdKeys = cli.Command{ Name: "keys", Usage: "This command queries the Gitea database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runKeys, Flags: []cli.Flag{ cli.StringFlag{ diff --git a/cmd/mailer.go b/cmd/mailer.go index 74bae1ab68..eaa5a1afe1 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 5a7ede4939..c19e28f13d 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 87bf1cce20..79052e58a8 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -44,6 +44,7 @@ var CmdServ = cli.Command{ Name: "serv", Usage: "This command should only be called by SSH shell", Description: "Serv provides access auth for repositories", + Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runServ, Flags: []cli.Flag{ cli.BoolFlag{ @@ -61,7 +62,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 3a46b90911..05f3b2ddb2 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -35,6 +35,7 @@ var CmdWeb = cli.Command{ Usage: "Start Gitea web server", Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, + Before: PrepareConsoleLoggerLevel(log.INFO), Action: runWeb, Flags: []cli.Flag{ cli.StringFlag{ @@ -101,12 +102,111 @@ func createPIDFile(pidPath string) { } } -func runWeb(ctx *cli.Context) error { - if ctx.Bool("verbose") { - setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout) - } else if ctx.Bool("quiet") { - setupConsoleLogger(log.FATAL, log.CanColorStdout, os.Stdout) +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 { defer func() { if panicked := recover(); panicked != nil { log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) @@ -128,75 +228,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() - }() + go servePprof() } - 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 - } - } - - // 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 +261,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 3405d7d429..230ed58269 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.InitWorkPathAndCfgProvider(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 0cb7e15bd6..42ff0ec693 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -119,10 +119,13 @@ RUN_USER = ; git ;; Permission for unix socket ;UNIX_SOCKET_PERMISSION = 666 ;; -;; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. -;; In most cases you do not need to change the default value. -;; Alter it only if your SSH server node is not the same as HTTP node. -;; Do not set this variable if PROTOCOL is set to 'unix'. +;; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. In +;; most cases you do not need to change the default value. Alter it only if +;; your SSH server node is not the same as HTTP node. For different protocol, the default +;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. +;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. +;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default +;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. ;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/ ;; ;; When making local connections pass the PROXY protocol header. @@ -344,9 +347,6 @@ NAME = gitea USER = root ;PASSWD = ;Use PASSWD = `your password` for quoting if you use special characters in the password. ;SSL_MODE = false ; either "false" (default), "true", or "skip-verify" -;CHARSET = utf8mb4 ;either "utf8" or "utf8mb4", default is "utf8mb4". -;; -;; NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; @@ -1346,10 +1346,10 @@ LEVEL = Info ;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve ;ISSUE_INDEXER_PATH = indexers/issues.bleve ; Relative paths will be made absolute against _`AppWorkPath`_. ;; -;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch -;ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 +;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch (e.g. http://elastic:password@localhost:9200) or meilisearch (e.g. http://:apikey@localhost:7700) +;ISSUE_INDEXER_CONN_STR = ;; -;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch +;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch. ;ISSUE_INDEXER_NAME = gitea_issues ;; ;; Timeout the indexer if it takes longer than this to start. @@ -2168,7 +2168,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/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl index 87e6e9dfc2..1ebf5b73c8 100644 --- a/docker/manifest.rootless.tmpl +++ b/docker/manifest.rootless.tmpl @@ -1,12 +1,14 @@ image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-rootless {{#if build.tags}} {{#unless (contains "-rc" build.tag)}} +{{#unless (contains "-dev" build.tag)}} tags: {{#each build.tags}} - {{this}}-rootless {{/each}} - "latest-rootless" {{/unless}} +{{/unless}} {{/if}} manifests: - diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl index 18847064f6..08ccf61b57 100644 --- a/docker/manifest.tmpl +++ b/docker/manifest.tmpl @@ -1,12 +1,14 @@ image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}} {{#if build.tags}} {{#unless (contains "-rc" build.tag)}} +{{#unless (contains "-dev" build.tag)}} tags: {{#each build.tags}} - {{this}} {{/each}} - "latest" {{/unless}} +{{/unless}} {{/if}} manifests: - diff --git a/docs/content/doc/administration/command-line.en-us.md b/docs/content/doc/administration/command-line.en-us.md index 37ba0c04da..d7e74ee24d 100644 --- a/docs/content/doc/administration/command-line.en-us.md +++ b/docs/content/doc/administration/command-line.en-us.md @@ -108,6 +108,14 @@ Admin operations: - `--all`, `-A`: Force a password change for all users - `--exclude username`, `-e username`: Exclude the given user. Can be set multiple times. - `--unset`: Revoke forced password change for the given users + - `generate-access-token`: + - Options: + - `--username value`, `-u value`: Username. Required. + - `--token-name value`, `-t value`: Token name. Required. + - `--scopes value`: Comma-separated list of scopes. Scopes follow the format `[read|write]:` or `all` where `` is one of the available visual groups you can see when opening the API page showing the available routes (for example `repo`). + - Examples: + - `gitea admin user generate-access-token --username myname --token-name mytoken` + - `gitea admin user generate-access-token --help` - `regenerate` - Options: - `hooks`: Regenerate Git Hooks for all repositories 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 660997a7db..342b072b24 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -317,8 +317,11 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `LOCAL_ROOT_URL`: **%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/**: Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. In most cases you do not need to change the default value. Alter it only if - your SSH server node is not the same as HTTP node. Do not set this variable - if `PROTOCOL` is set to `http+unix`. + your SSH server node is not the same as HTTP node. For different protocol, the default + values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. + If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. + If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default + value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. - `LOCAL_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)s**: When making local connections pass the PROXY protocol header. This should be set to false if the local connection will go through the proxy. - `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to -1 to @@ -446,7 +449,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SQLITE_TIMEOUT`: **500**: Query timeout for SQLite3 only. - `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE. - `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating. -- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `LOG_SQL`: **true**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. @@ -462,15 +464,15 @@ relation to port exhaustion. ## Indexer (`indexer`) - `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db`, `elasticsearch` or `meilisearch`. -- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch, or meilisearch. i.e. http://elastic:changeme@localhost:9200 -- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch +- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch (e.g. http://elastic:password@localhost:9200) or meilisearch (e.g. http://:apikey@localhost:7700) +- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch. - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch. Relative paths will be made absolute against _`AppWorkPath`_. - `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space, about 6 times more than the repository size). - `REPO_INDEXER_REPO_TYPES`: **sources,forks,mirrors,templates**: Repo indexer units. The items to index could be `sources`, `forks`, `mirrors`, `templates` or any combination of them separated by a comma. If empty then it defaults to `sources` only, as if you'd like to disable fully please see `REPO_INDEXER_ENABLED`. - `REPO_INDEXER_TYPE`: **bleve**: Code search engine type, could be `bleve` or `elasticsearch`. - `REPO_INDEXER_PATH`: **indexers/repos.bleve**: Index file used for code search. -- `REPO_INDEXER_CONN_STR`: ****: Code indexer connection string, available when `REPO_INDEXER_TYPE` is elasticsearch. i.e. http://elastic:changeme@localhost:9200 +- `REPO_INDEXER_CONN_STR`: ****: Code indexer connection string, available when `REPO_INDEXER_TYPE` is elasticsearch. i.e. http://elastic:password@localhost:9200 - `REPO_INDEXER_NAME`: **gitea_codes**: Code indexer name, available when `REPO_INDEXER_TYPE` is elasticsearch - `REPO_INDEXER_INCLUDE`: **empty**: A comma separated list of glob patterns (see https://github.com/gobwas/glob) to **include** in the index. Use `**.txt` to match any files with .txt extension. An empty list means include all files. @@ -1016,7 +1018,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.en-us.md b/docs/content/doc/help/faq.en-us.md index f609b6c867..ae59a9b880 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -396,8 +396,6 @@ Please run `gitea convert`, or run `ALTER DATABASE database_name CHARACTER SET u for the database_name and run `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` for each table in the database. -You will also need to change the app.ini database charset to `CHARSET=utf8mb4`. - ## Why are Emoji displaying only as placeholders or in monochrome Gitea requires the system or browser to have one of the supported Emoji fonts installed, which are Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji and Twemoji Mozilla. Generally, the operating system should already provide one of these fonts, but especially on Linux, it may be necessary to install them manually. diff --git a/docs/content/doc/help/faq.zh-cn.md b/docs/content/doc/help/faq.zh-cn.md index 3458534524..6a63b4530e 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 f5273054bc..f3d3110439 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 858cee2193..78db79775d 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 b46a61df02..0847a3b852 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 f5e8f9f762..83647a2eab 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 51446911d5..28dfbda81d 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 2e149c6a2b..fa545ee025 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 9a46562f06..9d287d111d 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 9812efaf94..46442845e7 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 0c278648b3..5687861b8c 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/go.mod b/go.mod index 7b7a51efbb..885bb34220 100644 --- a/go.mod +++ b/go.mod @@ -122,7 +122,7 @@ require ( mvdan.cc/xurls/v2 v2.4.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.12 - xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e + xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75 ) require ( diff --git a/go.sum b/go.sum index c3ac719f3f..9b4538bc65 100644 --- a/go.sum +++ b/go.sum @@ -1923,5 +1923,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1: xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM= xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e h1:d5PY6mwuQK5/7T6VKfFswaKMzLmGTHkJ/ZS7+cUIAjk= -xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= +xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75 h1:ReBAlO50dCIXCWF8Gbi0ZRa62AGAwCJNCPaUNUa7JSg= +xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/main.go b/main.go index 49093eb8a7..9b561376c3 100644 --- a/main.go +++ b/main.go @@ -33,162 +33,164 @@ 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 +// ./gitea admin help +// ./gitea admin auth help +// ./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.Commands = []cli.Command{ + app.EnableBashCompletion = true + + // these sub-commands need to use config file + subCmdWithIni := []cli.Command{ cmd.CmdWeb, cmd.CmdServ, cmd.CmdHook, cmd.CmdDump, - cmd.CmdCert, cmd.CmdAdmin, - cmd.CmdGenerate, cmd.CmdMigrate, cmd.CmdKeys, cmd.CmdConvert, cmd.CmdDoctor, cmd.CmdManager, - cmd.Cmdembedded, + cmd.CmdEmbedded, cmd.CmdMigrateStorage, - cmd.CmdDocs, cmd.CmdDumpRepository, cmd.CmdRestoreRepository, cmd.CmdActions, + cmdHelp, // TODO: the "help" sub-command was used to show the more information for "work path" and "custom config", in the future, it should avoid doing so + } + // these sub-commands do not need the config file, and they do not depend on any path or environment variable. + subCmdStandalone := []cli.Command{ + cmd.CmdCert, + cmd.CmdGenerate, + cmd.CmdDocs, } - // 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{ + // shared configuration flags, they are for global and for each sub-command at the same time + // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed + // keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore. + 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, 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 - for i := range app.Commands { - setFlagsAndBeforeOnSubcommands(&app.Commands[i], defaultFlags, establishCustomPath) + app.Flags = append(app.Flags, globalFlags...) + app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) // TODO: the web flags polluted the global flags, they are not really global flags + app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action) + app.HideHelp = true // use our own help action to show helps (with more information like default config) + app.Before = cmd.PrepareConsoleLoggerLevel(log.INFO) + for i := range subCmdWithIni { + prepareSubcommands(&subCmdWithIni[i], globalFlags) } - - app.EnableBashCompletion = true + app.Commands = append(app.Commands, subCmdWithIni...) + app.Commands = append(app.Commands, subCmdStandalone...) 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 +// 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(action any) func(ctx *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 == nil { - break + setting.InitWorkPathAndCommonConfig(os.Getenv, args) + if ctx.Bool("help") || action == nil { + // the default behavior of "urfave/cli": "nil action" means "show help" + return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx) } - if currentCtx.IsSet("custom-path") && len(providedCustom) == 0 { - providedCustom = currentCtx.String("custom-path") - } - if currentCtx.IsSet("config") && len(providedConf) == 0 { - providedConf = currentCtx.String("config") - } - if currentCtx.IsSet("work-path") && len(providedWorkPath) == 0 { - providedWorkPath = currentCtx.String("work-path") - } - currentCtx = currentCtx.Parent() - + return action.(func(*cli.Context) error)(ctx) } - 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 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.go b/models/actions/run.go index 0654809900..7b62ff884f 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -36,12 +36,13 @@ type ActionRun struct { TriggerUser *user_model.User `xorm:"-"` Ref string CommitSHA string - IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. - NeedApproval bool // may need approval if it's a fork pull request - ApprovedBy int64 `xorm:"index"` // who approved - Event webhook_module.HookEventType - EventPayload string `xorm:"LONGTEXT"` - Status Status `xorm:"index"` + IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. + NeedApproval bool // may need approval if it's a fork pull request + ApprovedBy int64 `xorm:"index"` // who approved + Event webhook_module.HookEventType // the webhook event that causes the workflow to run + EventPayload string `xorm:"LONGTEXT"` + TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow + Status Status `xorm:"index"` Started timeutil.TimeStamp Stopped timeutil.TimeStamp Created timeutil.TimeStamp `xorm:"created"` diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 56de8eb916..29ab193d57 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/task.go b/models/actions/task.go index 79b1d46dd0..719fd19365 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -344,6 +344,9 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { return err } +// UpdateTaskByState updates the task by the state. +// It will always update the task if the state is not final, even there is no change. +// So it will update ActionTask.Updated to avoid the task being judged as a zombie task. func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionTask, error) { stepStates := map[int64]*runnerv1.StepState{} for _, v := range state.Steps { @@ -384,6 +387,12 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT }, nil); err != nil { return nil, err } + } else { + // Force update ActionTask.Updated to avoid the task being judged as a zombie task + task.Updated = timeutil.TimeStampNow() + if err := UpdateTask(ctx, task, "updated"); err != nil { + return nil, err + } } if err := task.LoadAttributes(ctx); err != nil { diff --git a/models/actions/variable.go b/models/actions/variable.go new file mode 100644 index 0000000000..e0bb59ccbe --- /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/db/engine.go b/models/db/engine.go index 56dd209fd7..3eb16f8042 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -123,7 +123,10 @@ func newXORMEngine() (*xorm.Engine, error) { // SyncAllTables sync the schemas of all tables, is required by unit test code func SyncAllTables() error { - return x.StoreEngine("InnoDB").Sync2(tables...) + _, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ + WarnIfDatabaseColumnMissed: true, + }, tables...) + return err } // InitEngine initializes the xorm.Engine and sets it as db.DefaultContext diff --git a/models/db/search.go b/models/db/search.go index 105cb64c41..aa577f08e0 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -20,6 +20,10 @@ const ( SearchOrderByNewest SearchOrderBy = "created_unix DESC" SearchOrderBySize SearchOrderBy = "size ASC" SearchOrderBySizeReverse SearchOrderBy = "size DESC" + SearchOrderByGitSize SearchOrderBy = "git_size ASC" + SearchOrderByGitSizeReverse SearchOrderBy = "git_size DESC" + SearchOrderByLFSSize SearchOrderBy = "lfs_size ASC" + SearchOrderByLFSSizeReverse SearchOrderBy = "lfs_size DESC" SearchOrderByID SearchOrderBy = "id ASC" SearchOrderByIDReverse SearchOrderBy = "id DESC" SearchOrderByStars SearchOrderBy = "num_stars ASC" diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index bac1cb9eb6..3650ce057e 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -7,6 +7,7 @@ import ( "context" "errors" "io" + "io/fs" "os" "path/filepath" "strconv" @@ -21,6 +22,7 @@ var defaultFileBlockSize int64 = 32 * 1024 type File interface { io.ReadWriteCloser io.Seeker + fs.File } type file struct { @@ -193,10 +195,26 @@ func (f *file) Close() error { return nil } +func (f *file) Stat() (os.FileInfo, error) { + if f.metaID == 0 { + return nil, os.ErrInvalid + } + + fileMeta, err := findFileMetaByID(f.ctx, f.metaID) + if err != nil { + return nil, err + } + return fileMeta, nil +} + func timeToFileTimestamp(t time.Time) int64 { return t.UnixMicro() } +func fileTimestampToTime(timestamp int64) time.Time { + return time.UnixMicro(timestamp) +} + func (f *file) loadMetaByPath() (*dbfsMeta, error) { var fileMeta dbfsMeta if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil { diff --git a/models/dbfs/dbfs.go b/models/dbfs/dbfs.go index 6b5b3beeb2..f68b4a2b70 100644 --- a/models/dbfs/dbfs.go +++ b/models/dbfs/dbfs.go @@ -5,7 +5,10 @@ package dbfs import ( "context" + "io/fs" "os" + "path" + "time" "code.gitea.io/gitea/models/db" ) @@ -100,3 +103,29 @@ func Remove(ctx context.Context, name string) error { defer f.Close() return f.delete() } + +var _ fs.FileInfo = (*dbfsMeta)(nil) + +func (m *dbfsMeta) Name() string { + return path.Base(m.FullPath) +} + +func (m *dbfsMeta) Size() int64 { + return m.FileSize +} + +func (m *dbfsMeta) Mode() fs.FileMode { + return os.ModePerm +} + +func (m *dbfsMeta) ModTime() time.Time { + return fileTimestampToTime(m.ModifyTimestamp) +} + +func (m *dbfsMeta) IsDir() bool { + return false +} + +func (m *dbfsMeta) Sys() any { + return nil +} diff --git a/models/dbfs/dbfs_test.go b/models/dbfs/dbfs_test.go index 300758c623..96cb1014c7 100644 --- a/models/dbfs/dbfs_test.go +++ b/models/dbfs/dbfs_test.go @@ -111,6 +111,19 @@ func TestDbfsBasic(t *testing.T) { _, err = OpenFile(db.DefaultContext, "test2.txt", os.O_RDONLY) assert.Error(t, err) + + // test stat + f, err = OpenFile(db.DefaultContext, "test/test.txt", os.O_RDWR|os.O_CREATE) + assert.NoError(t, err) + stat, err := f.Stat() + assert.NoError(t, err) + assert.EqualValues(t, "test.txt", stat.Name()) + assert.EqualValues(t, 0, stat.Size()) + _, err = f.Write([]byte("0123456789")) + assert.NoError(t, err) + stat, err = f.Stat() + assert.NoError(t, err) + assert.EqualValues(t, 10, stat.Size()) } func TestDbfsReadWrite(t *testing.T) { diff --git a/models/issues/comment.go b/models/issues/comment.go index e5c90f265e..dbe4434ca7 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -167,7 +167,7 @@ func AsCommentType(typeName string) CommentType { func (t CommentType) HasContentSupport() bool { switch t { - case CommentTypeComment, CommentTypeCode, CommentTypeReview: + case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview: return true } return false diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 304ac4569f..d447d7542c 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -18,11 +18,11 @@ import ( type CodeComments map[string]map[int64][]*Comment // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line -func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) { - return fetchCodeCommentsByReview(ctx, issue, currentUser, nil) +func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool) (CodeComments, error) { + return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments) } -func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) { +func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) { pathToLineToComment := make(CodeComments) if review == nil { review = &Review{ID: 0} @@ -33,7 +33,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u ReviewID: review.ID, } - comments, err := findCodeComments(ctx, opts, issue, currentUser, review) + comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments) if err != nil { return nil, err } @@ -47,15 +47,17 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u return pathToLineToComment, nil } -func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) { +func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) { var comments CommentList if review == nil { review = &Review{ID: 0} } conds := opts.ToConds() - if review.ID == 0 { + + if !showOutdatedComments && review.ID == 0 { conds = conds.And(builder.Eq{"invalidated": false}) } + e := db.GetEngine(ctx) if err := e.Where(conds). Asc("comment.created_unix"). @@ -118,12 +120,12 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number -func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) { +func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) { opts := FindCommentsOptions{ Type: CommentTypeCode, IssueID: issue.ID, TreePath: treePath, Line: line, } - return findCodeComments(ctx, opts, issue, currentUser, nil) + return findCodeComments(ctx, opts, issue, currentUser, nil, showOutdatedComments) } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index 43bad1660f..d766625be3 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -50,7 +50,7 @@ func TestFetchCodeComments(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user) + res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) @@ -58,7 +58,7 @@ func TestFetchCodeComments(t *testing.T) { assert.Equal(t, int64(4), res["README.md"][4][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2) + res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false) assert.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index dad21c1477..9cc41ec6ab 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -229,39 +229,41 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { return nil } -func (issues IssueList) getProjectIDs() []int64 { - ids := make(container.Set[int64], len(issues)) - for _, issue := range issues { - ids.Add(issue.ProjectID()) - } - return ids.Values() -} +func (issues IssueList) LoadProjects(ctx context.Context) error { + issueIDs := issues.getIssueIDs() + projectMaps := make(map[int64]*project_model.Project, len(issues)) + left := len(issueIDs) -func (issues IssueList) loadProjects(ctx context.Context) error { - projectIDs := issues.getProjectIDs() - if len(projectIDs) == 0 { - return nil + type projectWithIssueID struct { + *project_model.Project `xorm:"extends"` + IssueID int64 } - projectMaps := make(map[int64]*project_model.Project, len(projectIDs)) - left := len(projectIDs) for left > 0 { limit := db.DefaultMaxInSize if left < limit { limit = left } + + projects := make([]*projectWithIssueID, 0, limit) err := db.GetEngine(ctx). - In("id", projectIDs[:limit]). - Find(&projectMaps) + Table("project"). + Select("project.*, project_issue.issue_id"). + Join("INNER", "project_issue", "project.id = project_issue.project_id"). + In("project_issue.issue_id", issueIDs[:limit]). + Find(&projects) if err != nil { return err } + for _, project := range projects { + projectMaps[project.IssueID] = project.Project + } left -= limit - projectIDs = projectIDs[limit:] + issueIDs = issueIDs[limit:] } for _, issue := range issues { - issue.Project = projectMaps[issue.ProjectID()] + issue.Project = projectMaps[issue.ID] } return nil } @@ -541,7 +543,7 @@ func (issues IssueList) loadAttributes(ctx context.Context) error { return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err) } - if err := issues.loadProjects(ctx); err != nil { + if err := issues.LoadProjects(ctx); err != nil { return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err) } diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 954a20ffe4..97ce9e43b3 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -66,8 +66,12 @@ func TestIssueList_LoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) + assert.NotNil(t, issue.Project) } else if issue.ID == int64(2) { assert.Equal(t, int64(3682), issue.TotalTrackedTime) + assert.Nil(t, issue.Project) + } else { + assert.Nil(t, issue.Project) } } } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 04d12e055c..b163c68357 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -27,11 +27,6 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) { return err } -// ProjectID return project id if issue was assigned to one -func (issue *Issue) ProjectID() int64 { - return issue.projectID(db.DefaultContext) -} - func (issue *Issue) projectID(ctx context.Context) int64 { var ip project_model.ProjectIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) diff --git a/models/issues/review.go b/models/issues/review.go index 3a1ab7468a..3685c65ce5 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -141,7 +141,7 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) { if err = r.loadIssue(ctx); err != nil { return } - r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r) + r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false) return err } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index ac65d654f2..1d7592926b 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -6,6 +6,7 @@ package issues import ( "context" "errors" + "fmt" "time" "code.gitea.io/gitea/models/db" @@ -173,10 +174,12 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim } if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - Content: util.SecToTime(amount), + Issue: issue, + Repo: issue.Repo, + Doer: user, + // Content before v1.21 did store the formated string instead of seconds, + // so use "|" as delimeter to mark the new format + Content: fmt.Sprintf("|%d", amount), Type: CommentTypeAddTimeManual, TimeID: t.ID, }); err != nil { @@ -199,8 +202,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 +214,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 +224,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, } return nil, err } - totalTimes[user] = util.SecToTime(total) + totalTimes[user] = total } return totalTimes, nil } @@ -251,10 +254,12 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { return err } if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - Content: "- " + util.SecToTime(removedTime), + Issue: issue, + Repo: issue.Repo, + Doer: user, + // Content before v1.21 did store the formated string instead of seconds, + // so use "|" as delimeter to mark the new format + Content: fmt.Sprintf("|%d", removedTime), Type: CommentTypeDeleteTimeManual, }); err != nil { return err @@ -280,10 +285,12 @@ func DeleteTime(t *TrackedTime) error { } if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: t.Issue, - Repo: t.Issue.Repo, - Doer: t.User, - Content: "- " + util.SecToTime(t.Time), + Issue: t.Issue, + Repo: t.Issue.Repo, + Doer: t.User, + // Content before v1.21 did store the formated string instead of seconds, + // so use "|" as delimeter to mark the new format + Content: fmt.Sprintf("|%d", t.Time), Type: CommentTypeDeleteTimeManual, }); err != nil { return err diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index 99b977cca5..37ba1cfdc4 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -35,7 +35,7 @@ func TestAddTime(t *testing.T) { assert.Equal(t, int64(3661), tt.Time) comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}) - assert.Equal(t, "1 hour 1 minute", comment.Content) + assert.Equal(t, "|3661", comment.Content) } func TestGetTrackedTimes(t *testing.T) { @@ -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 dd99a1eda2..c3100ba665 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 1ee82da436..175eadf61a 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -503,6 +503,12 @@ 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), + // v262 -> v263 + NewMigration("Add TriggerEvent to action_run table", v1_21.AddTriggerEventToActionRun), + // v263 -> v264 + NewMigration("Add git_size and lfs_size columns to repository table", v1_21.AddGitSizeAndLFSSizeToRepositoryTable), // to modify later NewMigration("Add size limit on repository", v1_21.AddSizeLimitOnRepo), } diff --git a/models/migrations/v1_21/v261.go b/models/migrations/v1_21/v261.go new file mode 100644 index 0000000000..4ec1160d0b --- /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/migrations/v1_21/v262.go b/models/migrations/v1_21/v262.go new file mode 100644 index 0000000000..23e900572a --- /dev/null +++ b/models/migrations/v1_21/v262.go @@ -0,0 +1,16 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "xorm.io/xorm" +) + +func AddTriggerEventToActionRun(x *xorm.Engine) error { + type ActionRun struct { + TriggerEvent string + } + + return x.Sync(new(ActionRun)) +} diff --git a/models/migrations/v1_21/v263.go b/models/migrations/v1_21/v263.go new file mode 100644 index 0000000000..88a5cb92b4 --- /dev/null +++ b/models/migrations/v1_21/v263.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "fmt" + + "xorm.io/xorm" +) + +// AddGitSizeAndLFSSizeToRepositoryTable: add GitSize and LFSSize columns to Repository +func AddGitSizeAndLFSSizeToRepositoryTable(x *xorm.Engine) error { + type Repository struct { + GitSize int64 `xorm:"NOT NULL DEFAULT 0"` + LFSSize int64 `xorm:"NOT NULL DEFAULT 0"` + } + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + if err := sess.Sync2(new(Repository)); err != nil { + return fmt.Errorf("Sync2: %w", err) + } + + _, err := sess.Exec(`UPDATE repository SET lfs_size=(SELECT SUM(size) FROM lfs_meta_object WHERE lfs_meta_object.repository_id=repository.ID) WHERE EXISTS (SELECT 1 FROM lfs_meta_object WHERE lfs_meta_object.repository_id=repository.ID)`) + if err != nil { + return err + } + + _, err = sess.Exec(`UPDATE repository SET git_size = size - lfs_size`) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 4ba84cd260..a503f6e16c 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -165,6 +166,8 @@ type Repository struct { SizeLimit int64 `xorm:"NOT NULL DEFAULT 0"` Size int64 `xorm:"NOT NULL DEFAULT 0"` EnableSizeLimit bool `xorm:"NOT NULL DEFAULT true"` + GitSize int64 `xorm:"NOT NULL DEFAULT 0"` + LFSSize int64 `xorm:"NOT NULL DEFAULT 0"` CodeIndexerStatus *RepoIndexerStatus `xorm:"-"` StatsIndexerStatus *RepoIndexerStatus `xorm:"-"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` @@ -198,6 +201,42 @@ func (repo *Repository) SanitizedOriginalURL() string { return u.String() } +// text representations to be returned in SizeDetail.Name +const ( + SizeDetailNameGit = "git" + SizeDetailNameLFS = "lfs" +) + +type SizeDetail struct { + Name string + Size int64 +} + +// SizeDetails forms a struct with various size details about repository +func (repo *Repository) SizeDetails() []SizeDetail { + sizeDetails := []SizeDetail{ + { + Name: SizeDetailNameGit, + Size: repo.GitSize, + }, + { + Name: SizeDetailNameLFS, + Size: repo.LFSSize, + }, + } + return sizeDetails +} + +// SizeDetailsString returns a concatenation of all repository size details as a string +func (repo *Repository) SizeDetailsString() string { + var str strings.Builder + sizeDetails := repo.SizeDetails() + for _, detail := range sizeDetails { + str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + } + return strings.TrimSuffix(str.String(), ", ") +} + func (repo *Repository) LogString() string { if repo == nil { return "" diff --git a/models/repo/update.go b/models/repo/update.go index 4894e0a1b9..c4fba32ad2 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -185,9 +185,11 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s } // UpdateRepoSize updates the repository size, calculating it using getDirectorySize -func UpdateRepoSize(ctx context.Context, repoID, size int64) error { - _, err := db.GetEngine(ctx).ID(repoID).Cols("size").NoAutoTime().Update(&Repository{ - Size: size, +func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error { + _, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{ + Size: gitSize + lfsSize, + GitSize: gitSize, + LFSSize: lfsSize, }) return err } diff --git a/models/secret/secret.go b/models/secret/secret.go index 8b23b6c35c..5a17cc37a5 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 5351ff1139..f926a65538 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/models/user/setting_keys.go b/models/user/setting_keys.go index 10255735b3..72b3974eee 100644 --- a/models/user/setting_keys.go +++ b/models/user/setting_keys.go @@ -8,6 +8,8 @@ const ( SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types" // SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour" + // SettingsKeyShowOutdatedComments is the setting key wether or not to show outdated comments in PRs + SettingsKeyShowOutdatedComments = "comment_code.show_outdated" // UserActivityPubPrivPem is user's private key UserActivityPubPrivPem = "activitypub.priv_pem" // UserActivityPubPubPem is user's public key diff --git a/modules/actions/github.go b/modules/actions/github.go index f3cb335da9..71f81a8903 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -8,33 +8,33 @@ import ( ) const ( - githubEventPullRequest = "pull_request" - githubEventPullRequestTarget = "pull_request_target" - githubEventPullRequestReviewComment = "pull_request_review_comment" - githubEventPullRequestReview = "pull_request_review" - githubEventRegistryPackage = "registry_package" - githubEventCreate = "create" - githubEventDelete = "delete" - githubEventFork = "fork" - githubEventPush = "push" - githubEventIssues = "issues" - githubEventIssueComment = "issue_comment" - githubEventRelease = "release" - githubEventPullRequestComment = "pull_request_comment" - githubEventGollum = "gollum" + GithubEventPullRequest = "pull_request" + GithubEventPullRequestTarget = "pull_request_target" + GithubEventPullRequestReviewComment = "pull_request_review_comment" + GithubEventPullRequestReview = "pull_request_review" + GithubEventRegistryPackage = "registry_package" + GithubEventCreate = "create" + GithubEventDelete = "delete" + GithubEventFork = "fork" + GithubEventPush = "push" + GithubEventIssues = "issues" + GithubEventIssueComment = "issue_comment" + GithubEventRelease = "release" + GithubEventPullRequestComment = "pull_request_comment" + GithubEventGollum = "gollum" ) // canGithubEventMatch check if the input Github event can match any Gitea event. func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool { switch eventName { - case githubEventRegistryPackage: + case GithubEventRegistryPackage: return triggedEvent == webhook_module.HookEventPackage // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum - case githubEventGollum: + case GithubEventGollum: return triggedEvent == webhook_module.HookEventWiki - case githubEventIssues: + case GithubEventIssues: switch triggedEvent { case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, @@ -46,7 +46,7 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } - case githubEventPullRequest, githubEventPullRequestTarget: + case GithubEventPullRequest, GithubEventPullRequestTarget: switch triggedEvent { case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, @@ -58,7 +58,7 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } - case githubEventPullRequestReview: + case GithubEventPullRequestReview: switch triggedEvent { case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewComment, diff --git a/modules/actions/github_test.go b/modules/actions/github_test.go index e7f4158ae2..4bf55ae03f 100644 --- a/modules/actions/github_test.go +++ b/modules/actions/github_test.go @@ -21,85 +21,85 @@ func TestCanGithubEventMatch(t *testing.T) { // registry_package event { "registry_package matches", - githubEventRegistryPackage, + GithubEventRegistryPackage, webhook_module.HookEventPackage, true, }, { "registry_package cannot match", - githubEventRegistryPackage, + GithubEventRegistryPackage, webhook_module.HookEventPush, false, }, // issues event { "issue matches", - githubEventIssues, + GithubEventIssues, webhook_module.HookEventIssueLabel, true, }, { "issue cannot match", - githubEventIssues, + GithubEventIssues, webhook_module.HookEventIssueComment, false, }, // issue_comment event { "issue_comment matches", - githubEventIssueComment, + GithubEventIssueComment, webhook_module.HookEventIssueComment, true, }, { "issue_comment cannot match", - githubEventIssueComment, + GithubEventIssueComment, webhook_module.HookEventIssues, false, }, // pull_request event { "pull_request matches", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequestSync, true, }, { "pull_request cannot match", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequestComment, false, }, // pull_request_target event { "pull_request_target matches", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequest, true, }, { "pull_request_target cannot match", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequestComment, false, }, // pull_request_review event { "pull_request_review matches", - githubEventPullRequestReview, + GithubEventPullRequestReview, webhook_module.HookEventPullRequestReviewComment, true, }, { "pull_request_review cannot match", - githubEventPullRequestReview, + GithubEventPullRequestReview, webhook_module.HookEventPullRequestComment, false, }, // other events { "create event", - githubEventCreate, + GithubEventCreate, webhook_module.HookEventCreate, true, }, diff --git a/modules/actions/log.go b/modules/actions/log.go index 3868101992..cdf18646aa 100644 --- a/modules/actions/log.go +++ b/modules/actions/log.go @@ -29,12 +29,28 @@ const ( ) func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runnerv1.LogRow) ([]int, error) { + flag := os.O_WRONLY + if offset == 0 { + // Create file only if offset is 0, or it could result in content holes if the file doesn't exist. + flag |= os.O_CREATE + } name := DBFSPrefix + filename - f, err := dbfs.OpenFile(ctx, name, os.O_WRONLY|os.O_CREATE) + f, err := dbfs.OpenFile(ctx, name, flag) if err != nil { return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err) } defer f.Close() + + stat, err := f.Stat() + if err != nil { + return nil, fmt.Errorf("dbfs Stat %q: %w", name, err) + } + if stat.Size() < offset { + // If the size is less than offset, refuse to write, or it could result in content holes. + // However, if the size is greater than offset, we can still write to overwrite the content. + return nil, fmt.Errorf("size of %q is less than offset", name) + } + if _, err := f.Seek(offset, io.SeekStart); err != nil { return nil, fmt.Errorf("dbfs Seek %q: %w", name, err) } @@ -57,7 +73,7 @@ func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runne } func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) { - f, err := openLogs(ctx, inStorage, filename) + f, err := OpenLogs(ctx, inStorage, filename) if err != nil { return nil, err } @@ -125,7 +141,7 @@ func RemoveLogs(ctx context.Context, inStorage bool, filename string) error { return nil } -func openLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { +func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { if !inStorage { name := DBFSPrefix + filename f, err := dbfs.Open(ctx, name) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index d9459288b1..3786f2a274 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -20,6 +20,14 @@ import ( "gopkg.in/yaml.v3" ) +type DetectedWorkflow struct { + EntryName string + TriggerEvent string + Commit *git.Commit + Ref string + Content []byte +} + func init() { model.OnDecodeNodeError = func(node yaml.Node, out interface{}, err error) { // Log the error instead of panic or fatal. @@ -89,13 +97,13 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { return events, nil } -func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) (map[string][]byte, error) { +func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) ([]*DetectedWorkflow, error) { entries, err := ListWorkflows(commit) if err != nil { return nil, err } - workflows := make(map[string][]byte, len(entries)) + workflows := make([]*DetectedWorkflow, 0, len(entries)) for _, entry := range entries { content, err := GetContentFromEntry(entry) if err != nil { @@ -109,7 +117,13 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy for _, evt := range events { log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) if detectMatched(commit, triggedEvent, payload, evt) { - workflows[entry.Name()] = content + dwf := &DetectedWorkflow{ + EntryName: entry.Name(), + TriggerEvent: evt.Name, + Commit: commit, + Content: content, + } + workflows = append(workflows, dwf) } } } diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 6ef5d59942..2c374d2c0d 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -23,77 +23,77 @@ func TestDetectMatched(t *testing.T) { expected bool }{ { - desc: "HookEventCreate(create) matches githubEventCreate(create)", + desc: "HookEventCreate(create) matches GithubEventCreate(create)", triggedEvent: webhook_module.HookEventCreate, payload: nil, yamlOn: "on: create", expected: true, }, { - desc: "HookEventIssues(issues) `opened` action matches githubEventIssues(issues)", + desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)", triggedEvent: webhook_module.HookEventIssues, payload: &api.IssuePayload{Action: api.HookIssueOpened}, yamlOn: "on: issues", expected: true, }, { - desc: "HookEventIssues(issues) `milestoned` action matches githubEventIssues(issues)", + desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)", triggedEvent: webhook_module.HookEventIssues, payload: &api.IssuePayload{Action: api.HookIssueMilestoned}, yamlOn: "on: issues", expected: true, }, { - desc: "HookEventPullRequestSync(pull_request_sync) matches githubEventPullRequest(pull_request)", + desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)", triggedEvent: webhook_module.HookEventPullRequestSync, payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized}, yamlOn: "on: pull_request", expected: true, }, { - desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match githubEventPullRequest(pull_request) with no activity type", + desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, yamlOn: "on: pull_request", expected: false, }, { - desc: "HookEventPullRequest(pull_request) `label_updated` action matches githubEventPullRequest(pull_request) with `label` activity type", + desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, yamlOn: "on:\n pull_request:\n types: [labeled]", expected: true, }, { - desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches githubEventPullRequestReviewComment(pull_request_review_comment)", + desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)", triggedEvent: webhook_module.HookEventPullRequestReviewComment, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, yamlOn: "on:\n pull_request_review_comment:\n types: [created]", expected: true, }, { - desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match githubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)", + desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)", triggedEvent: webhook_module.HookEventPullRequestReviewRejected, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, yamlOn: "on:\n pull_request_review:\n types: [dismissed]", expected: false, }, { - desc: "HookEventRelease(release) `published` action matches githubEventRelease(release) with `published` activity type", + desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type", triggedEvent: webhook_module.HookEventRelease, payload: &api.ReleasePayload{Action: api.HookReleasePublished}, yamlOn: "on:\n release:\n types: [published]", expected: true, }, { - desc: "HookEventPackage(package) `created` action doesn't match githubEventRegistryPackage(registry_package) with `updated` activity type", + desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type", triggedEvent: webhook_module.HookEventPackage, payload: &api.PackagePayload{Action: api.HookPackageCreated}, yamlOn: "on:\n registry_package:\n types: [updated]", expected: false, }, { - desc: "HookEventWiki(wiki) matches githubEventGollum(gollum)", + desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)", triggedEvent: webhook_module.HookEventWiki, payload: nil, yamlOn: "on: gollum", diff --git a/modules/base/tool.go b/modules/base/tool.go index a6579ab515..7fe7ffcddc 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -217,7 +217,7 @@ func EntryIcon(entry *git.TreeEntry) string { return "file-symlink-file" } if te.IsDir() { - return "file-submodule" + return "file-directory-symlink" } return "file-symlink-file" case entry.IsDir(): diff --git a/modules/context/base.go b/modules/context/base.go index 45f33feb08..839f3e10df 100644 --- a/modules/context/base.go +++ b/modules/context/base.go @@ -140,6 +140,10 @@ func (b *Base) JSONRedirect(redirect string) { b.JSON(http.StatusOK, map[string]any{"redirect": redirect}) } +func (b *Base) JSONOK() { + b.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it +} + func (b *Base) JSONError(msg string) { b.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg}) } diff --git a/modules/context/repo.go b/modules/context/repo.go index fd5f208576..003309f1b0 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -593,7 +593,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled if setting.Indexer.RepoIndexerEnabled { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } if ctx.IsSigned { diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go index 10838a7512..ceee322852 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 957152349c..3f62d587ab 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/indexer/code/bleve.go b/modules/indexer/code/bleve/bleve.go similarity index 69% rename from modules/indexer/code/bleve.go rename to modules/indexer/code/bleve/bleve.go index 5936613e3a..33cc4e02b5 100644 --- a/modules/indexer/code/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -1,14 +1,13 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package code +package bleve import ( "bufio" "context" "fmt" "io" - "os" "strconv" "strings" "time" @@ -17,12 +16,13 @@ import ( "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - gitea_bleve "code.gitea.io/gitea/modules/indexer/bleve" + "code.gitea.io/gitea/modules/indexer/code/internal" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/typesniffer" - "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" @@ -31,10 +31,8 @@ import ( "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" - "github.com/blevesearch/bleve/v2/index/upsidedown" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search/query" - "github.com/ethantkoenig/rupture" "github.com/go-enry/go-enry/v2" ) @@ -59,38 +57,6 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { }) } -// openBleveIndexer open the index at the specified path, checking for metadata -// updates and bleve version updates. If index needs to be created (or -// re-created), returns (nil, nil) -func openBleveIndexer(path string, latestVersion int) (bleve.Index, error) { - _, err := os.Stat(path) - if err != nil && os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - - metadata, err := rupture.ReadIndexMetadata(path) - if err != nil { - return nil, err - } - if metadata.Version < latestVersion { - // the indexer is using a previous version, so we should delete it and - // re-populate - return nil, util.RemoveAll(path) - } - - index, err := bleve.Open(path) - if err != nil && err == upsidedown.IncompatibleVersion { - // the indexer was built with a previous version of bleve, so we should - // delete it and re-populate - return nil, util.RemoveAll(path) - } else if err != nil { - return nil, err - } - return index, nil -} - // RepoIndexerData data stored in the repo indexer type RepoIndexerData struct { RepoID int64 @@ -111,8 +77,8 @@ const ( repoIndexerLatestVersion = 6 ) -// createBleveIndexer create a bleve repo indexer if one does not already exist -func createBleveIndexer(path string, latestVersion int) (bleve.Index, error) { +// generateBleveIndexMapping generates a bleve index mapping for the repo indexer +func generateBleveIndexMapping() (mapping.IndexMapping, error) { docMapping := bleve.NewDocumentMapping() numericFieldMapping := bleve.NewNumericFieldMapping() numericFieldMapping.IncludeInAll = false @@ -147,42 +113,28 @@ func createBleveIndexer(path string, latestVersion int) (bleve.Index, error) { mapping.AddDocumentMapping(repoIndexerDocType, docMapping) mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping()) - indexer, err := bleve.New(path, mapping) - if err != nil { - return nil, err - } - - if err = rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{ - Version: latestVersion, - }); err != nil { - return nil, err - } - return indexer, nil + return mapping, nil } -var _ Indexer = &BleveIndexer{} +var _ internal.Indexer = &Indexer{} -// BleveIndexer represents a bleve indexer implementation -type BleveIndexer struct { - indexDir string - indexer bleve.Index +// Indexer represents a bleve indexer implementation +type Indexer struct { + inner *inner_bleve.Indexer + indexer_internal.Indexer // do not composite inner_bleve.Indexer directly to avoid exposing too much } -// NewBleveIndexer creates a new bleve local indexer -func NewBleveIndexer(indexDir string) (*BleveIndexer, bool, error) { - indexer := &BleveIndexer{ - indexDir: indexDir, +// NewIndexer creates a new bleve local indexer +func NewIndexer(indexDir string) *Indexer { + inner := inner_bleve.NewIndexer(indexDir, repoIndexerLatestVersion, generateBleveIndexMapping) + return &Indexer{ + Indexer: inner, + inner: inner, } - created, err := indexer.init() - if err != nil { - indexer.Close() - return nil, false, err - } - return indexer, created, err } -func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string, - update fileUpdate, repo *repo_model.Repository, batch *gitea_bleve.FlushingBatch, +func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string, + update internal.FileUpdate, repo *repo_model.Repository, batch *inner_bleve.FlushingBatch, ) error { // Ignore vendored files in code search if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { @@ -227,7 +179,7 @@ func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteClose if _, err = batchReader.Discard(1); err != nil { return err } - id := filenameIndexerID(repo.ID, update.Filename) + id := internal.FilenameIndexerID(repo.ID, update.Filename) return batch.Index(id, &RepoIndexerData{ RepoID: repo.ID, CommitID: commitSha, @@ -237,50 +189,14 @@ func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteClose }) } -func (b *BleveIndexer) addDelete(filename string, repo *repo_model.Repository, batch *gitea_bleve.FlushingBatch) error { - id := filenameIndexerID(repo.ID, filename) +func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch *inner_bleve.FlushingBatch) error { + id := internal.FilenameIndexerID(repo.ID, filename) return batch.Delete(id) } -// init init the indexer -func (b *BleveIndexer) init() (bool, error) { - var err error - b.indexer, err = openBleveIndexer(b.indexDir, repoIndexerLatestVersion) - if err != nil { - return false, err - } - if b.indexer != nil { - return false, nil - } - - b.indexer, err = createBleveIndexer(b.indexDir, repoIndexerLatestVersion) - if err != nil { - return false, err - } - - return true, nil -} - -// Close close the indexer -func (b *BleveIndexer) Close() { - log.Debug("Closing repo indexer") - if b.indexer != nil { - err := b.indexer.Close() - if err != nil { - log.Error("Error whilst closing the repository indexer: %v", err) - } - } - log.Info("PID: %d Repository Indexer closed", os.Getpid()) -} - -// Ping does nothing -func (b *BleveIndexer) Ping() bool { - return true -} - // Index indexes the data -func (b *BleveIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error { - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) +func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! @@ -308,14 +224,14 @@ func (b *BleveIndexer) Index(ctx context.Context, repo *repo_model.Repository, s } // Delete deletes indexes by ids -func (b *BleveIndexer) Delete(repoID int64) error { +func (b *Indexer) Delete(_ context.Context, repoID int64) error { query := numericEqualityQuery(repoID, "RepoID") searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false) - result, err := b.indexer.Search(searchRequest) + result, err := b.inner.Indexer.Search(searchRequest) if err != nil { return err } - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) for _, hit := range result.Hits { if err = batch.Delete(hit.ID); err != nil { return err @@ -326,7 +242,7 @@ func (b *BleveIndexer) Delete(repoID int64) error { // Search searches for files in the specified repo. // Returns the matching file-paths -func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { +func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { var ( indexerQuery query.Query keywordQuery query.Query @@ -379,14 +295,14 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) } - result, err := b.indexer.SearchInContext(ctx, searchRequest) + result, err := b.inner.Indexer.SearchInContext(ctx, searchRequest) if err != nil { return 0, nil, nil, err } total := int64(result.Total) - searchResults := make([]*SearchResult, len(result.Hits)) + searchResults := make([]*internal.SearchResult, len(result.Hits)) for i, hit := range result.Hits { startIndex, endIndex := -1, -1 for _, locations := range hit.Locations["Content"] { @@ -405,11 +321,11 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke if t, err := time.Parse(time.RFC3339, hit.Fields["UpdatedAt"].(string)); err == nil { updatedUnix = timeutil.TimeStamp(t.Unix()) } - searchResults[i] = &SearchResult{ + searchResults[i] = &internal.SearchResult{ RepoID: int64(hit.Fields["RepoID"].(float64)), StartIndex: startIndex, EndIndex: endIndex, - Filename: filenameOfIndexerID(hit.ID), + Filename: internal.FilenameOfIndexerID(hit.ID), Content: hit.Fields["Content"].(string), CommitID: hit.Fields["CommitID"].(string), UpdatedUnix: updatedUnix, @@ -418,7 +334,7 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke } } - searchResultLanguages := make([]*SearchResultLanguages, 0, 10) + searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10) if len(language) > 0 { // Use separate query to go get all language counts facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false) @@ -426,7 +342,7 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke facetRequest.IncludeLocations = true facetRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) - if result, err = b.indexer.Search(facetRequest); err != nil { + if result, err = b.inner.Indexer.Search(facetRequest); err != nil { return 0, nil, nil, err } @@ -436,7 +352,7 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke if len(term.Term) == 0 { continue } - searchResultLanguages = append(searchResultLanguages, &SearchResultLanguages{ + searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{ Language: term.Term, Color: enry.GetColor(term.Term), Count: term.Count, diff --git a/modules/indexer/code/bleve_test.go b/modules/indexer/code/bleve_test.go deleted file mode 100644 index 00bcd5c90c..0000000000 --- a/modules/indexer/code/bleve_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package code - -import ( - "testing" - - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestBleveIndexAndSearch(t *testing.T) { - unittest.PrepareTestEnv(t) - - dir := t.TempDir() - - idx, _, err := NewBleveIndexer(dir) - if err != nil { - assert.Fail(t, "Unable to create bleve indexer Error: %v", err) - if idx != nil { - idx.Close() - } - return - } - defer idx.Close() - - testIndexer("beleve", t, idx) -} diff --git a/modules/indexer/code/elastic_search_test.go b/modules/indexer/code/elastic_search_test.go deleted file mode 100644 index e7506eefa6..0000000000 --- a/modules/indexer/code/elastic_search_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package code - -import ( - "os" - "testing" - - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestESIndexAndSearch(t *testing.T) { - unittest.PrepareTestEnv(t) - - u := os.Getenv("TEST_INDEXER_CODE_ES_URL") - if u == "" { - t.SkipNow() - return - } - - indexer, _, err := NewElasticSearchIndexer(u, "gitea_codes") - if err != nil { - assert.Fail(t, "Unable to create ES indexer Error: %v", err) - if indexer != nil { - indexer.Close() - } - return - } - defer indexer.Close() - - testIndexer("elastic_search", t, indexer) -} - -func TestIndexPos(t *testing.T) { - startIdx, endIdx := indexPos("test index start and end", "start", "end") - assert.EqualValues(t, 11, startIdx) - assert.EqualValues(t, 24, endIdx) -} diff --git a/modules/indexer/code/elastic_search.go b/modules/indexer/code/elasticsearch/elasticsearch.go similarity index 56% rename from modules/indexer/code/elastic_search.go rename to modules/indexer/code/elasticsearch/elasticsearch.go index 0e56a86588..88054585cd 100644 --- a/modules/indexer/code/elastic_search.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -1,25 +1,23 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package code +package elasticsearch import ( "bufio" "context" - "errors" "fmt" "io" - "net" "strconv" "strings" - "sync" - "time" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/indexer/code/internal" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -38,63 +36,22 @@ const ( esMultiMatchTypePhrasePrefix = "phrase_prefix" ) -var _ Indexer = &ElasticSearchIndexer{} +var _ internal.Indexer = &Indexer{} -// ElasticSearchIndexer implements Indexer interface -type ElasticSearchIndexer struct { - client *elastic.Client - indexerAliasName string - available bool - stopTimer chan struct{} - lock sync.RWMutex +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_elasticsearch.Indexer + indexer_internal.Indexer // do not composite inner_elasticsearch.Indexer directly to avoid exposing too much } -// NewElasticSearchIndexer creates a new elasticsearch indexer -func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, bool, error) { - opts := []elastic.ClientOptionFunc{ - elastic.SetURL(url), - elastic.SetSniff(false), - elastic.SetHealthcheckInterval(10 * time.Second), - elastic.SetGzip(false), +// NewIndexer creates a new elasticsearch indexer +func NewIndexer(url, indexerName string) *Indexer { + inner := inner_elasticsearch.NewIndexer(url, indexerName, esRepoIndexerLatestVersion, defaultMapping) + indexer := &Indexer{ + inner: inner, + Indexer: inner, } - - logger := log.GetLogger(log.DEFAULT) - - opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) - opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) - opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) - - client, err := elastic.NewClient(opts...) - if err != nil { - return nil, false, err - } - - indexer := &ElasticSearchIndexer{ - client: client, - indexerAliasName: indexerName, - available: true, - stopTimer: make(chan struct{}), - } - - ticker := time.NewTicker(10 * time.Second) - go func() { - for { - select { - case <-ticker.C: - indexer.checkAvailability() - case <-indexer.stopTimer: - ticker.Stop() - return - } - } - }() - - exists, err := indexer.init() - if err != nil { - indexer.Close() - return nil, false, err - } - return indexer, !exists, err + return indexer } const ( @@ -127,72 +84,7 @@ const ( }` ) -func (b *ElasticSearchIndexer) realIndexerName() string { - return fmt.Sprintf("%s.v%d", b.indexerAliasName, esRepoIndexerLatestVersion) -} - -// Init will initialize the indexer -func (b *ElasticSearchIndexer) init() (bool, error) { - ctx := graceful.GetManager().HammerContext() - exists, err := b.client.IndexExists(b.realIndexerName()).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !exists { - mapping := defaultMapping - - createIndex, err := b.client.CreateIndex(b.realIndexerName()).BodyString(mapping).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !createIndex.Acknowledged { - return false, fmt.Errorf("create index %s with %s failed", b.realIndexerName(), mapping) - } - } - - // check version - r, err := b.client.Aliases().Do(ctx) - if err != nil { - return false, b.checkError(err) - } - - realIndexerNames := r.IndicesByAlias(b.indexerAliasName) - if len(realIndexerNames) < 1 { - res, err := b.client.Alias(). - Add(b.realIndexerName(), b.indexerAliasName). - Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !res.Acknowledged { - return false, fmt.Errorf("create alias %s to index %s failed", b.indexerAliasName, b.realIndexerName()) - } - } else if len(realIndexerNames) >= 1 && realIndexerNames[0] < b.realIndexerName() { - log.Warn("Found older gitea indexer named %s, but we will create a new one %s and keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", - realIndexerNames[0], b.realIndexerName()) - res, err := b.client.Alias(). - Remove(realIndexerNames[0], b.indexerAliasName). - Add(b.realIndexerName(), b.indexerAliasName). - Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !res.Acknowledged { - return false, fmt.Errorf("change alias %s to index %s failed", b.indexerAliasName, b.realIndexerName()) - } - } - - return exists, nil -} - -// Ping checks if elastic is available -func (b *ElasticSearchIndexer) Ping() bool { - b.lock.RLock() - defer b.lock.RUnlock() - return b.available -} - -func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, sha string, update fileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { +func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, sha string, update internal.FileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { // Ignore vendored files in code search if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { return nil, nil @@ -235,11 +127,11 @@ func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.Wr if _, err = batchReader.Discard(1); err != nil { return nil, err } - id := filenameIndexerID(repo.ID, update.Filename) + id := internal.FilenameIndexerID(repo.ID, update.Filename) return []elastic.BulkableRequest{ elastic.NewBulkIndexRequest(). - Index(b.indexerAliasName). + Index(b.inner.VersionedIndexName()). Id(id). Doc(map[string]interface{}{ "repo_id": repo.ID, @@ -251,15 +143,15 @@ func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.Wr }, nil } -func (b *ElasticSearchIndexer) addDelete(filename string, repo *repo_model.Repository) elastic.BulkableRequest { - id := filenameIndexerID(repo.ID, filename) +func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elastic.BulkableRequest { + id := internal.FilenameIndexerID(repo.ID, filename) return elastic.NewBulkDeleteRequest(). - Index(b.indexerAliasName). + Index(b.inner.VersionedIndexName()). Id(id) } // Index will save the index data -func (b *ElasticSearchIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error { +func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { reqs := make([]elastic.BulkableRequest, 0) if len(changes.Updates) > 0 { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! @@ -288,21 +180,21 @@ func (b *ElasticSearchIndexer) Index(ctx context.Context, repo *repo_model.Repos } if len(reqs) > 0 { - _, err := b.client.Bulk(). - Index(b.indexerAliasName). + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). Add(reqs...). Do(ctx) - return b.checkError(err) + return err } return nil } // Delete deletes indexes by ids -func (b *ElasticSearchIndexer) Delete(repoID int64) error { - _, err := b.client.DeleteByQuery(b.indexerAliasName). +func (b *Indexer) Delete(ctx context.Context, repoID int64) error { + _, err := b.inner.Client.DeleteByQuery(b.inner.VersionedIndexName()). Query(elastic.NewTermsQuery("repo_id", repoID)). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) + Do(ctx) + return err } // indexPos find words positions for start and the following end on content. It will @@ -321,8 +213,8 @@ func indexPos(content, start, end string) (int, int) { return startIdx, startIdx + len(start) + endIdx + len(end) } -func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) (int64, []*SearchResult, []*SearchResultLanguages, error) { - hits := make([]*SearchResult, 0, pageSize) +func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { + hits := make([]*internal.SearchResult, 0, pageSize) for _, hit := range searchResult.Hits.Hits { // FIXME: There is no way to get the position the keyword on the content currently on the same request. // So we get it from content, this may made the query slower. See @@ -341,7 +233,7 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) panic(fmt.Sprintf("2===%#v", hit.Highlight)) } - repoID, fileName := parseIndexerID(hit.Id) + repoID, fileName := internal.ParseIndexerID(hit.Id) res := make(map[string]interface{}) if err := json.Unmarshal(hit.Source, &res); err != nil { return 0, nil, nil, err @@ -349,7 +241,7 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) language := res["language"].(string) - hits = append(hits, &SearchResult{ + hits = append(hits, &internal.SearchResult{ RepoID: repoID, Filename: fileName, CommitID: res["commit_id"].(string), @@ -365,14 +257,14 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) return searchResult.TotalHits(), hits, extractAggs(searchResult), nil } -func extractAggs(searchResult *elastic.SearchResult) []*SearchResultLanguages { - var searchResultLanguages []*SearchResultLanguages +func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLanguages { + var searchResultLanguages []*internal.SearchResultLanguages agg, found := searchResult.Aggregations.Terms("language") if found { - searchResultLanguages = make([]*SearchResultLanguages, 0, 10) + searchResultLanguages = make([]*internal.SearchResultLanguages, 0, 10) for _, bucket := range agg.Buckets { - searchResultLanguages = append(searchResultLanguages, &SearchResultLanguages{ + searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{ Language: bucket.Key.(string), Color: enry.GetColor(bucket.Key.(string)), Count: int(bucket.DocCount), @@ -383,7 +275,7 @@ func extractAggs(searchResult *elastic.SearchResult) []*SearchResultLanguages { } // Search searches for codes and language stats by given conditions. -func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { +func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { searchType := esMultiMatchTypeBestFields if isMatch { searchType = esMultiMatchTypePhrasePrefix @@ -412,8 +304,8 @@ func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, lang } if len(language) == 0 { - searchResult, err := b.client.Search(). - Index(b.indexerAliasName). + searchResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). Aggregation("language", aggregation). Query(query). Highlight( @@ -426,26 +318,26 @@ func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, lang From(start).Size(pageSize). Do(ctx) if err != nil { - return 0, nil, nil, b.checkError(err) + return 0, nil, nil, err } return convertResult(searchResult, kw, pageSize) } langQuery := elastic.NewMatchQuery("language", language) - countResult, err := b.client.Search(). - Index(b.indexerAliasName). + countResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). Aggregation("language", aggregation). Query(query). - Size(0). // We only needs stats information + Size(0). // We only need stats information Do(ctx) if err != nil { - return 0, nil, nil, b.checkError(err) + return 0, nil, nil, err } query = query.Must(langQuery) - searchResult, err := b.client.Search(). - Index(b.indexerAliasName). + searchResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). Query(query). Highlight( elastic.NewHighlight(). @@ -457,56 +349,10 @@ func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, lang From(start).Size(pageSize). Do(ctx) if err != nil { - return 0, nil, nil, b.checkError(err) + return 0, nil, nil, err } total, hits, _, err := convertResult(searchResult, kw, pageSize) return total, hits, extractAggs(countResult), err } - -// Close implements indexer -func (b *ElasticSearchIndexer) Close() { - select { - case <-b.stopTimer: - default: - close(b.stopTimer) - } -} - -func (b *ElasticSearchIndexer) checkError(err error) error { - var opErr *net.OpError - if !(elastic.IsConnErr(err) || (errors.As(err, &opErr) && (opErr.Op == "dial" || opErr.Op == "read"))) { - return err - } - - b.setAvailability(false) - - return err -} - -func (b *ElasticSearchIndexer) checkAvailability() { - if b.Ping() { - return - } - - // Request cluster state to check if elastic is available again - _, err := b.client.ClusterState().Do(graceful.GetManager().ShutdownContext()) - if err != nil { - b.setAvailability(false) - return - } - - b.setAvailability(true) -} - -func (b *ElasticSearchIndexer) setAvailability(available bool) { - b.lock.Lock() - defer b.lock.Unlock() - - if b.available == available { - return - } - - b.available = available -} diff --git a/modules/indexer/code/elasticsearch/elasticsearch_test.go b/modules/indexer/code/elasticsearch/elasticsearch_test.go new file mode 100644 index 0000000000..c6ba93e76d --- /dev/null +++ b/modules/indexer/code/elasticsearch/elasticsearch_test.go @@ -0,0 +1,16 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexPos(t *testing.T) { + startIdx, endIdx := indexPos("test index start and end", "start", "end") + assert.EqualValues(t, 11, startIdx) + assert.EqualValues(t, 24, endIdx) +} diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index bbcc6ba487..1ba6b849d1 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -10,23 +10,11 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) -type fileUpdate struct { - Filename string - BlobSha string - Size int64 - Sized bool -} - -// repoChanges changes (file additions/updates/removals) to a repo -type repoChanges struct { - Updates []fileUpdate - RemovedFilenames []string -} - func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (string, error) { stdout, _, err := git.NewCommand(ctx, "show-ref", "-s").AddDynamicArguments(git.BranchPrefix + repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { @@ -36,7 +24,7 @@ func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (stri } // getRepoChanges returns changes to repo since last indexer update -func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { +func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { status, err := repo_model.GetIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode) if err != nil { return nil, err @@ -67,16 +55,16 @@ func isIndexable(entry *git.TreeEntry) bool { } // parseGitLsTreeOutput parses the output of a `git ls-tree -r --full-name` command -func parseGitLsTreeOutput(stdout []byte) ([]fileUpdate, error) { +func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { entries, err := git.ParseTreeEntries(stdout) if err != nil { return nil, err } idxCount := 0 - updates := make([]fileUpdate, len(entries)) + updates := make([]internal.FileUpdate, len(entries)) for _, entry := range entries { if isIndexable(entry) { - updates[idxCount] = fileUpdate{ + updates[idxCount] = internal.FileUpdate{ Filename: entry.Name(), BlobSha: entry.ID.String(), Size: entry.Size(), @@ -89,8 +77,8 @@ func parseGitLsTreeOutput(stdout []byte) ([]fileUpdate, error) { } // genesisChanges get changes to add repo to the indexer for the first time -func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { - var changes repoChanges +func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { + var changes internal.RepoChanges stdout, _, runErr := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision).RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()}) if runErr != nil { return nil, runErr @@ -102,20 +90,20 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s } // nonGenesisChanges get changes since the previous indexer update -func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { +func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { diffCmd := git.NewCommand(ctx, "diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) stdout, _, runErr := diffCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if runErr != nil { // previous commit sha may have been removed by a force push, so // try rebuilding from scratch log.Warn("git diff: %v", runErr) - if err := indexer.Delete(repo.ID); err != nil { + if err := (*globalIndexer.Load()).Delete(ctx, repo.ID); err != nil { return nil, err } return genesisChanges(ctx, repo, revision) } - var changes repoChanges + var changes internal.RepoChanges var err error updatedFilenames := make([]string, 0, 10) for _, line := range strings.Split(stdout, "\n") { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index f38fd6000c..13d06874c9 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -7,86 +7,41 @@ import ( "context" "os" "runtime/pprof" - "strconv" - "strings" + "sync/atomic" "time" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/indexer/code/bleve" + "code.gitea.io/gitea/modules/indexer/code/elasticsearch" + "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) -// SearchResult result of performing a search in a repo -type SearchResult struct { - RepoID int64 - StartIndex int - EndIndex int - Filename string - Content string - CommitID string - UpdatedUnix timeutil.TimeStamp - Language string - Color string +var ( + indexerQueue *queue.WorkerPoolQueue[*internal.IndexerData] + // globalIndexer is the global indexer, it cannot be nil. + // When the real indexer is not ready, it will be a dummy indexer which will return error to explain it's not ready. + // So it's always safe use it as *globalIndexer.Load() and call its methods. + globalIndexer atomic.Pointer[internal.Indexer] + dummyIndexer *internal.Indexer +) + +func init() { + i := internal.NewDummyIndexer() + dummyIndexer = &i + globalIndexer.Store(dummyIndexer) } -// SearchResultLanguages result of top languages count in search results -type SearchResultLanguages struct { - Language string - Color string - Count int -} - -// Indexer defines an interface to index and search code contents -type Indexer interface { - Ping() bool - Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error - Delete(repoID int64) error - Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) - Close() -} - -func filenameIndexerID(repoID int64, filename string) string { - return indexerID(repoID) + "_" + filename -} - -func indexerID(id int64) string { - return strconv.FormatInt(id, 36) -} - -func parseIndexerID(indexerID string) (int64, string) { - index := strings.IndexByte(indexerID, '_') - if index == -1 { - log.Error("Unexpected ID in repo indexer: %s", indexerID) - } - repoID, _ := strconv.ParseInt(indexerID[:index], 36, 64) - return repoID, indexerID[index+1:] -} - -func filenameOfIndexerID(indexerID string) string { - index := strings.IndexByte(indexerID, '_') - if index == -1 { - log.Error("Unexpected ID in repo indexer: %s", indexerID) - } - return indexerID[index+1:] -} - -// IndexerData represents data stored in the code indexer -type IndexerData struct { - RepoID int64 -} - -var indexerQueue *queue.WorkerPoolQueue[*IndexerData] - -func index(ctx context.Context, indexer Indexer, repoID int64) error { +func index(ctx context.Context, indexer internal.Indexer, repoID int64) error { repo, err := repo_model.GetRepositoryByID(ctx, repoID) if repo_model.IsErrRepoNotExist(err) { - return indexer.Delete(repoID) + return indexer.Delete(ctx, repoID) } if err != nil { return err @@ -139,7 +94,7 @@ func index(ctx context.Context, indexer Indexer, repoID int64) error { // Init initialize the repo indexer func Init() { if !setting.Indexer.RepoIndexerEnabled { - indexer.Close() + (*globalIndexer.Load()).Close() return } @@ -153,7 +108,7 @@ func Init() { } cancel() log.Debug("Closing repository indexer") - indexer.Close() + (*globalIndexer.Load()).Close() log.Info("PID: %d Repository Indexer closed", os.Getpid()) finished() }) @@ -163,13 +118,8 @@ func Init() { // Create the Queue switch setting.Indexer.RepoType { case "bleve", "elasticsearch": - handler := func(items ...*IndexerData) (unhandled []*IndexerData) { - idx, err := indexer.get() - if idx == nil || err != nil { - log.Warn("Codes indexer handler: indexer is not ready, retry later.") - return items - } - + handler := func(items ...*internal.IndexerData) (unhandled []*internal.IndexerData) { + indexer := *globalIndexer.Load() for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) @@ -188,11 +138,7 @@ func Init() { code.gitea.io/gitea/modules/indexer/code.index(indexer.go:105) */ if err := index(ctx, indexer, indexerData.RepoID); err != nil { - if !idx.Ping() { - log.Error("Code indexer handler: indexer is unavailable.") - unhandled = append(unhandled, indexerData) - continue - } + unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } @@ -213,8 +159,8 @@ func Init() { pprof.SetGoroutineLabels(ctx) start := time.Now() var ( - rIndexer Indexer - populate bool + rIndexer internal.Indexer + existed bool err error ) switch setting.Indexer.RepoType { @@ -228,10 +174,11 @@ func Init() { } }() - rIndexer, populate, err = NewBleveIndexer(setting.Indexer.RepoPath) + rIndexer = bleve.NewIndexer(setting.Indexer.RepoPath) + existed, err = rIndexer.Init(ctx) if err != nil { cancel() - indexer.Close() + (*globalIndexer.Load()).Close() close(waitChannel) log.Fatal("PID: %d Unable to initialize the bleve Repository Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.RepoPath, err) } @@ -245,23 +192,31 @@ func Init() { } }() - rIndexer, populate, err = NewElasticSearchIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName) + rIndexer = elasticsearch.NewIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName) if err != nil { cancel() - indexer.Close() + (*globalIndexer.Load()).Close() + close(waitChannel) + log.Fatal("PID: %d Unable to create the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) + } + existed, err = rIndexer.Init(ctx) + if err != nil { + cancel() + (*globalIndexer.Load()).Close() close(waitChannel) log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) } + default: log.Fatal("PID: %d Unknown Indexer type: %s", os.Getpid(), setting.Indexer.RepoType) } - indexer.set(rIndexer) + globalIndexer.Store(&rIndexer) // Start processing the queue go graceful.GetManager().RunWithCancel(indexerQueue) - if populate { + if !existed { // populate the index because it's created for the first time go graceful.GetManager().RunWithShutdownContext(populateRepoIndexer) } select { @@ -283,18 +238,18 @@ func Init() { case <-graceful.GetManager().IsShutdown(): log.Warn("Shutdown before Repository Indexer completed initialization") cancel() - indexer.Close() + (*globalIndexer.Load()).Close() case duration, ok := <-waitChannel: if !ok { log.Warn("Repository Indexer Initialization failed") cancel() - indexer.Close() + (*globalIndexer.Load()).Close() return } log.Info("Repository Indexer Initialization took %v", duration) case <-time.After(timeout): cancel() - indexer.Close() + (*globalIndexer.Load()).Close() log.Fatal("Repository Indexer Initialization Timed-Out after: %v", timeout) } }() @@ -303,21 +258,15 @@ func Init() { // UpdateRepoIndexer update a repository's entries in the indexer func UpdateRepoIndexer(repo *repo_model.Repository) { - indexData := &IndexerData{RepoID: repo.ID} + indexData := &internal.IndexerData{RepoID: repo.ID} if err := indexerQueue.Push(indexData); err != nil { log.Error("Update repo index data %v failed: %v", indexData, err) } } // IsAvailable checks if issue indexer is available -func IsAvailable() bool { - idx, err := indexer.get() - if err != nil { - log.Error("IsAvailable(): unable to get indexer: %v", err) - return false - } - - return idx.Ping() +func IsAvailable(ctx context.Context) bool { + return (*globalIndexer.Load()).Ping(ctx) == nil } // populateRepoIndexer populate the repo indexer with pre-existing data. This @@ -368,7 +317,7 @@ func populateRepoIndexer(ctx context.Context) { return default: } - if err := indexerQueue.Push(&IndexerData{RepoID: id}); err != nil { + if err := indexerQueue.Push(&internal.IndexerData{RepoID: id}); err != nil { log.Error("indexerQueue.Push: %v", err) return } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 52f7e76e41..55616a0361 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -5,11 +5,15 @@ package code import ( "context" + "os" "path/filepath" "testing" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/indexer/code/bleve" + "code.gitea.io/gitea/modules/indexer/code/elasticsearch" + "code.gitea.io/gitea/modules/indexer/code/internal" _ "code.gitea.io/gitea/models" @@ -22,7 +26,7 @@ func TestMain(m *testing.M) { }) } -func testIndexer(name string, t *testing.T, indexer Indexer) { +func testIndexer(name string, t *testing.T, indexer internal.Indexer) { t.Run(name, func(t *testing.T) { var repoID int64 = 1 err := index(git.DefaultContext, indexer, repoID) @@ -81,6 +85,48 @@ func testIndexer(name string, t *testing.T, indexer Indexer) { }) } - assert.NoError(t, indexer.Delete(repoID)) + assert.NoError(t, indexer.Delete(context.Background(), repoID)) }) } + +func TestBleveIndexAndSearch(t *testing.T) { + unittest.PrepareTestEnv(t) + + dir := t.TempDir() + + idx := bleve.NewIndexer(dir) + _, err := idx.Init(context.Background()) + if err != nil { + assert.Fail(t, "Unable to create bleve indexer Error: %v", err) + if idx != nil { + idx.Close() + } + return + } + defer idx.Close() + + testIndexer("beleve", t, idx) +} + +func TestESIndexAndSearch(t *testing.T) { + unittest.PrepareTestEnv(t) + + u := os.Getenv("TEST_INDEXER_CODE_ES_URL") + if u == "" { + t.SkipNow() + return + } + + indexer := elasticsearch.NewIndexer(u, "gitea_codes") + if _, err := indexer.Init(context.Background()); err != nil { + assert.Fail(t, "Unable to init ES indexer Error: %v", err) + if indexer != nil { + indexer.Close() + } + return + } + + defer indexer.Close() + + testIndexer("elastic_search", t, indexer) +} diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go new file mode 100644 index 0000000000..da3ac3623c --- /dev/null +++ b/modules/indexer/code/internal/indexer.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "context" + "fmt" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/indexer/internal" +) + +// Indexer defines an interface to index and search code contents +type Indexer interface { + internal.Indexer + Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error + Delete(ctx context.Context, repoID int64) error + Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) +} + +// NewDummyIndexer returns a dummy indexer +func NewDummyIndexer() Indexer { + return &dummyIndexer{ + Indexer: internal.NewDummyIndexer(), + } +} + +type dummyIndexer struct { + internal.Indexer +} + +func (d *dummyIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { + return 0, nil, nil, fmt.Errorf("indexer is not ready") +} diff --git a/modules/indexer/code/internal/model.go b/modules/indexer/code/internal/model.go new file mode 100644 index 0000000000..f75263c83c --- /dev/null +++ b/modules/indexer/code/internal/model.go @@ -0,0 +1,44 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import "code.gitea.io/gitea/modules/timeutil" + +type FileUpdate struct { + Filename string + BlobSha string + Size int64 + Sized bool +} + +// RepoChanges changes (file additions/updates/removals) to a repo +type RepoChanges struct { + Updates []FileUpdate + RemovedFilenames []string +} + +// IndexerData represents data stored in the code indexer +type IndexerData struct { + RepoID int64 +} + +// SearchResult result of performing a search in a repo +type SearchResult struct { + RepoID int64 + StartIndex int + EndIndex int + Filename string + Content string + CommitID string + UpdatedUnix timeutil.TimeStamp + Language string + Color string +} + +// SearchResultLanguages result of top languages count in search results +type SearchResultLanguages struct { + Language string + Color string + Count int +} diff --git a/modules/indexer/code/internal/util.go b/modules/indexer/code/internal/util.go new file mode 100644 index 0000000000..689c4f4584 --- /dev/null +++ b/modules/indexer/code/internal/util.go @@ -0,0 +1,32 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "strings" + + "code.gitea.io/gitea/modules/indexer/internal" + "code.gitea.io/gitea/modules/log" +) + +func FilenameIndexerID(repoID int64, filename string) string { + return internal.Base36(repoID) + "_" + filename +} + +func ParseIndexerID(indexerID string) (int64, string) { + index := strings.IndexByte(indexerID, '_') + if index == -1 { + log.Error("Unexpected ID in repo indexer: %s", indexerID) + } + repoID, _ := internal.ParseBase36(indexerID[:index]) + return repoID, indexerID[index+1:] +} + +func FilenameOfIndexerID(indexerID string) string { + index := strings.IndexByte(indexerID, '_') + if index == -1 { + log.Error("Unexpected ID in repo indexer: %s", indexerID) + } + return indexerID[index+1:] +} diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index 1de9ffc224..1f9bddff7b 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -25,6 +26,8 @@ type Result struct { FormattedLines string } +type SearchResultLanguages = internal.SearchResultLanguages + func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { startIndex := selectionStartIndex numLinesBefore := 0 @@ -61,7 +64,7 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error { return nil } -func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, error) { +func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) { startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n") var formattedLinesBuffer bytes.Buffer @@ -109,12 +112,12 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro } // PerformSearch perform a search on a repository -func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*SearchResultLanguages, error) { +func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) { if len(keyword) == 0 { return 0, nil, nil, nil } - total, results, resultLanguages, err := indexer.Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) + total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) if err != nil { return 0, nil, nil, err } diff --git a/modules/indexer/code/wrapped.go b/modules/indexer/code/wrapped.go deleted file mode 100644 index 7eed3e8557..0000000000 --- a/modules/indexer/code/wrapped.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package code - -import ( - "context" - "fmt" - "sync" - - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/log" -) - -var indexer = newWrappedIndexer() - -// ErrWrappedIndexerClosed is the error returned if the indexer was closed before it was ready -var ErrWrappedIndexerClosed = fmt.Errorf("Indexer closed before ready") - -type wrappedIndexer struct { - internal Indexer - lock sync.RWMutex - cond *sync.Cond - closed bool -} - -func newWrappedIndexer() *wrappedIndexer { - w := &wrappedIndexer{} - w.cond = sync.NewCond(w.lock.RLocker()) - return w -} - -func (w *wrappedIndexer) set(indexer Indexer) { - w.lock.Lock() - defer w.lock.Unlock() - if w.closed { - // Too late! - indexer.Close() - } - w.internal = indexer - w.cond.Broadcast() -} - -func (w *wrappedIndexer) get() (Indexer, error) { - w.lock.RLock() - defer w.lock.RUnlock() - if w.internal == nil { - if w.closed { - return nil, ErrWrappedIndexerClosed - } - w.cond.Wait() - if w.closed { - return nil, ErrWrappedIndexerClosed - } - } - return w.internal, nil -} - -// Ping checks if elastic is available -func (w *wrappedIndexer) Ping() bool { - indexer, err := w.get() - if err != nil { - log.Warn("Failed to get indexer: %v", err) - return false - } - return indexer.Ping() -} - -func (w *wrappedIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error { - indexer, err := w.get() - if err != nil { - return err - } - return indexer.Index(ctx, repo, sha, changes) -} - -func (w *wrappedIndexer) Delete(repoID int64) error { - indexer, err := w.get() - if err != nil { - return err - } - return indexer.Delete(repoID) -} - -func (w *wrappedIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { - indexer, err := w.get() - if err != nil { - return 0, nil, nil, err - } - return indexer.Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) -} - -func (w *wrappedIndexer) Close() { - w.lock.Lock() - defer w.lock.Unlock() - if w.closed { - return - } - w.closed = true - w.cond.Broadcast() - if w.internal != nil { - w.internal.Close() - } -} diff --git a/modules/indexer/internal/base32.go b/modules/indexer/internal/base32.go new file mode 100644 index 0000000000..aca756c638 --- /dev/null +++ b/modules/indexer/internal/base32.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "fmt" + "strconv" +) + +func Base36(i int64) string { + return strconv.FormatInt(i, 36) +} + +func ParseBase36(s string) (int64, error) { + i, err := strconv.ParseInt(s, 36, 64) + if err != nil { + return 0, fmt.Errorf("invalid base36 integer %q: %w", s, err) + } + return i, nil +} diff --git a/modules/indexer/bleve/batch.go b/modules/indexer/internal/bleve/batch.go similarity index 100% rename from modules/indexer/bleve/batch.go rename to modules/indexer/internal/bleve/batch.go diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go new file mode 100644 index 0000000000..ce06b5afcb --- /dev/null +++ b/modules/indexer/internal/bleve/indexer.go @@ -0,0 +1,103 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package bleve + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/modules/indexer/internal" + "code.gitea.io/gitea/modules/log" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/ethantkoenig/rupture" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer represents a basic bleve indexer implementation +type Indexer struct { + Indexer bleve.Index + + indexDir string + version int + mappingGetter MappingGetter +} + +type MappingGetter func() (mapping.IndexMapping, error) + +func NewIndexer(indexDir string, version int, mappingGetter func() (mapping.IndexMapping, error)) *Indexer { + return &Indexer{ + indexDir: indexDir, + version: version, + mappingGetter: mappingGetter, + } +} + +// Init initializes the indexer +func (i *Indexer) Init(_ context.Context) (bool, error) { + if i == nil { + return false, fmt.Errorf("cannot init nil indexer") + } + + if i.Indexer != nil { + return false, fmt.Errorf("indexer is already initialized") + } + + indexer, version, err := openIndexer(i.indexDir, i.version) + if err != nil { + return false, err + } + if indexer != nil { + i.Indexer = indexer + return true, nil + } + + if version != 0 { + log.Warn("Found older bleve index with version %d, Gitea will remove it and rebuild", version) + } + + indexMapping, err := i.mappingGetter() + if err != nil { + return false, err + } + + indexer, err = bleve.New(i.indexDir, indexMapping) + if err != nil { + return false, err + } + + if err = rupture.WriteIndexMetadata(i.indexDir, &rupture.IndexMetadata{ + Version: i.version, + }); err != nil { + return false, err + } + + i.Indexer = indexer + + return false, nil +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(_ context.Context) error { + if i == nil { + return fmt.Errorf("cannot ping nil indexer") + } + if i.Indexer == nil { + return fmt.Errorf("indexer is not initialized") + } + return nil +} + +func (i *Indexer) Close() { + if i == nil { + return + } + + if err := i.Indexer.Close(); err != nil { + log.Error("Failed to close bleve indexer in %q: %v", i.indexDir, err) + } + i.Indexer = nil +} diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go new file mode 100644 index 0000000000..43a7c3c5ec --- /dev/null +++ b/modules/indexer/internal/bleve/util.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package bleve + +import ( + "errors" + "os" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/ethantkoenig/rupture" +) + +// openIndexer open the index at the specified path, checking for metadata +// updates and bleve version updates. If index needs to be created (or +// re-created), returns (nil, nil) +func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { + _, err := os.Stat(path) + if err != nil && os.IsNotExist(err) { + return nil, 0, nil + } else if err != nil { + return nil, 0, err + } + + metadata, err := rupture.ReadIndexMetadata(path) + if err != nil { + return nil, 0, err + } + if metadata.Version < latestVersion { + // the indexer is using a previous version, so we should delete it and + // re-populate + return nil, metadata.Version, util.RemoveAll(path) + } + + index, err := bleve.Open(path) + if err != nil { + if errors.Is(err, upsidedown.IncompatibleVersion) { + log.Warn("Indexer was built with a previous version of bleve, deleting and rebuilding") + return nil, 0, util.RemoveAll(path) + } + return nil, 0, err + } + + return index, 0, nil +} diff --git a/modules/indexer/internal/db/indexer.go b/modules/indexer/internal/db/indexer.go new file mode 100644 index 0000000000..3deec836c4 --- /dev/null +++ b/modules/indexer/internal/db/indexer.go @@ -0,0 +1,34 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + + "code.gitea.io/gitea/modules/indexer/internal" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer represents a basic db indexer implementation +type Indexer struct{} + +// Init initializes the indexer +func (i *Indexer) Init(_ context.Context) (bool, error) { + // Return true to indicate that the index was opened/existed. + // So that the indexer will not try to populate the index, the data is already there. + return true, nil +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(_ context.Context) error { + // No need to ping database to check if it is available. + // If the database goes down, Gitea will go down, so nobody will care if the indexer is available. + return nil +} + +// Close closes the indexer +func (i *Indexer) Close() { + // nothing to do +} diff --git a/modules/indexer/internal/elasticsearch/indexer.go b/modules/indexer/internal/elasticsearch/indexer.go new file mode 100644 index 0000000000..2c60efad56 --- /dev/null +++ b/modules/indexer/internal/elasticsearch/indexer.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/modules/indexer/internal" + + "github.com/olivere/elastic/v7" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer represents a basic elasticsearch indexer implementation +type Indexer struct { + Client *elastic.Client + + url string + indexName string + version int + mapping string +} + +func NewIndexer(url, indexName string, version int, mapping string) *Indexer { + return &Indexer{ + url: url, + indexName: indexName, + version: version, + mapping: mapping, + } +} + +// Init initializes the indexer +func (i *Indexer) Init(ctx context.Context) (bool, error) { + if i == nil { + return false, fmt.Errorf("cannot init nil indexer") + } + if i.Client != nil { + return false, fmt.Errorf("indexer is already initialized") + } + + client, err := i.initClient() + if err != nil { + return false, err + } + i.Client = client + + exists, err := i.Client.IndexExists(i.VersionedIndexName()).Do(ctx) + if err != nil { + return false, err + } + if exists { + return true, nil + } + + if err := i.createIndex(ctx); err != nil { + return false, err + } + + return exists, nil +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(ctx context.Context) error { + if i == nil { + return fmt.Errorf("cannot ping nil indexer") + } + if i.Client == nil { + return fmt.Errorf("indexer is not initialized") + } + + resp, err := i.Client.ClusterHealth().Do(ctx) + if err != nil { + return err + } + if resp.Status != "green" { + // see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html + return fmt.Errorf("status of elasticsearch cluster is %s", resp.Status) + } + return nil +} + +// Close closes the indexer +func (i *Indexer) Close() { + if i == nil { + return + } + i.Client = nil +} diff --git a/modules/indexer/internal/elasticsearch/util.go b/modules/indexer/internal/elasticsearch/util.go new file mode 100644 index 0000000000..9e034bd553 --- /dev/null +++ b/modules/indexer/internal/elasticsearch/util.go @@ -0,0 +1,68 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/modules/log" + + "github.com/olivere/elastic/v7" +) + +// VersionedIndexName returns the full index name with version +func (i *Indexer) VersionedIndexName() string { + return versionedIndexName(i.indexName, i.version) +} + +func versionedIndexName(indexName string, version int) string { + if version == 0 { + // Old index name without version + return indexName + } + return fmt.Sprintf("%s.v%d", indexName, version) +} + +func (i *Indexer) createIndex(ctx context.Context) error { + createIndex, err := i.Client.CreateIndex(i.VersionedIndexName()).BodyString(i.mapping).Do(ctx) + if err != nil { + return err + } + if !createIndex.Acknowledged { + return fmt.Errorf("create index %s with %s failed", i.VersionedIndexName(), i.mapping) + } + + i.checkOldIndexes(ctx) + + return nil +} + +func (i *Indexer) initClient() (*elastic.Client, error) { + opts := []elastic.ClientOptionFunc{ + elastic.SetURL(i.url), + elastic.SetSniff(false), + elastic.SetHealthcheckInterval(10 * time.Second), + elastic.SetGzip(false), + } + + logger := log.GetLogger(log.DEFAULT) + + opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) + opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) + opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) + + return elastic.NewClient(opts...) +} + +func (i *Indexer) checkOldIndexes(ctx context.Context) { + for v := 0; v < i.version; v++ { + indexName := versionedIndexName(i.indexName, v) + exists, err := i.Client.IndexExists(indexName).Do(ctx) + if err == nil && exists { + log.Warn("Found older elasticsearch index named %q, Gitea will keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", indexName) + } + } +} diff --git a/modules/indexer/internal/indexer.go b/modules/indexer/internal/indexer.go new file mode 100644 index 0000000000..c7f356da1e --- /dev/null +++ b/modules/indexer/internal/indexer.go @@ -0,0 +1,37 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "context" + "fmt" +) + +// Indexer defines an basic indexer interface +type Indexer interface { + // Init initializes the indexer + // returns true if the index was opened/existed (with data populated), false if it was created/not-existed (with no data) + Init(ctx context.Context) (bool, error) + // Ping checks if the indexer is available + Ping(ctx context.Context) error + // Close closes the indexer + Close() +} + +// NewDummyIndexer returns a dummy indexer +func NewDummyIndexer() Indexer { + return &dummyIndexer{} +} + +type dummyIndexer struct{} + +func (d *dummyIndexer) Init(ctx context.Context) (bool, error) { + return false, fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Ping(ctx context.Context) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Close() {} diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go new file mode 100644 index 0000000000..06747ff7e0 --- /dev/null +++ b/modules/indexer/internal/meilisearch/indexer.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package meilisearch + +import ( + "context" + "fmt" + + "github.com/meilisearch/meilisearch-go" +) + +// Indexer represents a basic meilisearch indexer implementation +type Indexer struct { + Client *meilisearch.Client + + url, apiKey string + indexName string + version int +} + +func NewIndexer(url, apiKey, indexName string, version int) *Indexer { + return &Indexer{ + url: url, + apiKey: apiKey, + indexName: indexName, + version: version, + } +} + +// Init initializes the indexer +func (i *Indexer) Init(_ context.Context) (bool, error) { + if i == nil { + return false, fmt.Errorf("cannot init nil indexer") + } + + if i.Client != nil { + return false, fmt.Errorf("indexer is already initialized") + } + + i.Client = meilisearch.NewClient(meilisearch.ClientConfig{ + Host: i.url, + APIKey: i.apiKey, + }) + + _, err := i.Client.GetIndex(i.VersionedIndexName()) + if err == nil { + return true, nil + } + _, err = i.Client.CreateIndex(&meilisearch.IndexConfig{ + Uid: i.VersionedIndexName(), + PrimaryKey: "id", + }) + if err != nil { + return false, err + } + + i.checkOldIndexes() + + _, err = i.Client.Index(i.VersionedIndexName()).UpdateFilterableAttributes(&[]string{"repo_id"}) + return false, err +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(ctx context.Context) error { + if i == nil { + return fmt.Errorf("cannot ping nil indexer") + } + if i.Client == nil { + return fmt.Errorf("indexer is not initialized") + } + resp, err := i.Client.Health() + if err != nil { + return err + } + if resp.Status != "available" { + // See https://docs.meilisearch.com/reference/api/health.html#status + return fmt.Errorf("status of meilisearch is not available: %s", resp.Status) + } + return nil +} + +// Close closes the indexer +func (i *Indexer) Close() { + if i == nil { + return + } + if i.Client == nil { + return + } + i.Client = nil +} diff --git a/modules/indexer/internal/meilisearch/util.go b/modules/indexer/internal/meilisearch/util.go new file mode 100644 index 0000000000..e6d8fefade --- /dev/null +++ b/modules/indexer/internal/meilisearch/util.go @@ -0,0 +1,38 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package meilisearch + +import ( + "fmt" + + "code.gitea.io/gitea/modules/log" +) + +// VersionedIndexName returns the full index name with version +func (i *Indexer) VersionedIndexName() string { + return versionedIndexName(i.indexName, i.version) +} + +func versionedIndexName(indexName string, version int) string { + if version == 0 { + // Old index name without version + return indexName + } + + // The format of the index name is _v, not .v like elasticsearch. + // Because meilisearch does not support "." in index name, it should contain only alphanumeric characters, hyphens (-) and underscores (_). + // See https://www.meilisearch.com/docs/learn/core_concepts/indexes#index-uid + + return fmt.Sprintf("%s_v%d", indexName, version) +} + +func (i *Indexer) checkOldIndexes() { + for v := 0; v < i.version; v++ { + indexName := versionedIndexName(i.indexName, v) + _, err := i.Client.GetIndex(indexName) + if err == nil { + log.Warn("Found older meilisearch index named %q, Gitea will keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", indexName) + } + } +} diff --git a/modules/indexer/issues/bleve.go b/modules/indexer/issues/bleve/bleve.go similarity index 52% rename from modules/indexer/issues/bleve.go rename to modules/indexer/issues/bleve/bleve.go index 60d9ef7617..bb0bc4b04a 100644 --- a/modules/indexer/issues/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -1,17 +1,14 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package issues +package bleve import ( "context" - "fmt" - "os" - "strconv" - gitea_bleve "code.gitea.io/gitea/modules/indexer/bleve" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" + "code.gitea.io/gitea/modules/indexer/issues/internal" "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" @@ -19,10 +16,8 @@ import ( "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" - "github.com/blevesearch/bleve/v2/index/upsidedown" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search/query" - "github.com/ethantkoenig/rupture" ) const ( @@ -31,20 +26,6 @@ const ( issueIndexerLatestVersion = 2 ) -// indexerID a bleve-compatible unique identifier for an integer id -func indexerID(id int64) string { - return strconv.FormatInt(id, 36) -} - -// idOfIndexerID the integer id associated with an indexer id -func idOfIndexerID(indexerID string) (int64, error) { - id, err := strconv.ParseInt(indexerID, 36, 64) - if err != nil { - return 0, fmt.Errorf("Unexpected indexer ID %s: %w", indexerID, err) - } - return id, nil -} - // numericEqualityQuery a numeric equality query for the given value and field func numericEqualityQuery(value int64, field string) *query.NumericRangeQuery { f := float64(value) @@ -72,49 +53,16 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { const maxBatchSize = 16 -// openIndexer open the index at the specified path, checking for metadata -// updates and bleve version updates. If index needs to be created (or -// re-created), returns (nil, nil) -func openIndexer(path string, latestVersion int) (bleve.Index, error) { - _, err := os.Stat(path) - if err != nil && os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - - metadata, err := rupture.ReadIndexMetadata(path) - if err != nil { - return nil, err - } - if metadata.Version < latestVersion { - // the indexer is using a previous version, so we should delete it and - // re-populate - return nil, util.RemoveAll(path) - } - - index, err := bleve.Open(path) - if err != nil && err == upsidedown.IncompatibleVersion { - // the indexer was built with a previous version of bleve, so we should - // delete it and re-populate - return nil, util.RemoveAll(path) - } else if err != nil { - return nil, err - } - - return index, nil -} - -// BleveIndexerData an update to the issue indexer -type BleveIndexerData IndexerData +// IndexerData an update to the issue indexer +type IndexerData internal.IndexerData // Type returns the document type, for bleve's mapping.Classifier interface. -func (i *BleveIndexerData) Type() string { +func (i *IndexerData) Type() string { return issueIndexerDocType } -// createIssueIndexer create an issue indexer if one does not already exist -func createIssueIndexer(path string, latestVersion int) (bleve.Index, error) { +// generateIssueIndexMapping generates the bleve index mapping for issues +func generateIssueIndexMapping() (mapping.IndexMapping, error) { mapping := bleve.NewIndexMapping() docMapping := bleve.NewDocumentMapping() @@ -144,68 +92,31 @@ func createIssueIndexer(path string, latestVersion int) (bleve.Index, error) { mapping.AddDocumentMapping(issueIndexerDocType, docMapping) mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping()) - index, err := bleve.New(path, mapping) - if err != nil { - return nil, err - } - - if err = rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{ - Version: latestVersion, - }); err != nil { - return nil, err - } - return index, nil + return mapping, nil } -var _ Indexer = &BleveIndexer{} +var _ internal.Indexer = &Indexer{} -// BleveIndexer implements Indexer interface -type BleveIndexer struct { - indexDir string - indexer bleve.Index +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_bleve.Indexer + indexer_internal.Indexer // do not composite inner_bleve.Indexer directly to avoid exposing too much } -// NewBleveIndexer creates a new bleve local indexer -func NewBleveIndexer(indexDir string) *BleveIndexer { - return &BleveIndexer{ - indexDir: indexDir, - } -} - -// Init will initialize the indexer -func (b *BleveIndexer) Init() (bool, error) { - var err error - b.indexer, err = openIndexer(b.indexDir, issueIndexerLatestVersion) - if err != nil { - return false, err - } - if b.indexer != nil { - return true, nil - } - - b.indexer, err = createIssueIndexer(b.indexDir, issueIndexerLatestVersion) - return false, err -} - -// Ping does nothing -func (b *BleveIndexer) Ping() bool { - return true -} - -// Close will close the bleve indexer -func (b *BleveIndexer) Close() { - if b.indexer != nil { - if err := b.indexer.Close(); err != nil { - log.Error("Error whilst closing indexer: %v", err) - } +// NewIndexer creates a new bleve local indexer +func NewIndexer(indexDir string) *Indexer { + inner := inner_bleve.NewIndexer(indexDir, issueIndexerLatestVersion, generateIssueIndexMapping) + return &Indexer{ + Indexer: inner, + inner: inner, } } // Index will save the index data -func (b *BleveIndexer) Index(issues []*IndexerData) error { - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) +func (b *Indexer) Index(_ context.Context, issues []*internal.IndexerData) error { + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) for _, issue := range issues { - if err := batch.Index(indexerID(issue.ID), struct { + if err := batch.Index(indexer_internal.Base36(issue.ID), struct { RepoID int64 Title string Content string @@ -223,10 +134,10 @@ func (b *BleveIndexer) Index(issues []*IndexerData) error { } // Delete deletes indexes by ids -func (b *BleveIndexer) Delete(ids ...int64) error { - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) +func (b *Indexer) Delete(_ context.Context, ids ...int64) error { + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) for _, id := range ids { - if err := batch.Delete(indexerID(id)); err != nil { + if err := batch.Delete(indexer_internal.Base36(id)); err != nil { return err } } @@ -235,7 +146,7 @@ func (b *BleveIndexer) Delete(ids ...int64) error { // Search searches for issues by given conditions. // Returns the matching issue IDs -func (b *BleveIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { +func (b *Indexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { var repoQueriesP []*query.NumericRangeQuery for _, repoID := range repoIDs { repoQueriesP = append(repoQueriesP, numericEqualityQuery(repoID, "RepoID")) @@ -255,20 +166,20 @@ func (b *BleveIndexer) Search(ctx context.Context, keyword string, repoIDs []int search := bleve.NewSearchRequestOptions(indexerQuery, limit, start, false) search.SortBy([]string{"-_score"}) - result, err := b.indexer.SearchInContext(ctx, search) + result, err := b.inner.Indexer.SearchInContext(ctx, search) if err != nil { return nil, err } - ret := SearchResult{ - Hits: make([]Match, 0, len(result.Hits)), + ret := internal.SearchResult{ + Hits: make([]internal.Match, 0, len(result.Hits)), } for _, hit := range result.Hits { - id, err := idOfIndexerID(hit.ID) + id, err := indexer_internal.ParseBase36(hit.ID) if err != nil { return nil, err } - ret.Hits = append(ret.Hits, Match{ + ret.Hits = append(ret.Hits, internal.Match{ ID: id, }) } diff --git a/modules/indexer/issues/bleve_test.go b/modules/indexer/issues/bleve/bleve_test.go similarity index 86% rename from modules/indexer/issues/bleve_test.go rename to modules/indexer/issues/bleve/bleve_test.go index 22827158e4..f890f8eb48 100644 --- a/modules/indexer/issues/bleve_test.go +++ b/modules/indexer/issues/bleve/bleve_test.go @@ -1,26 +1,28 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package issues +package bleve import ( "context" "testing" + "code.gitea.io/gitea/modules/indexer/issues/internal" + "github.com/stretchr/testify/assert" ) func TestBleveIndexAndSearch(t *testing.T) { dir := t.TempDir() - indexer := NewBleveIndexer(dir) + indexer := NewIndexer(dir) defer indexer.Close() - if _, err := indexer.Init(); err != nil { + if _, err := indexer.Init(context.Background()); err != nil { assert.Fail(t, "Unable to initialize bleve indexer: %v", err) return } - err := indexer.Index([]*IndexerData{ + err := indexer.Index(context.Background(), []*internal.IndexerData{ { ID: 1, RepoID: 2, diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go deleted file mode 100644 index 04c101c356..0000000000 --- a/modules/indexer/issues/db.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package issues - -import ( - "context" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" -) - -// DBIndexer implements Indexer interface to use database's like search -type DBIndexer struct{} - -// Init dummy function -func (i *DBIndexer) Init() (bool, error) { - return false, nil -} - -// Ping checks if database is available -func (i *DBIndexer) Ping() bool { - return db.GetEngine(db.DefaultContext).Ping() != nil -} - -// Index dummy function -func (i *DBIndexer) Index(issue []*IndexerData) error { - return nil -} - -// Delete dummy function -func (i *DBIndexer) Delete(ids ...int64) error { - return nil -} - -// Close dummy function -func (i *DBIndexer) Close() { -} - -// Search dummy function -func (i *DBIndexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { - total, ids, err := issues_model.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start) - if err != nil { - return nil, err - } - result := SearchResult{ - Total: total, - Hits: make([]Match, 0, limit), - } - for _, id := range ids { - result.Hits = append(result.Hits, Match{ - ID: id, - }) - } - return &result, nil -} diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go new file mode 100644 index 0000000000..17ed426b38 --- /dev/null +++ b/modules/indexer/issues/db/db.go @@ -0,0 +1,54 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_db "code.gitea.io/gitea/modules/indexer/internal/db" + "code.gitea.io/gitea/modules/indexer/issues/internal" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer implements Indexer interface to use database's like search +type Indexer struct { + indexer_internal.Indexer +} + +func NewIndexer() *Indexer { + return &Indexer{ + Indexer: &inner_db.Indexer{}, + } +} + +// Index dummy function +func (i *Indexer) Index(_ context.Context, _ []*internal.IndexerData) error { + return nil +} + +// Delete dummy function +func (i *Indexer) Delete(_ context.Context, _ ...int64) error { + return nil +} + +// Search searches for issues +func (i *Indexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { + total, ids, err := issues_model.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start) + if err != nil { + return nil, err + } + result := internal.SearchResult{ + Total: total, + Hits: make([]internal.Match, 0, limit), + } + for _, id := range ids { + result.Hits = append(result.Hits, internal.Match{ + ID: id, + }) + } + return &result, nil +} diff --git a/modules/indexer/issues/elastic_search.go b/modules/indexer/issues/elastic_search.go deleted file mode 100644 index ec62f857ad..0000000000 --- a/modules/indexer/issues/elastic_search.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package issues - -import ( - "context" - "errors" - "fmt" - "net" - "strconv" - "sync" - "time" - - "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/log" - - "github.com/olivere/elastic/v7" -) - -var _ Indexer = &ElasticSearchIndexer{} - -// ElasticSearchIndexer implements Indexer interface -type ElasticSearchIndexer struct { - client *elastic.Client - indexerName string - available bool - stopTimer chan struct{} - lock sync.RWMutex -} - -// NewElasticSearchIndexer creates a new elasticsearch indexer -func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, error) { - opts := []elastic.ClientOptionFunc{ - elastic.SetURL(url), - elastic.SetSniff(false), - elastic.SetHealthcheckInterval(10 * time.Second), - elastic.SetGzip(false), - } - - logger := log.GetLogger(log.DEFAULT) - opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) - opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) - opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) - - client, err := elastic.NewClient(opts...) - if err != nil { - return nil, err - } - - indexer := &ElasticSearchIndexer{ - client: client, - indexerName: indexerName, - available: true, - stopTimer: make(chan struct{}), - } - - ticker := time.NewTicker(10 * time.Second) - go func() { - for { - select { - case <-ticker.C: - indexer.checkAvailability() - case <-indexer.stopTimer: - ticker.Stop() - return - } - } - }() - - return indexer, nil -} - -const ( - defaultMapping = `{ - "mappings": { - "properties": { - "id": { - "type": "integer", - "index": true - }, - "repo_id": { - "type": "integer", - "index": true - }, - "title": { - "type": "text", - "index": true - }, - "content": { - "type": "text", - "index": true - }, - "comments": { - "type" : "text", - "index": true - } - } - } - }` -) - -// Init will initialize the indexer -func (b *ElasticSearchIndexer) Init() (bool, error) { - ctx := graceful.GetManager().HammerContext() - exists, err := b.client.IndexExists(b.indexerName).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - - if !exists { - mapping := defaultMapping - - createIndex, err := b.client.CreateIndex(b.indexerName).BodyString(mapping).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !createIndex.Acknowledged { - return false, errors.New("init failed") - } - - return false, nil - } - return true, nil -} - -// Ping checks if elastic is available -func (b *ElasticSearchIndexer) Ping() bool { - b.lock.RLock() - defer b.lock.RUnlock() - return b.available -} - -// Index will save the index data -func (b *ElasticSearchIndexer) Index(issues []*IndexerData) error { - if len(issues) == 0 { - return nil - } else if len(issues) == 1 { - issue := issues[0] - _, err := b.client.Index(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", issue.ID)). - BodyJson(map[string]interface{}{ - "id": issue.ID, - "repo_id": issue.RepoID, - "title": issue.Title, - "content": issue.Content, - "comments": issue.Comments, - }). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) - } - - reqs := make([]elastic.BulkableRequest, 0) - for _, issue := range issues { - reqs = append(reqs, - elastic.NewBulkIndexRequest(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", issue.ID)). - Doc(map[string]interface{}{ - "id": issue.ID, - "repo_id": issue.RepoID, - "title": issue.Title, - "content": issue.Content, - "comments": issue.Comments, - }), - ) - } - - _, err := b.client.Bulk(). - Index(b.indexerName). - Add(reqs...). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) -} - -// Delete deletes indexes by ids -func (b *ElasticSearchIndexer) Delete(ids ...int64) error { - if len(ids) == 0 { - return nil - } else if len(ids) == 1 { - _, err := b.client.Delete(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", ids[0])). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) - } - - reqs := make([]elastic.BulkableRequest, 0) - for _, id := range ids { - reqs = append(reqs, - elastic.NewBulkDeleteRequest(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", id)), - ) - } - - _, err := b.client.Bulk(). - Index(b.indexerName). - Add(reqs...). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) -} - -// Search searches for issues by given conditions. -// Returns the matching issue IDs -func (b *ElasticSearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { - kwQuery := elastic.NewMultiMatchQuery(keyword, "title", "content", "comments") - query := elastic.NewBoolQuery() - query = query.Must(kwQuery) - if len(repoIDs) > 0 { - repoStrs := make([]interface{}, 0, len(repoIDs)) - for _, repoID := range repoIDs { - repoStrs = append(repoStrs, repoID) - } - repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) - query = query.Must(repoQuery) - } - searchResult, err := b.client.Search(). - Index(b.indexerName). - Query(query). - Sort("_score", false). - From(start).Size(limit). - Do(ctx) - if err != nil { - return nil, b.checkError(err) - } - - hits := make([]Match, 0, limit) - for _, hit := range searchResult.Hits.Hits { - id, _ := strconv.ParseInt(hit.Id, 10, 64) - hits = append(hits, Match{ - ID: id, - }) - } - - return &SearchResult{ - Total: searchResult.TotalHits(), - Hits: hits, - }, nil -} - -// Close implements indexer -func (b *ElasticSearchIndexer) Close() { - select { - case <-b.stopTimer: - default: - close(b.stopTimer) - } -} - -func (b *ElasticSearchIndexer) checkError(err error) error { - var opErr *net.OpError - if !(elastic.IsConnErr(err) || (errors.As(err, &opErr) && (opErr.Op == "dial" || opErr.Op == "read"))) { - return err - } - - b.setAvailability(false) - - return err -} - -func (b *ElasticSearchIndexer) checkAvailability() { - if b.Ping() { - return - } - - // Request cluster state to check if elastic is available again - _, err := b.client.ClusterState().Do(graceful.GetManager().ShutdownContext()) - if err != nil { - b.setAvailability(false) - return - } - - b.setAvailability(true) -} - -func (b *ElasticSearchIndexer) setAvailability(available bool) { - b.lock.Lock() - defer b.lock.Unlock() - - if b.available == available { - return - } - - b.available = available -} diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go new file mode 100644 index 0000000000..33a7dfc21e --- /dev/null +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -0,0 +1,177 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "context" + "fmt" + "strconv" + + "code.gitea.io/gitea/modules/graceful" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" + "code.gitea.io/gitea/modules/indexer/issues/internal" + + "github.com/olivere/elastic/v7" +) + +const ( + issueIndexerLatestVersion = 0 +) + +var _ internal.Indexer = &Indexer{} + +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_elasticsearch.Indexer + indexer_internal.Indexer // do not composite inner_elasticsearch.Indexer directly to avoid exposing too much +} + +// NewIndexer creates a new elasticsearch indexer +func NewIndexer(url, indexerName string) *Indexer { + inner := inner_elasticsearch.NewIndexer(url, indexerName, issueIndexerLatestVersion, defaultMapping) + indexer := &Indexer{ + inner: inner, + Indexer: inner, + } + return indexer +} + +const ( + defaultMapping = `{ + "mappings": { + "properties": { + "id": { + "type": "integer", + "index": true + }, + "repo_id": { + "type": "integer", + "index": true + }, + "title": { + "type": "text", + "index": true + }, + "content": { + "type": "text", + "index": true + }, + "comments": { + "type" : "text", + "index": true + } + } + } + }` +) + +// Index will save the index data +func (b *Indexer) Index(ctx context.Context, issues []*internal.IndexerData) error { + if len(issues) == 0 { + return nil + } else if len(issues) == 1 { + issue := issues[0] + _, err := b.inner.Client.Index(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", issue.ID)). + BodyJson(map[string]interface{}{ + "id": issue.ID, + "repo_id": issue.RepoID, + "title": issue.Title, + "content": issue.Content, + "comments": issue.Comments, + }). + Do(ctx) + return err + } + + reqs := make([]elastic.BulkableRequest, 0) + for _, issue := range issues { + reqs = append(reqs, + elastic.NewBulkIndexRequest(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", issue.ID)). + Doc(map[string]interface{}{ + "id": issue.ID, + "repo_id": issue.RepoID, + "title": issue.Title, + "content": issue.Content, + "comments": issue.Comments, + }), + ) + } + + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). + Add(reqs...). + Do(graceful.GetManager().HammerContext()) + return err +} + +// Delete deletes indexes by ids +func (b *Indexer) Delete(ctx context.Context, ids ...int64) error { + if len(ids) == 0 { + return nil + } else if len(ids) == 1 { + _, err := b.inner.Client.Delete(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", ids[0])). + Do(ctx) + return err + } + + reqs := make([]elastic.BulkableRequest, 0) + for _, id := range ids { + reqs = append(reqs, + elastic.NewBulkDeleteRequest(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", id)), + ) + } + + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). + Add(reqs...). + Do(graceful.GetManager().HammerContext()) + return err +} + +// Search searches for issues by given conditions. +// Returns the matching issue IDs +func (b *Indexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { + kwQuery := elastic.NewMultiMatchQuery(keyword, "title", "content", "comments") + query := elastic.NewBoolQuery() + query = query.Must(kwQuery) + if len(repoIDs) > 0 { + repoStrs := make([]interface{}, 0, len(repoIDs)) + for _, repoID := range repoIDs { + repoStrs = append(repoStrs, repoID) + } + repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) + query = query.Must(repoQuery) + } + searchResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). + Query(query). + Sort("_score", false). + From(start).Size(limit). + Do(ctx) + if err != nil { + return nil, err + } + + hits := make([]internal.Match, 0, limit) + for _, hit := range searchResult.Hits.Hits { + id, _ := strconv.ParseInt(hit.Id, 10, 64) + hits = append(hits, internal.Match{ + ID: id, + }) + } + + return &internal.SearchResult{ + Total: searchResult.TotalHits(), + Hits: hits, + }, nil +} diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index f36ea10935..9e2f13371e 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -5,16 +5,20 @@ package issues import ( "context" - "fmt" "os" "runtime/pprof" - "sync" + "sync/atomic" "time" - "code.gitea.io/gitea/models/db" + db_model "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/indexer/issues/bleve" + "code.gitea.io/gitea/modules/indexer/issues/db" + "code.gitea.io/gitea/modules/indexer/issues/elasticsearch" + "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/indexer/issues/meilisearch" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" @@ -22,81 +26,22 @@ import ( "code.gitea.io/gitea/modules/util" ) -// IndexerData data stored in the issue indexer -type IndexerData struct { - ID int64 `json:"id"` - RepoID int64 `json:"repo_id"` - Title string `json:"title"` - Content string `json:"content"` - Comments []string `json:"comments"` - IsDelete bool `json:"is_delete"` - IDs []int64 `json:"ids"` -} - -// Match represents on search result -type Match struct { - ID int64 `json:"id"` - Score float64 `json:"score"` -} - -// SearchResult represents search results -type SearchResult struct { - Total int64 - Hits []Match -} - -// Indexer defines an interface to indexer issues contents -type Indexer interface { - Init() (bool, error) - Ping() bool - Index(issue []*IndexerData) error - Delete(ids ...int64) error - Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) - Close() -} - -type indexerHolder struct { - indexer Indexer - mutex sync.RWMutex - cond *sync.Cond - cancelled bool -} - -func newIndexerHolder() *indexerHolder { - h := &indexerHolder{} - h.cond = sync.NewCond(h.mutex.RLocker()) - return h -} - -func (h *indexerHolder) cancel() { - h.mutex.Lock() - defer h.mutex.Unlock() - h.cancelled = true - h.cond.Broadcast() -} - -func (h *indexerHolder) set(indexer Indexer) { - h.mutex.Lock() - defer h.mutex.Unlock() - h.indexer = indexer - h.cond.Broadcast() -} - -func (h *indexerHolder) get() Indexer { - h.mutex.RLock() - defer h.mutex.RUnlock() - if h.indexer == nil && !h.cancelled { - h.cond.Wait() - } - return h.indexer -} - var ( // issueIndexerQueue queue of issue ids to be updated - issueIndexerQueue *queue.WorkerPoolQueue[*IndexerData] - holder = newIndexerHolder() + issueIndexerQueue *queue.WorkerPoolQueue[*internal.IndexerData] + // globalIndexer is the global indexer, it cannot be nil. + // When the real indexer is not ready, it will be a dummy indexer which will return error to explain it's not ready. + // So it's always safe use it as *globalIndexer.Load() and call its methods. + globalIndexer atomic.Pointer[internal.Indexer] + dummyIndexer *internal.Indexer ) +func init() { + i := internal.NewDummyIndexer() + dummyIndexer = &i + globalIndexer.Store(dummyIndexer) +} + // InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until // all issue index done. func InitIssueIndexer(syncReindex bool) { @@ -107,33 +52,23 @@ func InitIssueIndexer(syncReindex bool) { // Create the Queue switch setting.Indexer.IssueType { case "bleve", "elasticsearch", "meilisearch": - handler := func(items ...*IndexerData) (unhandled []*IndexerData) { - indexer := holder.get() - if indexer == nil { - log.Warn("Issue indexer handler: indexer is not ready, retry later.") - return items - } - toIndex := make([]*IndexerData, 0, len(items)) + handler := func(items ...*internal.IndexerData) (unhandled []*internal.IndexerData) { + indexer := *globalIndexer.Load() + toIndex := make([]*internal.IndexerData, 0, len(items)) for _, indexerData := range items { log.Trace("IndexerData Process: %d %v %t", indexerData.ID, indexerData.IDs, indexerData.IsDelete) if indexerData.IsDelete { - if err := indexer.Delete(indexerData.IDs...); err != nil { + if err := indexer.Delete(ctx, indexerData.IDs...); err != nil { log.Error("Issue indexer handler: failed to from index: %v Error: %v", indexerData.IDs, err) - if !indexer.Ping() { - log.Error("Issue indexer handler: indexer is unavailable when deleting") - unhandled = append(unhandled, indexerData) - } + unhandled = append(unhandled, indexerData) } continue } toIndex = append(toIndex, indexerData) } - if err := indexer.Index(toIndex); err != nil { + if err := indexer.Index(ctx, toIndex); err != nil { log.Error("Error whilst indexing: %v Error: %v", toIndex, err) - if !indexer.Ping() { - log.Error("Issue indexer handler: indexer is unavailable when indexing") - unhandled = append(unhandled, toIndex...) - } + unhandled = append(unhandled, toIndex...) } return unhandled } @@ -144,7 +79,7 @@ func InitIssueIndexer(syncReindex bool) { log.Fatal("Unable to create issue indexer queue") } default: - issueIndexerQueue = queue.CreateSimpleQueue[*IndexerData](ctx, "issue_indexer", nil) + issueIndexerQueue = queue.CreateSimpleQueue[*internal.IndexerData](ctx, "issue_indexer", nil) } graceful.GetManager().RunAtTerminate(finished) @@ -154,7 +89,11 @@ func InitIssueIndexer(syncReindex bool) { pprof.SetGoroutineLabels(ctx) start := time.Now() log.Info("PID %d: Initializing Issue Indexer: %s", os.Getpid(), setting.Indexer.IssueType) - var populate bool + var ( + issueIndexer internal.Indexer + existed bool + err error + ) switch setting.Indexer.IssueType { case "bleve": defer func() { @@ -162,62 +101,45 @@ func InitIssueIndexer(syncReindex bool) { log.Error("PANIC whilst initializing issue indexer: %v\nStacktrace: %s", err, log.Stack(2)) log.Error("The indexer files are likely corrupted and may need to be deleted") log.Error("You can completely remove the %q directory to make Gitea recreate the indexes", setting.Indexer.IssuePath) - holder.cancel() + globalIndexer.Store(dummyIndexer) log.Fatal("PID: %d Unable to initialize the Bleve Issue Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.IssuePath, err) } }() - issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath) - exist, err := issueIndexer.Init() + issueIndexer = bleve.NewIndexer(setting.Indexer.IssuePath) + existed, err = issueIndexer.Init(ctx) if err != nil { - holder.cancel() log.Fatal("Unable to initialize Bleve Issue Indexer at path: %s Error: %v", setting.Indexer.IssuePath, err) } - populate = !exist - holder.set(issueIndexer) - graceful.GetManager().RunAtTerminate(func() { - log.Debug("Closing issue indexer") - issueIndexer := holder.get() - if issueIndexer != nil { - issueIndexer.Close() - } - log.Info("PID: %d Issue Indexer closed", os.Getpid()) - }) - log.Debug("Created Bleve Indexer") case "elasticsearch": - issueIndexer, err := NewElasticSearchIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName) - if err != nil { - log.Fatal("Unable to initialize Elastic Search Issue Indexer at connection: %s Error: %v", setting.Indexer.IssueConnStr, err) - } - exist, err := issueIndexer.Init() + issueIndexer = elasticsearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName) + existed, err = issueIndexer.Init(ctx) if err != nil { log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) } - populate = !exist - holder.set(issueIndexer) case "db": - issueIndexer := &DBIndexer{} - holder.set(issueIndexer) + issueIndexer = db.NewIndexer() case "meilisearch": - issueIndexer, err := NewMeilisearchIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName) - if err != nil { - log.Fatal("Unable to initialize Meilisearch Issue Indexer at connection: %s Error: %v", setting.Indexer.IssueConnStr, err) - } - exist, err := issueIndexer.Init() + issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName) + existed, err = issueIndexer.Init(ctx) if err != nil { log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) } - populate = !exist - holder.set(issueIndexer) default: - holder.cancel() log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) } + globalIndexer.Store(&issueIndexer) + + graceful.GetManager().RunAtTerminate(func() { + log.Debug("Closing issue indexer") + (*globalIndexer.Load()).Close() + log.Info("PID: %d Issue Indexer closed", os.Getpid()) + }) // Start processing the queue go graceful.GetManager().RunWithCancel(issueIndexerQueue) // Populate the index - if populate { + if !existed { if syncReindex { graceful.GetManager().RunWithShutdownContext(populateIssueIndexer) } else { @@ -266,8 +188,8 @@ func populateIssueIndexer(ctx context.Context) { default: } repos, _, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{ - ListOptions: db.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, - OrderBy: db.SearchOrderByID, + ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, + OrderBy: db_model.SearchOrderByID, Private: true, Collaborate: util.OptionalBoolFalse, }) @@ -320,7 +242,7 @@ func UpdateIssueIndexer(issue *issues_model.Issue) { comments = append(comments, comment.Content) } } - indexerData := &IndexerData{ + indexerData := &internal.IndexerData{ ID: issue.ID, RepoID: issue.RepoID, Title: issue.Title, @@ -345,7 +267,7 @@ func DeleteRepoIssueIndexer(ctx context.Context, repo *repo_model.Repository) { if len(ids) == 0 { return } - indexerData := &IndexerData{ + indexerData := &internal.IndexerData{ IDs: ids, IsDelete: true, } @@ -358,12 +280,7 @@ func DeleteRepoIssueIndexer(ctx context.Context, repo *repo_model.Repository) { // WARNNING: You have to ensure user have permission to visit repoIDs' issues func SearchIssuesByKeyword(ctx context.Context, repoIDs []int64, keyword string) ([]int64, error) { var issueIDs []int64 - indexer := holder.get() - - if indexer == nil { - log.Error("SearchIssuesByKeyword(): unable to get indexer!") - return nil, fmt.Errorf("unable to get issue indexer") - } + indexer := *globalIndexer.Load() res, err := indexer.Search(ctx, keyword, repoIDs, 50, 0) if err != nil { return nil, err @@ -375,12 +292,6 @@ func SearchIssuesByKeyword(ctx context.Context, repoIDs []int64, keyword string) } // IsAvailable checks if issue indexer is available -func IsAvailable() bool { - indexer := holder.get() - if indexer == nil { - log.Error("IsAvailable(): unable to get indexer!") - return false - } - - return indexer.Ping() +func IsAvailable(ctx context.Context) bool { + return (*globalIndexer.Load()).Ping(ctx) == nil } diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index a2d1794f4b..5962a4ee9c 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/indexer/issues/bleve" "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" @@ -42,8 +43,7 @@ func TestBleveSearchIssues(t *testing.T) { setting.LoadQueueSettings() InitIssueIndexer(true) defer func() { - indexer := holder.get() - if bleveIndexer, ok := indexer.(*BleveIndexer); ok { + if bleveIndexer, ok := (*globalIndexer.Load()).(*bleve.Indexer); ok { bleveIndexer.Close() } }() diff --git a/modules/indexer/issues/internal/indexer.go b/modules/indexer/issues/internal/indexer.go new file mode 100644 index 0000000000..553c8a573c --- /dev/null +++ b/modules/indexer/issues/internal/indexer.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/modules/indexer/internal" +) + +// Indexer defines an interface to indexer issues contents +type Indexer interface { + internal.Indexer + Index(ctx context.Context, issue []*IndexerData) error + Delete(ctx context.Context, ids ...int64) error + Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) +} + +// NewDummyIndexer returns a dummy indexer +func NewDummyIndexer() Indexer { + return &dummyIndexer{ + Indexer: internal.NewDummyIndexer(), + } +} + +type dummyIndexer struct { + internal.Indexer +} + +func (d *dummyIndexer) Index(ctx context.Context, issue []*IndexerData) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Delete(ctx context.Context, ids ...int64) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { + return nil, fmt.Errorf("indexer is not ready") +} diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go new file mode 100644 index 0000000000..8c206fc1cf --- /dev/null +++ b/modules/indexer/issues/internal/model.go @@ -0,0 +1,27 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +// IndexerData data stored in the issue indexer +type IndexerData struct { + ID int64 `json:"id"` + RepoID int64 `json:"repo_id"` + Title string `json:"title"` + Content string `json:"content"` + Comments []string `json:"comments"` + IsDelete bool `json:"is_delete"` + IDs []int64 `json:"ids"` +} + +// Match represents on search result +type Match struct { + ID int64 `json:"id"` + Score float64 `json:"score"` +} + +// SearchResult represents search results +type SearchResult struct { + Total int64 + Hits []Match +} diff --git a/modules/indexer/issues/meilisearch.go b/modules/indexer/issues/meilisearch.go deleted file mode 100644 index 990bc57a05..0000000000 --- a/modules/indexer/issues/meilisearch.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package issues - -import ( - "context" - "strconv" - "strings" - "sync" - "time" - - "github.com/meilisearch/meilisearch-go" -) - -var _ Indexer = &MeilisearchIndexer{} - -// MeilisearchIndexer implements Indexer interface -type MeilisearchIndexer struct { - client *meilisearch.Client - indexerName string - available bool - stopTimer chan struct{} - lock sync.RWMutex -} - -// MeilisearchIndexer creates a new meilisearch indexer -func NewMeilisearchIndexer(url, apiKey, indexerName string) (*MeilisearchIndexer, error) { - client := meilisearch.NewClient(meilisearch.ClientConfig{ - Host: url, - APIKey: apiKey, - }) - - indexer := &MeilisearchIndexer{ - client: client, - indexerName: indexerName, - available: true, - stopTimer: make(chan struct{}), - } - - ticker := time.NewTicker(10 * time.Second) - go func() { - for { - select { - case <-ticker.C: - indexer.checkAvailability() - case <-indexer.stopTimer: - ticker.Stop() - return - } - } - }() - - return indexer, nil -} - -// Init will initialize the indexer -func (b *MeilisearchIndexer) Init() (bool, error) { - _, err := b.client.GetIndex(b.indexerName) - if err == nil { - return true, nil - } - _, err = b.client.CreateIndex(&meilisearch.IndexConfig{ - Uid: b.indexerName, - PrimaryKey: "id", - }) - if err != nil { - return false, b.checkError(err) - } - - _, err = b.client.Index(b.indexerName).UpdateFilterableAttributes(&[]string{"repo_id"}) - return false, b.checkError(err) -} - -// Ping checks if meilisearch is available -func (b *MeilisearchIndexer) Ping() bool { - b.lock.RLock() - defer b.lock.RUnlock() - return b.available -} - -// Index will save the index data -func (b *MeilisearchIndexer) Index(issues []*IndexerData) error { - if len(issues) == 0 { - return nil - } - for _, issue := range issues { - _, err := b.client.Index(b.indexerName).AddDocuments(issue) - if err != nil { - return b.checkError(err) - } - } - // TODO: bulk send index data - return nil -} - -// Delete deletes indexes by ids -func (b *MeilisearchIndexer) Delete(ids ...int64) error { - if len(ids) == 0 { - return nil - } - - for _, id := range ids { - _, err := b.client.Index(b.indexerName).DeleteDocument(strconv.FormatInt(id, 10)) - if err != nil { - return b.checkError(err) - } - } - // TODO: bulk send deletes - return nil -} - -// Search searches for issues by given conditions. -// Returns the matching issue IDs -func (b *MeilisearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { - repoFilters := make([]string, 0, len(repoIDs)) - for _, repoID := range repoIDs { - repoFilters = append(repoFilters, "repo_id = "+strconv.FormatInt(repoID, 10)) - } - filter := strings.Join(repoFilters, " OR ") - searchRes, err := b.client.Index(b.indexerName).Search(keyword, &meilisearch.SearchRequest{ - Filter: filter, - Limit: int64(limit), - Offset: int64(start), - }) - if err != nil { - return nil, b.checkError(err) - } - - hits := make([]Match, 0, len(searchRes.Hits)) - for _, hit := range searchRes.Hits { - hits = append(hits, Match{ - ID: int64(hit.(map[string]interface{})["id"].(float64)), - }) - } - return &SearchResult{ - Total: searchRes.TotalHits, - Hits: hits, - }, nil -} - -// Close implements indexer -func (b *MeilisearchIndexer) Close() { - select { - case <-b.stopTimer: - default: - close(b.stopTimer) - } -} - -func (b *MeilisearchIndexer) checkError(err error) error { - return err -} - -func (b *MeilisearchIndexer) checkAvailability() { - _, err := b.client.Health() - if err != nil { - b.setAvailability(false) - return - } - b.setAvailability(true) -} - -func (b *MeilisearchIndexer) setAvailability(available bool) { - b.lock.Lock() - defer b.lock.Unlock() - - if b.available == available { - return - } - - b.available = available -} diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go new file mode 100644 index 0000000000..877c04f1dc --- /dev/null +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -0,0 +1,98 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package meilisearch + +import ( + "context" + "strconv" + "strings" + + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_meilisearch "code.gitea.io/gitea/modules/indexer/internal/meilisearch" + "code.gitea.io/gitea/modules/indexer/issues/internal" + + "github.com/meilisearch/meilisearch-go" +) + +const ( + issueIndexerLatestVersion = 0 +) + +var _ internal.Indexer = &Indexer{} + +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_meilisearch.Indexer + indexer_internal.Indexer // do not composite inner_meilisearch.Indexer directly to avoid exposing too much +} + +// NewIndexer creates a new meilisearch indexer +func NewIndexer(url, apiKey, indexerName string) *Indexer { + inner := inner_meilisearch.NewIndexer(url, apiKey, indexerName, issueIndexerLatestVersion) + indexer := &Indexer{ + inner: inner, + Indexer: inner, + } + return indexer +} + +// Index will save the index data +func (b *Indexer) Index(_ context.Context, issues []*internal.IndexerData) error { + if len(issues) == 0 { + return nil + } + for _, issue := range issues { + _, err := b.inner.Client.Index(b.inner.VersionedIndexName()).AddDocuments(issue) + if err != nil { + return err + } + } + // TODO: bulk send index data + return nil +} + +// Delete deletes indexes by ids +func (b *Indexer) Delete(_ context.Context, ids ...int64) error { + if len(ids) == 0 { + return nil + } + + for _, id := range ids { + _, err := b.inner.Client.Index(b.inner.VersionedIndexName()).DeleteDocument(strconv.FormatInt(id, 10)) + if err != nil { + return err + } + } + // TODO: bulk send deletes + return nil +} + +// Search searches for issues by given conditions. +// Returns the matching issue IDs +func (b *Indexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { + repoFilters := make([]string, 0, len(repoIDs)) + for _, repoID := range repoIDs { + repoFilters = append(repoFilters, "repo_id = "+strconv.FormatInt(repoID, 10)) + } + filter := strings.Join(repoFilters, " OR ") + searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{ + Filter: filter, + Limit: int64(limit), + Offset: int64(start), + }) + if err != nil { + return nil, err + } + + hits := make([]internal.Match, 0, len(searchRes.Hits)) + for _, hit := range searchRes.Hits { + hits = append(hits, internal.Match{ + ID: int64(hit.(map[string]interface{})["id"].(float64)), + }) + } + return &internal.SearchResult{ + Total: searchRes.TotalHits, + Hits: hits, + }, nil +} diff --git a/modules/indexer/stats/indexer.go b/modules/indexer/stats/indexer.go index 1c01e25e29..6bfa8bdedb 100644 --- a/modules/indexer/stats/indexer.go +++ b/modules/indexer/stats/indexer.go @@ -11,6 +11,7 @@ import ( ) // Indexer defines an interface to index repository stats +// TODO: this indexer is quite different from the others, maybe this package should be moved out from module/indexer type Indexer interface { Index(id int64) error Close() diff --git a/modules/log/logger_global.go b/modules/log/logger_global.go index 5ccef34b5b..994acfedbb 100644 --- a/modules/log/logger_global.go +++ b/modules/log/logger_global.go @@ -79,5 +79,5 @@ func SetConsoleLogger(loggerName, writerName string, level Level) { Colorize: CanColorStdout, WriterOption: WriterConsoleOption{}, }) - GetManager().GetLogger(loggerName).RemoveAllWriters().AddWriters(writer) + GetManager().GetLogger(loggerName).ReplaceAllWriters(writer) } diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go index 903d8cefc2..d38c6516ed 100644 --- a/modules/log/logger_impl.go +++ b/modules/log/logger_impl.go @@ -96,7 +96,10 @@ func (l *LoggerImpl) removeWriterInternal(w EventWriter) { func (l *LoggerImpl) AddWriters(writer ...EventWriter) { l.eventWriterMu.Lock() defer l.eventWriterMu.Unlock() + l.addWritersInternal(writer...) +} +func (l *LoggerImpl) addWritersInternal(writer ...EventWriter) { for _, w := range writer { if old, ok := l.eventWriters[w.GetWriterName()]; ok { l.removeWriterInternal(old) @@ -126,8 +129,8 @@ func (l *LoggerImpl) RemoveWriter(modeName string) error { return nil } -// RemoveAllWriters removes all writers from the logger, non-shared writers are closed and flushed -func (l *LoggerImpl) RemoveAllWriters() *LoggerImpl { +// ReplaceAllWriters replaces all writers from the logger, non-shared writers are closed and flushed +func (l *LoggerImpl) ReplaceAllWriters(writer ...EventWriter) { l.eventWriterMu.Lock() defer l.eventWriterMu.Unlock() @@ -135,8 +138,7 @@ func (l *LoggerImpl) RemoveAllWriters() *LoggerImpl { l.removeWriterInternal(w) } l.eventWriters = map[string]EventWriter{} - l.syncLevelInternal() - return l + l.addWritersInternal(writer...) } // DumpWriters dumps the writers as a JSON map, it's used for debugging and display purposes. @@ -161,7 +163,7 @@ func (l *LoggerImpl) DumpWriters() map[string]any { // Close closes the logger, non-shared writers are closed and flushed func (l *LoggerImpl) Close() { - l.RemoveAllWriters() + l.ReplaceAllWriters() l.ctxCancel() } @@ -233,7 +235,6 @@ func NewLoggerWithWriters(ctx context.Context, name string, writer ...EventWrite l.ctx, l.ctxCancel = newProcessTypedContext(ctx, "Logger: "+name) l.LevelLogger = BaseLoggerToGeneralLogger(l) l.eventWriters = map[string]EventWriter{} - l.syncLevelInternal() l.AddWriters(writer...) return l } diff --git a/modules/log/manager_test.go b/modules/log/manager_test.go index aa01f79980..b8fbf84613 100644 --- a/modules/log/manager_test.go +++ b/modules/log/manager_test.go @@ -23,7 +23,7 @@ func TestSharedWorker(t *testing.T) { loggerTest := m.GetLogger("test") loggerTest.AddWriters(w) loggerTest.Info("msg-1") - loggerTest.RemoveAllWriters() // the shared writer is not closed here + loggerTest.ReplaceAllWriters() // the shared writer is not closed here loggerTest.Info("never seen") // the shared writer can still be used later diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 5e5e4fecbb..a8d7ba7948 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 4bd2ca8d41..f2322b2554 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/notification/base/notifier.go b/modules/notification/base/notifier.go index e1762bb1ee..be4d774a8b 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -17,6 +17,7 @@ import ( // Notifier defines an interface to notify receiver type Notifier interface { Run() + NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) NotifyMigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 338790b356..56a25394f9 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -145,6 +145,10 @@ func (*NullNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *user_mod func (*NullNotifier) NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { } +// NotifyAdoptRepository places a place holder function +func (*NullNotifier) NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { +} + // NotifyDeleteRepository places a place holder function func (*NullNotifier) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) { } diff --git a/modules/notification/indexer/indexer.go b/modules/notification/indexer/indexer.go index 0661c2c1ab..bb652e3942 100644 --- a/modules/notification/indexer/indexer.go +++ b/modules/notification/indexer/indexer.go @@ -29,6 +29,10 @@ func NewNotifier() base.Notifier { return &indexerNotifier{} } +func (r *indexerNotifier) NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { + r.NotifyMigrateRepository(ctx, doer, u, repo) +} + func (r *indexerNotifier) NotifyCreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 6153c9e3d6..99e1a06ebd 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -274,6 +274,13 @@ func NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo } } +// NotifyAdoptRepository notifies the adoption of a repository to notifiers +func NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { + for _, notifier := range notifiers { + notifier.NotifyAdoptRepository(ctx, doer, u, repo) + } +} + // NotifyMigrateRepository notifies create repository to notifiers func NotifyMigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { for _, notifier := range notifiers { diff --git a/modules/repository/create.go b/modules/repository/create.go index c119096541..8289b93b31 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -332,7 +332,7 @@ func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { return fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err) } - return repo_model.UpdateRepoSize(ctx, repo.ID, size+lfsSize) + return repo_model.UpdateRepoSize(ctx, repo.ID, size, lfsSize) } // CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon... diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 31d5ebbb11..cb25daa10b 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 } + +var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`) + +// 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 { + return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_")) +} diff --git a/modules/repository/generate_test.go b/modules/repository/generate_test.go index 1cb9a50f67..b0f97d0ffb 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 526d69bbdc..94dd989850 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 filename == "" { - if !p.opts.AllowEmpty { - return fmt.Errorf("custom config path must not be empty") - } - return nil + if p.disableSaving { + return errDisableSaving } - if p.newFile { + filename := p.file + if filename == "" { + return fmt.Errorf("config file path must not be empty") + } + 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 17650edea4..7e7c6be2bb 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/database.go b/modules/setting/database.go index 7a7c7029a4..709655368c 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -12,8 +12,6 @@ import ( "path/filepath" "strings" "time" - - "code.gitea.io/gitea/modules/log" ) var ( @@ -36,7 +34,7 @@ var ( SSLMode string Path string LogSQL bool - Charset string + MysqlCharset string Timeout int // seconds SQLiteJournalMode string DBConnectRetries int @@ -60,11 +58,6 @@ func LoadDBSetting() { func loadDBSetting(rootCfg ConfigProvider) { sec := rootCfg.Section("database") Database.Type = DatabaseType(sec.Key("DB_TYPE").String()) - defaultCharset := "utf8" - - if Database.Type.IsMySQL() { - defaultCharset = "utf8mb4" - } Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() @@ -74,10 +67,7 @@ func loadDBSetting(rootCfg ConfigProvider) { } Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") - Database.Charset = sec.Key("CHARSET").In(defaultCharset, []string{"utf8", "utf8mb4"}) - if Database.Type.IsMySQL() && defaultCharset != "utf8mb4" { - log.Error("Deprecated database mysql charset utf8 support, please use utf8mb4 or convert utf8 to utf8mb4.") - } + Database.MysqlCharset = sec.Key("MYSQL_CHARSET").MustString("utf8mb4") // do not document it, end users won't need it. Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) @@ -101,9 +91,9 @@ func loadDBSetting(rootCfg ConfigProvider) { // DBConnStr returns database connection string func DBConnStr() (string, error) { var connStr string - Param := "?" - if strings.Contains(Database.Name, Param) { - Param = "&" + paramSep := "?" + if strings.Contains(Database.Name, paramSep) { + paramSep = "&" } switch Database.Type { case "mysql": @@ -116,15 +106,15 @@ func DBConnStr() (string, error) { tls = "false" } connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=%s&parseTime=true&tls=%s", - Database.User, Database.Passwd, connType, Database.Host, Database.Name, Param, Database.Charset, tls) + Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, Database.MysqlCharset, tls) case "postgres": - connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Param, Database.SSLMode) + connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, paramSep, Database.SSLMode) case "mssql": host, port := ParseMSSQLHostPort(Database.Host) connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, Database.Name, Database.User, Database.Passwd) case "sqlite3": if !EnableSQLite3 { - return "", errors.New("this binary version does not build support for SQLite3") + return "", errors.New("this Gitea binary was not built with SQLite3 support") } if err := os.MkdirAll(path.Dir(Database.Path), os.ModePerm); err != nil { return "", fmt.Errorf("Failed to create directories: %w", err) @@ -136,7 +126,7 @@ func DBConnStr() (string, error) { connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate%s", Database.Path, Database.Timeout, journalMode) default: - return "", fmt.Errorf("Unknown database type: %s", Database.Type) + return "", fmt.Errorf("unknown database type: %s", Database.Type) } return connStr, nil diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index d68349be86..784a99582d 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -53,19 +53,26 @@ func loadLFSFrom(rootCfg ConfigProvider) error { return nil } + LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("lfs"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") + LFS.JWTSecretBytes = make([]byte, 32) n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64)) 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/log.go b/modules/setting/log.go index af64ea8d85..66206f8f4b 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -244,7 +244,7 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger eventWriters = append(eventWriters, eventWriter) } - manager.GetLogger(loggerName).RemoveAllWriters().AddWriters(eventWriters...) + manager.GetLogger(loggerName).ReplaceAllWriters(eventWriters...) } func InitSQLLoggersForCli(level log.Level) { diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 836a2bb25f..78a9462de9 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -116,6 +116,12 @@ func loadOAuth2From(rootCfg ConfigProvider) { return } + if !OAuth2.Enable { + return + } + + OAuth2.JWTSecretBase64 = loadSecret(rootCfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET") + if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) } @@ -130,8 +136,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 0000000000..163f1d1590 --- /dev/null +++ b/modules/setting/path.go @@ -0,0 +1,195 @@ +// 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) { + InitWorkPathAndCfgProvider(getEnvFn, args) + LoadCommonSettings() +} + +// InitWorkPathAndCfgProvider will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf +func InitWorkPathAndCfgProvider(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 +} diff --git a/modules/setting/path_test.go b/modules/setting/path_test.go new file mode 100644 index 0000000000..fc6a2116dc --- /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 ce2e7711f1..5f1f9f4ade 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -76,7 +76,7 @@ func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string { // only file URIs are allowed default: - log.Fatal("Unsupported URI-Scheme %q (INTERNAL_TOKEN_URI = %q)", tempURI.Scheme, uri) + log.Fatal("Unsupported URI-Scheme %q (%q = %q)", tempURI.Scheme, uriKey, uri) return "" } } @@ -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 d937faca10..7c033bcc6b 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 539eb4b197..0d69847dbe 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 4bf57eafb7..923fa51d22 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/structs/repo.go b/modules/structs/repo.go index 73f5831a79..fcdf1dc80f 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -10,9 +10,9 @@ import ( // Permission represents a set of permissions type Permission struct { - Admin bool `json:"admin"` - Push bool `json:"push"` - Pull bool `json:"pull"` + Admin bool `json:"admin"` // Admin indicates if the user is an administrator of the repository. + Push bool `json:"push"` // Push indicates if the user can push code to the repository. + Pull bool `json:"pull"` // Pull indicates if the user can pull code from the repository. } // InternalTracker represents settings for internal tracker diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index a26c0531f8..d23103ce1b 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -81,16 +81,16 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa // RenderCommitBody extracts the body of a commit message without its title. func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { - msgLine := strings.TrimRightFunc(msg, unicode.IsSpace) + msgLine := strings.TrimSpace(msg) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { msgLine = msgLine[lineEnd+1:] } else { - return template.HTML("") + return "" } msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace) if len(msgLine) == 0 { - return template.HTML("") + return "" } renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go new file mode 100644 index 0000000000..29d3ed3a56 --- /dev/null +++ b/modules/templates/util_render_test.go @@ -0,0 +1,56 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "context" + "html/template" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderCommitBody(t *testing.T) { + type args struct { + ctx context.Context + msg string + urlPrefix string + metas map[string]string + } + tests := []struct { + name string + args args + want template.HTML + }{ + { + name: "multiple lines", + args: args{ + ctx: context.Background(), + msg: "first line\nsecond line", + }, + want: "second line", + }, + { + name: "multiple lines with leading newlines", + args: args{ + ctx: context.Background(), + msg: "\n\n\n\nfirst line\nsecond line", + }, + want: "second line", + }, + { + name: "multiple lines with trailing newlines", + args: args{ + ctx: context.Background(), + msg: "first line\nsecond line\n\n\n", + }, + want: "second line", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas), "RenderCommitBody(%v, %v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas) + }) + } +} diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index b4275e6005..6a0cee4a29 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/modules/util/path.go b/modules/util/path.go index 1a68bc7488..58258560dd 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -222,6 +222,8 @@ func isOSWindows() bool { return runtime.GOOS == "windows" } +var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/") + // FileURLToPath extracts the path information from a file://... url. func FileURLToPath(u *url.URL) (string, error) { if u.Scheme != "file" { @@ -235,8 +237,7 @@ func FileURLToPath(u *url.URL) (string, error) { } // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash. - re := regexp.MustCompile("/[A-Za-z]:/") - if re.MatchString(path) { + if driveLetterRegexp.MatchString(path) { return path[1:], nil } return path, nil diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index 017ed45f8c..ad0fb1a68b 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -15,7 +15,9 @@ import ( // 1563418 -> 2 weeks 4 days // 3937125s -> 1 month 2 weeks // 45677465s -> 1 year 6 months -func SecToTime(duration int64) string { +func SecToTime(durationVal any) string { + duration, _ := ToInt64(durationVal) + formattedTime := "" // The following four variables are calculated by taking diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5936451439..0979cbb491 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -129,6 +129,12 @@ concept_user_organization = Organization show_timestamps = Show timestamps show_log_seconds = Show seconds show_full_screen = Show full screen +download_logs = Download logs + +confirm_delete_selected = Confirm to delete all selected items? + +name = Name +value = Value [aria] navbar = Navigation Bar @@ -192,11 +198,9 @@ host = Host user = Username password = Password db_name = Database Name -db_helper = Note to MySQL users: please use the InnoDB storage engine and if you use "utf8mb4", your InnoDB version must be greater than 5.6 . db_schema = Schema db_schema_helper = Leave blank for database default ("public"). ssl_mode = SSL -charset = Charset path = Path sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if you run Gitea as a service. reinstall_error = You are trying to install into an existing Gitea database @@ -659,7 +663,7 @@ comment_type_group_project = Project comment_type_group_issue_ref = Issue reference saved_successfully = Your settings were saved successfully. privacy = Privacy -keep_activity_private = Hide the activity from the profile page +keep_activity_private = Hide Activity from profile page keep_activity_private_popup = Makes the activity visible only for you and the admins lookup_avatar_by_mail = Look Up Avatar by Email Address @@ -693,6 +697,7 @@ requires_activation = Requires activation primary_email = Make Primary activate_email = Send Activation activations_pending = Activations Pending +can_not_add_email_activations_pending = There is a pending activation, try again in a few minutes if you want to add a new email. delete_email = Remove email_deletion = Remove Email Address email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? @@ -1611,6 +1616,9 @@ issues.review.pending.tooltip = This comment is not currently visible to other u issues.review.review = Review issues.review.reviewers = Reviewers issues.review.outdated = Outdated +issues.review.outdated_description = Content has changed since this comment was made +issues.review.option.show_outdated_comments = Show outdated comments +issues.review.option.hide_outdated_comments = Hide outdated comments issues.review.show_outdated = Show outdated issues.review.hide_outdated = Hide outdated issues.review.show_resolved = Show resolved @@ -2793,6 +2801,7 @@ repos.stars = Stars repos.forks = Forks repos.issues = Issues repos.size = Size +repos.lfs_size = LFS Size packages.package_manage_panel = Package Management packages.total_size = Total Size: %s @@ -3399,8 +3408,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. @@ -3467,9 +3474,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/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 1818343f92..75971caa90 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -3122,7 +3122,7 @@ notices.delete_success=システム通知を削除しました。 [action] create_repo=がリポジトリ %s を作成しました -rename_repo=がリポジトリ名を %[1]s から [3]s へ変更しました +rename_repo=がリポジトリ名を %[1]s から %[3]s へ変更しました commit_repo=が %[4]s%[3]s にプッシュしました create_issue=`がイシュー %[3]s#%[2]s をオープンしました` close_issue=`がイシュー %[3]s#%[2]s をクローズしました` diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index ccfbcbf98a..528c394c63 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -79,6 +79,8 @@ milestones=Marcos ok=Ok cancel=Cancelar +rerun=Reexecutar +rerun_all=Reexecutar todas as tarefas save=Salvar add=Adicionar add_all=Adicionar todos @@ -113,11 +115,18 @@ unknown=Desconhecido rss_feed=Feed RSS +pin=Fixar +unpin=Desfixar +artifacts=Artefatos +concept_system_global=Global +concept_user_individual=Individual concept_code_repository=Repositório concept_user_organization=Organização +show_log_seconds=Mostrar segundos +show_full_screen=Mostrar tela cheia [aria] navbar=Barra de navegação @@ -142,6 +151,7 @@ buttons.list.unordered.tooltip=Adicionar uma lista com marcadores buttons.list.ordered.tooltip=Adicionar uma lista numerada buttons.list.task.tooltip=Adicionar uma lista de tarefas buttons.mention.tooltip=Mencionar um usuário ou equipe +buttons.ref.tooltip=Referenciar um issue ou um pull request buttons.switch_to_legacy.tooltip=Em vez disso, usar o editor legado buttons.enable_monospace_font=Habilitar fonte mono espaçada buttons.disable_monospace_font=Desabilitar fonte mono espaçada @@ -247,6 +257,7 @@ openid_signup_popup=Habilitar o auto-cadastro com base no OpenID. enable_captcha=Habilitar CAPTCHA ao registrar enable_captcha_popup=Obrigar validação por CAPTCHA para auto-cadastro de usuários. require_sign_in_view=Exigir acesso do usuário para a visualização de páginas +require_sign_in_view_popup=Limitar o acesso de página aos usuários autenticados. Os visitantes só verão as páginas de autenticação e cadastro. admin_setting_desc=Criar uma conta de administrador é opcional. O primeiro usuário cadastrado automaticamente se tornará um administrador. admin_title=Configurações da conta de administrador admin_name=Nome do usuário administrador @@ -312,6 +323,7 @@ repos=Repositórios users=Usuários organizations=Organizações search=Pesquisar +go_to=Ir para code=Código search.type.tooltip=Tipo de pesquisa search.fuzzy=Similar @@ -467,6 +479,7 @@ team_invite.text_3=Nota: este convite foi destinado a %[1]s. Se você não estav [modal] yes=Sim no=Não +confirm=Confirmar cancel=Cancelar modify=Atualizar @@ -514,6 +527,7 @@ lang_select_error=Selecione um idioma da lista. username_been_taken=O nome de usuário já está sendo usado. username_change_not_local_user=Usuários não-locais não são autorizados a alterar nome de usuário. +username_has_not_been_changed=Nome de usuário não foi alterado repo_name_been_taken=O nome de repositório já está sendo usado. repository_force_private=Forçar Privado está ativado: repositórios privados não podem ser tornados públicos. repository_files_already_exist=Arquivos já existem neste repositório. Contate o administrador. @@ -555,11 +569,14 @@ auth_failed=Autenticação falhou: %v still_own_repo=Sua conta possui um ou mais repositórios, exclua ou transfira-os primeiro. still_has_org=Sua conta é um membro de uma ou mais organizações, deixe-as primeiro. still_own_packages=Sua conta possui um ou mais pacotes, exclua-os primeiro. +org_still_own_repo=Esta organização ainda possui repositórios, exclua ou transfira-os primeiro. +org_still_own_packages=Esta organização ainda possui pacotes, exclua-os primeiro. target_branch_not_exist=O branch de destino não existe. [user] change_avatar=Altere seu avatar... +joined_on=Inscreveu-se em %s repositories=Repositórios activity=Atividade pública followers=Seguidores @@ -684,10 +701,12 @@ add_new_email=Adicionar novo endereço de e-mail add_new_openid=Adicionar novo URI OpenID add_email=Adicionar novo endereço de e-mail add_openid=Adicionar URI OpenID +add_email_confirmation_sent=Um e-mail de confirmação foi enviado para "%s". Verifique sua caixa de entrada nos próximos %s para confirmar seu endereço de e-mail. add_email_success=O novo endereço de e-mail foi adicionado. email_preference_set_success=Preferência de e-mail definida com sucesso. add_openid_success=O novo endereço de OpenID foi adicionado. keep_email_private=Ocultar endereço de e-mail +keep_email_private_popup=Seu endereço de e-mail ficará visível apenas para você e para os administradores openid_desc=OpenID permite delegar autenticação para um provedor externo. manage_ssh_keys=Gerenciar Chaves SSH @@ -721,6 +740,7 @@ gpg_token_help=Você pode gerar uma assinatura usando: gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Assinatura GPG blindada key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----' +verify_gpg_key_success=A chave GPG "%s" foi validada. ssh_key_verified=Chave validada ssh_key_verified_long=A chave foi validada com um token e pode ser usada para validar commits que correspondam a qualquer dos endereços de e-mail ativados deste usuário. ssh_key_verify=Validar @@ -730,11 +750,14 @@ ssh_token=Token ssh_token_help=Você pode gerar uma assinatura usando: ssh_token_signature=Assinatura SSH blindada key_signature_ssh_placeholder=Começa com '-----BEGIN SSH SIGNATURE-----' +verify_ssh_key_success=A chave SSH "%s" foi validada. subkeys=Subchaves key_id=ID da chave key_name=Nome da Chave key_content=Conteúdo principal_content=Conteúdo +add_key_success=A chave SSH "%s" foi adicionada. +add_gpg_key_success=A chave GPG "%s" foi adicionada. delete_key=Remover ssh_key_deletion=Remover a chave SSH gpg_key_deletion=Remover a chave GPG @@ -745,6 +768,8 @@ ssh_principal_deletion_desc=A exclusão de um Nome Principal de um Certificado S ssh_key_deletion_success=A chave SSH foi removida. gpg_key_deletion_success=A chave GPG foi removida. ssh_principal_deletion_success=O nome principal foi removido. +added_on=Adicionado em %s +valid_until_date=Válido até %s valid_forever=Válido para sempre last_used=Última vez usado em no_activity=Nenhuma atividade recente @@ -756,6 +781,7 @@ principal_state_desc=Este nome principal foi utilizado nos últimos 7 dias show_openid=Mostrar no perfil hide_openid=Ocultar no perfil ssh_disabled=SSH desabilitado +ssh_signonly=O SSH está desativado no momento, portanto, essas chaves são usadas apenas para verificação de assinatura de confirmação. ssh_externally_managed=Esta chave SSH para este usuário é gerenciada externamente manage_social=Gerenciar contas sociais associadas social_desc=Essas contas sociais estão vinculadas à sua conta do Gitea. Certifique-se de reconhecer todas elas, pois elas podem ser usadas para acessar a sua conta do Gitea. @@ -775,6 +801,11 @@ access_token_deletion_cancel_action=Cancelar access_token_deletion_confirm_action=Excluir access_token_deletion_desc=A exclusão de um token revoga o acesso à sua conta para aplicativos que o usam. Continuar? delete_token_success=O token foi excluído. Os aplicativos que o utilizam já não têm acesso à sua conta. +repo_and_org_access=Acesso ao Repositório e Organização +permissions_public_only=Apenas público +permissions_access_all=Todos (público, privado e limitado) +select_permissions=Selecionar permissões +permissions_list=Permissões: manage_oauth2_applications=Gerenciar aplicativos OAuth2 edit_oauth2_application=Editar aplicativo OAuth2 @@ -859,6 +890,7 @@ visibility=Visibilidade do usuário visibility.public=Pública visibility.public_tooltip=Visível para todos visibility.limited=Limitada +visibility.limited_tooltip=Visível apenas para usuários autenticados visibility.private=Privada visibility.private_tooltip=Visível apenas para membros da organização @@ -1012,6 +1044,7 @@ migrated_from_fake=Migrado de %[1]s migrate.migrate=Migrar de %s migrate.migrating=Migrando a partir de %s ... migrate.migrating_failed=Migração a partir de %s falhou. +migrate.migrating_failed.error=Falha ao migrar: %s migrate.migrating_failed_no_addr=A migração falhou. migrate.github.description=Migrar dados de github.com ou de outras instâncias do GitHub. migrate.git.description=Migrar um repositório somente de qualquer serviço Git. @@ -1028,6 +1061,8 @@ migrate.migrating_labels=Migrando Rótulos migrate.migrating_releases=Migrando Versões migrate.migrating_issues=Migrando Issues migrate.migrating_pulls=Migrando Pull Requests +migrate.cancel_migrating_title=Cancelar migração +migrate.cancel_migrating_confirm=Você quer cancelar essa migração? mirror_from=espelhamento de forked_from=feito fork de @@ -1112,6 +1147,7 @@ download_file=Baixar arquivo normal_view=Visão normal line=linha lines=linhas +from_comment=(comentário) editor.add_file=Adicionar Arquivo editor.new_file=Novo arquivo @@ -1133,6 +1169,9 @@ editor.cancel_lower=Cancelar editor.commit_signed_changes=Commit de alteradores assinadas editor.commit_changes=Aplicar commit das alterações editor.add_tmpl=Adicionar '' +editor.add=Adicionar %s +editor.update=Atualizar %s +editor.delete=Excluir %s editor.patch=Aplicar Correção editor.patching=Corrigindo: editor.new_patch=Nova correção @@ -1295,6 +1334,10 @@ issues.filter_label_exclude=`Use alt + clique/enter pa issues.filter_label_no_select=Todas as etiquetas issues.filter_label_select_no_label=Sem etiqueta issues.filter_milestone=Marco +issues.filter_milestone_all=Todos os marcos +issues.filter_milestone_none=Sem marcos +issues.filter_milestone_open=Marcos abertos +issues.filter_milestone_closed=Marcos fechados issues.filter_project=Projeto issues.filter_project_all=Todos os projetos issues.filter_project_none=Sem projeto @@ -1353,6 +1396,7 @@ issues.context.reference_issue=Referência em uma nova issue issues.context.edit=Editar issues.context.delete=Excluir issues.no_content=Ainda não há conteúdo. +issues.close=Fechar issue issues.close_comment_issue=Comentar e fechar issues.reopen_issue=Reabrir issues.reopen_comment_issue=Comentar e reabrir @@ -1403,6 +1447,7 @@ issues.attachment.open_tab=`Clique para ver "%s" em uma nova aba` issues.attachment.download=`Clique para baixar "%s"` issues.subscribe=Inscrever-se issues.unsubscribe=Desinscrever +issues.unpin_issue=Desfixar issue issues.lock=Bloquear conversação issues.unlock=Desbloquear conversação issues.lock.unknown_reason=Não pode-se bloquear uma issue com um motivo desconhecido. @@ -1630,6 +1675,7 @@ pulls.update_branch_rebase=Atualizar branch por rebase pulls.update_branch_success=Atualização do branch foi bem-sucedida pulls.update_not_allowed=Você não tem permissão para atualizar o branch pulls.outdated_with_base_branch=Este branch está desatualizado com o branch base +pulls.close=Fechar pull request pulls.closed_at=`fechou este pull request %[2]s` pulls.reopened_at=`reabriu este pull request %[2]s` pulls.merge_instruction_hint=`Você também pode ver as instruções para a linha de comandos.` @@ -1666,10 +1712,12 @@ milestones.desc=Descrição milestones.due_date=Data limite (opcional) milestones.clear=Limpar milestones.invalid_due_date_format=Formato da data limite deve ser 'dd/mm/aaaa'. +milestones.create_success=O marco "%s" foi criado. milestones.edit=Editar marco milestones.edit_subheader=Marcos organizam as issues e acompanham o progresso. milestones.cancel=Cancelar milestones.modify=Atualizar marco +milestones.edit_success=O marco "%s" foi atualizado. milestones.deletion=Excluir marco milestones.deletion_desc=A exclusão deste marco irá removê-lo de todas as issues. Tem certeza que deseja continuar? milestones.deletion_success=O marco foi excluído. @@ -2100,8 +2148,11 @@ settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo settings.require_signed_commits=Exibir commits assinados settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis. settings.protect_branch_name_pattern=Padrão de Nome de Branch Protegida +settings.protect_protected_file_patterns=Padrões de arquivos protegidos (separados usando ponto e vírgula ';'): settings.add_protected_branch=Habilitar proteção settings.delete_protected_branch=Desabilitar proteção +settings.remove_protected_branch_success=Proteção do branch "%s" foi desabilitada. +settings.remove_protected_branch_failed=Removendo regra de proteção de branch "%s" falhou. settings.protected_branch_deletion=Desabilitar proteção de branch settings.protected_branch_deletion_desc=Desabilitar a proteção de branch permite que os usuários com permissão de escrita realizem push. Continuar? settings.block_rejected_reviews=Bloquear merge em revisões rejeitadas @@ -2224,7 +2275,9 @@ diff.review.header=Enviar revisão diff.review.placeholder=Comentário da revisão diff.review.comment=Comentar diff.review.approve=Aprovar +diff.review.self_reject=Os autores do pull request não podem solicitar alterações em seus próprios pull request diff.review.reject=Solicitar alterações +diff.review.self_approve=Os autores do pull request não podem aprovar seu próprio pull request diff.committed_by=commit de diff.protected=Protegido diff.image.side_by_side=Lado a Lado diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 03e191ba46..3688a42d71 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -120,11 +120,14 @@ unpin=Открепить artifacts=Артефакты +concept_system_global=Глобально +concept_user_individual=Индивидуально concept_code_repository=Репозиторий concept_user_organization=Организация show_timestamps=Отображать время show_log_seconds=Показывать секунды +show_full_screen=Показать во весь экран [aria] navbar=Панель навигации @@ -133,6 +136,8 @@ footer.software=О программе footer.links=Ссылки [heatmap] +number_of_contributions_in_the_last_12_months=Принимал(а) участие %s раз за последние 12 месяцев +no_contributions=Не принимал(а) участия less=Меньше more=Больше @@ -572,6 +577,7 @@ target_branch_not_exist=Целевая ветка не существует. [user] change_avatar=Изменить свой аватар… +joined_on=Присоединил(ся/ась) %s repositories=Репозитории activity=Активность followers=Подписчики @@ -767,6 +773,8 @@ ssh_principal_deletion_desc=Удаление принципала сертифи ssh_key_deletion_success=Ключ SSH удален. gpg_key_deletion_success=Ключ GPG удалён. ssh_principal_deletion_success=Принципал удалён. +added_on=Добавлено %s +valid_until_date=Действительно до %s valid_forever=Действителен навсегда last_used=Последний раз использовался no_activity=Еще не применялся @@ -798,7 +806,13 @@ access_token_deletion_cancel_action=Отменить access_token_deletion_confirm_action=Удалить access_token_deletion_desc=Удаление токена отзовёт доступ к вашей учетной записи у приложений, использующих его. Это действие не может быть отменено. Продолжить? delete_token_success=Токен удалён. Приложения, использующие его, больше не имеют доступа к вашему аккаунту. +repo_and_org_access=Доступ к репозиторию и организации +permissions_public_only=Только публичные +permissions_access_all=Все (публичные, приватные и ограниченные) +select_permissions=Выбрать разрешения scoped_token_desc=Выбранные полномочия токена ограничивают аутентификацию только соответствующими маршрутами API. Читайте документацию для получения дополнительной информации. +at_least_one_permission=Необходимо выбрать хотя бы одно разрешение для создания токена +permissions_list=Разрешения: manage_oauth2_applications=Управление приложениями OAuth2 edit_oauth2_application=Изменить приложение OAuth2 @@ -957,6 +971,7 @@ mirror_password_blank_placeholder=(Отменено) mirror_password_help=Смените имя пользователя для удаления пароля. watchers=Наблюдатели stargazers=Звездочеты +stars_remove_warning=Данное действие удалит все звёзды из этого репозитория. forks=Форки reactions_more=и ещё %d unit_disabled=Администратор сайта отключил этот раздел репозитория. @@ -1000,6 +1015,7 @@ template.one_item=Необходимо выбрать хотя бы один э template.invalid=Необходимо выбрать хранилище шаблонов archive.title=Это репозиторий в архиве. Вы можете его клонировать или просматривать файлы, но не вносить изменения или открывать задачи/запросы на слияние. +archive.title_date=Этот репозиторий архивирован %s. Вы можете смотреть файлы или клонировать его, но не можете вносить изменения, открывать задачи или делать запросы на слияние. archive.issue.nocomment=Этот репозиторий в архиве. Вы не можете комментировать задачи. archive.pull.nocomment=Это репозиторий в архиве. Вы не можете комментировать запросы на слияние. @@ -1683,7 +1699,7 @@ pulls.no_merge_access=У вас нет права для слияния данн pulls.merge_pull_request=Создать коммит на слияние pulls.rebase_merge_pull_request=Выполнить Rebase, а затем fast-forward слияние pulls.rebase_merge_commit_pull_request=Выполнить rebase, а затем создать коммит слияния -pulls.squash_merge_pull_request=Создать объединенный (squash) коммит +pulls.squash_merge_pull_request=Создать объединённый коммит pulls.merge_manually=Слито вручную pulls.merge_commit_id=ID коммита слияния pulls.require_signed_wont_sign=Данная ветка ожидает подписанные коммиты, однако слияние не будет подписано @@ -1695,6 +1711,7 @@ pulls.rebase_conflict=Слияние не удалось: Произошел к pulls.rebase_conflict_summary=Сообщение об ошибке pulls.unrelated_histories=Слияние не удалось: У источника и цели слияния нет общей истории. Совет: попробуйте другую стратегию pulls.merge_out_of_date=Ошибка слияния: при создании слияния база данных была обновлена. Подсказка: попробуйте ещё раз. +pulls.head_out_of_date=Ошибка слияния: во время слияния головной коммит был обновлён. Попробуйте ещё раз. pulls.push_rejected=Слияние не удалось: отправка была отклонена. Проверьте Git-хуки для этого репозитория. pulls.push_rejected_summary=Полная ошибка отклонения pulls.push_rejected_no_message=Слияние не удалось: отправка была отклонена, но сервер не указал причину.
Проверьте Git-хуки для этого репозитория @@ -1899,6 +1916,10 @@ settings.githooks=Git-хуки settings.basic_settings=Основные параметры settings.mirror_settings=Настройки зеркалирования settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=В настоящее время это можно сделать только в меню «Новая миграция». Для получения дополнительной информации, пожалуйста, ознакомьтесь: +settings.mirror_settings.docs.no_new_mirrors=Ваш репозиторий зеркалирует изменения в другой репозиторий или из него. Пожалуйста, имейте в виду, что в данный момент невозможно создавать новые зеркала. +settings.mirror_settings.docs.can_still_use=Хотя вы не можете изменять существующие зеркала или создавать новые, вы можете по-прежнему использовать существующее зеркало. +settings.mirror_settings.docs.pull_mirror_instructions=Чтобы настроить pull-зеркало, пожалуйста, ознакомьтесь: +settings.mirror_settings.docs.doc_link_title=Как зеркалировать репозитории? settings.mirror_settings.docs.pulling_remote_title=Получение из удалённого репозитория settings.mirror_settings.mirrored_repository=Синхронизированное хранилище settings.mirror_settings.direction=Направление @@ -2012,6 +2033,7 @@ settings.delete_notices_2=- Эта операция навсегда удали settings.delete_notices_fork_1=- Все форки станут независимыми репозиториями после удаления. settings.deletion_success=Репозиторий удалён. settings.update_settings_success=Настройки репозитория обновлены. +settings.update_settings_no_unit=Должно быть разрешено хоть какое-то взаимодействие с репозиторием. settings.confirm_delete=Удалить репозиторий settings.add_collaborator=Добавить соавтора settings.add_collaborator_success=Соавтор добавлен. @@ -2111,6 +2133,7 @@ settings.event_pull_request_sync=Синхронизация запроса на settings.event_pull_request_sync_desc=Запрос на слияние синхронизирован. settings.event_pull_request_review_request=Запрошена рецензия для запроса на слияние settings.event_pull_request_review_request_desc=Создан или удалён запрос на рецензию для запроса на слияние. +settings.event_pull_request_approvals=Утверждения запросов на слияние settings.event_package=Пакеты settings.event_package_desc=Пакет создан или удален в репозитории. settings.branch_filter=Фильтр веток @@ -2185,8 +2208,11 @@ settings.protect_merge_whitelist_committers_desc=Разрешить приним settings.protect_merge_whitelist_users=Пользователи с правом на слияние: settings.protect_merge_whitelist_teams=Команды, члены которых обладают правом на слияние: settings.protect_check_status_contexts=Включить проверку статуса +settings.protect_status_check_patterns=Шаблоны проверки состояния: settings.protect_check_status_contexts_desc=Требуется пройти проверку состояния перед слиянием. Выберите, какие проверки состояния должны быть пройдены, прежде чем ветви можно будет объединить в ветвь, соответствующую этому правилу. Если этот параметр включен, коммиты сначала должны быть перемещены в другую ветвь, а затем объединены или перемещены непосредственно в ветвь, соответствующую этому правилу, после прохождения проверки состояния. Если контексты не выбраны, то последняя фиксация должна быть успешной независимо от контекста. settings.protect_check_status_contexts_list=Проверки состояния за последнюю неделю для этого репозитория +settings.protect_invalid_status_check_pattern=Неверный шаблон проверки состояния: «%s». +settings.protect_no_valid_status_check_patterns=Нет допустимых шаблонов проверки состояния. settings.protect_required_approvals=Необходимые одобрения: settings.protect_required_approvals_desc=Разрешить принятие запроса на слияние только с достаточным количеством положительных отзывов. settings.protect_approvals_whitelist_enabled=Ограничить утверждения белым списком пользователей или команд @@ -2198,6 +2224,7 @@ settings.dismiss_stale_approvals_desc=Когда новые коммиты, из settings.require_signed_commits=Требовать подписанные коммиты settings.require_signed_commits_desc=Отклонить отправку изменений в эту ветку, если они не подписаны или не проверяемы. settings.protect_branch_name_pattern=Шаблон имени для защищённых веток +settings.protect_patterns=Шаблоны settings.protect_protected_file_patterns=Шаблоны защищённых файлов (разделённые точкой с запятой ';'): settings.protect_protected_file_patterns_desc=Защищенные файлы нельзя изменить напрямую, даже если пользователь имеет право добавлять, редактировать или удалять файлы в этой ветке. Можно указать несколько шаблонов, разделяя их точкой с запятой (';'). О синтаксисе шаблонов читайте в документации github.com/gobwas/glob. Примеры: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Шаблоны незащищённых файлов (разделённые точкой с запятой ';'): @@ -2412,10 +2439,13 @@ branch.protected_deletion_failed=Ветка «%s» защищена. Её нел branch.default_deletion_failed=Ветка «%s» является веткой по умолчанию. Её нельзя удалить. branch.restore=Восстановить ветку «%s» branch.download=Скачать ветку «%s» +branch.rename=Переименовать ветку «%s» branch.included_desc=Эта ветка является частью ветки по умолчанию branch.included=Включено branch.create_new_branch=Создать ветку из ветви: branch.confirm_create_branch=Создать ветку +branch.warning_rename_default_branch=Вы переименовываете ветку по умолчанию. +branch.rename_branch_to=Переименовать ветку «%s» в: branch.confirm_rename_branch=Переименовать ветку branch.create_branch_operation=Создать ветку branch.new_branch=Создать новую ветку @@ -2746,6 +2776,7 @@ repos.size=Размер packages.package_manage_panel=Управление пакетами packages.total_size=Общий размер: %s +packages.unreferenced_size=Размер по ссылке: %s packages.owner=Владелец packages.creator=Автор packages.name=Наименование @@ -3012,8 +3043,10 @@ config.git_pull_timeout=Лимит времени получения измен config.git_gc_timeout=Лимит времени сборки мусора config.log_config=Конфигурация журнала +config.logger_name_fmt=Журнал: %s config.disabled_logger=Отключен config.access_log_mode=Режим доступа к журналу +config.access_log_template=Шаблон журнала доступа config.xorm_log_sql=Лог SQL config.get_setting_failed=Получить параметр %s не удалось @@ -3030,6 +3063,7 @@ monitor.execute_times=Количество выполнений monitor.process=Запущенные процессы monitor.stacktrace=Трассировки стека monitor.processes_count=%d процессов +monitor.download_diagnosis_report=Скачать диагностический отчёт monitor.desc=Описание monitor.start=Время начала monitor.execute_time=Время выполнения @@ -3050,9 +3084,10 @@ monitor.queue.numberinqueue=Позиция в очереди monitor.queue.review=Просмотр конфигурации monitor.queue.review_add=Просмотреть/добавить рабочих monitor.queue.settings.title=Настройки пула +monitor.queue.settings.desc=Пулы увеличиваются динамически в ответ на блокировку очередей своих рабочих. monitor.queue.settings.maxnumberworkers=Максимальное количество рабочих monitor.queue.settings.maxnumberworkers.placeholder=В настоящее время %[1]d -monitor.queue.settings.maxnumberworkers.error=Максимальное количество работников должно быть числом +monitor.queue.settings.maxnumberworkers.error=Максимальное количество рабочих должно быть числом monitor.queue.settings.submit=Обновить настройки monitor.queue.settings.changed=Настройки обновлены monitor.queue.settings.remove_all_items=Удалить все @@ -3166,7 +3201,7 @@ error.unit_not_allowed=У вас нет доступа к этому разде title=Пакеты desc=Управление пакетами репозитория. empty=Пока нет пакетов. -empty.documentation=Дополнительную информацию о реестре пакетов можно найти в документации. +empty.documentation=Дополнительную информацию о реестре пакетов можно найти в документации. empty.repo=Вы загрузили пакет, но он здесь не отображается? Перейдите в настройки пакета и свяжите его с этим репозиторием. filter.type=Тип filter.type.all=Все diff --git a/package-lock.json b/package-lock.json index e808ccf2a8..7b1ad9bae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,11 @@ "@github/relative-time-element": "4.3.0", "@github/text-expander-element": "2.5.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.3.0", + "@primer/octicons": "19.4.0", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", - "ansi-to-html": "0.7.2", - "asciinema-player": "3.4.0", + "ansi_up": "5.2.1", + "asciinema-player": "3.5.0", "clippie": "4.0.1", "css-loader": "6.8.1", "dropzone": "6.0.0-beta.2", @@ -28,26 +28,27 @@ "fast-glob": "3.2.12", "jquery": "3.7.0", "jquery.are-you-sure": "1.9.0", - "katex": "0.16.7", + "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "mermaid": "10.2.3", "mini-css-extract-plugin": "2.7.6", - "minimatch": "9.0.1", + "minimatch": "9.0.2", "monaco-editor": "0.39.0", "monaco-editor-webpack-plugin": "7.0.1", "pdfobject": "2.2.12", "pretty-ms": "8.0.0", "sortablejs": "1.15.0", - "swagger-ui-dist": "5.0.0", + "swagger-ui-dist": "5.1.0", "throttle-debounce": "5.0.0", "tippy.js": "6.3.7", + "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", "vue": "3.3.4", "vue-bar-graph": "2.0.0", "vue-loader": "17.2.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.87.0", + "webpack": "5.88.0", "webpack-cli": "5.1.4", "wrap-ansi": "8.1.0" }, @@ -67,17 +68,17 @@ "eslint-plugin-regexp": "1.15.0", "eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-unicorn": "47.0.0", - "eslint-plugin-vue": "9.14.1", + "eslint-plugin-vue": "9.15.1", "eslint-plugin-wc": "1.5.0", "jsdom": "22.1.0", "markdownlint-cli": "0.35.0", "postcss-html": "1.5.0", - "stylelint": "15.8.0", + "stylelint": "15.9.0", "stylelint-declaration-block-no-ignored-properties": "2.7.0", "stylelint-declaration-strict-value": "1.9.2", "stylelint-stylistic": "0.4.2", "svgo": "3.0.2", - "updates": "14.2.4", + "updates": "14.2.8", "vitest": "0.32.2" }, "engines": { @@ -402,9 +403,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.0.tgz", - "integrity": "sha512-MXkR+TeaS2q9IkpyO6jVCdtA/bfpABJxIrfkLswThFN8EZZgI2RfAHhm6sDNDuYV25d5+b8Lj1fpTccIcSLPsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.1.tgz", + "integrity": "sha512-pUjtFbaKbiFNjJo8pprrIaXLvQvWIlwPiFnRI4sEnc4F0NIGTOsw8kaJSR3CmZAKEvV8QYckovgAnWQC0bgLLQ==", "dev": true, "funding": [ { @@ -420,7 +421,7 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-parser-algorithms": "^2.2.0", "@csstools/css-tokenizer": "^2.1.1" } }, @@ -1235,9 +1236,9 @@ } }, "node_modules/@primer/octicons": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.3.0.tgz", - "integrity": "sha512-hyIo54VPC3VI7ZyAgosiJcbhxq1gZLbBspZwN9cg1uImRd2E8T9JST3kGeezezJYPjG367FuF7p1L+gmLmeESw==", + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.4.0.tgz", + "integrity": "sha512-92eXALm3ucZkzqpJmJbC+fR9ldiuNd4W4s2MZQNQIBahpg14emJ+I9fdHqCummFlfgyohLzXn++7rz0NlkqAJA==", "dependencies": { "object-assign": "^4.1.1" } @@ -1858,9 +1859,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + "version": "20.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", + "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -2465,6 +2466,14 @@ "ajv": "^8.8.2" } }, + "node_modules/ansi_up": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-5.2.1.tgz", + "integrity": "sha512-5bz5T/7FRmlxA37zDXhG6cAwlcZtfnmNLDJra66EEIT3kYlw5aPJdbkJEhm59D6kA4Wi5ict6u6IDYHJaQlH+g==", + "engines": { + "node": "*" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2487,20 +2496,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-to-html": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", - "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", - "dependencies": { - "entities": "^2.2.0" - }, - "bin": { - "ansi-to-html": "bin/ansi-to-html" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2610,9 +2605,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.4.0.tgz", - "integrity": "sha512-dX6jt5S3K6daItsVWzyY9mRDK+ivC2QgqCxFkdSiNslo0vY/ZqA4upcTzqIKZqBtxppovOZk44ltg9VnHG9QVg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.5.0.tgz", + "integrity": "sha512-o4B2AscBuCZo4+JB9TBGrfZ7GQL99wsbm08WwmuNJTPd1lyLQJq8wgacnBsdvb2sC0K875ScYr8T5XmfeH/6dg==", "dependencies": { "@babel/runtime": "^7.21.0", "solid-js": "^1.3.0" @@ -2882,9 +2877,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001504", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001504.tgz", - "integrity": "sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==", + "version": "1.0.30001508", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz", + "integrity": "sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw==", "funding": [ { "type": "opencollective", @@ -4167,9 +4162,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.433", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz", - "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==" + "version": "1.4.441", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.441.tgz", + "integrity": "sha512-LlCgQ8zgYZPymf5H4aE9itwiIWH4YlCiv1HFLmmcBeFYi5E+3eaIFnjHzYtcFQbaKfAW+CqZ9pgxo33DZuoqPg==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -4211,17 +4206,21 @@ } }, "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", "bin": { "envinfo": "dist/cli.js" }, @@ -4820,9 +4819,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.14.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz", - "integrity": "sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==", + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.15.1.tgz", + "integrity": "sha512-CJE/oZOslvmAR9hf8SClTdQ9JLweghT6JCBQNrT2Iel1uVw0W0OLJxzvPd6CxmABKCvLrtyDnqGV37O7KQv6+A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", @@ -5378,9 +5377,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.0.tgz", - "integrity": "sha512-lgbo68hHTQnFddybKbbs/RDRJnJT5YyGy2kQzVwbq+g67X73i+5MVTval34QxGkOe9X5Ujf1UYpCaphLyltjEg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz", + "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -6590,9 +6589,9 @@ "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" }, "node_modules/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-ftuDnJbcbOckGY11OO+zg3OofESlbR5DRl2cmN8HeWeeFIV7wTXvAOx8kEjZjobhA+9wh2fbKeO6cdcA9Mnovg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -6895,18 +6894,6 @@ "markdown-it": "bin/markdown-it.js" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/markdownlint": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.29.0.tgz", @@ -7669,9 +7656,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz", + "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7715,13 +7702,13 @@ } }, "node_modules/mlly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", - "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", + "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", "dev": true, "dependencies": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", + "acorn": "^8.9.0", + "pathe": "^1.1.1", "pkg-types": "^1.0.3", "ufo": "^1.1.2" } @@ -9076,14 +9063,14 @@ "dev": true }, "node_modules/run-con": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.11.tgz", - "integrity": "sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.12.tgz", + "integrity": "sha512-5257ILMYIF4RztL9uoZ7V9Q97zHtNHn5bN3NobeAnzB1P3ASLgg8qocM2u+R18ttp+VEM78N2LK8XcNVtnSRrg==", "dev": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~3.0.0", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "strip-json-comments": "~3.1.1" }, "bin": { @@ -9223,9 +9210,9 @@ } }, "node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9428,9 +9415,9 @@ "dev": true }, "node_modules/solid-js": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.6.tgz", - "integrity": "sha512-DXVOTjUh/bIAhE0fIqu3ezGLyQaez7v8EOw3uPLIi87DmLjg+hsuCAgKyNIZ+o4jUetOk3ZORccvJmE1yZUk8g==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.7.tgz", + "integrity": "sha512-SPdYVke/Z6Za24PBTbULyQYPrhGO1ZbPany76atO2zF2dmYn2pCotbsw1JtlgWnr9dK2JbwPGnA3ODTGPLhZNw==", "dependencies": { "csstype": "^3.1.0", "seroval": "^0.5.0" @@ -9726,9 +9713,9 @@ "dev": true }, "node_modules/stylelint": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.8.0.tgz", - "integrity": "sha512-x9qBk84F3MEjMEUNCE7MtWmfj9G9y5XzJ0cpQeJdy2l/IoqjC8Ih0N0ytmOTnXE4Yv0J7I1cmVRQUVNSPCxTsA==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.9.0.tgz", + "integrity": "sha512-sXtAZi64CllWr6A+8ymDWnlIaYwuAa7XRmGnJxLQXFNnLjd3Izm4HAD+loKVaZ7cpK6SLxhAUX1lwPJKGCn0mg==", "dev": true, "dependencies": { "@csstools/css-parser-algorithms": "^2.2.0", @@ -9840,9 +9827,9 @@ } }, "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, "node_modules/superstruct": { "version": "0.10.13", @@ -9924,9 +9911,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.0.0.tgz", - "integrity": "sha512-bwl6og9I9CAHKGSnYLKydjhBuH7d3oU6RX6uKN8oDCkLusTHXOW3sZMyBWjRtjGFnCMmN085oZoaR/4Wm9nIaQ==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.1.0.tgz", + "integrity": "sha512-c1KmAjuVODxw+vwkNLALQZrgdlBAuBbr2xSPfYrJgseEi7gFKcTvShysPmyuDI4kcUa1+5rFpjWvXdusKY74mg==" }, "node_modules/symbol-tree": { "version": "3.2.4", @@ -9971,9 +9958,9 @@ } }, "node_modules/terser": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.0.tgz", - "integrity": "sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz", + "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10136,6 +10123,11 @@ "node": ">=8.0" } }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -10228,9 +10220,9 @@ } }, "node_modules/tslib": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", - "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", "dev": true }, "node_modules/type-check": { @@ -10377,9 +10369,9 @@ } }, "node_modules/updates": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/updates/-/updates-14.2.4.tgz", - "integrity": "sha512-r54h4Q12lUAmQ9dENy7BnY22AnTfW4YGEZw73gv6RvNEWgcZ3qS88jPLc1ckPAzt/8TPKWwLkSVpbEpgGwglJw==", + "version": "14.2.8", + "resolved": "https://registry.npmjs.org/updates/-/updates-14.2.8.tgz", + "integrity": "sha512-Ca+M1vKKBBRiQSi3yrN8OdncmP9osIf1oJM/HpEIHeDvyGLs/noTi9X2LS4zl50VXRTSCqssF5CZN0XWzSPigg==", "dev": true, "bin": { "updates": "bin/updates.js" @@ -10542,9 +10534,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", - "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", + "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -10780,9 +10772,9 @@ } }, "node_modules/webpack": { - "version": "5.87.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.87.0.tgz", - "integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==", + "version": "5.88.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.0.tgz", + "integrity": "sha512-O3jDhG5e44qIBSi/P6KpcCcH7HD+nYIHVBhdWFxcLOcIGN8zGo5nqF3BjyNCxIh4p1vFdNnreZv2h2KkoAw3lw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", diff --git a/package.json b/package.json index 0701862cc2..da95145e4a 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "@github/relative-time-element": "4.3.0", "@github/text-expander-element": "2.5.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.3.0", + "@primer/octicons": "19.4.0", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", - "ansi-to-html": "0.7.2", - "asciinema-player": "3.4.0", + "ansi_up": "5.2.1", + "asciinema-player": "3.5.0", "clippie": "4.0.1", "css-loader": "6.8.1", "dropzone": "6.0.0-beta.2", @@ -27,26 +27,27 @@ "fast-glob": "3.2.12", "jquery": "3.7.0", "jquery.are-you-sure": "1.9.0", - "katex": "0.16.7", + "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "mermaid": "10.2.3", "mini-css-extract-plugin": "2.7.6", - "minimatch": "9.0.1", + "minimatch": "9.0.2", "monaco-editor": "0.39.0", "monaco-editor-webpack-plugin": "7.0.1", "pdfobject": "2.2.12", "pretty-ms": "8.0.0", "sortablejs": "1.15.0", - "swagger-ui-dist": "5.0.0", + "swagger-ui-dist": "5.1.0", "throttle-debounce": "5.0.0", "tippy.js": "6.3.7", + "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", "vue": "3.3.4", "vue-bar-graph": "2.0.0", "vue-loader": "17.2.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.87.0", + "webpack": "5.88.0", "webpack-cli": "5.1.4", "wrap-ansi": "8.1.0" }, @@ -66,17 +67,17 @@ "eslint-plugin-regexp": "1.15.0", "eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-unicorn": "47.0.0", - "eslint-plugin-vue": "9.14.1", + "eslint-plugin-vue": "9.15.1", "eslint-plugin-wc": "1.5.0", "jsdom": "22.1.0", "markdownlint-cli": "0.35.0", "postcss-html": "1.5.0", - "stylelint": "15.8.0", + "stylelint": "15.9.0", "stylelint-declaration-block-no-ignored-properties": "2.7.0", "stylelint-declaration-strict-value": "1.9.2", "stylelint-stylistic": "0.4.2", "svgo": "3.0.2", - "updates": "14.2.4", + "updates": "14.2.8", "vitest": "0.32.2" }, "browserslist": [ diff --git a/poetry.lock b/poetry.lock index 69258f749c..7d106e1551 100644 --- a/poetry.lock +++ b/poetry.lock @@ -42,13 +42,13 @@ six = ">=1.13.0" [[package]] name = "djlint" -version = "1.31.0" +version = "1.31.1" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.8.0,<4.0.0" files = [ - {file = "djlint-1.31.0-py3-none-any.whl", hash = "sha256:2b9200c67103b79835b7547ff732e910888d1f0ef684f5b329eb64b14d09c046"}, - {file = "djlint-1.31.0.tar.gz", hash = "sha256:8acb4b751b429c5aabb1aef5b6007bdf53224eceda25c5fbe04c42cc57c0a7ba"}, + {file = "djlint-1.31.1-py3-none-any.whl", hash = "sha256:9b2e2fc3a059a8e5a62f309edea15c1aeee331a279ab2699b9fb51a31d8c0934"}, + {file = "djlint-1.31.1.tar.gz", hash = "sha256:a11739e2f919f760b3986eb13d06e00171f3bd342b8d88e9bd914a4260eaa8ce"}, ] [package.dependencies] @@ -328,4 +328,4 @@ telegram = ["requests"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "22c4af11eadd8784b613951d6160d67be0f33500238a450741c3d75beb218dad" +content-hash = "f03ad8e7c4f6e797ac3c04630db8cc16438cd59642653c26fd401633cd62d696" diff --git a/public/img/svg/octicon-copilot-error.svg b/public/img/svg/octicon-copilot-error.svg index a0e2232f73..caaf0d5ec3 100644 --- a/public/img/svg/octicon-copilot-error.svg +++ b/public/img/svg/octicon-copilot-error.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/svg/octicon-copilot-warning.svg b/public/img/svg/octicon-copilot-warning.svg index a9d856384d..ce95645204 100644 --- a/public/img/svg/octicon-copilot-warning.svg +++ b/public/img/svg/octicon-copilot-warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/svg/octicon-file-directory-symlink.svg b/public/img/svg/octicon-file-directory-symlink.svg new file mode 100644 index 0000000000..ddc2e3fd67 --- /dev/null +++ b/public/img/svg/octicon-file-directory-symlink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ce5f475b27..7a30f59140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [] python = "^3.8" [tool.poetry.group.dev.dependencies] -djlint = "1.31.0" +djlint = "1.31.1" [tool.djlint] profile="golang" diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index e1d54134c6..ab70f622b3 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -9,6 +9,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" secret_model "code.gitea.io/gitea/models/secret" + actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -36,6 +37,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 { @@ -53,8 +55,10 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { secrets := map[string]string{} - if task.Job.Run.IsForkPullRequest { + if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget { // ignore secrets for fork pull request + // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch + // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target return secrets } @@ -88,10 +92,41 @@ 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) + // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 + // This fallback is for the old ActionRun that doesn't have the TriggerEvent field + // and should be removed in 1.22 + eventName := t.Job.Run.TriggerEvent + if eventName == "" { + eventName = t.Job.Run.Event.Event() + } + baseRef := "" headRef := "" if pullPayload, err := t.Job.Run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { @@ -113,13 +148,13 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. - "event_name": t.Job.Run.Event.Event(), // string, The name of the event that triggered the workflow run. + "event_name": eventName, // string, The name of the event that triggered the workflow run. "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. "graphql_url": "", // string, The URL of the GitHub GraphQL API. "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. "ref": t.Job.Run.Ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. - "ref_name": refName.String(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. + "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index be66cc5240..8b7f55976b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -777,11 +777,11 @@ func Routes() *web.Route { m.Group("/notifications", func() { m.Combo(""). Get(notify.ListNotifications). - Put(notify.ReadNotifications, reqToken()) + Put(reqToken(), notify.ReadNotifications) m.Get("/new", notify.NewAvailable) m.Combo("/threads/{id}"). Get(notify.GetThread). - Patch(notify.ReadThread, reqToken()) + Patch(reqToken(), notify.ReadThread) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) // Users (requires user scope) diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index bd3b86a6f1..e16c54a2c0 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.InternalServerError(err) + ctx.Error(http.StatusBadRequest, "Parse", err) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 2261610c09..a9c6b43617 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -132,7 +132,7 @@ func ReadNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.InternalServerError(err) + ctx.Error(http.StatusBadRequest, "Parse", err) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 8b7d7b1951..1aaefacdd5 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -561,12 +561,12 @@ func GetTeamRepos(ctx *context.APIContext) { } repos := make([]*api.Repository, len(teamRepos)) for i, repo := range teamRepos { - access, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) return } - repos[i] = convert.ToRepo(ctx, repo, access) + repos[i] = convert.ToRepo(ctx, repo, permission) } ctx.SetTotalCountHeader(int64(team.NumRepos)) ctx.JSON(http.StatusOK, repos) @@ -612,13 +612,13 @@ func GetTeamRepo(ctx *context.APIContext) { return } - access, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, access)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) } // getRepositoryByParams get repository by a team's organization ID and repo name diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 4cb3eddf58..f75153ab2d 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -60,12 +60,12 @@ func ListForks(ctx *context.APIContext) { } apiForks := make([]*api.Repository, len(forks)) for i, fork := range forks { - access, err := access_model.AccessLevel(ctx, ctx.Doer, fork) + permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return } - apiForks[i] = convert.ToRepo(ctx, fork, access) + apiForks[i] = convert.ToRepo(ctx, fork, permission) } ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks)) @@ -152,5 +152,5 @@ func CreateFork(ctx *context.APIContext) { } // TODO change back to 201 - ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, perm.AccessModeOwner)) + ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, access_model.Permission{AccessMode: perm.AccessModeOwner})) } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 39d83912b0..d0b77b5687 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -8,6 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -185,7 +186,7 @@ func TestHook(ctx *context.APIContext) { Commits: []*api.PayloadCommit{commit}, TotalCommits: 1, HeadCommit: commit, - Repo: convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeNone), + Repo: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}), Pusher: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), }); err != nil { diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index d496c4a73c..824880880a 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -13,6 +13,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" @@ -27,13 +28,13 @@ import ( func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) { apiKey.ReadOnly = key.Mode == perm.AccessModeRead if repository.ID == key.RepoID { - apiKey.Repository = convert.ToRepo(ctx, repository, key.Mode) + apiKey.Repository = convert.ToRepo(ctx, repository, access_model.Permission{AccessMode: key.Mode}) } else { repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) if err != nil { return apiKey, err } - apiKey.Repository = convert.ToRepo(ctx, repo, key.Mode) + apiKey.Repository = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: key.Mode}) } return apiKey, nil } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index b458cd122b..84327de5fb 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -211,7 +212,7 @@ func Migrate(ctx *context.APIContext) { } log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName) - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeAdmin)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeAdmin})) } func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, remoteAddr string, err error) { diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 06bfabe3d2..9d8497927e 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -258,7 +258,7 @@ func AddPushMirror(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/CreatePushMirrorOption" // responses: - // "201": + // "200": // "$ref": "#/responses/PushMirror" // "403": // "$ref": "#/responses/forbidden" diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 74631c9833..3abfb58981 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -211,14 +211,14 @@ func Search(ctx *context.APIContext) { }) return } - accessMode, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.JSON(http.StatusInternalServerError, api.SearchError{ OK: false, Error: err.Error(), }) } - results[i] = convert.ToRepo(ctx, repo, accessMode) + results[i] = convert.ToRepo(ctx, repo, permission) } ctx.SetLinkHeader(int(count), opts.PageSize) ctx.SetTotalCountHeader(count) @@ -273,7 +273,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) } - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeOwner)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) } // Create one repository of mine @@ -420,7 +420,7 @@ func Generate(ctx *context.APIContext) { } log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeOwner)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) } // CreateOrgRepoDeprecated create one repository of the organization @@ -538,7 +538,7 @@ func Get(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } // GetByID returns a single Repository @@ -569,15 +569,15 @@ func GetByID(ctx *context.APIContext) { return } - perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return - } else if !perm.HasAccess() { + } else if !permission.HasAccess() { ctx.NotFound() return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, perm.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) } // Edit edit repository properties @@ -639,7 +639,7 @@ func Edit(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) } // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index c1110ebce5..028e3083c6 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -264,7 +264,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) { return } - combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.AccessMode)) + combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, combiStatus) diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index ded8edd41c..8ff22a1193 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -122,12 +123,12 @@ func Transfer(ctx *context.APIContext) { if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeAdmin)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin})) return } log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) - ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeAdmin)) + ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin})) } // AcceptTransfer accept a repo transfer @@ -165,7 +166,7 @@ func AcceptTransfer(ctx *context.APIContext) { return } - ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } // RejectTransfer reject a repo transfer @@ -203,7 +204,7 @@ func RejectTransfer(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 7a8978cc4e..86af8cb440 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" @@ -38,13 +39,13 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { apiRepos := make([]*api.Repository, 0, len(repos)) for i := range repos { - access, err := access_model.AccessLevel(ctx, ctx.Doer, repos[i]) + permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return } - if ctx.IsSigned && ctx.Doer.IsAdmin || access >= perm.AccessModeRead { - apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], access)) + if ctx.IsSigned && ctx.Doer.IsAdmin || permission.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead { + apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission)) } } @@ -123,11 +124,11 @@ func ListMyRepos(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "LoadOwner", err) return } - accessMode, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) } - results[i] = convert.ToRepo(ctx, repo, accessMode) + results[i] = convert.ToRepo(ctx, repo, permission) } ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize) diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index ad5a8bee33..9399ad2b4d 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -28,11 +28,11 @@ func getStarredRepos(ctx std_context.Context, user *user_model.User, private boo repos := make([]*api.Repository, len(starredRepos)) for i, starred := range starredRepos { - access, err := access_model.AccessLevel(ctx, user, starred) + permission, err := access_model.GetUserRepoPermission(ctx, starred, user) if err != nil { return nil, err } - repos[i] = convert.ToRepo(ctx, starred, access) + repos[i] = convert.ToRepo(ctx, starred, permission) } return repos, nil } diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 211f36459a..172d9d5cc5 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -26,11 +26,11 @@ func getWatchedRepos(ctx std_context.Context, user *user_model.User, private boo repos := make([]*api.Repository, len(watchedRepos)) for i, watched := range watchedRepos { - access, err := access_model.AccessLevel(ctx, user, watched) + permission, err := access_model.GetUserRepoPermission(ctx, watched, user) if err != nil { return nil, 0, err } - repos[i] = convert.ToRepo(ctx, watched, access) + repos[i] = convert.ToRepo(ctx, watched, permission) } return repos, total, nil } diff --git a/routers/init.go b/routers/init.go index 54e8d2b8b3..ddbabcc397 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 16bb55b685..f121f31376 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" @@ -98,7 +99,6 @@ func Install(ctx *context.Context) { form.DbName = setting.Database.Name form.DbPath = setting.Database.Path form.DbSchema = setting.Database.Schema - form.Charset = setting.Database.Charset curDBType := setting.Database.Type.String() var isCurDBTypeSupported bool @@ -268,7 +268,6 @@ func SubmitInstall(ctx *context.Context) { setting.Database.Name = form.DbName setting.Database.Schema = form.DbSchema setting.Database.SSLMode = form.SSLMode - setting.Database.Charset = form.Charset setting.Database.Path = form.DbPath setting.Database.LogSQL = !setting.IsProd @@ -370,11 +369,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) @@ -382,13 +386,10 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd) cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema) cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode) - cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset) 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 +451,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 +513,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 c14843d8ee..0000000000 --- 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 be662c22ef..2c6989a71d 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/auth/auth.go b/routers/web/auth/auth.go index e0883a2696..bc8f6d58c9 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -383,7 +383,7 @@ func SignOut(ctx *context.Context) { }) } HandleSignOut(ctx) - ctx.Redirect(setting.AppSubURL + "/") + ctx.JSONRedirect(setting.AppSubURL + "/") } // SignUp render the register page diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 942b1f8378..94d83818fc 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -79,13 +79,13 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) if err != nil { - if code_indexer.IsAvailable() { + if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) return } ctx.Data["CodeIndexerUnavailable"] = true } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } loadRepoIDs := make([]int64, 0, len(searchResults)) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 365c9196a1..0a217767f3 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -73,6 +73,14 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { orderBy = db.SearchOrderBySizeReverse case "size": orderBy = db.SearchOrderBySize + case "reversegitsize": + orderBy = db.SearchOrderByGitSizeReverse + case "gitsize": + orderBy = db.SearchOrderByGitSize + case "reverselfssize": + orderBy = db.SearchOrderByLFSSizeReverse + case "lfssize": + orderBy = db.SearchOrderByLFSSize case "moststars": orderBy = db.SearchOrderByStarsReverse case "feweststars": diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index b3f6024b60..e525f2c43f 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -383,7 +383,7 @@ func ViewProject(ctx *context.Context) { ctx.HTML(http.StatusOK, tplProjectsView) } -func getActionIssues(ctx *context.Context) []*issues_model.Issue { +func getActionIssues(ctx *context.Context) issues_model.IssueList { commaSeparatedIssueIDs := ctx.FormString("issue_ids") if len(commaSeparatedIssueIDs) == 0 { return nil @@ -429,9 +429,14 @@ func UpdateIssueProject(ctx *context.Context) { return } + if err := issues.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + projectID := ctx.FormInt64("id") for _, issue := range issues { - oldProjectID := issue.ProjectID() + oldProjectID := issue.Project.ID if oldProjectID == projectID { continue } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 10acb46854..e1e07b5a72 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/actions/view.go b/routers/web/repo/actions/view.go index 7c2e9d63d6..537bc61807 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" actions_model "code.gitea.io/gitea/models/actions" @@ -310,6 +311,55 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro return nil } +func Logs(ctx *context_module.Context) { + runIndex := ctx.ParamsInt64("run") + jobIndex := ctx.ParamsInt64("job") + + job, _ := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + if job.TaskID == 0 { + ctx.Error(http.StatusNotFound, "job is not started") + return + } + + err := job.LoadRun(ctx) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + task, err := actions_model.GetTaskByID(ctx, job.TaskID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + if task.LogExpired { + ctx.Error(http.StatusNotFound, "logs have been cleaned up") + return + } + + reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + defer reader.Close() + + workflowName := job.Run.WorkflowID + if p := strings.Index(workflowName, "."); p > 0 { + workflowName = workflowName[0:p] + } + ctx.ServeContent(reader, &context_module.ServeHeaderOptions{ + Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), + ContentLength: &task.LogSize, + ContentType: "text/plain", + ContentTypeCharset: "utf-8", + Disposition: "attachment", + }) +} + func Cancel(ctx *context_module.Context) { runIndex := ctx.ParamsInt64("run") diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 7433a0a56b..2fea8a9532 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 6f9ca4874b..fb5ada1bdb 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 e9ab44fe69..226e2e81f4 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 9f087edc72..a0dd14e314 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -191,7 +191,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti if len(keyword) > 0 { issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{repo.ID}, keyword) if err != nil { - if issue_indexer.IsAvailable() { + if issue_indexer.IsAvailable(ctx) { ctx.ServerError("issueIndexer.Search", err) return } @@ -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) } @@ -1647,9 +1647,22 @@ func ViewIssue(ctx *context.Context) { return } } else if comment.Type == issues_model.CommentTypeAddTimeManual || - comment.Type == issues_model.CommentTypeStopTracking { + comment.Type == issues_model.CommentTypeStopTracking || + comment.Type == issues_model.CommentTypeDeleteTimeManual { // drop error since times could be pruned from DB.. _ = comment.LoadTime() + if comment.Content != "" { + // Content before v1.21 did store the formated string instead of seconds, + // so "|" is used as delimeter to mark the new format + if comment.Content[0] != '|' { + // handle old time comments that have formatted text stored + comment.RenderedContent = comment.Content + comment.Content = "" + } else { + // else it's just a duration in seconds to pass on to the frontend + comment.Content = comment.Content[1:] + } + } } if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull { @@ -1971,7 +1984,7 @@ func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) { } } -func getActionIssues(ctx *context.Context) []*issues_model.Issue { +func getActionIssues(ctx *context.Context) issues_model.IssueList { commaSeparatedIssueIDs := ctx.FormString("issue_ids") if len(commaSeparatedIssueIDs) == 0 { return nil @@ -2705,6 +2718,20 @@ func ListIssues(ctx *context.Context) { ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues)) } +func BatchDeleteIssues(ctx *context.Context) { + issues := getActionIssues(ctx) + if ctx.Written() { + return + } + for _, issue := range issues { + if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { + ctx.ServerError("DeleteIssue", err) + return + } + } + ctx.JSONOK() +} + // UpdateIssueStatus change issue's status func UpdateIssueStatus(ctx *context.Context) { issues := getActionIssues(ctx) @@ -2722,7 +2749,7 @@ func UpdateIssueStatus(ctx *context.Context) { log.Warn("Unrecognized action: %s", action) } - if _, err := issues_model.IssueList(issues).LoadRepositories(ctx); err != nil { + if _, err := issues.LoadRepositories(ctx); err != nil { ctx.ServerError("LoadRepositories", err) return } @@ -2740,9 +2767,7 @@ func UpdateIssueStatus(ctx *context.Context) { } } } - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) + ctx.JSONOK() } // NewComment create a comment for issue @@ -3475,7 +3500,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/issue_lock.go b/routers/web/repo/issue_lock.go index 08b76e555f..93f5a588d9 100644 --- a/routers/web/repo/issue_lock.go +++ b/routers/web/repo/issue_lock.go @@ -20,14 +20,12 @@ func LockIssue(ctx *context.Context) { } if issue.IsLocked { - ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate")) - ctx.Redirect(issue.Link()) + ctx.JSONError(ctx.Tr("repo.issues.lock_duplicate")) return } if !form.HasValidReason() { - ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason")) - ctx.Redirect(issue.Link()) + ctx.JSONError(ctx.Tr("repo.issues.lock.unknown_reason")) return } @@ -40,7 +38,7 @@ func LockIssue(ctx *context.Context) { return } - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } // UnlockIssue unlocks a previously locked issue. @@ -51,8 +49,7 @@ func UnlockIssue(ctx *context.Context) { } if !issue.IsLocked { - ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error")) - ctx.Redirect(issue.Link()) + ctx.JSONError(ctx.Tr("repo.issues.unlock_error")) return } @@ -64,5 +61,5 @@ func UnlockIssue(ctx *context.Context) { return } - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go index 6586372fc5..7c1a306e6c 100644 --- a/routers/web/repo/issue_pin.go +++ b/routers/web/repo/issue_pin.go @@ -31,7 +31,7 @@ func IssuePinOrUnpin(ctx *context.Context) { return } - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } // IssueUnpin unpins a Issue diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index 5c38b31154..216550ca99 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -5,6 +5,7 @@ package repo import ( "fmt" + "strconv" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" @@ -88,3 +89,27 @@ func SetWhitespaceBehavior(ctx *context.Context) { ctx.Data["WhitespaceBehavior"] = whitespaceBehavior } } + +// SetShowOutdatedComments set the show outdated comments option as context variable +func SetShowOutdatedComments(ctx *context.Context) { + showOutdatedCommentsValue := ctx.FormString("show-outdated") + // var showOutdatedCommentsValue string + + if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" { + // invalid or no value for this form string -> use default or stored user setting + if ctx.IsSigned { + showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, "false") + } else { + // not logged in user -> use the default value + showOutdatedCommentsValue = "false" + } + } else { + // valid value -> update user setting if user is logged in + if ctx.IsSigned { + _ = user_model.SetUserSetting(ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue) + } + } + + showOutdatedComments, _ := strconv.ParseBool(showOutdatedCommentsValue) + ctx.Data["ShowOutdatedComments"] = showOutdatedComments +} diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 5ee5ead121..6da9edfd0b 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -378,9 +378,14 @@ func UpdateIssueProject(ctx *context.Context) { return } + if err := issues.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + projectID := ctx.FormInt64("id") for _, issue := range issues { - oldProjectID := issue.ProjectID() + oldProjectID := issue.Project.ID if oldProjectID == projectID { continue } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 09dbc23eac..f2a58a35a7 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -37,7 +37,6 @@ import ( "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/automerge" @@ -762,7 +761,7 @@ func ViewPullFiles(ctx *context.Context) { "numberOfViewedFiles": diff.NumViewedFiles, } - if err = diff.LoadComments(ctx, issue, ctx.Doer); err != nil { + if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil { ctx.ServerError("LoadComments", err) return } @@ -810,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() { @@ -1206,36 +1205,12 @@ func CompareAndPullRequestPost(ctx *context.Context) { } if ctx.HasError() { - middleware.AssignForm(form, ctx.Data) - - // This stage is already stop creating new pull request, so it does not matter if it has - // something to compare or not. - PrepareCompareDiff(ctx, ci, - gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) - if ctx.Written() { - return - } - - if len(form.Title) > 255 { - var trailer string - form.Title, trailer = util.SplitStringAtByteN(form.Title, 255) - - form.Content = trailer + "\n\n" + form.Content - } - middleware.AssignForm(form, ctx.Data) - - ctx.HTML(http.StatusOK, tplCompareDiff) + ctx.JSONError(ctx.GetErrMsg()) return } if util.IsEmptyString(form.Title) { - PrepareCompareDiff(ctx, ci, - gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) - if ctx.Written() { - return - } - - ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplCompareDiff, form) + ctx.JSONError(ctx.Tr("repo.issues.new.title_empty")) return } @@ -1278,20 +1253,20 @@ func CompareAndPullRequestPost(ctx *context.Context) { pushrejErr := err.(*git.ErrPushRejected) message := pushrejErr.Message if len(message) == 0 { - ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message")) - } else { - flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{ - "Message": ctx.Tr("repo.pulls.push_rejected"), - "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), - }) - if err != nil { - ctx.ServerError("CompareAndPullRequest.HTMLString", err) - return - } - ctx.Flash.Error(flashError) + ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message")) + return } - ctx.Redirect(pullIssue.Link()) + flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{ + "Message": ctx.Tr("repo.pulls.push_rejected"), + "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), + "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + }) + if err != nil { + ctx.ServerError("CompareAndPullRequest.HTMLString", err) + return + } + ctx.Flash.Error(flashError) + ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost return } ctx.ServerError("NewPullRequest", err) @@ -1299,7 +1274,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { } log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID) - ctx.Redirect(pullIssue.Link()) + ctx.JSONRedirect(pullIssue.Link()) } // CleanUpPullRequest responses for delete merged branch when PR has been merged diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 69d36ff4a4..5aa5811367 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -159,7 +159,7 @@ func UpdateResolveConversation(ctx *context.Context) { } func renderConversation(ctx *context.Context, comment *issues_model.Comment) { - comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line) + comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool)) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) return diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index afba1f18bf..5fddddb344 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/search.go b/routers/web/repo/search.go index a043198472..3c0fa4bc00 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -45,13 +45,13 @@ func Search(ctx *context.Context) { total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID}, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) if err != nil { - if code_indexer.IsAvailable() { + if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) return } ctx.Data["CodeIndexerUnavailable"] = true } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } ctx.Data["SourcePath"] = ctx.Repo.Repository.Link() diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index 444f16f86c..3d7a057602 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 0000000000..1005d1d9c6 --- /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/repo/webhook.go b/routers/web/repo/webhook.go index 5139c0b091..b358c7260d 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/base" @@ -685,7 +686,7 @@ func TestWebhook(ctx *context.Context) { Commits: []*api.PayloadCommit{apiCommit}, TotalCommits: 1, HeadCommit: apiCommit, - Repo: convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeNone), + Repo: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}), Pusher: apiUser, Sender: apiUser, } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 115418887d..22cfe6dd25 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -273,6 +273,16 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return nil, nil } + if rctx.SidebarTocNode != nil { + sb := &strings.Builder{} + err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode) + if err != nil { + log.Error("Failed to render wiki sidebar TOC: %v", err) + } else { + ctx.Data["sidebarTocContent"] = sb.String() + } + } + if !isSideBar { buf.Reset() ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent) @@ -303,16 +313,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Data["footerPresent"] = false } - if rctx.SidebarTocNode != nil { - sb := &strings.Builder{} - err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode) - if err != nil { - log.Error("Failed to render wiki sidebar TOC: %v", err) - } else { - ctx.Data["sidebarTocContent"] = sb.String() - } - } - // get commit count - wiki revisions commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go new file mode 100644 index 0000000000..8d1516c91c --- /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 a0d648f908..c09ce51499 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") - - s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Title, content) - 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)) + if err := actions.NameRegexMatch(form.Name); err != nil { + ctx.JSONError(ctx.Tr("secrets.creation.failed")) + return } - ctx.Redirect(redirectURL) + s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data)) + if err != nil { + log.Error("InsertEncryptedSecret: %v", err) + ctx.JSONError(ctx.Tr("secrets.creation.failed")) + return + } + + 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/user/code.go b/routers/web/user/code.go index b3adbcb8d3..15524de7d6 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -71,13 +71,13 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) if err != nil { - if code_indexer.IsAvailable() { + if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) return } ctx.Data["CodeIndexerUnavailable"] = true } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } loadRepoIDs := make([]int64, 0, len(searchResults)) diff --git a/routers/web/web.go b/routers/web/web.go index 7261e2c873..9190c40166 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) @@ -761,6 +771,7 @@ func registerRoutes(m *web.Route) { m.Get("", org_setting.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() + addSettingVariablesRoutes() }, actions.MustEnableActions) m.RouteMethods("/delete", "GET,POST", org.SettingsDelete) @@ -942,6 +953,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)) @@ -1025,7 +1037,8 @@ func registerRoutes(m *web.Route) { m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest) m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) - m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation) + m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) + m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation) m.Post("/attachments", repo.UploadIssueAttachment) m.Post("/attachments/remove", repo.DeleteAttachment) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) @@ -1195,6 +1208,7 @@ func registerRoutes(m *web.Route) { Get(actions.View). Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne) + m.Get("/logs", actions.Logs) }) m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/approve", reqRepoActionsWriter, actions.Approve) @@ -1273,10 +1287,10 @@ func registerRoutes(m *web.Route) { m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) m.Group("/files", func() { - m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles) + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFiles) m.Group("/reviews", func() { m.Get("/new_comment", repo.RenderNewCodeCommentForm) - m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.CreateCodeComment) + m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) }, context.RepoMustNotBeArchived()) }) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 0616a5fc0d..d2893e4f23 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -56,12 +56,20 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { return nil }); err != nil { log.Warn("Cannot stop task %v: %v", task.ID, err) - // go on - } else if remove, err := actions.TransferLogs(ctx, task.LogFilename); err != nil { - log.Warn("Cannot transfer logs of task %v: %v", task.ID, err) - } else { - remove() + continue } + + remove, err := actions.TransferLogs(ctx, task.LogFilename) + if err != nil { + log.Warn("Cannot transfer logs of task %v: %v", task.ID, err) + continue + } + task.LogInStorage = true + if err := actions_model.UpdateTask(ctx, task, "log_in_storage"); err != nil { + log.Warn("Cannot update task %v: %v", task.ID, err) + continue + } + remove() } CreateCommitStatus(ctx, jobs...) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index da870bb84c..507eeaacf6 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -45,13 +45,13 @@ func (n *actionsNotifier) NotifyNewIssue(ctx context.Context, issue *issues_mode log.Error("issue.LoadPoster: %v", err) return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{ Action: api.HookIssueOpened, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, issue.Poster, nil), }).Notify(withMethod(ctx, "NotifyNewIssue")) } @@ -59,7 +59,7 @@ func (n *actionsNotifier) NotifyNewIssue(ctx context.Context, issue *issues_mode // NotifyIssueChangeStatus notifies close or reopen issue to notifiers func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) { ctx = withMethod(ctx, "NotifyIssueChangeStatus") - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest: %v", err) @@ -69,7 +69,7 @@ func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(db.DefaultContext, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, } @@ -88,7 +88,7 @@ func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiIssue := &api.IssuePayload{ Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if isClosed { @@ -118,7 +118,7 @@ func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) @@ -134,7 +134,7 @@ func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, perm_model.AccessModeNone), + Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), }). WithPullRequest(issue.PullRequest). @@ -147,7 +147,7 @@ func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). Notify(ctx) @@ -159,7 +159,7 @@ func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us ) { ctx = withMethod(ctx, "NotifyCreateIssueComment") - mode, _ := access_model.AccessLevel(ctx, doer, repo) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { @@ -172,7 +172,7 @@ func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us Action: api.HookIssueCommentCreated, Issue: convert.ToAPIIssue(ctx, issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, repo, mode), + Repository: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: true, }). @@ -186,7 +186,7 @@ func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us Action: api.HookIssueCommentCreated, Issue: convert.ToAPIIssue(ctx, issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, repo, mode), + Repository: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: false, }). @@ -209,14 +209,14 @@ func (n *actionsNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues return } - mode, _ := access_model.AccessLevel(ctx, pull.Issue.Poster, pull.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, pull.Issue.Repo, pull.Issue.Poster) newNotifyInputFromIssue(pull.Issue, webhook_module.HookEventPullRequest). WithPayload(&api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), - Repository: convert.ToRepo(ctx, pull.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission), Sender: convert.ToUser(ctx, pull.Issue.Poster, nil), }). WithPullRequest(pull). @@ -228,7 +228,7 @@ func (n *actionsNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u newNotifyInput(repo, doer, webhook_module.HookEventRepository).WithPayload(&api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) @@ -237,13 +237,13 @@ func (n *actionsNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u func (n *actionsNotifier) NotifyForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { ctx = withMethod(ctx, "NotifyForkRepository") - oldMode, _ := access_model.AccessLevel(ctx, doer, oldRepo) - mode, _ := access_model.AccessLevel(ctx, doer, repo) + oldPermission, _ := access_model.GetUserRepoPermission(ctx, oldRepo, doer) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) // forked webhook newNotifyInput(oldRepo, doer, webhook_module.HookEventFork).WithPayload(&api.ForkPayload{ - Forkee: convert.ToRepo(ctx, oldRepo, oldMode), - Repo: convert.ToRepo(ctx, repo, mode), + Forkee: convert.ToRepo(ctx, oldRepo, oldPermission), + Repo: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) @@ -255,7 +255,7 @@ func (n *actionsNotifier) NotifyForkRepository(ctx context.Context, doer *user_m WithRef(oldRepo.DefaultBranch). WithPayload(&api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) @@ -285,9 +285,9 @@ func (n *actionsNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue return } - mode, err := access_model.AccessLevel(ctx, review.Issue.Poster, review.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, review.Issue.Repo, review.Issue.Poster) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } @@ -297,7 +297,7 @@ func (n *actionsNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue Action: api.HookIssueReviewed, Index: review.Issue.Index, PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), - Repository: convert.ToRepo(ctx, review.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, review.Issue.Repo, permission), Sender: convert.ToUser(ctx, review.Reviewer, nil), Review: &api.ReviewPayload{ Type: string(reviewHookType), @@ -325,9 +325,9 @@ func (*actionsNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m return } - mode, err := access_model.AccessLevel(ctx, doer, pr.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, doer) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } @@ -335,7 +335,7 @@ func (*actionsNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m apiPullRequest := &api.PullRequestPayload{ Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), Action: api.HookIssueClosed, } @@ -366,7 +366,7 @@ func (n *actionsNotifier) NotifyPushCommits(ctx context.Context, pusher *user_mo CompareURL: setting.AppURL + commits.CompareURL, Commits: apiCommits, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }). @@ -377,7 +377,7 @@ func (n *actionsNotifier) NotifyCreateRef(ctx context.Context, pusher *user_mode ctx = withMethod(ctx, "NotifyCreateRef") apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm_model.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}) newNotifyInput(repo, pusher, webhook_module.HookEventCreate). WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name @@ -395,7 +395,7 @@ func (n *actionsNotifier) NotifyDeleteRef(ctx context.Context, pusher *user_mode ctx = withMethod(ctx, "NotifyDeleteRef") apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm_model.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}) newNotifyInput(repo, pusher, webhook_module.HookEventDelete). WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name @@ -429,7 +429,7 @@ func (n *actionsNotifier) NotifySyncPushCommits(ctx context.Context, pusher *use Commits: apiCommits, TotalCommits: commits.Len, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }). @@ -494,7 +494,7 @@ func (n *actionsNotifier) NotifyPullRequestSynchronized(ctx context.Context, doe Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, perm_model.AccessModeNone), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), }). WithPullRequest(pr). @@ -514,7 +514,7 @@ func (n *actionsNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex return } - mode, _ := access_model.AccessLevel(ctx, pr.Issue.Poster, pr.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, pr.Issue.Poster) newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequest). WithPayload(&api.PullRequestPayload{ Action: api.HookIssueEdited, @@ -525,7 +525,7 @@ func (n *actionsNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex }, }, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). WithPullRequest(pr). @@ -537,7 +537,7 @@ func (n *actionsNotifier) NotifyNewWikiPage(ctx context.Context, doer *user_mode newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ Action: api.HookWikiCreated, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -549,7 +549,7 @@ func (n *actionsNotifier) NotifyEditWikiPage(ctx context.Context, doer *user_mod newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ Action: api.HookWikiEdited, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -561,7 +561,7 @@ func (n *actionsNotifier) NotifyDeleteWikiPage(ctx context.Context, doer *user_m newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ Action: api.HookWikiDeleted, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, }).Notify(ctx) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 5e41241d18..8e6cdcf680 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -142,13 +142,46 @@ func notify(ctx context.Context, input *notifyInput) error { return fmt.Errorf("gitRepo.GetCommit: %w", err) } + var detectedWorkflows []*actions_module.DetectedWorkflow workflows, err := actions_module.DetectWorkflows(commit, input.Event, input.Payload) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } - if len(workflows) == 0 { log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) + } else { + for _, wf := range workflows { + if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget { + wf.Ref = ref + detectedWorkflows = append(detectedWorkflows, wf) + } + } + } + + if input.PullRequest != nil { + // detect pull_request_target workflows + baseRef := git.BranchPrefix + input.PullRequest.BaseBranch + baseCommit, err := gitRepo.GetCommit(baseRef) + if err != nil { + return fmt.Errorf("gitRepo.GetCommit: %w", err) + } + baseWorkflows, err := actions_module.DetectWorkflows(baseCommit, input.Event, input.Payload) + if err != nil { + return fmt.Errorf("DetectWorkflows: %w", err) + } + if len(baseWorkflows) == 0 { + log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID) + } else { + for _, wf := range baseWorkflows { + if wf.TriggerEvent == actions_module.GithubEventPullRequestTarget { + wf.Ref = baseRef + detectedWorkflows = append(detectedWorkflows, wf) + } + } + } + } + + if len(detectedWorkflows) == 0 { return nil } @@ -172,18 +205,19 @@ func notify(ctx context.Context, input *notifyInput) error { } } - for id, content := range workflows { + for _, dwf := range detectedWorkflows { run := &actions_model.ActionRun{ Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], RepoID: input.Repo.ID, OwnerID: input.Repo.OwnerID, - WorkflowID: id, + WorkflowID: dwf.EntryName, TriggerUserID: input.Doer.ID, - Ref: ref, - CommitSHA: commit.ID.String(), + Ref: dwf.Ref, + CommitSHA: dwf.Commit.ID.String(), IsForkPullRequest: isForkPullRequest, Event: input.Event, EventPayload: string(p), + TriggerEvent: dwf.TriggerEvent, Status: actions_model.StatusWaiting, } if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil { @@ -193,7 +227,7 @@ func notify(ctx context.Context, input *notifyInput) error { run.NeedApproval = need } - jobs, err := jobparser.Parse(content) + jobs, err := jobparser.Parse(dwf.Content) if err != nil { log.Error("jobparser.Parse: %v", err) continue @@ -222,14 +256,14 @@ func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.R return } - mode, _ := access_model.AccessLevel(ctx, doer, rel.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer) newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease). WithRef(git.RefNameFromTag(rel.TagName).String()). WithPayload(&api.ReleasePayload{ Action: action, Release: convert.ToRelease(ctx, rel), - Repository: convert.ToRepo(ctx, rel.Repo, mode), + Repository: convert.ToRepo(ctx, rel.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). Notify(ctx) @@ -259,8 +293,10 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo } func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, user *user_model.User) (bool, error) { - // don't need approval if it's not a fork PR - if !run.IsForkPullRequest { + // 1. don't need approval if it's not a fork PR + // 2. don't need approval if the event is `pull_request_target` since the workflow will run in the context of base branch + // see https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks + if !run.IsForkPullRequest || run.TriggerEvent == actions_module.GithubEventPullRequestTarget { return false, nil } diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 89e99b5e60..3f3219adb9 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -76,7 +76,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str } if len(sr.Mail) == 0 { - sr.Mail = fmt.Sprintf("%s@localhost", sr.Username) + sr.Mail = fmt.Sprintf("%s@localhost.local", sr.Username) } user = &user_model.User{ diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 4571ff6540..43ee32c84b 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 + } + + 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)] + } + + 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 len(su.Mail) == 0 { - su.Mail = fmt.Sprintf("%s@localhost", su.Username) - } - - var usr *user_model.User - for userPos < len(users) && users[userPos].LowerName < su.LowerName { - userPos++ - } - if userPos < len(users) && users[userPos].LowerName == su.LowerName { - usr = users[userPos] - existingUsers = append(existingUsers, userPos) + su.Mail = fmt.Sprintf("%s@localhost.local", 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/convert/activity.go b/services/convert/activity.go index 2aaa86607b..71a2722a49 100644 --- a/services/convert/activity.go +++ b/services/convert/activity.go @@ -28,7 +28,7 @@ func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_mod ActUserID: ac.ActUserID, ActUser: ToUser(ctx, ac.ActUser, doer), RepoID: ac.RepoID, - Repo: ToRepo(ctx, ac.Repo, p.AccessMode), + Repo: ToRepo(ctx, ac.Repo, p), RefName: ac.RefName, IsPrivate: ac.IsPrivate, Content: ac.Content, diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index 2810c6c9bc..db48faa69e 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -11,6 +11,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) // ToComment converts a issues_model.Comment to the api.Comment format @@ -66,6 +67,17 @@ func ToTimelineComment(ctx context.Context, c *issues_model.Comment, doer *user_ return nil } + if c.Content != "" { + if (c.Type == issues_model.CommentTypeAddTimeManual || + c.Type == issues_model.CommentTypeStopTracking || + c.Type == issues_model.CommentTypeDeleteTimeManual) && + c.Content[0] == '|' { + // TimeTracking Comments from v1.21 on store the seconds instead of an formated string + // so we check for the "|" delimeter and convert new to legacy format on demand + c.Content = util.SecToTime(c.Content[1:]) + } + } + comment := &api.TimelineComment{ ID: c.ID, Type: c.Type.String(), diff --git a/services/convert/notification.go b/services/convert/notification.go index 5d3b078a25..3906fa9b38 100644 --- a/services/convert/notification.go +++ b/services/convert/notification.go @@ -9,6 +9,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" api "code.gitea.io/gitea/modules/structs" ) @@ -24,7 +25,7 @@ func ToNotificationThread(n *activities_model.Notification) *api.NotificationThr // since user only get notifications when he has access to use minimal access mode if n.Repository != nil { - result.Repository = ToRepo(db.DefaultContext, n.Repository, perm.AccessModeRead) + result.Repository = ToRepo(db.DefaultContext, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead}) // This permission is not correct and we should not be reporting it for repository := result.Repository; repository != nil; repository = repository.Parent { diff --git a/services/convert/package.go b/services/convert/package.go index 7d170ccc25..276856594b 100644 --- a/services/convert/package.go +++ b/services/convert/package.go @@ -22,7 +22,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m } if permission.HasAccess() { - repo = ToRepo(ctx, pd.Repository, permission.AccessMode) + repo = ToRepo(ctx, pd.Repository, permission) } } diff --git a/services/convert/pull.go b/services/convert/pull.go index 1ac0f4e96f..e4e3097056 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -80,7 +80,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u Name: pr.BaseBranch, Ref: pr.BaseBranch, RepoID: pr.BaseRepoID, - Repository: ToRepo(ctx, pr.BaseRepo, p.AccessMode), + Repository: ToRepo(ctx, pr.BaseRepo, p), }, Head: &api.PRBranchInfo{ Name: pr.HeadBranch, @@ -152,7 +152,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u } apiPullRequest.Head.RepoID = pr.HeadRepo.ID - apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p.AccessMode) + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) if err != nil { diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go index 0915d096e6..e069fa4a68 100644 --- a/services/convert/pull_test.go +++ b/services/convert/pull_test.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" @@ -31,7 +32,7 @@ func TestPullRequest_APIFormat(t *testing.T) { Ref: "refs/pull/2/head", Sha: "4a357436d925b5c974181ff12a994538ddc5a269", RepoID: 1, - Repository: ToRepo(db.DefaultContext, headRepo, perm.AccessModeRead), + Repository: ToRepo(db.DefaultContext, headRepo, access_model.Permission{AccessMode: perm.AccessModeRead}), }, apiPullRequest.Head) // withOut HeadRepo diff --git a/services/convert/repository.go b/services/convert/repository.go index 54a61efe43..6f77b4932e 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" @@ -16,18 +17,26 @@ import ( ) // ToRepo converts a Repository to api.Repository -func ToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode) *api.Repository { - return innerToRepo(ctx, repo, mode, false) +func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository { + return innerToRepo(ctx, repo, permissionInRepo, false) } -func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode, isParent bool) *api.Repository { +func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository { var parent *api.Repository + if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil { + // If Units and UnitsMode are both nil, it means that it's a hard coded permission, + // like access_model.Permission{AccessMode: perm.AccessModeAdmin}. + // So we need to load units for the repo, or UnitAccessMode will always return perm.AccessModeNone. + _ = repo.LoadUnits(ctx) // the error is not important, so ignore it + permissionInRepo.Units = repo.Units + } + cloneLink := repo.CloneLink() permission := &api.Permission{ - Admin: mode >= perm.AccessModeAdmin, - Push: mode >= perm.AccessModeWrite, - Pull: mode >= perm.AccessModeRead, + Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin, + Push: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite, + Pull: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead, } if !isParent { err := repo.GetBaseRepo(ctx) @@ -35,7 +44,12 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc return nil } if repo.BaseRepo != nil { - parent = innerToRepo(ctx, repo.BaseRepo, mode, true) + // FIXME: The permission of the parent repo is not correct. + // It's the permission of the current repo, so it's probably different from the parent repo. + // But there isn't a good way to get the permission of the parent repo, because the doer is not passed in. + // Use the permission of the current repo to keep the behavior consistent with the old API. + // Maybe the right way is setting the permission of the parent repo to nil, empty is better than wrong. + parent = innerToRepo(ctx, repo.BaseRepo, permissionInRepo, true) } } @@ -154,7 +168,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc return &api.Repository{ ID: repo.ID, - Owner: ToUserWithAccessMode(ctx, repo.Owner, mode), + Owner: ToUserWithAccessMode(ctx, repo.Owner, permissionInRepo.AccessMode), Name: repo.Name, FullName: repo.FullName(), Description: repo.Description, diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index 3e0dbd132e..acf2d3373c 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 1315fb237b..1f5abf94ee 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -27,7 +27,6 @@ type InstallForm struct { DbPasswd string DbName string SSLMode string - Charset string `binding:"Required;In(utf8,utf8mb4)"` DbPath string DbSchema string @@ -367,8 +366,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 +376,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/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index b6a75f6098..9adf3b9400 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -450,8 +450,8 @@ type Diff struct { } // LoadComments loads comments into each line -func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User) error { - allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser) +func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error { + allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments) if err != nil { return err } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 389f787dfc..e270e46fd4 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -594,16 +594,26 @@ func setupDefaultDiff() *Diff { } } -func TestDiff_LoadComments(t *testing.T) { +func TestDiff_LoadCommentsNoOutdated(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff := setupDefaultDiff() - assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user)) + assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, false)) assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2) } +func TestDiff_LoadCommentsWithOutdated(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + diff := setupDefaultDiff() + assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, true)) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 3) +} + func TestDiffLine_CanComment(t *testing.T) { assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment()) assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment()) diff --git a/services/lfs/server.go b/services/lfs/server.go index a18e752d47..b32f218785 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -77,6 +77,8 @@ func CheckAcceptMediaType(ctx *context.Context) { } } +var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) + // DownloadHandler gets the content from the content store func DownloadHandler(ctx *context.Context) { rc := getRequestContext(ctx) @@ -92,8 +94,7 @@ func DownloadHandler(ctx *context.Context) { toByte = meta.Size - 1 statusCode := http.StatusOK if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" { - regex := regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) - match := regex.FindStringSubmatch(rangeHdr) + match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr) if len(match) > 1 { statusCode = http.StatusPartialContent fromByte, _ = strconv.ParseInt(match[1], 10, 32) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 55e77a78a7..e07ff35041 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -70,9 +70,17 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_mo if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil { return err } - if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { + + // Re-fetch the repository from database before updating it (else it would + // override changes that were done earlier with sql) + if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %w", err) + } + + if err := adoptRepository(ctx, repoPath, doer, repo, opts.DefaultBranch); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { return fmt.Errorf("checkDaemonExportOK: %w", err) } @@ -95,12 +103,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_mo return nil, err } - notification.NotifyCreateRepository(ctx, doer, u, repo) + notification.NotifyAdoptRepository(ctx, doer, u, repo) return repo, nil } -func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts repo_module.CreateRepoOptions) (err error) { +func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, defaultBranch string) (err error) { isExist, err := util.IsExist(repoPath) if err != nil { log.Error("Unable to check if %s exists. Error: %v", repoPath, err) @@ -114,12 +122,6 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r return fmt.Errorf("createDelegateHooks: %w", err) } - // Re-fetch the repository from database before updating it (else it would - // override changes that were done earlier with sql) - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { - return fmt.Errorf("getRepositoryByID: %w", err) - } - repo.IsEmpty = false // Don't bother looking this repo in the context it won't be there @@ -129,8 +131,8 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r } defer gitRepo.Close() - if len(opts.DefaultBranch) > 0 { - repo.DefaultBranch = opts.DefaultBranch + if len(defaultBranch) > 0 { + repo.DefaultBranch = defaultBranch if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index bccd477852..3332d5d4aa 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -50,7 +49,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { @@ -62,7 +61,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user Action: api.HookIssueLabelCleared, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -70,7 +69,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user Action: api.HookIssueLabelCleared, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -80,13 +79,13 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user } func (m *webhookNotifier) NotifyForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { - oldMode, _ := access_model.AccessLevel(ctx, doer, oldRepo) - mode, _ := access_model.AccessLevel(ctx, doer, repo) + oldPermission, _ := access_model.GetUserRepoPermission(ctx, oldRepo, doer) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) // forked webhook if err := PrepareWebhooks(ctx, EventSource{Repository: oldRepo}, webhook_module.HookEventFork, &api.ForkPayload{ - Forkee: convert.ToRepo(ctx, oldRepo, oldMode), - Repo: convert.ToRepo(ctx, repo, mode), + Forkee: convert.ToRepo(ctx, oldRepo, oldPermission), + Repo: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err) @@ -98,7 +97,7 @@ func (m *webhookNotifier) NotifyForkRepository(ctx context.Context, doer *user_m if u.IsOrganization() { if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -111,7 +110,7 @@ func (m *webhookNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u // Add to hook queue for created repo after session commit. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -122,7 +121,7 @@ func (m *webhookNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u func (m *webhookNotifier) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) { if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoDeleted, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, repo.MustOwner(ctx), nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -134,7 +133,7 @@ func (m *webhookNotifier) NotifyMigrateRepository(ctx context.Context, doer, u * // Add to hook queue for created repo after session commit. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -144,7 +143,7 @@ func (m *webhookNotifier) NotifyMigrateRepository(ctx context.Context, doer, u * func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { if issue.IsPull { - mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest failed: %v", err) @@ -153,7 +152,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if removed { @@ -167,11 +166,11 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u return } } else { - mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypeIssues) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) apiIssue := &api.IssuePayload{ Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if removed { @@ -188,7 +187,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u } func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) { - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { @@ -204,7 +203,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user }, }, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -217,7 +216,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user }, }, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -228,7 +227,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user } func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { @@ -239,7 +238,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, } @@ -253,7 +252,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiIssue := &api.IssuePayload{ Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, } @@ -279,12 +278,12 @@ func (m *webhookNotifier) NotifyNewIssue(ctx context.Context, issue *issues_mode return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueOpened, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, issue.Poster, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) @@ -305,12 +304,12 @@ func (m *webhookNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues return } - mode, _ := access_model.AccessLevel(ctx, pull.Issue.Poster, pull.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, pull.Issue.Repo, pull.Issue.Poster) if err := PrepareWebhooks(ctx, EventSource{Repository: pull.Issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), - Repository: convert.ToRepo(ctx, pull.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission), Sender: convert.ToUser(ctx, pull.Issue.Poster, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) @@ -323,7 +322,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { @@ -339,7 +338,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us }, }, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -352,7 +351,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us }, }, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -383,7 +382,7 @@ func (m *webhookNotifier) NotifyUpdateComment(ctx context.Context, doer *user_mo eventType = webhook_module.HookEventIssueComment } - mode, _ := access_model.AccessLevel(ctx, doer, c.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, c.Issue.Repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{ Action: api.HookIssueCommentEdited, Issue: convert.ToAPIIssue(ctx, c.Issue), @@ -393,7 +392,7 @@ func (m *webhookNotifier) NotifyUpdateComment(ctx context.Context, doer *user_mo From: oldContent, }, }, - Repository: convert.ToRepo(ctx, c.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, c.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: c.Issue.IsPull, }); err != nil { @@ -411,12 +410,12 @@ func (m *webhookNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us eventType = webhook_module.HookEventIssueComment } - mode, _ := access_model.AccessLevel(ctx, doer, repo) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{ Action: api.HookIssueCommentCreated, Issue: convert.ToAPIIssue(ctx, issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, repo, mode), + Repository: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: issue.IsPull, }); err != nil { @@ -448,12 +447,12 @@ func (m *webhookNotifier) NotifyDeleteComment(ctx context.Context, doer *user_mo eventType = webhook_module.HookEventIssueComment } - mode, _ := access_model.AccessLevel(ctx, doer, comment.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{ Action: api.HookIssueCommentDeleted, Issue: convert.ToAPIIssue(ctx, comment.Issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, comment.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: comment.Issue.IsPull, }); err != nil { @@ -465,7 +464,7 @@ func (m *webhookNotifier) NotifyNewWikiPage(ctx context.Context, doer *user_mode // Add to hook queue for created wiki page. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -478,7 +477,7 @@ func (m *webhookNotifier) NotifyEditWikiPage(ctx context.Context, doer *user_mod // Add to hook queue for edit wiki page. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiEdited, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -491,7 +490,7 @@ func (m *webhookNotifier) NotifyDeleteWikiPage(ctx context.Context, doer *user_m // Add to hook queue for edit wiki page. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiDeleted, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, }); err != nil { @@ -514,7 +513,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) @@ -528,7 +527,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, perm.AccessModeNone), + Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -536,7 +535,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -559,7 +558,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer * return } - mode, _ := access_model.AccessLevel(ctx, doer, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if issue.IsPull { err = issue.PullRequest.LoadIssue(ctx) if err != nil { @@ -570,7 +569,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer * Action: hookAction, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -578,7 +577,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer * Action: hookAction, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -603,7 +602,7 @@ func (m *webhookNotifier) NotifyPushCommits(ctx context.Context, pusher *user_mo Commits: apiCommits, TotalCommits: commits.Len, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }); err != nil { @@ -633,9 +632,9 @@ func (*webhookNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m return } - mode, err := access_model.AccessLevel(ctx, doer, pr.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, doer) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } @@ -643,7 +642,7 @@ func (*webhookNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m apiPullRequest := &api.PullRequestPayload{ Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), Action: api.HookIssueClosed, } @@ -661,7 +660,7 @@ func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex issue := pr.Issue - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + mode, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, @@ -699,16 +698,16 @@ func (m *webhookNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue return } - mode, err := access_model.AccessLevel(ctx, review.Issue.Poster, review.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, review.Issue.Repo, review.Issue.Poster) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } if err := PrepareWebhooks(ctx, EventSource{Repository: review.Issue.Repo}, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueReviewed, Index: review.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, review.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, review.Issue.Repo, permission), Sender: convert.ToUser(ctx, review.Reviewer, nil), Review: &api.ReviewPayload{ Type: string(reviewHookType), @@ -724,7 +723,7 @@ func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, do log.Warn("NotifyPullRequestReviewRequest: issue is not a pull request: %v", issue.ID) return } - mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest failed: %v", err) return @@ -733,7 +732,7 @@ func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, do Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), RequestedReviewer: convert.ToUser(ctx, reviewer, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if isRequest { @@ -749,7 +748,7 @@ func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, do func (m *webhookNotifier) NotifyCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}) refName := refFullName.ShortName() if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventCreate, &api.CreatePayload{ @@ -777,7 +776,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(ctx context.Context, doe Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, perm.AccessModeNone), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) @@ -786,7 +785,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(ctx context.Context, doe func (m *webhookNotifier) NotifyDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}) refName := refFullName.ShortName() if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventDelete, &api.DeletePayload{ @@ -806,11 +805,11 @@ func sendReleaseHook(ctx context.Context, doer *user_model.User, rel *repo_model return } - mode, _ := access_model.AccessLevel(ctx, doer, rel.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: rel.Repo}, webhook_module.HookEventRelease, &api.ReleasePayload{ Action: action, Release: convert.ToRelease(ctx, rel), - Repository: convert.ToRepo(ctx, rel.Repo, mode), + Repository: convert.ToRepo(ctx, rel.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) @@ -845,7 +844,7 @@ func (m *webhookNotifier) NotifySyncPushCommits(ctx context.Context, pusher *use Commits: apiCommits, TotalCommits: commits.Len, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }); err != nil { diff --git a/templates/admin/base/search.tmpl b/templates/admin/base/search.tmpl index ae1a4d2ac5..19977f05a9 100644 --- a/templates/admin/base/search.tmpl +++ b/templates/admin/base/search.tmpl @@ -1,6 +1,12 @@ -