1package hooks
2
3import (
4 "bytes"
5 "context"
6 "os"
7 "path/filepath"
8 "text/template"
9
10 log "github.com/charmbracelet/log/v2"
11 "github.com/charmbracelet/soft-serve/pkg/config"
12 "github.com/charmbracelet/soft-serve/pkg/utils"
13)
14
15// The names of git server-side hooks.
16const (
17 PreReceiveHook = "pre-receive"
18 UpdateHook = "update"
19 PostReceiveHook = "post-receive"
20 PostUpdateHook = "post-update"
21)
22
23// GenerateHooks generates git server-side hooks for a repository. Currently, it supports the following hooks:
24// - pre-receive
25// - update
26// - post-receive
27// - post-update
28//
29// This function should be called by the backend when a repository is created.
30// TODO: support context.
31func GenerateHooks(_ context.Context, cfg *config.Config, repo string) error {
32 repo = utils.SanitizeRepo(repo) + ".git"
33 hooksPath := filepath.Join(cfg.DataPath, "repos", repo, "hooks")
34 if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil {
35 return err
36 }
37
38 for _, hook := range []string{
39 PreReceiveHook,
40 UpdateHook,
41 PostReceiveHook,
42 PostUpdateHook,
43 } {
44 var data bytes.Buffer
45 var args string
46
47 // Hooks script/directory path
48 hp := filepath.Join(hooksPath, hook)
49
50 // Write the hooks primary script
51 if err := os.WriteFile(hp, []byte(hookTemplate), os.ModePerm); err != nil { //nolint:gosec
52 return err
53 }
54
55 // Create ${hook}.d directory.
56 hp += ".d"
57 if err := os.MkdirAll(hp, os.ModePerm); err != nil {
58 return err
59 }
60
61 switch hook {
62 case UpdateHook:
63 args = "$1 $2 $3"
64 case PostUpdateHook:
65 args = "$@"
66 }
67
68 if err := hooksTmpl.Execute(&data, struct {
69 Executable string
70 Hook string
71 Args string
72 }{
73 Executable: "\"${SOFT_SERVE_BIN_PATH}\"",
74 Hook: hook,
75 Args: args,
76 }); err != nil {
77 log.WithPrefix("hooks").Error("failed to execute hook template", "err", err)
78 continue
79 }
80
81 // Write the soft-serve hook inside ${hook}.d directory.
82 hp = filepath.Join(hp, "soft-serve")
83 err := os.WriteFile(hp, data.Bytes(), os.ModePerm) //nolint:gosec
84 if err != nil {
85 log.WithPrefix("hooks").Error("failed to write hook", "err", err)
86 continue
87 }
88 }
89
90 return nil
91}
92
93const (
94 // hookTemplate allows us to run multiple hooks from a directory. It should
95 // support every type of git hook, as it proxies both stdin and arguments.
96 hookTemplate = `#!/usr/bin/env bash
97# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
98data=$(cat)
99exitcodes=""
100hookname=$(basename $0)
101GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
102for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
103 # Avoid running non-executable hooks
104 test -x "${hook}" && test -f "${hook}" || continue
105
106 # Run the actual hook
107 echo "${data}" | "${hook}" "$@"
108
109 # Store the exit code for later use
110 exitcodes="${exitcodes} $?"
111done
112
113# Exit on the first non-zero exit code.
114for i in ${exitcodes}; do
115 [ ${i} -eq 0 ] || exit ${i}
116done
117`
118)
119
120// hooksTmpl is the soft-serve hook that will be run by the git hooks
121// inside the hooks directory.
122var hooksTmpl = template.Must(template.New("hooks").Parse(`#!/usr/bin/env bash
123# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
124if [ -z "$SOFT_SERVE_REPO_NAME" ]; then
125 echo "Warning: SOFT_SERVE_REPO_NAME not defined. Skipping hooks."
126 exit 0
127fi
128{{ .Executable }} hook {{ .Hook }} {{ .Args }}
129`))