forked from tangled.org/core
this repo has no description

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>

Changed files
+171 -1
cmd
knot
hook
knotserver
+2
cmd/knot/main.go
···
"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"
···
guard.Command(),
knotserver.Command(),
keyfetch.Command(),
+
hook.Command(),
},
}
+1 -1
hook/hook.go
···
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)
}
+158
hook/setup.go
···
+
// 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)
+
}
+10
knotserver/server.go
···
"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"
···
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")