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 log "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)
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 stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
121 // If the custom hook is executable, run it
122 if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
123 logger.Error("failed to run custom hook", "err", err)
124 }
125 }
126
127 return nil
128 }
129
130 preReceiveCmd = &cobra.Command{
131 Use: "pre-receive",
132 Short: "Run git pre-receive hook",
133 RunE: hooksRunE,
134 }
135
136 updateCmd = &cobra.Command{
137 Use: "update",
138 Short: "Run git update hook",
139 Args: cobra.ExactArgs(3),
140 RunE: hooksRunE,
141 }
142
143 postReceiveCmd = &cobra.Command{
144 Use: "post-receive",
145 Short: "Run git post-receive hook",
146 RunE: hooksRunE,
147 }
148
149 postUpdateCmd = &cobra.Command{
150 Use: "post-update",
151 Short: "Run git post-update hook",
152 RunE: hooksRunE,
153 }
154)
155
156func 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}
165
166func 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 = in
169 cmd.Stdout = out
170 cmd.Stderr = err
171 return cmd.Run()
172}