From 935fb85e8b2a5f6a9c9c6c0b78a234abc14df4fe 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 987df92935ecc03e36f1754b7cbe7a81df8f000f)
(cherry picked from commit 78e7eee59ae43b0e38ff5ab4cdf7fbca33cb6789)
---
 cmd/forgejo/forgejo.go                | 124 ++++++++++++++++++++++++++
 main.go                               |  38 +++++++-
 tests/integration/cmd_forgejo_test.go |  39 ++++++++
 3 files changed, 200 insertions(+), 1 deletion(-)
 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..07953e30c8
--- /dev/null
+++ b/cmd/forgejo/forgejo.go
@@ -0,0 +1,124 @@
+// Copyright The Forgejo Authors.
+// SPDX-License-Identifier: MIT
+
+package forgejo
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"code.gitea.io/gitea/modules/private"
+
+	"github.com/urfave/cli"
+)
+
+type key int
+
+const (
+	noInstallSignalsKey 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 ContextSetNoInstallSignals(ctx context.Context, value bool) context.Context {
+	return context.WithValue(ctx, noInstallSignalsKey, value)
+}
+
+func ContextGetNoInstallSignals(ctx context.Context) bool {
+	value, ok := ctx.Value(noInstallSignalsKey).(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
+}
+
+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..20e04d0d03
--- /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.ContextSetNoInstallSignals(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
+}