Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

at master 4.2 kB View raw
1// heavily inspired by gitea's model 2 3package hook 4 5import ( 6 "errors" 7 "fmt" 8 "io/fs" 9 "log/slog" 10 "os" 11 "path/filepath" 12 "strings" 13 14 "github.com/go-git/go-git/v5" 15) 16 17var ErrNoGitRepo = errors.New("not a git repo") 18var ErrCreatingHookDir = errors.New("failed to create hooks directory") 19var ErrCreatingHook = errors.New("failed to create hook") 20var ErrCreatingDelegate = errors.New("failed to create delegate hook") 21 22type config struct { 23 scanPath string 24 internalApi string 25} 26 27type setupOpt func(*config) 28 29func WithScanPath(scanPath string) setupOpt { 30 return func(c *config) { 31 c.scanPath = scanPath 32 } 33} 34 35func WithInternalApi(api string) setupOpt { 36 return func(c *config) { 37 c.internalApi = api 38 } 39} 40 41func Config(opts ...setupOpt) config { 42 config := config{} 43 for _, o := range opts { 44 o(&config) 45 } 46 return config 47} 48 49// setup hooks for all users 50// 51// directory structure is typically like so: 52// 53// did:plc:repo1 54// did:plc:repo2 55// did:web:repo1 56func Setup(config config) error { 57 // iterate over all directories in current directory: 58 repoDirs, err := os.ReadDir(config.scanPath) 59 if errors.Is(err, fs.ErrNotExist) { 60 return os.MkdirAll(config.scanPath, 0755) 61 } 62 if err != nil { 63 return err 64 } 65 66 for _, repo := range repoDirs { 67 if !repo.IsDir() { 68 continue 69 } 70 71 did := repo.Name() 72 if !strings.HasPrefix(did, "did:") { 73 continue 74 } 75 76 userPath := filepath.Join(config.scanPath, did) 77 if _, err := os.Stat(userPath); errors.Is(err, fs.ErrPermission) { 78 slog.Warn("hook setup: skipping inaccessible repo", "path", userPath) 79 continue 80 } 81 if err := SetupRepo(config, userPath); err != nil { 82 if errors.Is(err, ErrNoGitRepo) { 83 slog.Warn("hook setup: skipping non-repo entry", "path", userPath, "err", err) 84 continue 85 } 86 return err 87 } 88 } 89 90 return nil 91} 92 93// setup hook in /scanpath/did:plc:repo 94func SetupRepo(config config, path string) error { 95 if _, err := git.PlainOpen(path); err != nil { 96 return fmt.Errorf("%s: %w", path, ErrNoGitRepo) 97 } 98 99 preReceiveD := filepath.Join(path, "hooks", "post-receive.d") 100 if err := os.MkdirAll(preReceiveD, 0755); err != nil { 101 return fmt.Errorf("%s: %w", preReceiveD, ErrCreatingHookDir) 102 } 103 104 notify := filepath.Join(preReceiveD, "40-notify.sh") 105 if err := mkHook(config, notify); err != nil { 106 return fmt.Errorf("%s: %w", notify, ErrCreatingHook) 107 } 108 109 delegate := filepath.Join(path, "hooks", "post-receive") 110 if err := mkDelegate(delegate); err != nil { 111 return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate) 112 } 113 114 return nil 115} 116 117func mkHook(config config, hookPath string) error { 118 // use the absolute path to the underlying binary rather than a bare 119 // `knot` lookup. on NixOS, bare `knot` resolves to /run/wrappers/bin/knot 120 // which has restrictive perms (only the git group can exec it), so hooks 121 // running as a virtual UID fail with EACCES. the underlying binary in 122 // /nix/store is world-readable. hooks are regenerated on every deploy 123 // so the store path stays fresh. 124 executablePath, err := os.Executable() 125 if err != nil { 126 return err 127 } 128 129 hookContent := fmt.Sprintf(`#!/usr/bin/env bash 130# AUTO GENERATED BY KNOT, DO NOT MODIFY 131push_options=() 132for ((i=0; i<GIT_PUSH_OPTION_COUNT; i++)); do 133 option_var="GIT_PUSH_OPTION_$i" 134 push_options+=(-push-option "${!option_var}") 135done 136%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive 137 `, executablePath, config.internalApi) 138 139 if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil { 140 return err 141 } 142 // os.WriteFile doesn't change the mode on existing files; chmod explicitly. 143 return os.Chmod(hookPath, 0755) 144} 145 146func mkDelegate(path string) error { 147 content := fmt.Sprintf(`#!/usr/bin/env bash 148# AUTO GENERATED BY KNOT, DO NOT MODIFY 149data=$(cat) 150exitcodes="" 151hookname=$(basename $0) 152GIT_DIR="$PWD" 153 154for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do 155 test -x "${hook}" && test -f "${hook}" || continue 156 echo "${data}" | "${hook}" 157 exitcodes="${exitcodes} $?" 158done 159 160for i in ${exitcodes}; do 161 [ ${i} -eq 0 ] || exit ${i} 162done 163 `) 164 165 if err := os.WriteFile(path, []byte(content), 0755); err != nil { 166 return err 167 } 168 return os.Chmod(path, 0755) 169}