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