1package main
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"context"
  7	"fmt"
  8	"io"
  9	"os"
 10	"os/exec"
 11	"path/filepath"
 12	"strings"
 13
 14	"github.com/charmbracelet/soft-serve/server/backend"
 15	"github.com/charmbracelet/soft-serve/server/config"
 16	"github.com/charmbracelet/soft-serve/server/hooks"
 17	"github.com/spf13/cobra"
 18)
 19
 20var (
 21	// Deprecated: this flag is ignored.
 22	configPath string
 23
 24	hookCmd = &cobra.Command{
 25		Use:                "hook",
 26		Short:              "Run git server hooks",
 27		Long:               "Handles Soft Serve git server hooks.",
 28		Hidden:             true,
 29		PersistentPreRunE:  initBackendContext,
 30		PersistentPostRunE: closeDBContext,
 31	}
 32
 33	// Git hooks read the config from the environment, based on
 34	// $SOFT_SERVE_DATA_PATH. We already parse the config when the binary
 35	// starts, so we don't need to do it again.
 36	// The --config flag is now deprecated.
 37	hooksRunE = func(cmd *cobra.Command, args []string) error {
 38		ctx := cmd.Context()
 39		hks := backend.FromContext(ctx)
 40		cfg := config.FromContext(ctx)
 41
 42		// This is set in the server before invoking git-receive-pack/git-upload-pack
 43		repoName := os.Getenv("SOFT_SERVE_REPO_NAME")
 44
 45		stdin := cmd.InOrStdin()
 46		stdout := cmd.OutOrStdout()
 47		stderr := cmd.ErrOrStderr()
 48
 49		cmdName := cmd.Name()
 50		customHookPath := filepath.Join(cfg.DataPath, "hooks", cmdName)
 51
 52		var buf bytes.Buffer
 53		opts := make([]hooks.HookArg, 0)
 54
 55		switch cmdName {
 56		case hooks.PreReceiveHook, hooks.PostReceiveHook:
 57			scanner := bufio.NewScanner(stdin)
 58			for scanner.Scan() {
 59				buf.Write(scanner.Bytes())
 60				fields := strings.Fields(scanner.Text())
 61				if len(fields) != 3 {
 62					return fmt.Errorf("invalid hook input: %s", scanner.Text())
 63				}
 64				opts = append(opts, hooks.HookArg{
 65					OldSha:  fields[0],
 66					NewSha:  fields[1],
 67					RefName: fields[2],
 68				})
 69			}
 70
 71			switch cmdName {
 72			case hooks.PreReceiveHook:
 73				hks.PreReceive(ctx, stdout, stderr, repoName, opts)
 74			case hooks.PostReceiveHook:
 75				hks.PostReceive(ctx, stdout, stderr, repoName, opts)
 76			}
 77		case hooks.UpdateHook:
 78			if len(args) != 3 {
 79				return fmt.Errorf("invalid update hook input: %s", args)
 80			}
 81
 82			hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{
 83				OldSha:  args[0],
 84				NewSha:  args[1],
 85				RefName: args[2],
 86			})
 87		case hooks.PostUpdateHook:
 88			hks.PostUpdate(ctx, stdout, stderr, repoName, args...)
 89		}
 90
 91		// Custom hooks
 92		if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
 93			// If the custom hook is executable, run it
 94			if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
 95				return fmt.Errorf("failed to run custom hook: %w", err)
 96			}
 97		}
 98
 99		return nil
100	}
101
102	preReceiveCmd = &cobra.Command{
103		Use:   "pre-receive",
104		Short: "Run git pre-receive hook",
105		RunE:  hooksRunE,
106	}
107
108	updateCmd = &cobra.Command{
109		Use:   "update",
110		Short: "Run git update hook",
111		Args:  cobra.ExactArgs(3),
112		RunE:  hooksRunE,
113	}
114
115	postReceiveCmd = &cobra.Command{
116		Use:   "post-receive",
117		Short: "Run git post-receive hook",
118		RunE:  hooksRunE,
119	}
120
121	postUpdateCmd = &cobra.Command{
122		Use:   "post-update",
123		Short: "Run git post-update hook",
124		RunE:  hooksRunE,
125	}
126)
127
128func init() {
129	hookCmd.PersistentFlags().StringVar(&configPath, "config", "", "path to config file (deprecated)")
130	hookCmd.AddCommand(
131		preReceiveCmd,
132		updateCmd,
133		postReceiveCmd,
134		postUpdateCmd,
135	)
136}
137
138func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, name string, args ...string) error {
139	cmd := exec.CommandContext(ctx, name, args...)
140	cmd.Stdin = in
141	cmd.Stdout = out
142	cmd.Stderr = err
143	return cmd.Run()
144}
145
146const updateHookExample = `#!/bin/sh
147#
148# An example hook script to echo information about the push
149# and send it to the client.
150#
151# To enable this hook, rename this file to "update" and make it executable.
152
153refname="$1"
154oldrev="$2"
155newrev="$3"
156
157# Safety check
158if [ -z "$GIT_DIR" ]; then
159        echo "Don't run this script from the command line." >&2
160        echo " (if you want, you could supply GIT_DIR then run" >&2
161        echo "  $0 <ref> <oldrev> <newrev>)" >&2
162        exit 1
163fi
164
165if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
166        echo "usage: $0 <ref> <oldrev> <newrev>" >&2
167        exit 1
168fi
169
170# Check types
171# if $newrev is 0000...0000, it's a commit to delete a ref.
172zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
173if [ "$newrev" = "$zero" ]; then
174        newrev_type=delete
175else
176        newrev_type=$(git cat-file -t $newrev)
177fi
178
179echo "Hi from Soft Serve update hook!"
180echo
181echo "Repository: $SOFT_SERVE_REPO_NAME"
182echo "RefName: $refname"
183echo "Change Type: $newrev_type"
184echo "Old SHA1: $oldrev"
185echo "New SHA1: $newrev"
186
187exit 0
188`