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 "github.com/charmbracelet/log"
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 stdin := cmd.InOrStdin()
69 stdout := cmd.OutOrStdout()
70 stderr := cmd.ErrOrStderr()
71
72 cmdName := cmd.Name()
73 customHookPath := filepath.Join(cfg.DataPath, "hooks", cmdName)
74
75 var buf bytes.Buffer
76 opts := make([]hooks.HookArg, 0)
77
78 switch cmdName {
79 case hooks.PreReceiveHook, hooks.PostReceiveHook:
80 scanner := bufio.NewScanner(stdin)
81 for scanner.Scan() {
82 buf.Write(scanner.Bytes())
83 fields := strings.Fields(scanner.Text())
84 if len(fields) != 3 {
85 return fmt.Errorf("invalid hook input: %s", scanner.Text())
86 }
87 opts = append(opts, hooks.HookArg{
88 OldSha: fields[0],
89 NewSha: fields[1],
90 RefName: fields[2],
91 })
92 }
93
94 switch cmdName {
95 case hooks.PreReceiveHook:
96 hks.PreReceive(ctx, stdout, stderr, repoName, opts)
97 case hooks.PostReceiveHook:
98 hks.PostReceive(ctx, stdout, stderr, repoName, opts)
99 }
100 case hooks.UpdateHook:
101 if len(args) != 3 {
102 return fmt.Errorf("invalid update hook input: %s", args)
103 }
104
105 hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{
106 RefName: args[0],
107 OldSha: args[1],
108 NewSha: args[2],
109 })
110 case hooks.PostUpdateHook:
111 hks.PostUpdate(ctx, stdout, stderr, repoName, args...)
112 }
113
114 // Custom hooks
115 if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
116 // If the custom hook is executable, run it
117 if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
118 return fmt.Errorf("failed to run custom hook: %w", err)
119 }
120 }
121
122 return nil
123 }
124
125 preReceiveCmd = &cobra.Command{
126 Use: "pre-receive",
127 Short: "Run git pre-receive hook",
128 RunE: hooksRunE,
129 }
130
131 updateCmd = &cobra.Command{
132 Use: "update",
133 Short: "Run git update hook",
134 Args: cobra.ExactArgs(3),
135 RunE: hooksRunE,
136 }
137
138 postReceiveCmd = &cobra.Command{
139 Use: "post-receive",
140 Short: "Run git post-receive hook",
141 RunE: hooksRunE,
142 }
143
144 postUpdateCmd = &cobra.Command{
145 Use: "post-update",
146 Short: "Run git post-update hook",
147 RunE: hooksRunE,
148 }
149)
150
151func init() {
152 Command.PersistentFlags().StringVar(&configPath, "config", "", "path to config file (deprecated)")
153 Command.AddCommand(
154 preReceiveCmd,
155 updateCmd,
156 postReceiveCmd,
157 postUpdateCmd,
158 )
159}
160
161func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, name string, args ...string) error {
162 cmd := exec.CommandContext(ctx, name, args...)
163 cmd.Stdin = in
164 cmd.Stdout = out
165 cmd.Stderr = err
166 return cmd.Run()
167}