1package hooks23import (4 "bytes"5 "context"6 "os"7 "path/filepath"8 "text/template"910 "github.com/charmbracelet/log/v2"11 "github.com/charmbracelet/soft-serve/pkg/config"12 "github.com/charmbracelet/soft-serve/pkg/utils"13)1415// The names of git server-side hooks.16const (17 PreReceiveHook = "pre-receive"18 UpdateHook = "update"19 PostReceiveHook = "post-receive"20 PostUpdateHook = "post-update"21)2223// GenerateHooks generates git server-side hooks for a repository. Currently, it supports the following hooks:24// - pre-receive25// - update26// - post-receive27// - post-update28//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 err36 }3738 for _, hook := range []string{39 PreReceiveHook,40 UpdateHook,41 PostReceiveHook,42 PostUpdateHook,43 } {44 var data bytes.Buffer45 var args string4647 // Hooks script/directory path48 hp := filepath.Join(hooksPath, hook)4950 // Write the hooks primary script51 if err := os.WriteFile(hp, []byte(hookTemplate), os.ModePerm); err != nil { //nolint:gosec52 return err53 }5455 // Create ${hook}.d directory.56 hp += ".d"57 if err := os.MkdirAll(hp, os.ModePerm); err != nil {58 return err59 }6061 switch hook {62 case UpdateHook:63 args = "$1 $2 $3"64 case PostUpdateHook:65 args = "$@"66 }6768 if err := hooksTmpl.Execute(&data, struct {69 Executable string70 Hook string71 Args string72 }{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 continue79 }8081 // 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:gosec84 if err != nil {85 log.WithPrefix("hooks").Error("failed to write hook", "err", err)86 continue87 }88 }8990 return nil91}9293const (94 // hookTemplate allows us to run multiple hooks from a directory. It should95 // support every type of git hook, as it proxies both stdin and arguments.96 hookTemplate = `#!/usr/bin/env bash97# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY98data=$(cat)99exitcodes=""100hookname=$(basename $0)101GIT_DIR=${GIT_DIR:-$(dirname $0)/..}102for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do103 # Avoid running non-executable hooks104 test -x "${hook}" && test -f "${hook}" || continue105106 # Run the actual hook107 echo "${data}" | "${hook}" "$@"108109 # Store the exit code for later use110 exitcodes="${exitcodes} $?"111done112113# Exit on the first non-zero exit code.114for i in ${exitcodes}; do115 [ ${i} -eq 0 ] || exit ${i}116done117`118)119120// hooksTmpl is the soft-serve hook that will be run by the git hooks121// inside the hooks directory.122var hooksTmpl = template.Must(template.New("hooks").Parse(`#!/usr/bin/env bash123# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY124if [ -z "$SOFT_SERVE_REPO_NAME" ]; then125 echo "Warning: SOFT_SERVE_REPO_NAME not defined. Skipping hooks."126 exit 0127fi128{{ .Executable }} hook {{ .Hook }} {{ .Args }}129`))