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