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