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