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
39// setup hooks for all users
40//
41// directory structure is typically like so:
42//
43// did:plc:foobar/repo1
44// did:plc:foobar/repo2
45// did:web:barbaz/repo1
46func Setup(opts ...setupOpt) error {
47 config := config{}
48 for _, o := range opts {
49 o(&config)
50 }
51 // iterate over all directories in current directory:
52 userDirs, err := os.ReadDir(config.scanPath)
53 if err != nil {
54 return err
55 }
56
57 for _, user := range userDirs {
58 if !user.IsDir() {
59 continue
60 }
61
62 did := user.Name()
63 if !strings.HasPrefix(did, "did:") {
64 continue
65 }
66
67 userPath := filepath.Join(config.scanPath, did)
68 if err := setupUser(&config, userPath); err != nil {
69 return err
70 }
71 }
72
73 return nil
74}
75
76// setup hooks in /scanpath/did:plc:user
77func setupUser(config *config, userPath string) error {
78 repos, err := os.ReadDir(userPath)
79 if err != nil {
80 return err
81 }
82
83 for _, repo := range repos {
84 if !repo.IsDir() {
85 continue
86 }
87
88 path := filepath.Join(userPath, repo.Name())
89 if err := setup(config, path); err != nil {
90 if errors.Is(err, ErrNoGitRepo) {
91 continue
92 }
93 return err
94 }
95 }
96
97 return nil
98}
99
100// setup hook in /scanpath/did:plc:user/repo
101func setup(config *config, path string) error {
102 if _, err := git.PlainOpen(path); err != nil {
103 return fmt.Errorf("%s: %w", path, ErrNoGitRepo)
104 }
105
106 preReceiveD := filepath.Join(path, "hooks", "post-receive.d")
107 if err := os.MkdirAll(preReceiveD, 0755); err != nil {
108 return fmt.Errorf("%s: %w", preReceiveD, ErrCreatingHookDir)
109 }
110
111 notify := filepath.Join(preReceiveD, "40-notify.sh")
112 if err := mkHook(config, notify); err != nil {
113 return fmt.Errorf("%s: %w", notify, ErrCreatingHook)
114 }
115
116 delegate := filepath.Join(path, "hooks", "post-receive")
117 if err := mkDelegate(delegate); err != nil {
118 return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate)
119 }
120
121 return nil
122}
123
124func mkHook(config *config, hookPath string) error {
125 executablePath, err := os.Executable()
126 if err != nil {
127 return err
128 }
129
130 hookContent := fmt.Sprintf(`#!/usr/bin/env bash
131# AUTO GENERATED BY KNOT, DO NOT MODIFY
132%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" post-recieve
133 `, executablePath, config.internalApi)
134
135 return os.WriteFile(hookPath, []byte(hookContent), 0755)
136}
137
138func mkDelegate(path string) error {
139 content := fmt.Sprintf(`#!/usr/bin/env bash
140# AUTO GENERATED BY KNOT, DO NOT MODIFY
141data=$(cat)
142exitcodes=""
143hookname=$(basename $0)
144GIT_DIR="$PWD"
145
146for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
147 test -x "${hook}" && test -f "${hook}" || continue
148 echo "${data}" | "${hook}"
149 exitcodes="${exitcodes} $?"
150done
151
152for i in ${exitcodes}; do
153 [ ${i} -eq 0 ] || exit ${i}
154done
155 `)
156
157 return os.WriteFile(path, []byte(content), 0755)
158}