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