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.
// 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{
{"lefthook", []string{
// YAML variants
@@ -38,6 +40,7 @@ var hookManagerConfigs = []hookManagerConfig{
"lefthook.json", ".lefthook.json", ".config/lefthook.json",
}},
{"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"}},
{"overcommit", []string{".overcommit.yml"}},
{"yorkie", []string{".yorkie"}},
@@ -92,9 +95,12 @@ type hookManagerPattern struct {
}
// 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{
{"lefthook", regexp.MustCompile(`(?i)lefthook`)},
{"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)`)},
{"simple-git-hooks", regexp.MustCompile(`(?i)simple-git-hooks`)},
}
@@ -470,8 +476,13 @@ func checkManagerBdIntegration(name, path string) *HookIntegrationStatus {
return CheckLefthookBdIntegration(path)
case "husky":
return CheckHuskyBdIntegration(path)
case "pre-commit":
return CheckPrecommitBdIntegration(path)
case "pre-commit", "prek":
// 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:
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",
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",
hookContent: "#!/bin/sh\n# simple-git-hooks\nnpm run lint\n",

View File

@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
@@ -12,6 +13,11 @@ import (
"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
func hooksInstalled() bool {
gitDir, err := git.GetGitDir()
@@ -64,12 +70,12 @@ func hooksInstalled() bool {
// hookInfo contains information about an existing hook
type hookInfo struct {
name string
path string
exists bool
isBdHook bool
isPreCommit bool
content string
name string
path string
exists bool
isBdHook bool
isPreCommitFramework bool // true for pre-commit or prek
content string
}
// detectExistingHooks scans for existing git hooks
@@ -91,10 +97,10 @@ func detectExistingHooks() []hookInfo {
hooks[i].exists = true
hooks[i].content = string(content)
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 {
hooks[i].isPreCommit = strings.Contains(hooks[i].content, "pre-commit run") ||
strings.Contains(hooks[i].content, ".pre-commit-config")
hooks[i].isPreCommitFramework = preCommitFrameworkPattern.MatchString(hooks[i].content)
}
}
}
@@ -108,8 +114,8 @@ func promptHookAction(existingHooks []hookInfo) string {
for _, hook := range existingHooks {
if hook.exists && !hook.isBdHook {
hookType := "custom script"
if hook.isPreCommit {
hookType = "pre-commit framework"
if hook.isPreCommitFramework {
hookType = "pre-commit/prek framework"
}
fmt.Printf(" - %s (%s)\n", hook.name, hookType)
}

View File

@@ -24,12 +24,12 @@ func TestDetectExistingHooks(t *testing.T) {
hooksDir := filepath.Join(gitDirPath, "hooks")
tests := []struct {
name string
setupHook string
hookContent string
wantExists bool
wantIsBdHook bool
wantIsPreCommit bool
name string
setupHook string
hookContent string
wantExists bool
wantIsBdHook bool
wantIsPreCommitFramework bool
}{
{
name: "no hook",
@@ -44,11 +44,11 @@ func TestDetectExistingHooks(t *testing.T) {
wantIsBdHook: true,
},
{
name: "pre-commit framework hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# pre-commit framework\npre-commit run",
wantExists: true,
wantIsPreCommit: true,
name: "pre-commit framework hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# pre-commit framework\npre-commit run",
wantExists: true,
wantIsPreCommitFramework: true,
},
{
name: "custom hook",
@@ -90,8 +90,8 @@ func TestDetectExistingHooks(t *testing.T) {
if found.isBdHook != tt.wantIsBdHook {
t.Errorf("isBdHook = %v, want %v", found.isBdHook, tt.wantIsBdHook)
}
if found.isPreCommit != tt.wantIsPreCommit {
t.Errorf("isPreCommit = %v, want %v", found.isPreCommit, tt.wantIsPreCommit)
if found.isPreCommitFramework != tt.wantIsPreCommitFramework {
t.Errorf("isPreCommitFramework = %v, want %v", found.isPreCommitFramework, tt.wantIsPreCommitFramework)
}
})
}