1package hooks
  2
  3import (
  4	"bytes"
  5	"context"
  6	"os"
  7	"path/filepath"
  8	"text/template"
  9
 10	"github.com/charmbracelet/log"
 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`))