1package hook23import (4 "bufio"5 "bytes"6 "context"7 "errors"8 "fmt"9 "io"10 "os"11 "os/exec"12 "path/filepath"13 "strings"1415 "github.com/charmbracelet/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)2223var (24 // ErrInternalServerError indicates that an internal server error occurred.25 ErrInternalServerError = errors.New("internal server error")2627 // Deprecated: this flag is ignored.28 configPath string2930 // 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 ErrInternalServerError41 }4243 return nil44 },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 ErrInternalServerError50 }5152 return nil53 },54 }5556 // Git hooks read the config from the environment, based on57 // $SOFT_SERVE_DATA_PATH. We already parse the config when the binary58 // 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)6465 // This is set in the server before invoking git-receive-pack/git-upload-pack66 repoName := os.Getenv("SOFT_SERVE_REPO_NAME")6768 logger := log.FromContext(ctx).With("repo", repoName)6970 stdin := cmd.InOrStdin()71 stdout := cmd.OutOrStdout()72 stderr := cmd.ErrOrStderr()7374 cmdName := cmd.Name()75 customHookPath := filepath.Join(cfg.DataPath, "hooks", cmdName)7677 var buf bytes.Buffer78 opts := make([]hooks.HookArg, 0)7980 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 continue90 }91 opts = append(opts, hooks.HookArg{92 OldSha: fields[0],93 NewSha: fields[1],94 RefName: fields[2],95 })96 }9798 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 break108 }109110 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 }118119 // Custom hooks120 if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {121 // If the custom hook is executable, run it122 if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {123 logger.Error("failed to run custom hook", "err", err)124 }125 }126127 return nil128 }129130 preReceiveCmd = &cobra.Command{131 Use: "pre-receive",132 Short: "Run git pre-receive hook",133 RunE: hooksRunE,134 }135136 updateCmd = &cobra.Command{137 Use: "update",138 Short: "Run git update hook",139 Args: cobra.ExactArgs(3),140 RunE: hooksRunE,141 }142143 postReceiveCmd = &cobra.Command{144 Use: "post-receive",145 Short: "Run git post-receive hook",146 RunE: hooksRunE,147 }148149 postUpdateCmd = &cobra.Command{150 Use: "post-update",151 Short: "Run git post-update hook",152 RunE: hooksRunE,153 }154)155156func init() {157 Command.PersistentFlags().StringVar(&configPath, "config", "", "path to config file (deprecated)")158 Command.AddCommand(159 preReceiveCmd,160 updateCmd,161 postReceiveCmd,162 postUpdateCmd,163 )164}165166func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, name string, args ...string) error {167 cmd := exec.CommandContext(ctx, name, args...)168 cmd.Stdin = in169 cmd.Stdout = out170 cmd.Stderr = err171 return cmd.Run()172}