This repository has no description
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:repo1
52// did:plc:repo2
53// did:web:repo1
54func Setup(config config) error {
55 // iterate over all directories in current directory:
56 repoDirs, err := os.ReadDir(config.scanPath)
57 if err != nil {
58 return err
59 }
60
61 for _, repo := range repoDirs {
62 if !repo.IsDir() {
63 continue
64 }
65
66 did := repo.Name()
67 if !strings.HasPrefix(did, "did:") {
68 continue
69 }
70
71 userPath := filepath.Join(config.scanPath, did)
72 if err := SetupRepo(config, userPath); err != nil {
73 return err
74 }
75 }
76
77 return nil
78}
79
80// setup hook in /scanpath/did:plc:repo
81func SetupRepo(config config, path string) error {
82 if _, err := git.PlainOpen(path); err != nil {
83 return fmt.Errorf("%s: %w", path, ErrNoGitRepo)
84 }
85
86 preReceiveD := filepath.Join(path, "hooks", "post-receive.d")
87 if err := os.MkdirAll(preReceiveD, 0755); err != nil {
88 return fmt.Errorf("%s: %w", preReceiveD, ErrCreatingHookDir)
89 }
90
91 notify := filepath.Join(preReceiveD, "40-notify.sh")
92 if err := mkHook(config, notify); err != nil {
93 return fmt.Errorf("%s: %w", notify, ErrCreatingHook)
94 }
95
96 delegate := filepath.Join(path, "hooks", "post-receive")
97 if err := mkDelegate(delegate); err != nil {
98 return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate)
99 }
100
101 return nil
102}
103
104func mkHook(config config, hookPath string) error {
105 executablePath, err := os.Executable()
106 if err != nil {
107 return err
108 }
109
110 hookContent := fmt.Sprintf(`#!/usr/bin/env bash
111# AUTO GENERATED BY KNOT, DO NOT MODIFY
112push_options=()
113for ((i=0; i<GIT_PUSH_OPTION_COUNT; i++)); do
114 option_var="GIT_PUSH_OPTION_$i"
115 push_options+=(-push-option "${!option_var}")
116done
117%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive
118 `, executablePath, config.internalApi)
119
120 return os.WriteFile(hookPath, []byte(hookContent), 0755)
121}
122
123func mkDelegate(path string) error {
124 content := fmt.Sprintf(`#!/usr/bin/env bash
125# AUTO GENERATED BY KNOT, DO NOT MODIFY
126data=$(cat)
127exitcodes=""
128hookname=$(basename $0)
129GIT_DIR="$PWD"
130
131for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
132 test -x "${hook}" && test -f "${hook}" || continue
133 echo "${data}" | "${hook}"
134 exitcodes="${exitcodes} $?"
135done
136
137for i in ${exitcodes}; do
138 [ ${i} -eq 0 ] || exit ${i}
139done
140 `)
141
142 return os.WriteFile(path, []byte(content), 0755)
143}