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