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>
1087 lines
28 KiB
Go
1087 lines
28 KiB
Go
package fix
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestDetectExternalHookManagers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(dir string) error
|
|
expected []string // Expected manager names
|
|
}{
|
|
{
|
|
name: "no hook managers",
|
|
setup: func(dir string) error {
|
|
return nil
|
|
},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "lefthook.yml",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.yml"), []byte("pre-commit:\n"), 0644)
|
|
},
|
|
expected: []string{"lefthook"},
|
|
},
|
|
{
|
|
name: "lefthook.yaml",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.yaml"), []byte("pre-commit:\n"), 0644)
|
|
},
|
|
expected: []string{"lefthook"},
|
|
},
|
|
{
|
|
name: "lefthook.toml",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.toml"), []byte("[pre-commit]\n"), 0644)
|
|
},
|
|
expected: []string{"lefthook"},
|
|
},
|
|
{
|
|
name: "lefthook.json",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.json"), []byte(`{"pre-commit":{}}`), 0644)
|
|
},
|
|
expected: []string{"lefthook"},
|
|
},
|
|
{
|
|
name: ".lefthook.yml (hidden)",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, ".lefthook.yml"), []byte("pre-commit:\n"), 0644)
|
|
},
|
|
expected: []string{"lefthook"},
|
|
},
|
|
{
|
|
name: ".config/lefthook.yml",
|
|
setup: func(dir string) error {
|
|
configDir := filepath.Join(dir, ".config")
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(configDir, "lefthook.yml"), []byte("pre-commit:\n"), 0644)
|
|
},
|
|
expected: []string{"lefthook"},
|
|
},
|
|
{
|
|
name: ".husky directory",
|
|
setup: func(dir string) error {
|
|
return os.MkdirAll(filepath.Join(dir, ".husky"), 0755)
|
|
},
|
|
expected: []string{"husky"},
|
|
},
|
|
{
|
|
name: ".pre-commit-config.yaml",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, ".pre-commit-config.yaml"), []byte("repos:\n"), 0644)
|
|
},
|
|
expected: []string{"pre-commit"},
|
|
},
|
|
{
|
|
name: ".overcommit.yml",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, ".overcommit.yml"), []byte("PreCommit:\n"), 0644)
|
|
},
|
|
expected: []string{"overcommit"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := tt.setup(dir); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
managers := DetectExternalHookManagers(dir)
|
|
|
|
if len(tt.expected) == 0 {
|
|
if len(managers) != 0 {
|
|
t.Errorf("expected no managers, got %v", managers)
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(managers) != len(tt.expected) {
|
|
t.Errorf("expected %d managers, got %d", len(tt.expected), len(managers))
|
|
return
|
|
}
|
|
|
|
for i, exp := range tt.expected {
|
|
if managers[i].Name != exp {
|
|
t.Errorf("expected manager %q, got %q", exp, managers[i].Name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckLefthookBdIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configFile string
|
|
configContent string
|
|
expectConfigured bool
|
|
expectHooksWithBd []string
|
|
expectHooksWithoutBd []string
|
|
expectNotInConfig []string
|
|
}{
|
|
{
|
|
name: "no config",
|
|
configFile: "",
|
|
// No file created
|
|
expectConfigured: false,
|
|
},
|
|
{
|
|
name: "yaml with bd hooks run",
|
|
configFile: "lefthook.yml",
|
|
configContent: `
|
|
pre-commit:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run pre-commit
|
|
post-merge:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run post-merge
|
|
pre-push:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run pre-push
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "yaml with partial bd integration",
|
|
configFile: "lefthook.yml",
|
|
configContent: `
|
|
pre-commit:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run pre-commit
|
|
lint:
|
|
run: eslint .
|
|
post-merge:
|
|
commands:
|
|
bd:
|
|
run: echo "no bd here"
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit"},
|
|
expectHooksWithoutBd: []string{"post-merge"},
|
|
expectNotInConfig: []string{"pre-push"},
|
|
},
|
|
{
|
|
name: "yaml without bd at all",
|
|
configFile: "lefthook.yml",
|
|
configContent: `
|
|
pre-commit:
|
|
commands:
|
|
lint:
|
|
run: eslint .
|
|
`,
|
|
expectConfigured: false,
|
|
expectHooksWithoutBd: []string{"pre-commit"},
|
|
expectNotInConfig: []string{"post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "toml with bd hooks run",
|
|
configFile: "lefthook.toml",
|
|
configContent: `
|
|
[pre-commit.commands.bd]
|
|
run = "bd hooks run pre-commit"
|
|
|
|
[post-merge.commands.bd]
|
|
run = "bd hooks run post-merge"
|
|
|
|
[pre-push.commands.bd]
|
|
run = "bd hooks run pre-push"
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "json with bd hooks run",
|
|
configFile: "lefthook.json",
|
|
configContent: `{
|
|
"pre-commit": {
|
|
"commands": {
|
|
"bd": {
|
|
"run": "bd hooks run pre-commit"
|
|
}
|
|
}
|
|
},
|
|
"post-merge": {
|
|
"commands": {
|
|
"bd": {
|
|
"run": "bd hooks run post-merge"
|
|
}
|
|
}
|
|
},
|
|
"pre-push": {
|
|
"commands": {
|
|
"bd": {
|
|
"run": "bd hooks run pre-push"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "hidden config .lefthook.yml",
|
|
configFile: ".lefthook.yml",
|
|
configContent: `
|
|
pre-commit:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run pre-commit
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit"},
|
|
expectNotInConfig: []string{"post-merge", "pre-push"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
if tt.configFile != "" {
|
|
configPath := filepath.Join(dir, tt.configFile)
|
|
// Handle .config/ subdirectory
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
|
t.Fatalf("failed to create config dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(configPath, []byte(tt.configContent), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
}
|
|
|
|
status := CheckLefthookBdIntegration(dir)
|
|
|
|
if tt.configFile == "" {
|
|
if status != nil {
|
|
t.Errorf("expected nil status for no config, got %+v", status)
|
|
}
|
|
return
|
|
}
|
|
|
|
if status == nil {
|
|
t.Fatal("expected non-nil status")
|
|
}
|
|
|
|
if status.Configured != tt.expectConfigured {
|
|
t.Errorf("Configured: expected %v, got %v", tt.expectConfigured, status.Configured)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksWithBd, tt.expectHooksWithBd) {
|
|
t.Errorf("HooksWithBd: expected %v, got %v", tt.expectHooksWithBd, status.HooksWithBd)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksWithoutBd, tt.expectHooksWithoutBd) {
|
|
t.Errorf("HooksWithoutBd: expected %v, got %v", tt.expectHooksWithoutBd, status.HooksWithoutBd)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksNotInConfig, tt.expectNotInConfig) {
|
|
t.Errorf("HooksNotInConfig: expected %v, got %v", tt.expectNotInConfig, status.HooksNotInConfig)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckHuskyBdIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hooks map[string]string // hookName -> content
|
|
expectConfigured bool
|
|
expectHooksWithBd []string
|
|
expectHooksWithoutBd []string
|
|
expectNotInConfig []string
|
|
}{
|
|
{
|
|
name: "no .husky directory",
|
|
hooks: nil,
|
|
expectConfigured: false,
|
|
},
|
|
{
|
|
name: "all hooks with bd",
|
|
hooks: map[string]string{
|
|
"pre-commit": "#!/bin/sh\nbd hooks run pre-commit\n",
|
|
"post-merge": "#!/bin/sh\nbd hooks run post-merge\n",
|
|
"pre-push": "#!/bin/sh\nbd hooks run pre-push\n",
|
|
},
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "partial bd integration",
|
|
hooks: map[string]string{
|
|
"pre-commit": "#!/bin/sh\nbd hooks run pre-commit\n",
|
|
"post-merge": "#!/bin/sh\necho 'no bd'\n",
|
|
},
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit"},
|
|
expectHooksWithoutBd: []string{"post-merge"},
|
|
expectNotInConfig: []string{"pre-push"},
|
|
},
|
|
{
|
|
name: "no bd at all",
|
|
hooks: map[string]string{
|
|
"pre-commit": "#!/bin/sh\neslint .\n",
|
|
},
|
|
expectConfigured: false,
|
|
expectHooksWithoutBd: []string{"pre-commit"},
|
|
expectNotInConfig: []string{"post-merge", "pre-push"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
if tt.hooks != nil {
|
|
huskyDir := filepath.Join(dir, ".husky")
|
|
if err := os.MkdirAll(huskyDir, 0755); err != nil {
|
|
t.Fatalf("failed to create .husky: %v", err)
|
|
}
|
|
for hookName, content := range tt.hooks {
|
|
if err := os.WriteFile(filepath.Join(huskyDir, hookName), []byte(content), 0755); err != nil {
|
|
t.Fatalf("failed to write hook %s: %v", hookName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
status := CheckHuskyBdIntegration(dir)
|
|
|
|
if tt.hooks == nil {
|
|
if status != nil {
|
|
t.Errorf("expected nil status for no .husky, got %+v", status)
|
|
}
|
|
return
|
|
}
|
|
|
|
if status == nil {
|
|
t.Fatal("expected non-nil status")
|
|
}
|
|
|
|
if status.Configured != tt.expectConfigured {
|
|
t.Errorf("Configured: expected %v, got %v", tt.expectConfigured, status.Configured)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksWithBd, tt.expectHooksWithBd) {
|
|
t.Errorf("HooksWithBd: expected %v, got %v", tt.expectHooksWithBd, status.HooksWithBd)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksWithoutBd, tt.expectHooksWithoutBd) {
|
|
t.Errorf("HooksWithoutBd: expected %v, got %v", tt.expectHooksWithoutBd, status.HooksWithoutBd)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksNotInConfig, tt.expectNotInConfig) {
|
|
t.Errorf("HooksNotInConfig: expected %v, got %v", tt.expectNotInConfig, status.HooksNotInConfig)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckPrecommitBdIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configContent string
|
|
expectNil bool
|
|
expectConfigured bool
|
|
expectHooksWithBd []string
|
|
expectHooksNotInConfig []string
|
|
}{
|
|
{
|
|
name: "no config",
|
|
expectNil: true,
|
|
},
|
|
{
|
|
name: "all hooks with bd",
|
|
configContent: `repos:
|
|
- repo: local
|
|
hooks:
|
|
- id: bd-pre-commit
|
|
entry: bd hooks run pre-commit
|
|
language: system
|
|
stages: [pre-commit]
|
|
- id: bd-post-merge
|
|
entry: bd hooks run post-merge
|
|
language: system
|
|
stages: [post-merge]
|
|
- id: bd-pre-push
|
|
entry: bd hooks run pre-push
|
|
language: system
|
|
stages: [pre-push]
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "only pre-commit hook",
|
|
configContent: `repos:
|
|
- repo: local
|
|
hooks:
|
|
- id: bd-pre-commit
|
|
entry: bd hooks run pre-commit
|
|
language: system
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit"},
|
|
expectHooksNotInConfig: []string{"post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "legacy stage names (pre-3.2.0)",
|
|
configContent: `repos:
|
|
- repo: local
|
|
hooks:
|
|
- id: bd-commit
|
|
entry: bd hooks run pre-commit
|
|
language: system
|
|
stages: [commit]
|
|
- id: bd-push
|
|
entry: bd hooks run pre-push
|
|
language: system
|
|
stages: [push]
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit", "pre-push"},
|
|
expectHooksNotInConfig: []string{"post-merge"},
|
|
},
|
|
{
|
|
name: "no bd hooks at all",
|
|
configContent: `repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v4.5.0
|
|
hooks:
|
|
- id: trailing-whitespace
|
|
- id: end-of-file-fixer
|
|
`,
|
|
expectConfigured: false,
|
|
expectHooksNotInConfig: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "mixed bd and other hooks",
|
|
configContent: `repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v4.5.0
|
|
hooks:
|
|
- id: trailing-whitespace
|
|
- repo: local
|
|
hooks:
|
|
- id: bd-pre-commit
|
|
entry: bd hooks run pre-commit
|
|
language: system
|
|
`,
|
|
expectConfigured: true,
|
|
expectHooksWithBd: []string{"pre-commit"},
|
|
expectHooksNotInConfig: []string{"post-merge", "pre-push"},
|
|
},
|
|
{
|
|
name: "empty repos list",
|
|
configContent: `repos: []
|
|
`,
|
|
expectConfigured: false,
|
|
expectHooksNotInConfig: []string{"pre-commit", "post-merge", "pre-push"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
if tt.configContent != "" {
|
|
configPath := filepath.Join(dir, ".pre-commit-config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(tt.configContent), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
}
|
|
|
|
status := CheckPrecommitBdIntegration(dir)
|
|
|
|
if tt.expectNil {
|
|
if status != nil {
|
|
t.Errorf("expected nil status, got %+v", status)
|
|
}
|
|
return
|
|
}
|
|
|
|
if status == nil {
|
|
t.Fatal("expected non-nil status")
|
|
}
|
|
|
|
if status.Manager != "pre-commit" {
|
|
t.Errorf("Manager: expected 'pre-commit', got %q", status.Manager)
|
|
}
|
|
|
|
if status.Configured != tt.expectConfigured {
|
|
t.Errorf("Configured: expected %v, got %v", tt.expectConfigured, status.Configured)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksWithBd, tt.expectHooksWithBd) {
|
|
t.Errorf("HooksWithBd: expected %v, got %v", tt.expectHooksWithBd, status.HooksWithBd)
|
|
}
|
|
|
|
if !slicesEqual(status.HooksNotInConfig, tt.expectHooksNotInConfig) {
|
|
t.Errorf("HooksNotInConfig: expected %v, got %v", tt.expectHooksNotInConfig, status.HooksNotInConfig)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBdHookPatternMatching(t *testing.T) {
|
|
tests := []struct {
|
|
content string
|
|
matches bool
|
|
}{
|
|
{"bd hooks run pre-commit", true},
|
|
{"bd hooks run pre-commit", true},
|
|
{"bd hooks run post-merge", true},
|
|
{`bd hooks run pre-push "$@"`, true},
|
|
{"if command -v bd; then bd hooks run pre-commit; fi", true},
|
|
{"# bd hooks run is recommended", true},
|
|
// Word boundary tests - should NOT match partial words
|
|
{"kbd hooks runner", false}, // 'kbd' contains 'bd' but is different word
|
|
{"bd_hooks_run", false}, // underscores make different tokens
|
|
{"bd sync", false},
|
|
{"bd export", false},
|
|
{".beads/", false},
|
|
{"eslint .", false},
|
|
{"echo hello", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.content, func(t *testing.T) {
|
|
if got := bdHookPattern.MatchString(tt.content); got != tt.matches {
|
|
t.Errorf("bdHookPattern.MatchString(%q) = %v, want %v", tt.content, got, tt.matches)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectActiveHookManager(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hookContent string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "lefthook signature",
|
|
hookContent: "#!/bin/sh\n# lefthook\nexec lefthook run pre-commit\n",
|
|
expected: "lefthook",
|
|
},
|
|
{
|
|
name: "husky signature",
|
|
hookContent: "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\nnpm test\n",
|
|
expected: "husky",
|
|
},
|
|
{
|
|
name: "pre-commit framework signature",
|
|
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",
|
|
expected: "simple-git-hooks",
|
|
},
|
|
{
|
|
name: "no manager signature",
|
|
hookContent: "#!/bin/sh\necho 'custom hook'\n",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "empty hook",
|
|
hookContent: "",
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Initialize real git repo
|
|
cmd := exec.Command("git", "init")
|
|
cmd.Dir = dir
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("failed to init git repo: %v", err)
|
|
}
|
|
|
|
// Write hook file
|
|
if tt.hookContent != "" {
|
|
hooksDir := filepath.Join(dir, ".git", "hooks")
|
|
hookPath := filepath.Join(hooksDir, "pre-commit")
|
|
if err := os.WriteFile(hookPath, []byte(tt.hookContent), 0755); err != nil {
|
|
t.Fatalf("failed to write hook: %v", err)
|
|
}
|
|
}
|
|
|
|
result := DetectActiveHookManager(dir)
|
|
if result != tt.expected {
|
|
t.Errorf("DetectActiveHookManager() = %q, want %q", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectActiveHookManager_CustomHooksPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Initialize real git repo
|
|
cmd := exec.Command("git", "init")
|
|
cmd.Dir = dir
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("failed to init git repo: %v", err)
|
|
}
|
|
|
|
// Create custom hooks directory outside .git
|
|
customHooksDir := filepath.Join(dir, "my-hooks")
|
|
if err := os.MkdirAll(customHooksDir, 0755); err != nil {
|
|
t.Fatalf("failed to create custom hooks dir: %v", err)
|
|
}
|
|
|
|
// Write hook in custom location with lefthook signature
|
|
hookContent := "#!/bin/sh\n# lefthook\nexec lefthook run pre-commit\n"
|
|
if err := os.WriteFile(filepath.Join(customHooksDir, "pre-commit"), []byte(hookContent), 0755); err != nil {
|
|
t.Fatalf("failed to write hook: %v", err)
|
|
}
|
|
|
|
// Set core.hooksPath via git config
|
|
configCmd := exec.Command("git", "config", "core.hooksPath", "my-hooks")
|
|
configCmd.Dir = dir
|
|
if err := configCmd.Run(); err != nil {
|
|
t.Fatalf("failed to set core.hooksPath: %v", err)
|
|
}
|
|
|
|
result := DetectActiveHookManager(dir)
|
|
if result != "lefthook" {
|
|
t.Errorf("DetectActiveHookManager() with core.hooksPath = %q, want %q", result, "lefthook")
|
|
}
|
|
}
|
|
|
|
func TestHasBdInCommands(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
section interface{}
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "bd hooks run in commands",
|
|
section: map[string]interface{}{
|
|
"commands": map[string]interface{}{
|
|
"bd": map[string]interface{}{
|
|
"run": "bd hooks run pre-commit",
|
|
},
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "no bd in commands",
|
|
section: map[string]interface{}{
|
|
"commands": map[string]interface{}{
|
|
"lint": map[string]interface{}{
|
|
"run": "eslint .",
|
|
},
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "bd mentioned in comment not run field",
|
|
section: map[string]interface{}{
|
|
"commands": map[string]interface{}{
|
|
"lint": map[string]interface{}{
|
|
"run": "eslint .",
|
|
"tags": "bd hooks run should be added",
|
|
},
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "nil section",
|
|
section: nil,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "non-map section",
|
|
section: "string value",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "no commands key",
|
|
section: map[string]interface{}{
|
|
"parallel": true,
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := hasBdInCommands(tt.section)
|
|
if result != tt.expected {
|
|
t.Errorf("hasBdInCommands() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// slicesEqual compares two string slices for equality (order-insensitive)
|
|
func slicesEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
aMap := make(map[string]bool)
|
|
for _, v := range a {
|
|
aMap[v] = true
|
|
}
|
|
for _, v := range b {
|
|
if !aMap[v] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestCheckExternalHookManagerIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(dir string) error
|
|
expectNil bool
|
|
expectManager string
|
|
expectConfigured bool
|
|
expectDetectionOnly bool
|
|
}{
|
|
{
|
|
name: "no managers",
|
|
setup: func(dir string) error {
|
|
return nil
|
|
},
|
|
expectNil: true,
|
|
},
|
|
{
|
|
name: "lefthook with bd integration",
|
|
setup: func(dir string) error {
|
|
config := `pre-commit:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run pre-commit
|
|
`
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.yml"), []byte(config), 0644)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "lefthook",
|
|
expectConfigured: true,
|
|
expectDetectionOnly: false,
|
|
},
|
|
{
|
|
name: "husky with bd integration",
|
|
setup: func(dir string) error {
|
|
huskyDir := filepath.Join(dir, ".husky")
|
|
if err := os.MkdirAll(huskyDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(huskyDir, "pre-commit"), []byte("#!/bin/sh\nbd hooks run pre-commit\n"), 0755)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "husky",
|
|
expectConfigured: true,
|
|
expectDetectionOnly: false,
|
|
},
|
|
{
|
|
name: "pre-commit framework without bd",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, ".pre-commit-config.yaml"), []byte("repos:\n"), 0644)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "pre-commit",
|
|
expectConfigured: false,
|
|
expectDetectionOnly: false, // pre-commit is now fully supported
|
|
},
|
|
{
|
|
name: "pre-commit framework with bd integration",
|
|
setup: func(dir string) error {
|
|
config := `repos:
|
|
- repo: local
|
|
hooks:
|
|
- id: bd-pre-commit
|
|
entry: bd hooks run pre-commit
|
|
language: system
|
|
`
|
|
return os.WriteFile(filepath.Join(dir, ".pre-commit-config.yaml"), []byte(config), 0644)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "pre-commit",
|
|
expectConfigured: true,
|
|
expectDetectionOnly: false,
|
|
},
|
|
{
|
|
name: "unsupported manager (overcommit)",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, ".overcommit.yml"), []byte("PreCommit:\n"), 0644)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "overcommit",
|
|
expectConfigured: false,
|
|
expectDetectionOnly: true,
|
|
},
|
|
{
|
|
name: "lefthook without bd",
|
|
setup: func(dir string) error {
|
|
config := `pre-commit:
|
|
commands:
|
|
lint:
|
|
run: eslint .
|
|
`
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.yml"), []byte(config), 0644)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "lefthook",
|
|
expectConfigured: false,
|
|
expectDetectionOnly: false, // lefthook IS supported, we can verify its config
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := tt.setup(dir); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
result := CheckExternalHookManagerIntegration(dir)
|
|
|
|
if tt.expectNil {
|
|
if result != nil {
|
|
t.Errorf("expected nil, got %+v", result)
|
|
}
|
|
return
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
|
|
if result.Manager != tt.expectManager {
|
|
t.Errorf("Manager: expected %q, got %q", tt.expectManager, result.Manager)
|
|
}
|
|
|
|
if result.Configured != tt.expectConfigured {
|
|
t.Errorf("Configured: expected %v, got %v", tt.expectConfigured, result.Configured)
|
|
}
|
|
|
|
if result.DetectionOnly != tt.expectDetectionOnly {
|
|
t.Errorf("DetectionOnly: expected %v, got %v", tt.expectDetectionOnly, result.DetectionOnly)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultipleManagersDetected(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(dir string) error
|
|
expectManager string
|
|
}{
|
|
{
|
|
name: "lefthook and husky both present - lefthook wins by priority",
|
|
setup: func(dir string) error {
|
|
// Create lefthook config
|
|
config := `pre-commit:
|
|
commands:
|
|
bd:
|
|
run: bd hooks run pre-commit
|
|
`
|
|
if err := os.WriteFile(filepath.Join(dir, "lefthook.yml"), []byte(config), 0644); err != nil {
|
|
return err
|
|
}
|
|
// Create husky directory
|
|
huskyDir := filepath.Join(dir, ".husky")
|
|
if err := os.MkdirAll(huskyDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(huskyDir, "pre-commit"), []byte("#!/bin/sh\nbd hooks run pre-commit\n"), 0755)
|
|
},
|
|
expectManager: "lefthook",
|
|
},
|
|
{
|
|
name: "husky only",
|
|
setup: func(dir string) error {
|
|
huskyDir := filepath.Join(dir, ".husky")
|
|
if err := os.MkdirAll(huskyDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(huskyDir, "pre-commit"), []byte("#!/bin/sh\nbd hooks run pre-commit\n"), 0755)
|
|
},
|
|
expectManager: "husky",
|
|
},
|
|
{
|
|
name: "multiple unsupported managers",
|
|
setup: func(dir string) error {
|
|
// overcommit and yorkie (both unsupported)
|
|
if err := os.WriteFile(filepath.Join(dir, ".overcommit.yml"), []byte("PreCommit:\n"), 0644); err != nil {
|
|
return err
|
|
}
|
|
return os.MkdirAll(filepath.Join(dir, ".yorkie"), 0755)
|
|
},
|
|
expectManager: "overcommit, yorkie", // Falls through to basic status
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := tt.setup(dir); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
result := CheckExternalHookManagerIntegration(dir)
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
|
|
if result.Manager != tt.expectManager {
|
|
t.Errorf("Manager: expected %q, got %q", tt.expectManager, result.Manager)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManagerNames(t *testing.T) {
|
|
tests := []struct {
|
|
managers []ExternalHookManager
|
|
expected string
|
|
}{
|
|
{nil, ""},
|
|
{[]ExternalHookManager{}, ""},
|
|
{[]ExternalHookManager{{Name: "lefthook"}}, "lefthook"},
|
|
{[]ExternalHookManager{{Name: "lefthook"}, {Name: "husky"}}, "lefthook, husky"},
|
|
{[]ExternalHookManager{{Name: "a"}, {Name: "b"}, {Name: "c"}}, "a, b, c"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
result := ManagerNames(tt.managers)
|
|
if result != tt.expected {
|
|
t.Errorf("ManagerNames() = %q, want %q", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckManagerBdIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
managerName string
|
|
setup func(dir string) error
|
|
expectNil bool
|
|
expectManager string
|
|
}{
|
|
{
|
|
name: "unknown manager returns nil",
|
|
managerName: "unknown-manager",
|
|
setup: func(dir string) error { return nil },
|
|
expectNil: true,
|
|
},
|
|
{
|
|
name: "lefthook integration",
|
|
managerName: "lefthook",
|
|
setup: func(dir string) error {
|
|
return os.WriteFile(filepath.Join(dir, "lefthook.yml"), []byte("pre-commit:\n commands:\n bd:\n run: bd hooks run pre-commit\n"), 0644)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "lefthook",
|
|
},
|
|
{
|
|
name: "husky integration",
|
|
managerName: "husky",
|
|
setup: func(dir string) error {
|
|
huskyDir := filepath.Join(dir, ".husky")
|
|
if err := os.MkdirAll(huskyDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(huskyDir, "pre-commit"), []byte("bd hooks run pre-commit"), 0755)
|
|
},
|
|
expectNil: false,
|
|
expectManager: "husky",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := tt.setup(dir); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
result := checkManagerBdIntegration(tt.managerName, dir)
|
|
|
|
if tt.expectNil {
|
|
if result != nil {
|
|
t.Errorf("expected nil, got %+v", result)
|
|
}
|
|
return
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
|
|
if result.Manager != tt.expectManager {
|
|
t.Errorf("Manager: expected %q, got %q", tt.expectManager, result.Manager)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectExternalHookManagers_MultiplePresentInOrder(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Create configs for multiple managers
|
|
// lefthook (priority 0)
|
|
if err := os.WriteFile(filepath.Join(dir, "lefthook.yml"), []byte("pre-commit:\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// husky (priority 1)
|
|
if err := os.MkdirAll(filepath.Join(dir, ".husky"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// pre-commit (priority 2)
|
|
if err := os.WriteFile(filepath.Join(dir, ".pre-commit-config.yaml"), []byte("repos:\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
managers := DetectExternalHookManagers(dir)
|
|
|
|
if len(managers) != 3 {
|
|
t.Fatalf("expected 3 managers, got %d: %v", len(managers), managers)
|
|
}
|
|
|
|
// Verify order matches priority
|
|
expectedOrder := []string{"lefthook", "husky", "pre-commit"}
|
|
for i, exp := range expectedOrder {
|
|
if managers[i].Name != exp {
|
|
t.Errorf("managers[%d]: expected %q, got %q", i, exp, managers[i].Name)
|
|
}
|
|
}
|
|
}
|