From b6c1bcc008fcff0e297d570a0069bf41bc74e53d Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 9 Jul 2023 14:52:21 +0200 Subject: [PATCH] [CLI] implement forgejo-cli (cherry picked from commit 2555e315f7561302484b15576d34c5da0d4cdb12) (cherry picked from commit 51b9c9092e21a451695ee0154e7d49753574f525) [CLI] implement forgejo-cli (squash) support initDB (cherry picked from commit 5c31ae602a45f1d9a90b86bece5393bc9faddf25) (cherry picked from commit bbf76489a73bad83d68ca7c8e7a75cf8e27b2198) Conflicts: because of d0dbe52e76f3038777c3b50066e3636105387ca3 upgrade to https://pkg.go.dev/github.com/urfave/cli/v2 --- cmd/forgejo/forgejo.go | 145 ++++++++++++++++++++++++++ cmd/main.go | 39 ++++++- tests/integration/cmd_forgejo_test.go | 39 +++++++ 3 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 cmd/forgejo/forgejo.go create mode 100644 tests/integration/cmd_forgejo_test.go diff --git a/cmd/forgejo/forgejo.go b/cmd/forgejo/forgejo.go new file mode 100644 index 0000000000..64c2132130 --- /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/v2" +) + +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.Exit(extra.Error, 1) +} diff --git a/cmd/main.go b/cmd/main.go index e44f9382b7..947fa714ee 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,11 +4,14 @@ package cmd import ( + "context" "fmt" "os" + "path/filepath" "reflect" "strings" + "code.gitea.io/gitea/cmd/forgejo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -58,7 +61,8 @@ func appGlobalFlags() []cli.Flag { return []cli.Flag{ // make the builtin flags at the top helpFlag, - cli.VersionFlag, + // Forgejo: commented out because it would conflict at runtime with the --version + // cli.VersionFlag, // shared configuration flags, they are for global and for each sub-command at the same time // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed @@ -151,6 +155,37 @@ func checkCommandFlags(c any) bool { } func NewMainApp() *cli.App { + 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{} + } 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()), + CmdActions, + } + } + + return newMainApp(subCmds...) +} + +func newMainApp(subCmds ...*cli.Command) *cli.App { app := cli.NewApp() app.EnableBashCompletion = true @@ -169,13 +204,13 @@ func NewMainApp() *cli.App { CmdMigrateStorage, CmdDumpRepository, CmdRestoreRepository, - CmdActions, cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config" } cmdConvert := util.ToPointer(*cmdDoctorConvert) cmdConvert.Hidden = true // still support the legacy "./gitea doctor" by the hidden sub-command, remove it in next release subCmdWithConfig = append(subCmdWithConfig, cmdConvert) + subCmdWithConfig = append(subCmdWithConfig, subCmds...) // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ diff --git a/tests/integration/cmd_forgejo_test.go b/tests/integration/cmd_forgejo_test.go new file mode 100644 index 0000000000..5e331e665a --- /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/v2" +) + +func cmdForgejoCaptureOutput(t *testing.T, args []string, stdin ...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 +}