+2
cmd/knot/main.go
+2
cmd/knot/main.go
···
6
6
7
7
"github.com/urfave/cli/v3"
8
8
"tangled.sh/tangled.sh/core/guard"
9
+
"tangled.sh/tangled.sh/core/hook"
9
10
"tangled.sh/tangled.sh/core/keyfetch"
10
11
"tangled.sh/tangled.sh/core/knotserver"
11
12
"tangled.sh/tangled.sh/core/log"
···
19
20
guard.Command(),
20
21
knotserver.Command(),
21
22
keyfetch.Command(),
23
+
hook.Command(),
22
24
},
23
25
}
24
26
+1
-1
hook/hook.go
+1
-1
hook/hook.go
···
58
58
59
59
client := &http.Client{}
60
60
61
-
req, err := http.NewRequest("POST", endpoint+"/hooks/post-receive", strings.NewReader(payload))
61
+
req, err := http.NewRequest("POST", "http://"+endpoint+"/hooks/post-receive", strings.NewReader(payload))
62
62
if err != nil {
63
63
return fmt.Errorf("failed to create request: %w", err)
64
64
}
+158
hook/setup.go
+158
hook/setup.go
···
1
+
// heavily inspired by gitea's model
2
+
3
+
package hook
4
+
5
+
import (
6
+
"errors"
7
+
"fmt"
8
+
"os"
9
+
"path/filepath"
10
+
"strings"
11
+
12
+
"github.com/go-git/go-git/v5"
13
+
)
14
+
15
+
var ErrNoGitRepo = errors.New("not a git repo")
16
+
var ErrCreatingHookDir = errors.New("failed to create hooks directory")
17
+
var ErrCreatingHook = errors.New("failed to create hook")
18
+
var ErrCreatingDelegate = errors.New("failed to create delegate hook")
19
+
20
+
type config struct {
21
+
scanPath string
22
+
internalApi string
23
+
}
24
+
25
+
type setupOpt func(*config)
26
+
27
+
func WithScanPath(scanPath string) setupOpt {
28
+
return func(c *config) {
29
+
c.scanPath = scanPath
30
+
}
31
+
}
32
+
33
+
func 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
46
+
func 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
77
+
func 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
101
+
func 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", "pre-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", "pre-receive")
117
+
if err := mkDelegate(delegate); err != nil {
118
+
return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate)
119
+
}
120
+
121
+
return nil
122
+
}
123
+
124
+
func 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
+
138
+
func mkDelegate(path string) error {
139
+
content := fmt.Sprintf(`#!/usr/bin/env bash
140
+
# AUTO GENERATED BY KNOT, DO NOT MODIFY
141
+
data=$(cat)
142
+
exitcodes=""
143
+
hookname=$(basename $0)
144
+
GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
145
+
146
+
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
147
+
test -x "${hook}" && test -f "${hook}" || continue
148
+
echo "${data}" | "${hook}"
149
+
exitcodes="${exitcodes} $?"
150
+
done
151
+
152
+
for i in ${exitcodes}; do
153
+
[ ${i} -eq 0 ] || exit ${i}
154
+
done
155
+
`)
156
+
157
+
return os.WriteFile(path, []byte(content), 0755)
158
+
}
+10
knotserver/server.go
+10
knotserver/server.go
···
7
7
8
8
"github.com/urfave/cli/v3"
9
9
"tangled.sh/tangled.sh/core/api/tangled"
10
+
"tangled.sh/tangled.sh/core/hook"
10
11
"tangled.sh/tangled.sh/core/jetstream"
11
12
"tangled.sh/tangled.sh/core/knotserver/config"
12
13
"tangled.sh/tangled.sh/core/knotserver/db"
···
43
44
if err != nil {
44
45
return fmt.Errorf("failed to load config: %w", err)
45
46
}
47
+
48
+
err = hook.Setup(
49
+
hook.WithScanPath(c.Repo.ScanPath),
50
+
hook.WithInternalApi(c.Server.InternalListenAddr),
51
+
)
52
+
if err != nil {
53
+
return fmt.Errorf("failed to setup hooks: %w", err)
54
+
}
55
+
l.Info("successfully finished setting up hooks")
46
56
47
57
if c.Server.Dev {
48
58
l.Info("running in dev mode, signature verification is disabled")