Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170// heavily inspired by gitea's model
package hook
import ( "errors" "fmt" "io/fs" "log/slog" "os" "path/filepath" "strings"
"github.com/go-git/go-git/v5")
var ErrNoGitRepo = errors.New("not a git repo")var ErrCreatingHookDir = errors.New("failed to create hooks directory")var ErrCreatingHook = errors.New("failed to create hook")var ErrCreatingDelegate = errors.New("failed to create delegate hook")
type config struct { scanPath string internalApi string}
type setupOpt func(*config)
func WithScanPath(scanPath string) setupOpt { return func(c *config) { c.scanPath = scanPath }}
func WithInternalApi(api string) setupOpt { return func(c *config) { c.internalApi = api }}
func Config(opts ...setupOpt) config { config := config{} for _, o := range opts { o(&config) } return config}
// setup hooks for all users//// directory structure is typically like so://// did:plc:repo1// did:plc:repo2// did:web:repo1func Setup(config config) error { // iterate over all directories in current directory: repoDirs, err := os.ReadDir(config.scanPath) if errors.Is(err, fs.ErrNotExist) { return os.MkdirAll(config.scanPath, 0755) } if err != nil { return err }
for _, repo := range repoDirs { if !repo.IsDir() { continue }
did := repo.Name() if !strings.HasPrefix(did, "did:") { continue }
userPath := filepath.Join(config.scanPath, did) if _, err := os.Stat(userPath); errors.Is(err, fs.ErrPermission) { slog.Warn("hook setup: skipping inaccessible repo", "path", userPath) continue } if err := SetupRepo(config, userPath); err != nil { if errors.Is(err, ErrNoGitRepo) { slog.Warn("hook setup: skipping non-repo entry", "path", userPath, "err", err) continue } return err } }
return nil}
// setup hook in /scanpath/did:plc:repofunc SetupRepo(config config, path string) error { if _, err := git.PlainOpen(path); err != nil { return fmt.Errorf("%s: %w", path, ErrNoGitRepo) }
preReceiveD := filepath.Join(path, "hooks", "post-receive.d") if err := os.MkdirAll(preReceiveD, 0755); err != nil { return fmt.Errorf("%s: %w", preReceiveD, ErrCreatingHookDir) }
notify := filepath.Join(preReceiveD, "40-notify.sh") if err := mkHook(config, notify); err != nil { return fmt.Errorf("%s: %w", notify, ErrCreatingHook) }
delegate := filepath.Join(path, "hooks", "post-receive") if err := mkDelegate(delegate); err != nil { return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate) }
return nil}
func mkHook(config config, hookPath string) error { // use the absolute path to the underlying binary rather than a bare // `knot` lookup. on NixOS, bare `knot` resolves to /run/wrappers/bin/knot // which has restrictive perms (only the git group can exec it), so hooks // running as a virtual UID fail with EACCES. the underlying binary in // /nix/store is world-readable. hooks are regenerated on every deploy // so the store path stays fresh. executablePath, err := os.Executable() if err != nil { return err }
hookContent := fmt.Sprintf(`#!/usr/bin/env bash# AUTO GENERATED BY KNOT, DO NOT MODIFYpush_options=()for ((i=0; i<GIT_PUSH_OPTION_COUNT; i++)); do option_var="GIT_PUSH_OPTION_$i" push_options+=(-push-option "${!option_var}")done%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive `, executablePath, config.internalApi)
if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil { return err } // os.WriteFile doesn't change the mode on existing files; chmod explicitly. return os.Chmod(hookPath, 0755)}
func mkDelegate(path string) error { content := fmt.Sprintf(`#!/usr/bin/env bash# AUTO GENERATED BY KNOT, DO NOT MODIFYdata=$(cat)exitcodes=""hookname=$(basename $0)GIT_DIR="$PWD"
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue echo "${data}" | "${hook}" exitcodes="${exitcodes} $?"done
for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i}done `)
if err := os.WriteFile(path, []byte(content), 0755); err != nil { return err } return os.Chmod(path, 0755)}