diff --git a/hook/hook.go b/hook/hook.go --- a/hook/hook.go +++ b/hook/hook.go @@ -58,7 +58,7 @@ client := &http.Client{} - req, err := http.NewRequest("POST", endpoint+"/hooks/post-receive", strings.NewReader(payload)) + req, err := http.NewRequest("POST", "http://"+endpoint+"/hooks/post-receive", strings.NewReader(payload)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/hook/setup.go b/hook/setup.go new file mode 100644 --- /dev/null +++ b/hook/setup.go @@ -0,0 +1,158 @@ +// heavily inspired by gitea's model + +package hook + +import ( + "errors" + "fmt" + "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 + } +} + +// setup hooks for all users +// +// directory structure is typically like so: +// +// did:plc:foobar/repo1 +// did:plc:foobar/repo2 +// did:web:barbaz/repo1 +func Setup(opts ...setupOpt) error { + config := config{} + for _, o := range opts { + o(&config) + } + // iterate over all directories in current directory: + userDirs, err := os.ReadDir(config.scanPath) + if err != nil { + return err + } + + for _, user := range userDirs { + if !user.IsDir() { + continue + } + + did := user.Name() + if !strings.HasPrefix(did, "did:") { + continue + } + + userPath := filepath.Join(config.scanPath, did) + if err := setupUser(&config, userPath); err != nil { + return err + } + } + + return nil +} + +// setup hooks in /scanpath/did:plc:user +func setupUser(config *config, userPath string) error { + repos, err := os.ReadDir(userPath) + if err != nil { + return err + } + + for _, repo := range repos { + if !repo.IsDir() { + continue + } + + path := filepath.Join(userPath, repo.Name()) + if err := setup(config, path); err != nil { + if errors.Is(err, ErrNoGitRepo) { + continue + } + return err + } + } + + return nil +} + +// setup hook in /scanpath/did:plc:user/repo +func setup(config *config, path string) error { + if _, err := git.PlainOpen(path); err != nil { + return fmt.Errorf("%s: %w", path, ErrNoGitRepo) + } + + preReceiveD := filepath.Join(path, "hooks", "pre-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", "pre-receive") + if err := mkDelegate(delegate); err != nil { + return fmt.Errorf("%s: %w", delegate, ErrCreatingDelegate) + } + + return nil +} + +func mkHook(config *config, hookPath string) error { + executablePath, err := os.Executable() + if err != nil { + return err + } + + hookContent := fmt.Sprintf(`#!/usr/bin/env bash +# AUTO GENERATED BY KNOT, DO NOT MODIFY +%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" post-recieve + `, executablePath, config.internalApi) + + return os.WriteFile(hookPath, []byte(hookContent), 0755) +} + +func mkDelegate(path string) error { + content := fmt.Sprintf(`#!/usr/bin/env bash +# AUTO GENERATED BY KNOT, DO NOT MODIFY +data=$(cat) +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)/..} + +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 + `) + + return os.WriteFile(path, []byte(content), 0755) +} diff --git a/knotserver/server.go b/knotserver/server.go --- a/knotserver/server.go +++ b/knotserver/server.go @@ -7,6 +7,7 @@ "github.com/urfave/cli/v3" "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/hook" "tangled.sh/tangled.sh/core/jetstream" "tangled.sh/tangled.sh/core/knotserver/config" "tangled.sh/tangled.sh/core/knotserver/db" @@ -43,6 +44,15 @@ if err != nil { return fmt.Errorf("failed to load config: %w", err) } + + err = hook.Setup( + hook.WithScanPath(c.Repo.ScanPath), + hook.WithInternalApi(c.Server.InternalListenAddr), + ) + if err != nil { + return fmt.Errorf("failed to setup hooks: %w", err) + } + l.Info("successfully finished setting up hooks") if c.Server.Dev { l.Info("running in dev mode, signature verification is disabled") diff --git a/cmd/knot/main.go b/cmd/knot/main.go --- a/cmd/knot/main.go +++ b/cmd/knot/main.go @@ -6,6 +6,7 @@ "github.com/urfave/cli/v3" "tangled.sh/tangled.sh/core/guard" + "tangled.sh/tangled.sh/core/hook" "tangled.sh/tangled.sh/core/keyfetch" "tangled.sh/tangled.sh/core/knotserver" "tangled.sh/tangled.sh/core/log" @@ -19,6 +20,7 @@ guard.Command(), knotserver.Command(), keyfetch.Command(), + hook.Command(), }, }