package hooks import ( "embed" "fmt" "io/fs" "os" "path/filepath" ) // templateFS is the checked-in git template directory. It ships real, copy-able // hook scripts (one per git hook name) under template/hooks/. The daemon // materializes it to /template at startup and CreateRepo passes // --template=/template to `git init --bare`, so every new repo gets these // hooks verbatim. This is the "template repo": a versioned, inspectable artifact // rather than something generated from a Go string. // //go:embed template var templateFS embed.FS // TemplateDir returns the on-disk git template directory for root: /template. // CreateRepo passes --template= to `git init --bare`. func TemplateDir(root string) string { return filepath.Join(root, "template") } // EnsureTemplate materializes the embedded template directory to /template // and returns the template dir. Files under hooks/ are made executable (go:embed // does not preserve the executable bit). Idempotent: rewrites every file each // call, so an updated template is picked up on the next daemon start. func EnsureTemplate(root string) (string, error) { dst := TemplateDir(root) if err := os.MkdirAll(dst, 0o755); err != nil { return "", fmt.Errorf("hooks: mkdir template: %w", err) } err := fs.WalkDir(templateFS, "template", func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return os.MkdirAll(filepath.Join(root, p), 0o755) } data, err := templateFS.ReadFile(p) if err != nil { return fmt.Errorf("hooks: read embedded %s: %w", p, err) } out := filepath.Join(root, p) if err := os.WriteFile(out, data, 0o644); err != nil { return fmt.Errorf("hooks: write %s: %w", p, err) } // Hook scripts must be executable; go:embed does not preserve mode bits. if filepath.Dir(p) == "template/hooks" { if err := os.Chmod(out, 0o755); err != nil { return fmt.Errorf("hooks: chmod %s: %w", p, err) } } return nil }) if err != nil { return "", err } return dst, nil }