forked from tangled.org/core
Monorepo for Tangled

knotserver,hook: setup default post-receive hook on startup

this also populates the script with the correct executable path for
the `knot` command.

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by Tangled c5352373 9b2e1362

Changed files
+171 -1
cmd
knot
hook
knotserver
+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
··· 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
··· 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
··· 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")