1package main
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/server/backend"
17 "github.com/charmbracelet/soft-serve/server/config"
18 "github.com/charmbracelet/soft-serve/server/hooks"
19 "github.com/spf13/cobra"
20)
21
22var (
23 // ErrInternalServerError indicates that an internal server error occurred.
24 ErrInternalServerError = errors.New("internal server error")
25
26 // Deprecated: this flag is ignored.
27 configPath string
28
29 hookCmd = &cobra.Command{
30 Use: "hook",
31 Short: "Run git server hooks",
32 Long: "Handles Soft Serve git server hooks.",
33 Hidden: true,
34 PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
35 logger := log.FromContext(cmd.Context())
36 if err := initBackendContext(cmd, args); err != nil {
37 logger.Error("failed to initialize backend context", "err", err)
38 return ErrInternalServerError
39 }
40
41 return nil
42 },
43 PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
44 logger := log.FromContext(cmd.Context())
45 if err := closeDBContext(cmd, args); err != nil {
46 logger.Error("failed to close backend", "err", err)
47 return ErrInternalServerError
48 }
49
50 return nil
51 },
52 }
53
54 // Git hooks read the config from the environment, based on
55 // $SOFT_SERVE_DATA_PATH. We already parse the config when the binary
56 // starts, so we don't need to do it again.
57 // The --config flag is now deprecated.
58 hooksRunE = func(cmd *cobra.Command, args []string) error {
59 ctx := cmd.Context()
60 hks := backend.FromContext(ctx)
61 cfg := config.FromContext(ctx)
62
63 // This is set in the server before invoking git-receive-pack/git-upload-pack
64 repoName := os.Getenv("SOFT_SERVE_REPO_NAME")
65
66 stdin := cmd.InOrStdin()
67 stdout := cmd.OutOrStdout()
68 stderr := cmd.ErrOrStderr()
69
70 cmdName := cmd.Name()
71 customHookPath := filepath.Join(cfg.DataPath, "hooks", cmdName)
72
73 var buf bytes.Buffer
74 opts := make([]hooks.HookArg, 0)
75
76 switch cmdName {
77 case hooks.PreReceiveHook, hooks.PostReceiveHook:
78 scanner := bufio.NewScanner(stdin)
79 for scanner.Scan() {
80 buf.Write(scanner.Bytes())
81 fields := strings.Fields(scanner.Text())
82 if len(fields) != 3 {
83 return fmt.Errorf("invalid hook input: %s", scanner.Text())
84 }
85 opts = append(opts, hooks.HookArg{
86 OldSha: fields[0],
87 NewSha: fields[1],
88 RefName: fields[2],
89 })
90 }
91
92 switch cmdName {
93 case hooks.PreReceiveHook:
94 hks.PreReceive(ctx, stdout, stderr, repoName, opts)
95 case hooks.PostReceiveHook:
96 hks.PostReceive(ctx, stdout, stderr, repoName, opts)
97 }
98 case hooks.UpdateHook:
99 if len(args) != 3 {
100 return fmt.Errorf("invalid update hook input: %s", args)
101 }
102
103 hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{
104 OldSha: args[0],
105 NewSha: args[1],
106 RefName: args[2],
107 })
108 case hooks.PostUpdateHook:
109 hks.PostUpdate(ctx, stdout, stderr, repoName, args...)
110 }
111
112 // Custom hooks
113 if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 {
114 // If the custom hook is executable, run it
115 if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil {
116 return fmt.Errorf("failed to run custom hook: %w", err)
117 }
118 }
119
120 return nil
121 }
122
123 preReceiveCmd = &cobra.Command{
124 Use: "pre-receive",
125 Short: "Run git pre-receive hook",
126 RunE: hooksRunE,
127 }
128
129 updateCmd = &cobra.Command{
130 Use: "update",
131 Short: "Run git update hook",
132 Args: cobra.ExactArgs(3),
133 RunE: hooksRunE,
134 }
135
136 postReceiveCmd = &cobra.Command{
137 Use: "post-receive",
138 Short: "Run git post-receive hook",
139 RunE: hooksRunE,
140 }
141
142 postUpdateCmd = &cobra.Command{
143 Use: "post-update",
144 Short: "Run git post-update hook",
145 RunE: hooksRunE,
146 }
147)
148
149func init() {
150 hookCmd.PersistentFlags().StringVar(&configPath, "config", "", "path to config file (deprecated)")
151 hookCmd.AddCommand(
152 preReceiveCmd,
153 updateCmd,
154 postReceiveCmd,
155 postUpdateCmd,
156 )
157}
158
159func runCommand(ctx context.Context, in io.Reader, out io.Writer, err io.Writer, name string, args ...string) error {
160 cmd := exec.CommandContext(ctx, name, args...)
161 cmd.Stdin = in
162 cmd.Stdout = out
163 cmd.Stderr = err
164 return cmd.Run()
165}
166
167const updateHookExample = `#!/bin/sh
168#
169# An example hook script to echo information about the push
170# and send it to the client.
171#
172# To enable this hook, rename this file to "update" and make it executable.
173
174refname="$1"
175oldrev="$2"
176newrev="$3"
177
178# Safety check
179if [ -z "$GIT_DIR" ]; then
180 echo "Don't run this script from the command line." >&2
181 echo " (if you want, you could supply GIT_DIR then run" >&2
182 echo " $0 <ref> <oldrev> <newrev>)" >&2
183 exit 1
184fi
185
186if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
187 echo "usage: $0 <ref> <oldrev> <newrev>" >&2
188 exit 1
189fi
190
191# Check types
192# if $newrev is 0000...0000, it's a commit to delete a ref.
193zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
194if [ "$newrev" = "$zero" ]; then
195 newrev_type=delete
196else
197 newrev_type=$(git cat-file -t $newrev)
198fi
199
200echo "Hi from Soft Serve update hook!"
201echo
202echo "Repository: $SOFT_SERVE_REPO_NAME"
203echo "RefName: $refname"
204echo "Change Type: $newrev_type"
205echo "Old SHA1: $oldrev"
206echo "New SHA1: $newrev"
207
208exit 0
209`