From af9e7d8e9e677b83197f15d7f36521bbb059b206 Mon Sep 17 00:00:00 2001
From: Earl Warren <contact@earl-warren.org>
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
(cherry picked from commit b6c1bcc008fcff0e297d570a0069bf41bc74e53d)
---
 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
+}