gen.go

  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`))