forked from
tangled.org/core
Monorepo for Tangled
1// heavily inspired by gitea's model
2
3package hook
4
5import (
6 "errors"
7 "fmt"
8 "io/fs"
9 "log/slog"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "github.com/go-git/go-git/v5"
15)
16
17var ErrNoGitRepo = errors.New("not a git repo")
18var ErrCreatingHookDir = errors.New("failed to create hooks directory")
19var ErrCreatingHook = errors.New("failed to create hook")
20var ErrCreatingDelegate = errors.New("failed to create delegate hook")
21
22type config struct {
23 scanPath string
24 internalApi string
25}
26
27type setupOpt func(*config)
28
29func WithScanPath(scanPath string) setupOpt {
30 return func(c *config) {
31 c.scanPath = scanPath
32 }
33}
34
35func WithInternalApi(api string) setupOpt {
36 return func(c *config) {
37 c.internalApi = api
38 }
39}
40
41func Config(opts ...setupOpt) config {
42 config := config{}
43 for _, o := range opts {
44 o(&config)
45 }
46 return config
47}
48
49// setup hooks for all users
50//
51// directory structure is typically like so:
52//
53// did:plc:repo1
54// did:plc:repo2
55// did:web:repo1
56func Setup(config config) error {
57 // iterate over all directories in current directory:
58 repoDirs, err := os.ReadDir(config.scanPath)
59 if errors.Is(err, fs.ErrNotExist) {
60 return os.MkdirAll(config.scanPath, 0755)
61 }
62 if err != nil {
63 return err
64 }
65
66 for _, repo := range repoDirs {
67 if !repo.IsDir() {
68 continue
69 }
70
71 did := repo.Name()
72 if !strings.HasPrefix(did, "did:") {
73 continue
74 }
75
76 userPath := filepath.Join(config.scanPath, did)
77 if err := SetupRepo(config, userPath); err != nil {
78 if errors.Is(err, ErrNoGitRepo) {
79 slog.Warn("hook setup: skipping non-repo entry", "path", userPath, "err", err)
80 continue
81 }
82 return err
83 }
84 }
85
86 return nil
87}
88
89// setup hook in /scanpath/did:plc:repo
90func SetupRepo(config config, path string) error {
91 if _, err := git.PlainOpen(path); err != nil {
92 return fmt.Errorf("%s: %w", path, ErrNoGitRepo)
93 }
94
95 preReceiveD := filepath.Join(path, "hooks", "post-receive.d")
96 if err := os.MkdirAll(preReceiveD, 0755); err != nil {
97 return fmt.Errorf("%s: %w", preReceiveD, ErrCreatingHookDir)
98 }
99
100 notify := filepath.Join(preReceiveD, "40-notify.sh")
101 if err := mkHook(config, notify); err != nil {
102 return fmt.Errorf("%s: %w", notify, ErrCreatingHook)
103 }
104
105 delegate := filepath.Join(path, "hooks", "post-receive")
106 if err := mkDelegate(delegate); err != nil {
107 return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate)
108 }
109
110 return nil
111}
112
113func mkHook(config config, hookPath string) error {
114 executablePath, err := os.Executable()
115 if err != nil {
116 return err
117 }
118
119 hookContent := fmt.Sprintf(`#!/usr/bin/env bash
120# AUTO GENERATED BY KNOT, DO NOT MODIFY
121push_options=()
122for ((i=0; i<GIT_PUSH_OPTION_COUNT; i++)); do
123 option_var="GIT_PUSH_OPTION_$i"
124 push_options+=(-push-option "${!option_var}")
125done
126%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive
127 `, executablePath, config.internalApi)
128
129 return os.WriteFile(hookPath, []byte(hookContent), 0755)
130}
131
132func mkDelegate(path string) error {
133 content := fmt.Sprintf(`#!/usr/bin/env bash
134# AUTO GENERATED BY KNOT, DO NOT MODIFY
135data=$(cat)
136exitcodes=""
137hookname=$(basename $0)
138GIT_DIR="$PWD"
139
140for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
141 test -x "${hook}" && test -f "${hook}" || continue
142 echo "${data}" | "${hook}"
143 exitcodes="${exitcodes} $?"
144done
145
146for i in ${exitcodes}; do
147 [ ${i} -eq 0 ] || exit ${i}
148done
149 `)
150
151 return os.WriteFile(path, []byte(content), 0755)
152}