diff --git a/cmd/forgejo/forgejo.go b/cmd/forgejo/forgejo.go new file mode 100644 index 0000000000..7b0e17358c --- /dev/null +++ b/cmd/forgejo/forgejo.go @@ -0,0 +1,145 @@ +// Copyright The Forgejo Authors. +// SPDX-License-Identifier: MIT + +package forgejo + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" + "syscall" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli" +) + +type key int + +const ( + noInitKey key = iota + 1 + noExitKey + stdoutKey + stderrKey + stdinKey +) + +func CmdForgejo(ctx context.Context) cli.Command { + return cli.Command{ + Name: "forgejo-cli", + Usage: "Forgejo CLI", + Flags: []cli.Flag{}, + Subcommands: []cli.Command{}, + } +} + +func ContextSetNoInit(ctx context.Context, value bool) context.Context { + return context.WithValue(ctx, noInitKey, value) +} + +func ContextGetNoInit(ctx context.Context) bool { + value, ok := ctx.Value(noInitKey).(bool) + return ok && value +} + +func ContextSetNoExit(ctx context.Context, value bool) context.Context { + return context.WithValue(ctx, noExitKey, value) +} + +func ContextGetNoExit(ctx context.Context) bool { + value, ok := ctx.Value(noExitKey).(bool) + return ok && value +} + +func ContextSetStderr(ctx context.Context, value io.Writer) context.Context { + return context.WithValue(ctx, stderrKey, value) +} + +func ContextGetStderr(ctx context.Context) io.Writer { + value, ok := ctx.Value(stderrKey).(io.Writer) + if !ok { + return os.Stderr + } + return value +} + +func ContextSetStdout(ctx context.Context, value io.Writer) context.Context { + return context.WithValue(ctx, stdoutKey, value) +} + +func ContextGetStdout(ctx context.Context) io.Writer { + value, ok := ctx.Value(stderrKey).(io.Writer) + if !ok { + return os.Stdout + } + return value +} + +func ContextSetStdin(ctx context.Context, value io.Reader) context.Context { + return context.WithValue(ctx, stdinKey, value) +} + +func ContextGetStdin(ctx context.Context) io.Reader { + value, ok := ctx.Value(stdinKey).(io.Reader) + if !ok { + return os.Stdin + } + return value +} + +// copied from ../cmd.go +func initDB(ctx context.Context) error { + setting.MustInstalled() + setting.LoadDBSetting() + setting.InitSQLLoggersForCli(log.INFO) + + if setting.Database.Type == "" { + log.Fatal(`Database settings are missing from the configuration file: %q. +Ensure you are running in the correct environment or set the correct configuration file with -c. +If this is the intended configuration file complete the [database] section.`, setting.CustomConf) + } + if err := db.InitEngine(ctx); err != nil { + return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %w", setting.CustomConf, err) + } + return nil +} + +// copied from ../cmd.go +func installSignals(ctx context.Context) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) + go func() { + // install notify + signalChannel := make(chan os.Signal, 1) + + signal.Notify( + signalChannel, + syscall.SIGINT, + syscall.SIGTERM, + ) + select { + case <-signalChannel: + case <-ctx.Done(): + } + cancel() + signal.Reset() + }() + + return ctx, cancel +} + +func handleCliResponseExtra(ctx context.Context, extra private.ResponseExtra) error { + if false && extra.UserMsg != "" { + if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", extra.UserMsg); err != nil { + panic(err) + } + } + if ContextGetNoExit(ctx) { + return extra.Error + } + return cli.NewExitError(extra.Error, 1) +} diff --git a/main.go b/main.go index 9b561376c3..e61cc85195 100644 --- a/main.go +++ b/main.go @@ -6,13 +6,16 @@ package main // import "code.gitea.io/gitea" import ( + "context" "fmt" "os" + "path/filepath" "runtime" "strings" "time" "code.gitea.io/gitea/cmd" + "code.gitea.io/gitea/cmd/forgejo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -81,6 +84,39 @@ DEFAULT CONFIGURATION: } func main() { + path, err := os.Executable() + if err != nil { + panic(err) + } + executable := filepath.Base(path) + + var subCmds []cli.Command + + // + // If the executable is forgejo-cli, provide a Forgejo specific CLI + // that is NOT compatible with Gitea. + // + if executable == "forgejo-cli" { + subCmds = []cli.Command{ + forgejo.CmdActions(context.Background()), + } + } else { + // + // Otherwise provide a Gitea compatible CLI which includes Forgejo + // specific additions under the forgejo-cli subcommand. It allows + // admins to migration from Gitea to Forgejo by replacing the gitea + // binary and rename it to forgejo if they want. + // + subCmds = []cli.Command{ + forgejo.CmdForgejo(context.Background()), + cmd.CmdActions, + } + } + + mainApp(subCmds...) +} + +func mainApp(subCmds ...cli.Command) { app := cli.NewApp() app.Name = "Gitea" app.Usage = "A painless self-hosted Git service" @@ -104,9 +140,9 @@ func main() { cmd.CmdMigrateStorage, cmd.CmdDumpRepository, cmd.CmdRestoreRepository, - cmd.CmdActions, cmdHelp, // TODO: the "help" sub-command was used to show the more information for "work path" and "custom config", in the future, it should avoid doing so } + subCmdWithIni = append(subCmdWithIni, subCmds...) // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []cli.Command{ cmd.CmdCert, diff --git a/tests/integration/cmd_forgejo_test.go b/tests/integration/cmd_forgejo_test.go new file mode 100644 index 0000000000..79b91f42c3 --- /dev/null +++ b/tests/integration/cmd_forgejo_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "context" + "flag" + "io" + "os" + "strings" + "testing" + + "code.gitea.io/gitea/cmd/forgejo" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func cmdForgejoCaptureOutput(t *testing.T, args []string) (string, error) { + r, w, err := os.Pipe() + assert.NoError(t, err) + set := flag.NewFlagSet("forgejo-cli", 0) + assert.NoError(t, set.Parse(args)) + cliContext := cli.NewContext(&cli.App{Writer: w, ErrWriter: w}, set, nil) + ctx := context.Background() + ctx = forgejo.ContextSetNoInit(ctx, true) + ctx = forgejo.ContextSetNoExit(ctx, true) + ctx = forgejo.ContextSetStdout(ctx, w) + ctx = forgejo.ContextSetStderr(ctx, w) + if len(stdin) > 0 { + ctx = forgejo.ContextSetStdin(ctx, strings.NewReader(strings.Join(stdin, ""))) + } + err = forgejo.CmdForgejo(ctx).Run(cliContext) + w.Close() + var buf bytes.Buffer + io.Copy(&buf, r) + return buf.String(), err +}