139 lines
4.0 KiB
Go
139 lines
4.0 KiB
Go
package hooks
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// RunClient implements the `simplegit hook` subcommand invoked by rendered hook
|
|
// scripts. It forwards one hook invocation to the daemon's hook bus over
|
|
// <root>/hook.sock and exits with the daemon's response code. It fails open:
|
|
// any IPC error (no socket, refused, timeout) exits 0 so the git operation is
|
|
// never blocked by the event bus.
|
|
//
|
|
// Usage: simplegit hook --root=PATH <hook-type> [hook-args...]
|
|
//
|
|
// --root is parsed manually (not via flag) so hook args that begin with '-' are
|
|
// passed through verbatim instead of being mistaken for flags.
|
|
func RunClient(args []string) {
|
|
root, rest := parseRoot(args)
|
|
if root == "" || len(rest) < 1 {
|
|
// Malformed invocation (e.g. manual call): fail open, do not break git.
|
|
fmt.Fprintln(os.Stderr, "simplegit hook: usage: simplegit hook --root=PATH <hook-type> [args...]")
|
|
os.Exit(0)
|
|
}
|
|
hookType := rest[0]
|
|
hookArgs := rest[1:]
|
|
|
|
stdin, _ := io.ReadAll(os.Stdin)
|
|
cwd, _ := os.Getwd()
|
|
|
|
ev := HookEvent{
|
|
Type: hookType,
|
|
Repo: resolveRepo(root, os.Getenv("GIT_DIR"), cwd),
|
|
RepoPath: cwd,
|
|
Args: hookArgs,
|
|
Stdin: string(stdin),
|
|
Env: captureGitEnv(),
|
|
}
|
|
os.Exit(forward(ev, SockPath(root)))
|
|
}
|
|
|
|
// parseRoot extracts a --root=PATH (or --root PATH) flag from args and returns
|
|
// it plus the remaining positional args.
|
|
func parseRoot(args []string) (root string, rest []string) {
|
|
for i := 0; i < len(args); i++ {
|
|
a := args[i]
|
|
switch {
|
|
case strings.HasPrefix(a, "--root="):
|
|
root = strings.TrimPrefix(a, "--root=")
|
|
case a == "--root" && i+1 < len(args):
|
|
root = args[i+1]
|
|
i++
|
|
default:
|
|
rest = append(rest, a)
|
|
}
|
|
}
|
|
return root, rest
|
|
}
|
|
|
|
// forward sends ev to the hook socket and returns the daemon's requested exit
|
|
// code. Returns 0 on any error (fail open).
|
|
func forward(ev HookEvent, sock string) int {
|
|
body, err := json.Marshal(ev)
|
|
if err != nil {
|
|
log.Printf("hook %s: marshal: %v", ev.Type, err)
|
|
return 0
|
|
}
|
|
conn, err := net.DialTimeout("unix", sock, 5*time.Second)
|
|
if err != nil {
|
|
// Daemon not running / socket absent: fail open and stay quiet (common in
|
|
// isolated mode or during a daemon restart).
|
|
return 0
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
if _, err := conn.Write(append(body, '\n')); err != nil {
|
|
log.Printf("hook %s: write: %v", ev.Type, err)
|
|
return 0
|
|
}
|
|
reader := bufio.NewReader(conn)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
log.Printf("hook %s: read response: %v", ev.Type, err)
|
|
return 0
|
|
}
|
|
var resp struct {
|
|
Exit int `json:"exit"`
|
|
}
|
|
if err := json.Unmarshal([]byte(line), &resp); err != nil {
|
|
log.Printf("hook %s: parse response: %v", ev.Type, err)
|
|
return 0
|
|
}
|
|
return resp.Exit
|
|
}
|
|
|
|
// resolveRepo turns the hook's repo directory into "ns/name". git sets GIT_DIR
|
|
// for the hook (relative "." under git http-backend / SSH receive-pack, which
|
|
// chdir into the repo); cwd is the fallback when GIT_DIR is unset. Both sides
|
|
// are symlink-resolved so a root passed as /tmp and a cwd that resolves to
|
|
// /private/tmp (macOS) still match. Returns "" if not under <root>/repos.
|
|
func resolveRepo(root, gitDir, cwd string) string {
|
|
dir := gitDir
|
|
if dir == "" || !filepath.IsAbs(dir) {
|
|
dir = filepath.Join(cwd, dir)
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(dir); err == nil {
|
|
dir = resolved
|
|
}
|
|
reposRoot := filepath.Join(root, "repos")
|
|
if resolved, err := filepath.EvalSymlinks(reposRoot); err == nil {
|
|
reposRoot = resolved
|
|
}
|
|
rel, err := filepath.Rel(reposRoot, dir)
|
|
if err != nil || strings.HasPrefix(rel, "..") || rel == "." {
|
|
return ""
|
|
}
|
|
return strings.TrimSuffix(rel, ".git")
|
|
}
|
|
|
|
// captureGitEnv collects GIT_* env vars (GIT_DIR, GIT_PUSH_CERT_*, etc.) so the
|
|
// event consumer sees the context git handed the hook.
|
|
func captureGitEnv() map[string]string {
|
|
env := map[string]string{}
|
|
for _, kv := range os.Environ() {
|
|
if k, v, ok := strings.Cut(kv, "="); ok && strings.HasPrefix(k, "GIT_") {
|
|
env[k] = v
|
|
}
|
|
}
|
|
return env
|
|
}
|