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