feat: add prek support as pre-commit alternative (#1040)

prek (https://prek.j178.dev) is a faster Rust-based alternative to
pre-commit that uses the same .pre-commit-config.yaml config files.

Changes:
- Add prek detection pattern in hookManagerPatterns (before pre-commit
  to ensure correct detection since prek hooks may contain 'pre-commit')
- Handle prek in checkManagerBdIntegration using same config parser
- Update init_git_hooks.go to recognize prek-installed hooks
- Rename isPreCommit field to isPreCommitFramework for clarity
- Use regex pattern matching all pre-commit/prek signatures consistently
- Add test cases for prek run and prek hook-impl signatures

Co-authored-by: Ismar Iljazovic <ismar@gmail.com>
This commit is contained in:
Ismar
2026-01-13 02:29:57 +01:00
committed by GitHub
parent a8f7c21a74
commit d931f81427
4 changed files with 53 additions and 26 deletions

View File

@@ -27,6 +27,8 @@ type hookManagerConfig struct {
// hookManagerConfigs defines external hook managers in priority order. // hookManagerConfigs defines external hook managers in priority order.
// See https://lefthook.dev/configuration/ for lefthook config options. // See https://lefthook.dev/configuration/ for lefthook config options.
// Note: prek (https://prek.j178.dev) uses the same config files as pre-commit
// but is a faster Rust-based alternative. We detect both from the same config.
var hookManagerConfigs = []hookManagerConfig{ var hookManagerConfigs = []hookManagerConfig{
{"lefthook", []string{ {"lefthook", []string{
// YAML variants // YAML variants
@@ -38,6 +40,7 @@ var hookManagerConfigs = []hookManagerConfig{
"lefthook.json", ".lefthook.json", ".config/lefthook.json", "lefthook.json", ".lefthook.json", ".config/lefthook.json",
}}, }},
{"husky", []string{".husky"}}, {"husky", []string{".husky"}},
// pre-commit and prek share the same config files; we detect which is active from git hooks
{"pre-commit", []string{".pre-commit-config.yaml", ".pre-commit-config.yml"}}, {"pre-commit", []string{".pre-commit-config.yaml", ".pre-commit-config.yml"}},
{"overcommit", []string{".overcommit.yml"}}, {"overcommit", []string{".overcommit.yml"}},
{"yorkie", []string{".yorkie"}}, {"yorkie", []string{".yorkie"}},
@@ -92,9 +95,12 @@ type hookManagerPattern struct {
} }
// hookManagerPatterns identifies which hook manager installed a git hook (in priority order). // hookManagerPatterns identifies which hook manager installed a git hook (in priority order).
// Note: prek must come before pre-commit since prek hooks may also contain "pre-commit" in paths.
var hookManagerPatterns = []hookManagerPattern{ var hookManagerPatterns = []hookManagerPattern{
{"lefthook", regexp.MustCompile(`(?i)lefthook`)}, {"lefthook", regexp.MustCompile(`(?i)lefthook`)},
{"husky", regexp.MustCompile(`(?i)(\.husky|husky\.sh)`)}, {"husky", regexp.MustCompile(`(?i)(\.husky|husky\.sh)`)},
// prek (https://prek.j178.dev) - faster Rust-based pre-commit alternative
{"prek", regexp.MustCompile(`(?i)(prek\s+run|prek\s+hook-impl)`)},
{"pre-commit", regexp.MustCompile(`(?i)(pre-commit\s+run|\.pre-commit-config|INSTALL_PYTHON|PRE_COMMIT)`)}, {"pre-commit", regexp.MustCompile(`(?i)(pre-commit\s+run|\.pre-commit-config|INSTALL_PYTHON|PRE_COMMIT)`)},
{"simple-git-hooks", regexp.MustCompile(`(?i)simple-git-hooks`)}, {"simple-git-hooks", regexp.MustCompile(`(?i)simple-git-hooks`)},
} }
@@ -470,8 +476,13 @@ func checkManagerBdIntegration(name, path string) *HookIntegrationStatus {
return CheckLefthookBdIntegration(path) return CheckLefthookBdIntegration(path)
case "husky": case "husky":
return CheckHuskyBdIntegration(path) return CheckHuskyBdIntegration(path)
case "pre-commit": case "pre-commit", "prek":
return CheckPrecommitBdIntegration(path) // prek uses the same config format as pre-commit
status := CheckPrecommitBdIntegration(path)
if status != nil {
status.Manager = name // Use the actual detected manager name
}
return status
default: default:
return nil return nil
} }

View File

@@ -586,6 +586,16 @@ func TestDetectActiveHookManager(t *testing.T) {
hookContent: "#!/usr/bin/env bash\n# PRE_COMMIT hook\npre-commit run --all-files\n", hookContent: "#!/usr/bin/env bash\n# PRE_COMMIT hook\npre-commit run --all-files\n",
expected: "pre-commit", expected: "pre-commit",
}, },
{
name: "prek run signature",
hookContent: "#!/bin/sh\nprek run pre-commit\n",
expected: "prek",
},
{
name: "prek hook-impl signature",
hookContent: "#!/usr/bin/env bash\nexec prek hook-impl --hook-type=pre-commit\n",
expected: "prek",
},
{ {
name: "simple-git-hooks signature", name: "simple-git-hooks signature",
hookContent: "#!/bin/sh\n# simple-git-hooks\nnpm run lint\n", hookContent: "#!/bin/sh\n# simple-git-hooks\nnpm run lint\n",

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@@ -12,6 +13,11 @@ import (
"github.com/steveyegge/beads/internal/ui" "github.com/steveyegge/beads/internal/ui"
) )
// preCommitFrameworkPattern matches pre-commit or prek framework hooks.
// Uses same patterns as hookManagerPatterns in doctor/fix/hooks.go for consistency.
// Includes all detection patterns: pre-commit run, prek run/hook-impl, config file refs, and pre-commit env vars.
var preCommitFrameworkPattern = regexp.MustCompile(`(?i)(pre-commit\s+run|prek\s+run|prek\s+hook-impl|\.pre-commit-config|INSTALL_PYTHON|PRE_COMMIT)`)
// hooksInstalled checks if bd git hooks are installed // hooksInstalled checks if bd git hooks are installed
func hooksInstalled() bool { func hooksInstalled() bool {
gitDir, err := git.GetGitDir() gitDir, err := git.GetGitDir()
@@ -68,7 +74,7 @@ type hookInfo struct {
path string path string
exists bool exists bool
isBdHook bool isBdHook bool
isPreCommit bool isPreCommitFramework bool // true for pre-commit or prek
content string content string
} }
@@ -91,10 +97,10 @@ func detectExistingHooks() []hookInfo {
hooks[i].exists = true hooks[i].exists = true
hooks[i].content = string(content) hooks[i].content = string(content)
hooks[i].isBdHook = strings.Contains(hooks[i].content, "bd (beads)") hooks[i].isBdHook = strings.Contains(hooks[i].content, "bd (beads)")
// Only detect pre-commit framework if not a bd hook // Only detect pre-commit/prek framework if not a bd hook
// Use regex for consistency with DetectActiveHookManager patterns
if !hooks[i].isBdHook { if !hooks[i].isBdHook {
hooks[i].isPreCommit = strings.Contains(hooks[i].content, "pre-commit run") || hooks[i].isPreCommitFramework = preCommitFrameworkPattern.MatchString(hooks[i].content)
strings.Contains(hooks[i].content, ".pre-commit-config")
} }
} }
} }
@@ -108,8 +114,8 @@ func promptHookAction(existingHooks []hookInfo) string {
for _, hook := range existingHooks { for _, hook := range existingHooks {
if hook.exists && !hook.isBdHook { if hook.exists && !hook.isBdHook {
hookType := "custom script" hookType := "custom script"
if hook.isPreCommit { if hook.isPreCommitFramework {
hookType = "pre-commit framework" hookType = "pre-commit/prek framework"
} }
fmt.Printf(" - %s (%s)\n", hook.name, hookType) fmt.Printf(" - %s (%s)\n", hook.name, hookType)
} }

View File

@@ -29,7 +29,7 @@ func TestDetectExistingHooks(t *testing.T) {
hookContent string hookContent string
wantExists bool wantExists bool
wantIsBdHook bool wantIsBdHook bool
wantIsPreCommit bool wantIsPreCommitFramework bool
}{ }{
{ {
name: "no hook", name: "no hook",
@@ -48,7 +48,7 @@ func TestDetectExistingHooks(t *testing.T) {
setupHook: "pre-commit", setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# pre-commit framework\npre-commit run", hookContent: "#!/bin/sh\n# pre-commit framework\npre-commit run",
wantExists: true, wantExists: true,
wantIsPreCommit: true, wantIsPreCommitFramework: true,
}, },
{ {
name: "custom hook", name: "custom hook",
@@ -90,8 +90,8 @@ func TestDetectExistingHooks(t *testing.T) {
if found.isBdHook != tt.wantIsBdHook { if found.isBdHook != tt.wantIsBdHook {
t.Errorf("isBdHook = %v, want %v", found.isBdHook, tt.wantIsBdHook) t.Errorf("isBdHook = %v, want %v", found.isBdHook, tt.wantIsBdHook)
} }
if found.isPreCommit != tt.wantIsPreCommit { if found.isPreCommitFramework != tt.wantIsPreCommitFramework {
t.Errorf("isPreCommit = %v, want %v", found.isPreCommit, tt.wantIsPreCommit) t.Errorf("isPreCommitFramework = %v, want %v", found.isPreCommitFramework, tt.wantIsPreCommitFramework)
} }
}) })
} }