From d931f81427884bbbcb7f06ed0eb2beba6e35e058 Mon Sep 17 00:00:00 2001 From: Ismar Date: Tue, 13 Jan 2026 02:29:57 +0100 Subject: [PATCH] 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 --- cmd/bd/doctor/fix/hooks.go | 15 +++++++++++++-- cmd/bd/doctor/fix/hooks_test.go | 10 ++++++++++ cmd/bd/init_git_hooks.go | 28 +++++++++++++++++----------- cmd/bd/init_hooks_test.go | 26 +++++++++++++------------- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/cmd/bd/doctor/fix/hooks.go b/cmd/bd/doctor/fix/hooks.go index 124f13ea..c7e72e63 100644 --- a/cmd/bd/doctor/fix/hooks.go +++ b/cmd/bd/doctor/fix/hooks.go @@ -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 } diff --git a/cmd/bd/doctor/fix/hooks_test.go b/cmd/bd/doctor/fix/hooks_test.go index c4c240c1..2ed7366f 100644 --- a/cmd/bd/doctor/fix/hooks_test.go +++ b/cmd/bd/doctor/fix/hooks_test.go @@ -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", diff --git a/cmd/bd/init_git_hooks.go b/cmd/bd/init_git_hooks.go index 40afbf24..82559e22 100644 --- a/cmd/bd/init_git_hooks.go +++ b/cmd/bd/init_git_hooks.go @@ -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) } diff --git a/cmd/bd/init_hooks_test.go b/cmd/bd/init_hooks_test.go index 57c51cbc..fb0ee9b3 100644 --- a/cmd/bd/init_hooks_test.go +++ b/cmd/bd/init_hooks_test.go @@ -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) } }) }