diff --git a/Dockerfile b/Dockerfile index 78a556497a..b60d94cc47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,20 +26,16 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ && make clean-all build -# Begin env-to-ini build -RUN go build contrib/environment-to-ini/environment-to-ini.go - # Copy local files COPY docker/root /tmp/local # Set permissions RUN chmod 755 /tmp/local/usr/bin/entrypoint \ - /tmp/local/usr/local/bin/gitea \ + /tmp/local/usr/local/bin/* \ /tmp/local/etc/s6/gitea/* \ /tmp/local/etc/s6/openssh/* \ /tmp/local/etc/s6/.s6-svscan/* \ - /go/src/code.gitea.io/gitea/gitea \ - /go/src/code.gitea.io/gitea/environment-to-ini + /go/src/code.gitea.io/gitea/gitea FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" @@ -82,4 +78,3 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"] COPY --from=build-env /tmp/local / COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea -COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini diff --git a/Dockerfile.rootless b/Dockerfile.rootless index e83c1af33b..f7a0412be2 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -26,18 +26,12 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ && make clean-all build -# Begin env-to-ini build -RUN go build contrib/environment-to-ini/environment-to-ini.go - # Copy local files COPY docker/rootless /tmp/local # Set permissions -RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ - /tmp/local/usr/local/bin/docker-setup.sh \ - /tmp/local/usr/local/bin/gitea \ - /go/src/code.gitea.io/gitea/gitea \ - /go/src/code.gitea.io/gitea/environment-to-ini +RUN chmod 755 /tmp/local/usr/local/bin/* \ + /go/src/code.gitea.io/gitea/gitea FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" @@ -71,7 +65,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea COPY --from=build-env /tmp/local / COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea -COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini # git:git USER 1000:1000 diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000000..5303b0e1e0 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,156 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli/v3" +) + +func cmdConfig() *cli.Command { + subcmdConfigEditIni := &cli.Command{ + Name: "edit-ini", + Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.", + Description: ` +Help users to edit the Gitea configuration INI file. + +# Keep Specified Keys + +If you need to re-create the configuration file with only a subset of keys, +you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag. +For example, if a helm chart needs to reset the settings and only keep SECRET_KEY, +it can use a template file (only keys take effect, values are ignored): + + [security] + SECRET_KEY= + +$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini + +# Map Environment Variables to INI Configuration + +Environment variables of the form "GITEA__section_name__KEY_NAME" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value as provided. + +Environment variables of the form "GITEA__section_name__KEY_NAME__FILE" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value loaded from the specified file. + +Environment variable keys can only contain characters "0-9A-Z_", +if a section or key name contains dot ".", it needs to be escaped as _0x2E_. +For example, to apply this config: + + [git.config] + foo.bar=val + +$ export GITEA__git_0x2E_config__foo_0x2E_bar=val + +# Put All Together + +$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini} +`, + Flags: []cli.Flag{ + // "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper + // "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file + &cli.BoolFlag{ + Name: "in-place", + Usage: "Output to the same config file as input. This flag will be ignored if --out is set.", + }, + &cli.StringFlag{ + Name: "config-keep-keys", + Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.", + }, + &cli.BoolFlag{ + Name: "apply-env", + Usage: "Apply all GITEA__* variables from the environment to the config.", + }, + &cli.StringFlag{ + Name: "out", + Usage: "Destination config file to write to.", + }, + }, + Action: runConfigEditIni, + } + + return &cli.Command{ + Name: "config", + Usage: "Manage Gitea configuration", + Commands: []*cli.Command{ + subcmdConfigEditIni, + }, + } +} + +func runConfigEditIni(_ context.Context, c *cli.Command) error { + // the config system may change the environment variables, so get a copy first, to be used later + env := append([]string{}, os.Environ()...) + + // don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly + if !c.IsSet("config") { + return errors.New("flag is required but not set: --config") + } + configFileIn := c.String("config") + + cfgIn, err := setting.NewConfigProviderFromFile(configFileIn) + if err != nil { + return fmt.Errorf("failed to load config file %q: %v", configFileIn, err) + } + + // determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file + inPlace := c.Bool("in-place") + configFileOut := c.String("out") + if configFileOut == "" { + if !inPlace { + return errors.New("either --in-place or --out must be specified") + } + configFileOut = configFileIn // in-place edit + } + + needWriteOut := configFileOut != configFileIn + + cfgOut := cfgIn + configKeepKeys := c.String("config-keep-keys") + if configKeepKeys != "" { + needWriteOut = true + cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys) + if err != nil { + return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err) + } + + for _, secOut := range cfgOut.Sections() { + for _, keyOut := range secOut.Keys() { + secIn := cfgIn.Section(secOut.Name()) + keyIn := setting.ConfigSectionKey(secIn, keyOut.Name()) + if keyIn != nil { + keyOut.SetValue(keyIn.String()) + } else { + secOut.DeleteKey(keyOut.Name()) + } + } + if len(secOut.Keys()) == 0 { + cfgOut.DeleteSection(secOut.Name()) + } + } + } + + if c.Bool("apply-env") { + if setting.EnvironmentToConfig(cfgOut, env) { + needWriteOut = true + } + } + + if needWriteOut { + err = cfgOut.SaveTo(configFileOut) + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 0000000000..d123daa617 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,85 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigEdit(t *testing.T) { + tmpDir := t.TempDir() + configOld := tmpDir + "/app-old.ini" + configTemplate := tmpDir + "/app-template.ini" + _ = os.WriteFile(configOld, []byte(` +[sec] +k1=v1 +k2=v2 +`), os.ModePerm) + + _ = os.WriteFile(configTemplate, []byte(` +[sec] +k1=in-template + +[sec2] +k3=v3 +`), os.ModePerm) + + t.Setenv("GITEA__EnV__KeY", "val") + + t.Run("OutputToNewWithEnv", func(t *testing.T) { + configNew := tmpDir + "/app-new.ini" + err := NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "--config", configOld, + "config", "edit-ini", + "--apply-env", + "--config-keep-keys", configTemplate, + "--out", configNew, + }) + require.NoError(t, err) + + // "k1" old value is kept because its key is in the template + // "k2" is removed because it isn't in the template + // "k3" isn't in new config because it isn't in the old config + // [env] is applied from environment variable + data, _ := os.ReadFile(configNew) + require.Equal(t, `[sec] +k1 = v1 + +[env] +KeY = val +`, string(data)) + }) + + t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) { + // the legacy "environment-to-ini" (now a wrapper script) behavior: + // if no "--out", then "--in-place" must be used to overwrite the existing "--config" file + err := NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "config", "edit-ini", + "--apply-env", + "--config", configOld, + }) + require.ErrorContains(t, err, "either --in-place or --out must be specified") + + // simulate the "environment-to-ini" behavior with "--in-place" + err = NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "config", "edit-ini", + "--in-place", + "--apply-env", + "--config", configOld, + }) + require.NoError(t, err) + + data, _ := os.ReadFile(configOld) + require.Equal(t, `[sec] +k1 = v1 +k2 = v2 + +[env] +KeY = val +`, string(data)) + }) +} diff --git a/cmd/main.go b/cmd/main.go index 3fdaf48ed9..3a38d675a1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -128,6 +128,7 @@ func NewMainApp(appVer AppVersion) *cli.Command { // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ + cmdConfig(), cmdCert(), CmdGenerate, CmdDocs, diff --git a/cmd/web.go b/cmd/web.go index 4723ddbbdd..6e39db2178 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -156,7 +156,6 @@ func serveInstall(cmd *cli.Command) error { case <-graceful.GetManager().IsShutdown(): <-graceful.GetManager().Done() log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() return err default: } @@ -231,7 +230,6 @@ func serveInstalled(c *cli.Command) error { err := listen(webRoutes, true) <-graceful.GetManager().Done() log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() return err } diff --git a/contrib/environment-to-ini/README b/contrib/environment-to-ini/README deleted file mode 100644 index f1d3f2ae83..0000000000 --- a/contrib/environment-to-ini/README +++ /dev/null @@ -1,47 +0,0 @@ -Environment To Ini -================== - -Multiple docker users have requested that the Gitea docker is changed -to permit arbitrary configuration via environment variables. - -Gitea needs to use an ini file for configuration because the running -environment that starts the docker may not be the same as that used -by the hooks. An ini file also gives a good default and means that -users do not have to completely provide a full environment. - -With those caveats above, this command provides a generic way of -converting suitably structured environment variables into any ini -value. - -To use the command is very simple just run it and the default gitea -app.ini will be rewritten to take account of the variables provided, -however there are various options to give slightly different -behavior and these can be interrogated with the `-h` option. - -The environment variables should be of the form: - - GITEA__SECTION_NAME__KEY_NAME - -Note, SECTION_NAME in the notation above is case-insensitive. - -Environment variables are usually restricted to a reduced character -set "0-9A-Z_" - in order to allow the setting of sections with -characters outside of that set, they should be escaped as following: -"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names -can be escaped as a UTF8 byte string if necessary. E.g. to configure: - - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ - -You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" -and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found -on the configuration cheat sheet. - -To build locally, run: - - go build contrib/environment-to-ini/environment-to-ini.go diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go deleted file mode 100644 index 5eb576c6fe..0000000000 --- a/contrib/environment-to-ini/environment-to-ini.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "context" - "os" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - - "github.com/urfave/cli/v3" -) - -func main() { - app := cli.Command{} - app.Name = "environment-to-ini" - app.Usage = "Use provided environment to update configuration ini" - app.Description = `As a helper to allow docker users to update the gitea configuration - through the environment, this command allows environment variables to - be mapped to values in the ini. - - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME" - will be mapped to the ini section "[section_name]" and the key - "KEY_NAME" with the value as provided. - - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME__FILE" - will be mapped to the ini section "[section_name]" and the key - "KEY_NAME" with the value loaded from the specified file. - - Environment variables are usually restricted to a reduced character - set "0-9A-Z_" - in order to allow the setting of sections with - characters outside of that set, they should be escaped as following: - "_0X2E_" for ".". The entire section and key names can be escaped as - a UTF8 byte string if necessary. E.g. to configure: - - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ - - You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" - and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found - on the configuration cheat sheet.` - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "custom-path", - Aliases: []string{"C"}, - Value: setting.CustomPath, - Usage: "Custom path file path", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Value: setting.CustomConf, - Usage: "Custom configuration file path", - }, - &cli.StringFlag{ - Name: "work-path", - Aliases: []string{"w"}, - Value: setting.AppWorkPath, - Usage: "Set the gitea working path", - }, - &cli.StringFlag{ - Name: "out", - Aliases: []string{"o"}, - Value: "", - Usage: "Destination file to write to", - }, - } - app.Action = runEnvironmentToIni - err := app.Run(context.Background(), os.Args) - if err != nil { - log.Fatal("Failed to run app with %s: %v", os.Args, err) - } -} - -func runEnvironmentToIni(_ context.Context, c *cli.Command) error { - // the config system may change the environment variables, so get a copy first, to be used later - env := append([]string{}, os.Environ()...) - 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.CustomConf) - if err != nil { - log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) - } - - changed := setting.EnvironmentToConfig(cfg, env) - - // try to save the config file - destination := c.String("out") - if len(destination) == 0 { - destination = setting.CustomConf - } - if destination != setting.CustomConf || changed { - log.Info("Settings saved to: %q", destination) - err = cfg.SaveTo(destination) - if err != nil { - return err - } - } - - return nil -} diff --git a/docker/root/usr/local/bin/environment-to-ini b/docker/root/usr/local/bin/environment-to-ini new file mode 100644 index 0000000000..bb0c540685 --- /dev/null +++ b/docker/root/usr/local/bin/environment-to-ini @@ -0,0 +1,2 @@ +#!/bin/bash +exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@" diff --git a/docker/rootless/usr/local/bin/environment-to-ini b/docker/rootless/usr/local/bin/environment-to-ini new file mode 100644 index 0000000000..bb0c540685 --- /dev/null +++ b/docker/rootless/usr/local/bin/environment-to-ini @@ -0,0 +1,2 @@ +#!/bin/bash +exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@" diff --git a/main.go b/main.go index 2c25bac4e3..bc2121b1e7 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,7 @@ func main() { } app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()}) _ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp + // flush the queued logs before exiting, it is a MUST, otherwise there will be log loss log.GetManager().Close() } diff --git a/models/actions/runner.go b/models/actions/runner.go index 81d4249ae0..84398b143b 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -173,6 +174,13 @@ func (r *ActionRunner) GenerateToken() (err error) { return err } +// CanMatchLabels checks whether the runner's labels can match a job's "runs-on" +// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on +func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool { + runnerLabelSet := container.SetOf(r.AgentLabels...) + return runnerLabelSet.Contains(jobRunsOn...) // match all labels +} + func init() { db.RegisterModel(&ActionRunner{}) } diff --git a/models/actions/task.go b/models/actions/task.go index 7417af8b45..8b4ecf28f7 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -13,7 +13,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) for _, v := range jobs { - if isSubset(runner.AgentLabels, v.RunsOn) { + if runner.CanMatchLabels(v.RunsOn) { job = v break } @@ -475,20 +474,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim Find(&tasks) } -func isSubset(set, subset []string) bool { - m := make(container.Set[string], len(set)) - for _, v := range set { - m.Add(v) - } - - for _, v := range subset { - if !m.Contains(v) { - return false - } - } - return true -} - func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { return timeutil.TimeStamp(0) diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 09dfa6cccb..b9688dd5f5 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -139,3 +139,23 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 804 + title: "use a private action" + repo_id: 60 + owner_id: 40 + workflow_id: "run.yaml" + index: 189 + trigger_user_id: 40 + ref: "refs/heads/master" + commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 6c06d94aa4..337e83605a 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -129,3 +129,17 @@ status: 5 started: 1683636528 stopped: 1683636626 +- + id: 205 + run_id: 804 + repo_id: 6 + owner_id: 10 + commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 48 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index c79fb07050..e09fd6f2ec 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -177,3 +177,23 @@ log_length: 0 log_size: 0 log_expired: 0 +- + id: 55 + job_id: 205 + attempt: 1 + runner_id: 1 + status: 6 # 6 is the status code for "running" + started: 1683636528 + stopped: 1683636626 + repo_id: 6 + owner_id: 10 + commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b + token_salt: ERxJGHvg3I + token_last_eight: 182199eb + log_filename: collaborative-owner-test/1a/49.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 717230149b..a17999091e 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -225,3 +225,27 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 27 + repo_id: 1 + name: 'DefaultBranch' + commit_id: '90c1019714259b24fb81711d4416ac0f18667dfa' + commit_message: 'add license' + commit_time: 1709345946 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 28 + repo_id: 1 + name: 'sub-home-md-img-check' + commit_id: '4649299398e4d39a5c09eb4f534df6f1e1eb87cc' + commit_message: "Test how READMEs render images when found in a subfolder" + commit_time: 1678403550 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index f6b6252da1..f8bb8ef0d3 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -733,3 +733,10 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 + +- + id: 111 + repo_id: 3 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index df96db8d5a..ba7544f343 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -264,13 +264,22 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito if err != nil { return perm, err } - if task.RepoID != repo.ID { - // FIXME allow public repo read access if tokenless pull is enabled - return perm, nil - } var accessMode perm_model.AccessMode - if task.IsForkPullRequest { + if task.RepoID != repo.ID { + taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID) + if err != nil || !exist { + return perm, err + } + actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate { + // The task repo can access the current repo only if the task repo is private and + // the owner of the task repo is a collaborative owner of the current repo. + // FIXME allow public repo read access if tokenless pull is enabled + return perm, nil + } + accessMode = perm_model.AccessModeRead + } else if task.IsForkPullRequest { accessMode = perm_model.AccessModeRead } else { accessMode = perm_model.AccessModeWrite diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index a5207bc22a..ad0bb9d3f8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -170,6 +170,9 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle { type ActionsConfig struct { DisabledWorkflows []string + // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. + // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. + CollaborativeOwnerIDs []int64 } func (cfg *ActionsConfig) EnableWorkflow(file string) { @@ -192,6 +195,20 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) { cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { + if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { + cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) + } +} + +func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) { + cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID) +} + +func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool { + return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) +} + // FromDB fills up a ActionsConfig from serialized format. func (cfg *ActionsConfig) FromDB(bs []byte) error { return json.UnmarshalHandleDoubleEncode(bs, &cfg) diff --git a/models/user/search.go b/models/user/search.go index cfd0d011bc..db4b07f64a 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -6,6 +6,7 @@ package user import ( "context" "fmt" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -22,7 +23,7 @@ type SearchUserOptions struct { db.ListOptions Keyword string - Type UserType + Types []UserType UID int64 LoginName string // this option should be used only for admin user SourceID int64 // this option should be used only for admin user @@ -43,16 +44,16 @@ type SearchUserOptions struct { func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { var cond builder.Cond - cond = builder.Eq{"type": opts.Type} + cond = builder.In("type", opts.Types) if opts.IncludeReserved { - switch opts.Type { - case UserTypeIndividual: + switch { + case slices.Contains(opts.Types, UserTypeIndividual): cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or( builder.Eq{"type": UserTypeBot}, ).Or( builder.Eq{"type": UserTypeRemoteUser}, ) - case UserTypeOrganization: + case slices.Contains(opts.Types, UserTypeOrganization): cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved}) } } diff --git a/models/user/user.go b/models/user/user.go index 4d27abeb1e..1075f2bd82 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1450,3 +1450,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { } return &setting.Admin.UserDisabledFeatures } + +// GetUserOrOrgIDByName returns the id for a user or an org by name +func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { + var id int64 + has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Cols("id").Get(&id) + if err != nil { + return 0, err + } else if !has { + return 0, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist) + } + return id, nil +} diff --git a/models/user/user_test.go b/models/user/user_test.go index 6a530553d7..923f2cd40e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -126,7 +126,7 @@ func TestSearchUsers(t *testing.T) { // test orgs testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) { - opts.Type = user_model.UserTypeOrganization + opts.Types = []user_model.UserType{user_model.UserTypeOrganization} testSuccess(opts, expectedOrgIDs) } @@ -150,7 +150,7 @@ func TestSearchUsers(t *testing.T) { // test users testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) { - opts.Type = user_model.UserTypeIndividual + opts.Types = []user_model.UserType{user_model.UserTypeIndividual} testSuccess(opts, expectedUserIDs) } diff --git a/modules/graceful/server.go b/modules/graceful/server.go index 2525a83e77..b440f68ab5 100644 --- a/modules/graceful/server.go +++ b/modules/graceful/server.go @@ -11,7 +11,6 @@ import ( "os" "strings" "sync" - "sync/atomic" "syscall" "time" @@ -30,12 +29,15 @@ type ServeFunction = func(net.Listener) error // Server represents our graceful server type Server struct { - network string - address string - listener net.Listener - wg sync.WaitGroup - state state - lock *sync.RWMutex + network string + address string + listener net.Listener + + lock sync.RWMutex + state state + connCounter int64 + connEmptyCond *sync.Cond + BeforeBegin func(network, address string) OnShutdown func() PerWriteTimeout time.Duration @@ -50,14 +52,13 @@ func NewServer(network, address, name string) *Server { log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid()) } srv := &Server{ - wg: sync.WaitGroup{}, state: stateInit, - lock: &sync.RWMutex{}, network: network, address: address, PerWriteTimeout: setting.PerWriteTimeout, PerWritePerKbTimeout: setting.PerWritePerKbTimeout, } + srv.connEmptyCond = sync.NewCond(&srv.lock) srv.BeforeBegin = func(network, addr string) { log.Debug("Starting server on %s:%s (PID: %d)", network, addr, syscall.Getpid()) @@ -154,7 +155,7 @@ func (srv *Server) Serve(serve ServeFunction) error { GetManager().RegisterServer() err := serve(srv.listener) log.Debug("Waiting for connections to finish... (PID: %d)", syscall.Getpid()) - srv.wg.Wait() + srv.waitForActiveConnections() srv.setState(stateTerminate) GetManager().ServerDone() // use of closed means that the listeners are closed - i.e. we should be shutting down - return nil @@ -178,16 +179,62 @@ func (srv *Server) setState(st state) { srv.state = st } +func (srv *Server) waitForActiveConnections() { + srv.lock.Lock() + for srv.connCounter > 0 { + srv.connEmptyCond.Wait() + } + srv.lock.Unlock() +} + +func (srv *Server) wrapConnection(c net.Conn) (net.Conn, error) { + srv.lock.Lock() + defer srv.lock.Unlock() + + if srv.state != stateRunning { + _ = c.Close() + return nil, syscall.EINVAL // same as AcceptTCP + } + + srv.connCounter++ + return &wrappedConn{Conn: c, server: srv}, nil +} + +func (srv *Server) removeConnection(_ *wrappedConn) { + srv.lock.Lock() + defer srv.lock.Unlock() + + srv.connCounter-- + if srv.connCounter <= 0 { + srv.connEmptyCond.Broadcast() + } +} + +// closeAllConnections forcefully closes all active connections +func (srv *Server) closeAllConnections() { + srv.lock.Lock() + if srv.connCounter > 0 { + log.Warn("After graceful shutdown period, %d connections are still active. Forcefully close.", srv.connCounter) + srv.connCounter = 0 // OS will close all the connections after the process exits, so we just assume there is no active connection now + } + srv.lock.Unlock() + srv.connEmptyCond.Broadcast() +} + type filer interface { File() (*os.File, error) } type wrappedListener struct { net.Listener - stopped bool - server *Server + server *Server } +var ( + _ net.Listener = (*wrappedListener)(nil) + _ filer = (*wrappedListener)(nil) +) + func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { return &wrappedListener{ Listener: l, @@ -195,46 +242,24 @@ func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { } } -func (wl *wrappedListener) Accept() (net.Conn, error) { - var c net.Conn - // Set keepalive on TCPListeners connections. +func (wl *wrappedListener) Accept() (c net.Conn, err error) { if tcl, ok := wl.Listener.(*net.TCPListener); ok { + // Set keepalive on TCPListeners connections if possible, see http.tcpKeepAliveListener tc, err := tcl.AcceptTCP() if err != nil { return nil, err } - _ = tc.SetKeepAlive(true) // see http.tcpKeepAliveListener - _ = tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener + _ = tc.SetKeepAlive(true) + _ = tc.SetKeepAlivePeriod(3 * time.Minute) c = tc } else { - var err error c, err = wl.Listener.Accept() if err != nil { return nil, err } } - closed := int32(0) - - c = &wrappedConn{ - Conn: c, - server: wl.server, - closed: &closed, - perWriteTimeout: wl.server.PerWriteTimeout, - perWritePerKbTimeout: wl.server.PerWritePerKbTimeout, - } - - wl.server.wg.Add(1) - return c, nil -} - -func (wl *wrappedListener) Close() error { - if wl.stopped { - return syscall.EINVAL - } - - wl.stopped = true - return wl.Listener.Close() + return wl.server.wrapConnection(c) } func (wl *wrappedListener) File() (*os.File, error) { @@ -244,17 +269,14 @@ func (wl *wrappedListener) File() (*os.File, error) { type wrappedConn struct { net.Conn - server *Server - closed *int32 - deadline time.Time - perWriteTimeout time.Duration - perWritePerKbTimeout time.Duration + server *Server + deadline time.Time } func (w *wrappedConn) Write(p []byte) (n int, err error) { - if w.perWriteTimeout > 0 { - minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout - minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout) + if w.server.PerWriteTimeout > 0 { + minTimeout := time.Duration(len(p)/1024) * w.server.PerWritePerKbTimeout + minDeadline := time.Now().Add(minTimeout).Add(w.server.PerWriteTimeout) w.deadline = w.deadline.Add(minTimeout) if minDeadline.After(w.deadline) { @@ -266,19 +288,6 @@ func (w *wrappedConn) Write(p []byte) (n int, err error) { } func (w *wrappedConn) Close() error { - if atomic.CompareAndSwapInt32(w.closed, 0, 1) { - defer func() { - if err := recover(); err != nil { - select { - case <-GetManager().IsHammer(): - // Likely deadlocked request released at hammertime - log.Warn("Panic during connection close! %v. Likely there has been a deadlocked request which has been released by forced shutdown.", err) - default: - log.Error("Panic during connection close! %v", err) - } - } - }() - w.server.wg.Done() - } + w.server.removeConnection(w) return w.Conn.Close() } diff --git a/modules/graceful/server_hooks.go b/modules/graceful/server_hooks.go index 9b67589571..b800c32503 100644 --- a/modules/graceful/server_hooks.go +++ b/modules/graceful/server_hooks.go @@ -5,7 +5,6 @@ package graceful import ( "os" - "runtime" "code.gitea.io/gitea/modules/log" ) @@ -48,26 +47,8 @@ func (srv *Server) doShutdown() { } func (srv *Server) doHammer() { - defer func() { - // We call srv.wg.Done() until it panics. - // This happens if we call Done() when the WaitGroup counter is already at 0 - // So if it panics -> we're done, Serve() will return and the - // parent will goroutine will exit. - if r := recover(); r != nil { - log.Error("WaitGroup at 0: Error: %v", r) - } - }() if srv.getState() != stateShuttingDown { return } - log.Warn("Forcefully shutting down parent") - for { - if srv.getState() == stateTerminate { - break - } - srv.wg.Done() - - // Give other goroutines a chance to finish before we forcibly stop them. - runtime.Gosched() - } + srv.closeAllConnections() } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 777885cb4f..b6f9f07f98 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -41,6 +41,7 @@ type ConfigSection interface { HasKey(key string) bool NewKey(name, value string) (ConfigKey, error) Key(key string) ConfigKey + DeleteKey(key string) Keys() []ConfigKey ChildSections() []ConfigSection } @@ -51,6 +52,7 @@ type ConfigProvider interface { Sections() []ConfigSection NewSection(name string) (ConfigSection, error) GetSection(name string) (ConfigSection, error) + DeleteSection(name string) Save() error SaveTo(filename string) error @@ -168,6 +170,10 @@ func (s *iniConfigSection) Keys() (keys []ConfigKey) { return keys } +func (s *iniConfigSection) DeleteKey(key string) { + s.sec.DeleteKey(key) +} + func (s *iniConfigSection) ChildSections() (sections []ConfigSection) { for _, s := range s.sec.ChildSections() { sections = append(sections, &iniConfigSection{s}) @@ -249,6 +255,10 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) { return &iniConfigSection{sec: sec}, nil } +func (p *iniConfigProvider) DeleteSection(name string) { + p.ini.DeleteSection(name) +} + var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save") // Save saves the content into file diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 4729bde491..59665062b7 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -24,13 +24,6 @@ type FileOptions struct { Signoff bool `json:"signoff"` } -type FileOptionsWithSHA struct { - FileOptions - // the blob ID (SHA) for the file that already exists, it is required for changing existing files - // required: true - SHA string `json:"sha" binding:"Required"` -} - func (f *FileOptions) GetFileOptions() *FileOptions { return f } @@ -41,7 +34,7 @@ type FileOptionsInterface interface { var _ FileOptionsInterface = (*FileOptions)(nil) -// CreateFileOptions options for creating files +// CreateFileOptions options for creating a file // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type CreateFileOptions struct { FileOptions @@ -50,16 +43,21 @@ type CreateFileOptions struct { ContentBase64 string `json:"content"` } -// DeleteFileOptions options for deleting files (used for other File structs below) +// DeleteFileOptions options for deleting a file // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type DeleteFileOptions struct { - FileOptionsWithSHA + FileOptions + // the blob ID (SHA) for the file to delete + // required: true + SHA string `json:"sha" binding:"Required"` } -// UpdateFileOptions options for updating files +// UpdateFileOptions options for updating or creating a file // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type UpdateFileOptions struct { - FileOptionsWithSHA + FileOptions + // the blob ID (SHA) for the file that already exists to update, or leave it empty to create a new file + SHA string `json:"sha"` // content must be base64 encoded // required: true ContentBase64 string `json:"content"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 15a359898a..fb038cdb76 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3914,6 +3914,15 @@ variables.update.success = The variable has been edited. logs.always_auto_scroll = Always auto scroll logs logs.always_expand_running = Always expand running logs +general = General +general.enable_actions = Enable Actions +general.collaborative_owners_management = Collaborative Owners Management +general.collaborative_owners_management_help = A collaborative owner is a user or an organization whose private repository has access to the actions and workflows of this repository. +general.add_collaborative_owner = Add Collaborative Owner +general.collaborative_owner_not_exist = The collaborative owner does not exist. +general.remove_collaborative_owner = Remove Collaborative Owner +general.remove_collaborative_owner_desc = Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue? + [projects] deleted.display_name = Deleted Project type-1.display_name = Individual Project diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index c3473372f2..62afcb00d9 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -103,7 +103,7 @@ func GetAllOrgs(ctx *context.APIContext) { users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, OrderBy: db.SearchOrderByAlphabetically, ListOptions: listOptions, Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate}, diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 494bace585..6afa651448 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -425,7 +425,7 @@ func SearchUsers(ctx *context.APIContext) { users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, LoginName: ctx.FormTrim("login_name"), SourceID: ctx.FormInt64("source_id"), OrderBy: db.SearchOrderByAlphabetically, diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index cd67686065..08e37e8df4 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -202,7 +202,7 @@ func GetAll(ctx *context.APIContext) { publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, ListOptions: listOptions, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, OrderBy: db.SearchOrderByAlphabetically, Visible: vMode, }) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 65fac45aa1..a703ca6909 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -225,7 +225,7 @@ func CreateBranch(ctx *context.APIContext) { return } } else if len(opt.OldBranchName) > 0 { //nolint:staticcheck // deprecated field - if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, opt.OldBranchName) { //nolint:staticcheck // deprecated field + if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, opt.OldBranchName); exist { //nolint:staticcheck // deprecated field oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint:staticcheck // deprecated field if err != nil { ctx.APIErrorInternal(err) @@ -1011,7 +1011,11 @@ func EditBranchProtection(ctx *context.APIContext) { isPlainRule := !git_model.IsRuleNameSpecial(bpName) var isBranchExist bool if isPlainRule { - isBranchExist = gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, bpName) + isBranchExist, err = git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, bpName) + if err != nil { + ctx.APIErrorInternal(err) + return + } } if isBranchExist { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 2b6348c2fb..ba98263819 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -525,7 +525,7 @@ func CreateFile(ctx *context.APIContext) { func UpdateFile(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile // --- - // summary: Update a file in a repository + // summary: Update a file in a repository if SHA is set, or create the file if SHA is not set // consumes: // - application/json // produces: @@ -554,6 +554,8 @@ func UpdateFile(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/FileResponse" + // "201": + // "$ref": "#/responses/FileResponse" // "403": // "$ref": "#/responses/error" // "404": @@ -572,8 +574,9 @@ func UpdateFile(ctx *context.APIContext) { ctx.APIError(http.StatusUnprocessableEntity, err) return } + willCreate := apiOpts.SHA == "" opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ - Operation: "update", + Operation: util.Iif(willCreate, "create", "update"), ContentReader: contentReader, SHA: apiOpts.SHA, FromTreePath: apiOpts.FromPath, @@ -587,7 +590,7 @@ func UpdateFile(ctx *context.APIContext) { handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) - ctx.JSON(http.StatusOK, fileResponse) + ctx.JSON(util.Iif(willCreate, http.StatusCreated, http.StatusOK), fileResponse) } } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 2561ff3975..9305ad8c2d 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -13,6 +13,7 @@ import ( "time" activities_model "code.gitea.io/gitea/models/activities" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" @@ -755,7 +756,12 @@ func EditPullRequest(ctx *context.APIContext) { // change pull target branch if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch { - if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Base) { + branchExist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Base) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if !branchExist { ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base)) return } diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 6de1125c40..f7b9301795 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -77,7 +77,7 @@ func Search(ctx *context.APIContext) { Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), UID: uid, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, SearchByEmail: true, Visible: visible, ListOptions: listOptions, diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index 1cfe01a639..b872dcbd2c 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -6,6 +6,7 @@ package utils import ( "errors" + git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -27,7 +28,7 @@ func ResolveRefCommit(ctx reqctx.RequestContext, repo *repo_model.Repository, in return nil, err } refCommit := RefCommit{InputRef: inputRef} - if gitrepo.IsBranchExist(ctx, repo, inputRef) { + if exist, _ := git_model.IsBranchExist(ctx, repo.ID, inputRef); exist { refCommit.RefName = git.RefNameFromBranch(inputRef) } else if gitrepo.IsTagExist(ctx, repo, inputRef) { refCommit.RefName = git.RefNameFromTag(inputRef) diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 9d69bbcedf..88e8b466f1 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -21,7 +21,9 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/agit" gitea_context "code.gitea.io/gitea/services/context" pull_service "code.gitea.io/gitea/services/pull" ) @@ -452,25 +454,18 @@ func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) { return } - baseBranchName := refFullName.ForBranchName() - - baseBranchExist := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, baseBranchName) - - if !baseBranchExist { - for p, v := range baseBranchName { - if v == '/' && gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, baseBranchName[:p]) && p != len(baseBranchName)-1 { - baseBranchExist = true - break - } + _, _, err := agit.GetAgitBranchInfo(ctx, ctx.Repo.Repository.ID, refFullName.ForBranchName()) + if err != nil { + if !errors.Is(err, util.ErrNotExist) { + ctx.JSON(http.StatusForbidden, private.Response{ + UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName), + }) + } else { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) } } - - if !baseBranchExist { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName), - }) - return - } } func generateGitEnv(opts *private.HookOptions) (env []string) { diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index e34f203aaf..62a8b30b13 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -29,7 +29,7 @@ func Organizations(ctx *context.Context) { explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, IncludeReserved: true, // administrator needs to list all accounts include reserved ListOptions: db.ListOptions{ PageSize: setting.UI.Admin.OrgPagingNum, diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 27577cd35b..1f22d800a9 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -67,7 +67,7 @@ func Users(ctx *context.Context) { explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{ PageSize: setting.UI.Admin.UserPagingNum, }, diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index f8f7f5c18c..4d25f4ec2d 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -46,7 +46,7 @@ func Organizations(ctx *context.Context) { RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeOrganization, + Types: []user_model.UserType{user_model.UserTypeOrganization}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, Visible: visibleTypes, diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 40d3e2a060..4b3c269410 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -153,7 +153,7 @@ func Users(ctx *context.Context) { RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate}, diff --git a/routers/web/home.go b/routers/web/home.go index 4b15ee83c2..7efa5f344e 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -69,7 +69,7 @@ func HomeSitemap(ctx *context.Context) { m := sitemap.NewSitemapIndex() if !setting.Service.Explore.DisableUsersPage { _, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ - Type: user_model.UserTypeIndividual, + Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{PageSize: 1}, IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic}, diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 202da407d2..43c56752e8 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -28,7 +28,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - "github.com/nektos/act/pkg/model" + act_model "github.com/nektos/act/pkg/model" "gopkg.in/yaml.v3" ) @@ -38,9 +38,10 @@ const ( tplViewActions templates.TplName = "repo/actions/view" ) -type Workflow struct { - Entry git.TreeEntry - ErrMsg string +type WorkflowInfo struct { + Entry git.TreeEntry + ErrMsg string + Workflow *act_model.Workflow } // MustEnableActions check if actions are enabled in settings @@ -77,7 +78,11 @@ func List(ctx *context.Context) { return } - workflows := prepareWorkflowDispatchTemplate(ctx, commit) + workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit) + if ctx.Written() { + return + } + prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID) if ctx.Written() { return } @@ -112,55 +117,41 @@ func WorkflowDispatchInputs(ctx *context.Context) { ctx.ServerError("GetTagCommit/GetBranchCommit", err) return } - prepareWorkflowDispatchTemplate(ctx, commit) + workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit) + if ctx.Written() { + return + } + prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID) if ctx.Written() { return } ctx.HTML(http.StatusOK, tplDispatchInputsActions) } -func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) { - workflowID := ctx.FormString("workflow") - ctx.Data["CurWorkflow"] = workflowID - ctx.Data["CurWorkflowExists"] = false - - var curWorkflow *model.Workflow +func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) { + curWorkflowID = ctx.FormString("workflow") _, entries, err := actions.ListWorkflows(commit) if err != nil { ctx.ServerError("ListWorkflows", err) - return nil + return nil, "" } - // Get all runner labels - runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ - RepoID: ctx.Repo.Repository.ID, - IsOnline: optional.Some(true), - WithAvailable: true, - }) - if err != nil { - ctx.ServerError("FindRunners", err) - return nil - } - allRunnerLabels := make(container.Set[string]) - for _, r := range runners { - allRunnerLabels.AddMultiple(r.AgentLabels...) - } - - workflows = make([]Workflow, 0, len(entries)) + workflows = make([]WorkflowInfo, 0, len(entries)) for _, entry := range entries { - workflow := Workflow{Entry: *entry} + workflow := WorkflowInfo{Entry: *entry} content, err := actions.GetContentFromEntry(entry) if err != nil { ctx.ServerError("GetContentFromEntry", err) - return nil + return nil, "" } - wf, err := model.ReadWorkflow(bytes.NewReader(content)) + wf, err := act_model.ReadWorkflow(bytes.NewReader(content)) if err != nil { workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) workflows = append(workflows, workflow) continue } + workflow.Workflow = wf // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. hasJobWithoutNeeds := false // Check whether you have matching runner and a job without "needs" @@ -173,22 +164,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) ( if !hasJobWithoutNeeds && len(j.Needs()) == 0 { hasJobWithoutNeeds = true } - runsOnList := j.RunsOn() - for _, ro := range runsOnList { - if strings.Contains(ro, "${{") { - // Skip if it contains expressions. - // The expressions could be very complex and could not be evaluated here, - // so just skip it, it's OK since it's just a tooltip message. - continue - } - if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) - break - } - } - if workflow.ErrMsg != "" { - break - } } if !hasJobWithoutNeeds { workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") @@ -197,61 +172,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) ( workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") } workflows = append(workflows, workflow) - - if workflow.Entry.Name() == workflowID { - curWorkflow = wf - ctx.Data["CurWorkflowExists"] = true - } } ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() - + ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin() actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig + ctx.Data["CurWorkflow"] = curWorkflowID + ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID) - if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) { - ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin() - isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID) - ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled - - if !isWorkflowDisabled && curWorkflow != nil { - workflowDispatchConfig := workflowDispatchConfig(curWorkflow) - if workflowDispatchConfig != nil { - ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig - - branchOpts := git_model.FindBranchOptions{ - RepoID: ctx.Repo.Repository.ID, - IsDeletedBranch: optional.Some(false), - ListOptions: db.ListOptions{ - ListAll: true, - }, - } - branches, err := git_model.FindBranchNames(ctx, branchOpts) - if err != nil { - ctx.ServerError("FindBranchNames", err) - return nil - } - // always put default branch on the top if it exists - if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { - branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch) - branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) - } - ctx.Data["Branches"] = branches - - tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) - return nil - } - ctx.Data["Tags"] = tags - } - } - } - return workflows + return workflows, curWorkflowID } -func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { +func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) { + actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) { + return + } + + var curWorkflow *act_model.Workflow + for _, workflowInfo := range workflowInfos { + if workflowInfo.Entry.Name() == curWorkflowID { + if workflowInfo.Workflow == nil { + log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID) + return + } + curWorkflow = workflowInfo.Workflow + break + } + } + + if curWorkflow == nil { + return + } + + ctx.Data["CurWorkflowExists"] = true + curWfDispatchCfg := workflowDispatchConfig(curWorkflow) + if curWfDispatchCfg == nil { + return + } + + ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg + + branchOpts := git_model.FindBranchOptions{ + RepoID: ctx.Repo.Repository.ID, + IsDeletedBranch: optional.Some(false), + ListOptions: db.ListOptions{ + ListAll: true, + }, + } + branches, err := git_model.FindBranchNames(ctx, branchOpts) + if err != nil { + ctx.ServerError("FindBranchNames", err) + return + } + // always put default branch on the top + branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch) + branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) + ctx.Data["Branches"] = branches + + tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.ServerError("GetTagNamesByRepoID", err) + return + } + ctx.Data["Tags"] = tags +} + +func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { actorID := ctx.FormInt64("actor") status := ctx.FormInt("status") workflowID := ctx.FormString("workflow") @@ -302,6 +291,45 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { log.Error("LoadIsRefDeleted", err) } + // Check for each run if there is at least one online runner that can run its jobs + runErrors := make(map[int64]string) + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: ctx.Repo.Repository.ID, + IsOnline: optional.Some(true), + WithAvailable: true, + }) + if err != nil { + ctx.ServerError("FindRunners", err) + return + } + for _, run := range runs { + if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) { + continue + } + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + ctx.ServerError("GetRunJobsByRunID", err) + return + } + for _, job := range jobs { + if !job.Status.IsWaiting() { + continue + } + hasOnlineRunner := false + for _, runner := range runners { + if runner.CanMatchLabels(job.RunsOn) { + hasOnlineRunner = true + break + } + } + if !hasOnlineRunner { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) + break + } + } + } + ctx.Data["RunErrors"] = runErrors + ctx.Data["Runs"] = runs actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) @@ -362,7 +390,7 @@ type WorkflowDispatch struct { Inputs []WorkflowDispatchInput } -func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch { +func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch { switch w.RawOn.Kind { case yaml.ScalarNode: var val string diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 08bd0a7e74..f3375e4898 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -306,7 +306,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { // Check if base branch is valid. baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch) - baseIsBranch := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, ci.BaseBranch) + baseIsBranch, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ci.BaseBranch) baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch) if !baseIsCommit && !baseIsBranch && !baseIsTag { @@ -508,7 +508,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { // Check if head branch is valid. headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch) - headIsBranch := gitrepo.IsBranchExist(ctx, ci.HeadRepo, ci.HeadBranch) + headIsBranch, _ := git_model.IsBranchExist(ctx, ci.HeadRepo.ID, ci.HeadBranch) headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, ci.HeadBranch) if !headIsCommit && !headIsBranch && !headIsTag { // Check if headBranch is short sha commit hash diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 1b1c272a8d..c6f5f74e4b 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -191,7 +191,7 @@ func httpBase(ctx *context.Context) *serviceHandler { taskID := ctx.Data["ActionsTaskID"].(int64) p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetActionsUserRepoPermission", err) return nil } diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 4e7f245296..edad756b6b 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -11,11 +11,11 @@ import ( "strconv" "strings" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/renderhelper" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" @@ -121,7 +121,7 @@ func NewComment(ctx *context.Context) { ctx.ServerError("Unable to load head repo", err) return } - if ok := gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.BaseBranch); !ok { + if exist, _ := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.BaseBranch); !exist { // todo localize ctx.JSONError("The origin branch is delete, cannot reopen.") return diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 5866ddc402..82a776db5b 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -26,7 +26,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" @@ -566,8 +565,10 @@ func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue pull := issue.PullRequest isPullBranchDeletable := canDelete && pull.HeadRepo != nil && - gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) && (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"]) + if isPullBranchDeletable { + isPullBranchDeletable, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch) + } if isPullBranchDeletable && pull.HasMerged { exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 56d9093eaf..b07562620f 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -358,7 +358,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_ defer baseGitRepo.Close() } - if !gitrepo.IsBranchExist(ctx, pull.BaseRepo, pull.BaseBranch) { + if exist, _ := git_model.IsBranchExist(ctx, pull.BaseRepo.ID, pull.BaseBranch); !exist { ctx.Data["BaseBranchNotExist"] = true ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch @@ -415,7 +415,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_ defer closer.Close() if pull.Flow == issues_model.PullRequestFlowGithub { - headBranchExist = gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) + headBranchExist, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch) } else { headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName()) } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 36ea20c23e..4ed9e0bdbd 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -18,7 +18,6 @@ import ( "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/gitrepo" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" @@ -424,7 +423,7 @@ func NewReleasePost(ctx *context.Context) { return } - if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Target) { + if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist { ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) return } diff --git a/routers/web/repo/setting/actions.go b/routers/web/repo/setting/actions.go new file mode 100644 index 0000000000..9c2c9242d3 --- /dev/null +++ b/routers/web/repo/setting/actions.go @@ -0,0 +1,121 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + "strings" + + 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/templates" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +const tplRepoActionsGeneralSettings templates.TplName = "repo/settings/actions" + +func ActionsGeneralSettings(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.general") + ctx.Data["PageType"] = "general" + ctx.Data["PageIsActionsSettingsGeneral"] = true + + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil && !repo_model.IsErrUnitTypeNotExist(err) { + ctx.ServerError("GetUnit", err) + return + } + if actionsUnit == nil { // no actions unit + ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings) + return + } + + if ctx.Repo.Repository.IsPrivate { + collaborativeOwnerIDs := actionsUnit.ActionsConfig().CollaborativeOwnerIDs + collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs) + if err != nil { + ctx.ServerError("GetUsersByIDs", err) + return + } + ctx.Data["CollaborativeOwners"] = collaborativeOwners + } + + ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings) +} + +func ActionsUnitPost(ctx *context.Context) { + redirectURL := ctx.Repo.RepoLink + "/settings/actions/general" + enableActionsUnit := ctx.FormBool("enable_actions") + repo := ctx.Repo.Repository + + var err error + if enableActionsUnit && !unit_model.TypeActions.UnitGlobalDisabled() { + err = repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{newRepoUnit(repo, unit_model.TypeActions, nil)}, nil) + } else if !unit_model.TypeActions.UnitGlobalDisabled() { + err = repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions}) + } + + if err != nil { + ctx.ServerError("UpdateRepositoryUnits", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(redirectURL) +} + +func AddCollaborativeOwner(ctx *context.Context) { + name := strings.ToLower(ctx.FormString("collaborative_owner")) + + ownerID, err := user_model.GetUserOrOrgIDByName(ctx, name) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + ctx.JSONErrorNotFound() + } else { + ctx.ServerError("GetUserOrOrgIDByName", err) + } + return + } + + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil { + ctx.ServerError("GetUnit", err) + return + } + actionsCfg := actionsUnit.ActionsConfig() + actionsCfg.AddCollaborativeOwner(ownerID) + if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil { + ctx.ServerError("UpdateRepoUnit", err) + return + } + + ctx.JSONOK() +} + +func DeleteCollaborativeOwner(ctx *context.Context) { + ownerID := ctx.FormInt64("id") + + actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions) + if err != nil { + ctx.ServerError("GetUnit", err) + return + } + actionsCfg := actionsUnit.ActionsConfig() + if !actionsCfg.IsCollaborativeOwner(ownerID) { + ctx.Flash.Error(ctx.Tr("actions.general.collaborative_owner_not_exist")) + ctx.JSONErrorNotFound() + return + } + actionsCfg.RemoveCollaborativeOwner(ownerID) + if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil { + ctx.ServerError("UpdateRepoUnit", err) + return + } + + ctx.JSONOK() +} diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index dd887d6edf..0b0c990ae0 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -613,12 +613,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) } - if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { - units = append(units, newRepoUnit(repo, unit_model.TypeActions, nil)) - } else if !unit_model.TypeActions.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) - } - if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{ IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, diff --git a/routers/web/user/search.go b/routers/web/user/search.go index 9acb9694d7..b2a15bf90e 100644 --- a/routers/web/user/search.go +++ b/routers/web/user/search.go @@ -16,10 +16,14 @@ import ( // SearchCandidates searches candidate users for dropdown list func SearchCandidates(ctx *context.Context) { + searchUserTypes := []user_model.UserType{user_model.UserTypeIndividual} + if ctx.FormBool("orgs") { + searchUserTypes = append(searchUserTypes, user_model.UserTypeOrganization) + } users, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), - Type: user_model.UserTypeIndividual, + Types: searchUserTypes, IsActive: optional.Some(true), ListOptions: db.ListOptions{PageSize: setting.UI.MembersPagingNum}, }) diff --git a/routers/web/web.go b/routers/web/web.go index 9352a51942..58797bdadd 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1167,11 +1167,21 @@ func registerWebRoutes(m *web.Router) { m.Post("/{lid}/unlock", repo_setting.LFSUnlock) }) }) + m.Group("/actions/general", func() { + m.Get("", repo_setting.ActionsGeneralSettings) + m.Post("/actions_unit", repo_setting.ActionsUnitPost) + }) m.Group("/actions", func() { m.Get("", shared_actions.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() + m.Group("/general", func() { + m.Group("/collaborative_owner", func() { + m.Post("/add", repo_setting.AddCollaborativeOwner) + m.Post("/delete", repo_setting.DeleteCollaborativeOwner) + }) + }) }, actions.MustEnableActions) // the follow handler must be under "settings", otherwise this incomplete repo can't be accessed m.Group("/migrate", func() { diff --git a/services/agit/agit.go b/services/agit/agit.go index 8ba14f9b22..15fc2e8fb5 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -6,6 +6,7 @@ package agit import ( "context" "encoding/base64" + "errors" "fmt" "strings" @@ -32,6 +33,34 @@ func parseAgitPushOptionValue(s string) string { return s } +func GetAgitBranchInfo(ctx context.Context, repoID int64, baseBranchName string) (string, string, error) { + baseBranchExist, err := git_model.IsBranchExist(ctx, repoID, baseBranchName) + if err != nil { + return "", "", err + } + if baseBranchExist { + return baseBranchName, "", nil + } + + // try match / + // refs/for have been trimmed to get baseBranchName + for p, v := range baseBranchName { + if v != '/' { + continue + } + + baseBranchExist, err := git_model.IsBranchExist(ctx, repoID, baseBranchName[:p]) + if err != nil { + return "", "", err + } + if baseBranchExist { + return baseBranchName[:p], baseBranchName[p+1:], nil + } + } + + return "", "", util.NewNotExistErrorf("base branch does not exist") +} + // ProcReceive handle proc receive work func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) @@ -70,17 +99,19 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. continue } - baseBranchName := opts.RefFullNames[i].ForBranchName() - currentTopicBranch := "" - if !gitrepo.IsBranchExist(ctx, repo, baseBranchName) { - // try match refs/for// - for p, v := range baseBranchName { - if v == '/' && gitrepo.IsBranchExist(ctx, repo, baseBranchName[:p]) && p != len(baseBranchName)-1 { - currentTopicBranch = baseBranchName[p+1:] - baseBranchName = baseBranchName[:p] - break - } + baseBranchName, currentTopicBranch, err := GetAgitBranchInfo(ctx, repo.ID, opts.RefFullNames[i].ForBranchName()) + if err != nil { + if !errors.Is(err, util.ErrNotExist) { + return nil, fmt.Errorf("failed to get branch information. Error: %w", err) } + // If branch does not exist, we can continue + results = append(results, private.HookProcReceiveRefResult{ + OriginalRef: opts.RefFullNames[i], + OldOID: opts.OldCommitIDs[i], + NewOID: opts.NewCommitIDs[i], + Err: "base-branch does not exist", + }) + continue } if len(topicBranch) == 0 && len(currentTopicBranch) == 0 { diff --git a/services/agit/agit_test.go b/services/agit/agit_test.go index feaf7dca9b..21224a41e9 100644 --- a/services/agit/agit_test.go +++ b/services/agit/agit_test.go @@ -6,11 +6,56 @@ package agit import ( "testing" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" + "github.com/stretchr/testify/assert" ) +func TestMain(m *testing.M) { + unittest.MainTest(m) +} + func TestParseAgitPushOptionValue(t *testing.T) { assert.Equal(t, "a", parseAgitPushOptionValue("a")) assert.Equal(t, "a", parseAgitPushOptionValue("{base64}YQ==")) assert.Equal(t, "{base64}invalid value", parseAgitPushOptionValue("{base64}invalid value")) } + +func TestGetAgitBranchInfo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + _, _, err := GetAgitBranchInfo(t.Context(), 1, "non-exist-basebranch") + assert.ErrorIs(t, err, util.ErrNotExist) + + baseBranch, currentTopicBranch, err := GetAgitBranchInfo(t.Context(), 1, "master") + assert.NoError(t, err) + assert.Equal(t, "master", baseBranch) + assert.Empty(t, currentTopicBranch) + + baseBranch, currentTopicBranch, err = GetAgitBranchInfo(t.Context(), 1, "master/topicbranch") + assert.NoError(t, err) + assert.Equal(t, "master", baseBranch) + assert.Equal(t, "topicbranch", currentTopicBranch) + + baseBranch, currentTopicBranch, err = GetAgitBranchInfo(t.Context(), 1, "master/") + assert.NoError(t, err) + assert.Equal(t, "master", baseBranch) + assert.Empty(t, currentTopicBranch) + + _, _, err = GetAgitBranchInfo(t.Context(), 1, "/") + assert.ErrorIs(t, err, util.ErrNotExist) + + _, _, err = GetAgitBranchInfo(t.Context(), 1, "//") + assert.ErrorIs(t, err, util.ErrNotExist) + + baseBranch, currentTopicBranch, err = GetAgitBranchInfo(t.Context(), 1, "master/topicbranch/") + assert.NoError(t, err) + assert.Equal(t, "master", baseBranch) + assert.Equal(t, "topicbranch/", currentTopicBranch) + + baseBranch, currentTopicBranch, err = GetAgitBranchInfo(t.Context(), 1, "master/topicbranch/1") + assert.NoError(t, err) + assert.Equal(t, "master", baseBranch) + assert.Equal(t, "topicbranch/1", currentTopicBranch) +} diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index eabfeff143..f14911f880 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -11,6 +11,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" @@ -207,7 +208,10 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { switch pr.Flow { case issues_model.PullRequestFlowGithub: - headBranchExist := pr.HeadRepo != nil && gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch) + headBranchExist := pr.HeadRepo != nil + if headBranchExist { + headBranchExist, _ = git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch) + } if !headBranchExist { log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch) return diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index e7c14a5e7a..25860fc1a8 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -96,8 +96,12 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR } defer closer.Close() - if pr.Flow == issues_model.PullRequestFlowGithub && !gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch) { - return "", errors.New("Head branch does not exist, can not merge") + if pr.Flow == issues_model.PullRequestFlowGithub { + if exist, err := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch); err != nil { + return "", errors.Wrap(err, "IsBranchExist") + } else if !exist { + return "", errors.New("Head branch does not exist, can not merge") + } } if pr.Flow == issues_model.PullRequestFlowAGit && !gitrepo.IsReferenceExist(ctx, pr.HeadRepo, pr.GetGitHeadRefName()) { return "", errors.New("Head branch does not exist, can not merge") diff --git a/services/pull/protected_branch.go b/services/pull/protected_branch.go index 181bd32f44..68ae7bc2ed 100644 --- a/services/pull/protected_branch.go +++ b/services/pull/protected_branch.go @@ -8,7 +8,6 @@ import ( git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/gitrepo" ) func CreateOrUpdateProtectedBranch(ctx context.Context, repo *repo_model.Repository, @@ -22,8 +21,7 @@ func CreateOrUpdateProtectedBranch(ctx context.Context, repo *repo_model.Reposit isPlainRule := !git_model.IsRuleNameSpecial(protectBranch.RuleName) var isBranchExist bool if isPlainRule { - // TODO: read the database directly to check if the branch exists - isBranchExist = gitrepo.IsBranchExist(ctx, repo, protectBranch.RuleName) + isBranchExist, _ = git_model.IsBranchExist(ctx, repo.ID, protectBranch.RuleName) } if isBranchExist { diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 4f7a504b11..113d1cb49e 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -16,7 +16,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" ) @@ -182,7 +181,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("fetch").AddArguments(fetchArgs...).AddDynamicArguments(remoteRepoName, headBranch+":"+trackingBranch)). Run(ctx); err != nil { cancel() - if !gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch) { + if exist, _ := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch); !exist { return nil, nil, git_model.ErrBranchNotExist{ BranchName: pr.HeadBranch, } diff --git a/services/repository/branch.go b/services/repository/branch.go index b49478422c..57eefbb741 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -409,11 +409,11 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m return "target_exist", nil } - if gitrepo.IsBranchExist(ctx, repo, to) { + if exist, _ := git_model.IsBranchExist(ctx, repo.ID, to); exist { return "target_exist", nil } - if !gitrepo.IsBranchExist(ctx, repo, from) { + if exist, _ := git_model.IsBranchExist(ctx, repo.ID, from); !exist { return "from_not_exist", nil } @@ -624,7 +624,7 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, newB return nil } - if !gitrepo.IsBranchExist(ctx, repo, newBranchName) { + if exist, _ := git_model.IsBranchExist(ctx, repo.ID, newBranchName); !exist { return git_model.ErrBranchNotExist{ BranchName: newBranchName, } diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl index 7d782c0ade..fe8c26b523 100644 --- a/templates/repo/actions/list.tmpl +++ b/templates/repo/actions/list.tmpl @@ -7,14 +7,14 @@ {{if .HasWorkflowsOrRuns}}
-
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index fc42056e0a..b4680431b8 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -509,18 +509,6 @@
- {{if .EnableActions}} - {{$isActionsEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeActions}} - {{$isActionsGlobalDisabled := ctx.Consts.RepoUnitTypeActions.UnitGlobalDisabled}} -
- -
- - -
-
- {{end}} - {{if not .IsMirror}}
{{$pullRequestEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePullRequests}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 966aff12f8..0cefa6795f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7634,7 +7634,7 @@ "tags": [ "repository" ], - "summary": "Update a file in a repository", + "summary": "Update a file in a repository if SHA is set, or create the file if SHA is not set", "operationId": "repoUpdateFile", "parameters": [ { @@ -7671,6 +7671,9 @@ "200": { "$ref": "#/responses/FileResponse" }, + "201": { + "$ref": "#/responses/FileResponse" + }, "403": { "$ref": "#/responses/error" }, @@ -22886,7 +22889,7 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "CreateFileOptions": { - "description": "CreateFileOptions options for creating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", + "description": "CreateFileOptions options for creating a file\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", "required": [ "content" @@ -23904,7 +23907,7 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "DeleteFileOptions": { - "description": "DeleteFileOptions options for deleting files (used for other File structs below)\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", + "description": "DeleteFileOptions options for deleting a file\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", "required": [ "sha" @@ -23940,7 +23943,7 @@ "x-go-name": "NewBranchName" }, "sha": { - "description": "the blob ID (SHA) for the file that already exists, it is required for changing existing files", + "description": "the blob ID (SHA) for the file to delete", "type": "string", "x-go-name": "SHA" }, @@ -28700,10 +28703,9 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "UpdateFileOptions": { - "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", + "description": "UpdateFileOptions options for updating or creating a file\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", "required": [ - "sha", "content" ], "properties": { @@ -28747,7 +28749,7 @@ "x-go-name": "NewBranchName" }, "sha": { - "description": "the blob ID (SHA) for the file that already exists, it is required for changing existing files", + "description": "the blob ID (SHA) for the file that already exists to update, or leave it empty to create a new file", "type": "string", "x-go-name": "SHA" }, diff --git a/tests/integration/actions_settings_test.go b/tests/integration/actions_settings_test.go new file mode 100644 index 0000000000..935d8bbceb --- /dev/null +++ b/tests/integration/actions_settings_test.go @@ -0,0 +1,62 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestActionsCollaborativeOwner(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // user2 is the owner of "reusable_workflow" repo + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + repo := createActionsTestRepo(t, user2Token, "reusable_workflow", true) + + // a private repo(id=6) of user10 will try to clone "reusable_workflow" repo + user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}) + // task id is 55 and its repo_id=6 + task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 55, RepoID: 6}) + taskToken := "674f727a81ed2f195bccab036cccf86a182199eb" + tokenHash := auth_model.HashToken(taskToken, task.TokenSalt) + assert.Equal(t, task.TokenHash, tokenHash) + + dstPath := t.TempDir() + u.Path = fmt.Sprintf("%s/%s.git", repo.Owner.UserName, repo.Name) + u.User = url.UserPassword("gitea-actions", taskToken) + + // the git clone will fail + doGitCloneFail(u)(t) + + // add user10 to the list of collaborative owners + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", repo.Owner.UserName, repo.Name), map[string]string{ + "_csrf": GetUserCSRFToken(t, user2Session), + "collaborative_owner": user10.Name, + }) + user2Session.MakeRequest(t, req, http.StatusOK) + + // the git clone will be successful + doGitClone(dstPath, u)(t) + + // remove user10 from the list of collaborative owners + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", repo.Owner.UserName, repo.Name, user10.ID), map[string]string{ + "_csrf": GetUserCSRFToken(t, user2Session), + }) + user2Session.MakeRequest(t, req, http.StatusOK) + + // the git clone will fail + doGitCloneFail(u)(t) + }) +} diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 12e2b18312..2147ef9d0d 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -303,7 +303,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) { RepoID: 1, }) assert.NoError(t, err) - assert.Len(t, branches, 6) + assert.Len(t, branches, 8) // make a broke repository with no branch on database _, err = db.DeleteByBean(t.Context(), git_model.Branch{RepoID: 1}) @@ -320,7 +320,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) { RepoID: 1, }) assert.NoError(t, err) - assert.Len(t, branches, 7) + assert.Len(t, branches, 9) branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{ RepoID: 1, diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go index 9dd47f93e6..59e2131618 100644 --- a/tests/integration/api_repo_file_delete_test.go +++ b/tests/integration/api_repo_file_delete_test.go @@ -20,21 +20,19 @@ import ( func getDeleteFileOptions() *api.DeleteFileOptions { return &api.DeleteFileOptions{ - FileOptionsWithSHA: api.FileOptionsWithSHA{ - FileOptions: api.FileOptions{ - BranchName: "master", - NewBranchName: "master", - Message: "Removing the file new/file.txt", - Author: api.Identity{ - Name: "John Doe", - Email: "johndoe@example.com", - }, - Committer: api.Identity{ - Name: "Jane Doe", - Email: "janedoe@example.com", - }, + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", + FileOptions: api.FileOptions{ + BranchName: "master", + NewBranchName: "master", + Message: "Removing the file new/file.txt", + Author: api.Identity{ + Name: "John Doe", + Email: "johndoe@example.com", + }, + Committer: api.Identity{ + Name: "Jane Doe", + Email: "janedoe@example.com", }, - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", }, } } diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 9a56711da6..6e6aae389f 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -28,21 +28,19 @@ func getUpdateFileOptions() *api.UpdateFileOptions { content := "This is updated text" contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) return &api.UpdateFileOptions{ - FileOptionsWithSHA: api.FileOptionsWithSHA{ - FileOptions: api.FileOptions{ - BranchName: "master", - NewBranchName: "master", - Message: "My update of new/file.txt", - Author: api.Identity{ - Name: "John Doe", - Email: "johndoe@example.com", - }, - Committer: api.Identity{ - Name: "Anne Doe", - Email: "annedoe@example.com", - }, + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", + FileOptions: api.FileOptions{ + BranchName: "master", + NewBranchName: "master", + Message: "My update of new/file.txt", + Author: api.Identity{ + Name: "John Doe", + Email: "johndoe@example.com", + }, + Committer: api.Identity{ + Name: "Anne Doe", + Email: "annedoe@example.com", }, - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", }, ContentBase64: contentEncoded, } @@ -180,6 +178,15 @@ func TestAPIUpdateFile(t *testing.T) { assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL) assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message) + // Test updating a file without SHA (should create the file) + updateFileOptions = getUpdateFileOptions() + updateFileOptions.SHA = "" + req = NewRequestWithJSON(t, "PUT", "/api/v1/repos/user2/repo1/contents/update-create.txt", &updateFileOptions).AddTokenAuth(token2) + resp = MakeRequest(t, req, http.StatusCreated) + DecodeJSON(t, resp, &fileResponse) + assert.Equal(t, "08bd14b2e2852529157324de9c226b3364e76136", fileResponse.Content.SHA) + assert.Equal(t, setting.AppURL+"user2/repo1/raw/branch/master/update-create.txt", *fileResponse.Content.DownloadURL) + // Test updating a file and renaming it updateFileOptions = getUpdateFileOptions() updateFileOptions.BranchName = repo1.DefaultBranch diff --git a/web_src/css/base.css b/web_src/css/base.css index 8b77e55fa3..a09839ea1e 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1104,6 +1104,7 @@ table th[data-sortt-desc] .svg { } .ui.list.flex-items-block > .item, +.ui.vertical.menu.flex-items-block > .item, .ui.form .field > label.flex-text-block, /* override fomantic "block" style */ .flex-items-block > .item, .flex-text-block { diff --git a/web_src/js/features/comp/SearchUserBox.ts b/web_src/js/features/comp/SearchUserBox.ts index 4b13a2141f..9cba18b356 100644 --- a/web_src/js/features/comp/SearchUserBox.ts +++ b/web_src/js/features/comp/SearchUserBox.ts @@ -10,10 +10,11 @@ export function initCompSearchUserBox() { const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true'; const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined; + const includeOrgs = searchUserBox.getAttribute('data-include-orgs') === 'true'; fomanticQuery(searchUserBox).search({ minCharacters: 2, apiSettings: { - url: `${appSubUrl}/user/search_candidates?q={query}`, + url: `${appSubUrl}/user/search_candidates?q={query}&orgs=${includeOrgs}`, onResponse(response: any) { const resultItems = []; const searchQuery = searchUserBox.querySelector('input').value;