Parent directory

hook.go

4592 bytes
  1package hook
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"context"
  7	"errors"
  8	"fmt"
  9	"io"
 10	"os"
 11	"os/exec"
 12	"path/filepath"
 13	"strings"
 14
 15	"charm.land/log/v2"
 16	"github.com/charmbracelet/soft-serve/cmd"
 17	"github.com/charmbracelet/soft-serve/pkg/backend"
 18	"github.com/charmbracelet/soft-serve/pkg/config"
 19	"github.com/charmbracelet/soft-serve/pkg/hooks"
 20	"github.com/spf13/cobra"
 21)
 22
 23var (
 24	// ErrInternalServerError indicates that an internal server error occurred.
 25	ErrInternalServerError = errors.New("internal server error")
 26
 27	// Deprecated: this flag is ignored.
 28	configPath string
 29
 30	// Command is the hook command.
 31	Command = &cobra.Command{
 32		Use:    "hook",
 33		Short:  "Run git server hooks",
 34		Long:   "Handles Soft Serve git server hooks.",
 35		Hidden: true,
 36		PersistentPreRunE: func(c *cobra.Command, args []string) error {
 37			logger := log.FromContext(c.Context())
 38			if err := cmd.InitBackendContext(c, args); err != nil {
 39				logger.Error("failed to initialize backend context", "err", err)
 40				return ErrInternalServerError
 41			}
 42
 43			return nil
 44		},
 45		PersistentPostRunE: func(c *cobra.Command, args []string) error {
 46			logger := log.FromContext(c.Context())
 47			if err := cmd.CloseDBContext(c, args); err != nil {
 48				logger.Error("failed to close backend", "err", err)
 49				return ErrInternalServerError
 50			}
 51
 52			return nil
 53		},
 54	}
 55
 56	// Git hooks read the config from the environment, based on
 57	// $SOFT_SERVE_DATA_PATH. We already parse the config when the binary
 58	// starts, so we don't need to do it again.
 59	// The --config flag is now deprecated.
 60	hooksRunE = func(cmd *cobra.Command, args []string) error {
 61		ctx := cmd.Context()
 62		hks := backend.FromContext(ctx)
 63		cfg := config.FromContext(ctx)
 64
 65		// This is set in the server before invoking git-receive-pack/git-upload-pack
 66		repoName := os.Getenv("SOFT_SERVE_REPO_NAME")
 67
 68		logger := log.FromContext(ctx).With("repo", repoName)
 69
 70		stdin := cmd.InOrStdin()
 71		stdout := cmd.OutOrStdout()
 72		stderr := cmd.ErrOrStderr()
 73
 74		cmdName := cmd.Name()
 75		customHookPath := filepath.Join(cfg.DataPath, "hooks", cmdName)
 76
 77		var buf bytes.Buffer
 78		opts := make([]hooks.HookArg, 0)
 79
 80		switch cmdName {
 81		case hooks.PreReceiveHook, hooks.PostReceiveHook:
 82			scanner := bufio.NewScanner(stdin)
 83			for scanner.Scan() {
 84				buf.Write(scanner.Bytes())
 85				buf.WriteByte('\n')
 86				fields := strings.Fields(scanner.Text())
 87				if len(fields) != 3 {
 88					logger.Error(fmt.Sprintf("invalid %s hook input", cmdName), "input", scanner.Text())
 89					continue
 90				}
 91				opts = append(opts, hooks.HookArg{
 92					OldSha:  fields[0],
 93					NewSha:  fields[1],
 94					RefName: fields[2],
 95				})
 96			}
 97
 98			switch cmdName {
 99			case hooks.PreReceiveHook:
100				hks.PreReceive(ctx, stdout, stderr, repoName, opts)
101			case hooks.PostReceiveHook:
102				hks.PostReceive(ctx, stdout, stderr, repoName, opts)
103			}
104		case hooks.UpdateHook:
105			if len(args) != 3 {
106				logger.Error("invalid update hook input", "input", args)
107				break
108			}
109
110			hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{
111				RefName: args[0],
112				OldSha:  args[1],
113				NewSha:  args[2],
114			})
115		case hooks.PostUpdateHook:
116			hks.PostUpdate(ctx, stdout, stderr, repoName, args...)
117		}
118
119		// Custom hooks
120		if err := runCustomHook(ctx, customHookPath, &buf, stdout, stderr, args...); err != nil {
121			logger.Error("failed to run custom hook", "err", err)
122			return err
123		}
124
125		return nil
126	}
127
128	preReceiveCmd = &cobra.Command{
129		Use:   "pre-receive",
130		Short: "Run git pre-receive hook",
131		RunE:  hooksRunE,
132	}
133
134	updateCmd = &cobra.Command{
135		Use:   "update",
136		Short: "Run git update hook",
137		Args:  cobra.ExactArgs(3),
138		RunE:  hooksRunE,
139	}
140
141	postReceiveCmd = &cobra.Command{
142		Use:   "post-receive",
143		Short: "Run git post-receive hook",
144		RunE:  hooksRunE,
145	}
146
147	postUpdateCmd = &cobra.Command{
148		Use:   "post-update",
149		Short: "Run git post-update hook",
150		RunE:  hooksRunE,
151	}
152)
153
154func init() {
155	Command.PersistentFlags().StringVar(&configPath, "config", "", "path to config file (deprecated)")
156	Command.AddCommand(
157		preReceiveCmd,
158		updateCmd,
159		postReceiveCmd,
160		postUpdateCmd,
161	)
162}
163
164func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, name string, args ...string) error {
165	cmd := exec.CommandContext(ctx, name, args...)
166	cmd.Stdin = in
167	cmd.Stdout = out
168	cmd.Stderr = err
169	return cmd.Run()
170}
171
172func runCustomHook(ctx context.Context, path string, in io.Reader, out io.Writer, err io.Writer, args ...string) error {
173	stat, statErr := os.Stat(path)
174	if statErr != nil || stat.IsDir() || stat.Mode()&0o111 == 0 {
175		return nil
176	}
177
178	return runCommand(ctx, in, out, err, path, args...)
179}