From e75b2285762f189b3dc162396be28f514c1a3c1c Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 23 Nov 2025 12:39:11 -0800 Subject: [PATCH] Add tests and documentation for --shared hooks feature - Fix existing tests to work with new installHooks signature - Add TestInstallHooksShared to verify shared hooks functionality - Update git-hooks README with comprehensive --shared documentation - Document benefits, use cases, and workflow for shared hooks --- cmd/bd/hooks_test.go | 68 +++++++++++++++++++++++++++++++++--- examples/git-hooks/README.md | 30 ++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/cmd/bd/hooks_test.go b/cmd/bd/hooks_test.go index 73f7f93d..66ef7c0f 100644 --- a/cmd/bd/hooks_test.go +++ b/cmd/bd/hooks_test.go @@ -2,6 +2,7 @@ package main import ( "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -50,7 +51,7 @@ func TestInstallHooks(t *testing.T) { } // Install hooks - if err := installHooks(hooks, false); err != nil { + if err := installHooks(hooks, false, false); err != nil { t.Fatalf("installHooks() failed: %v", err) } @@ -103,7 +104,7 @@ func TestInstallHooksBackup(t *testing.T) { } // Install hooks (should backup existing) - if err := installHooks(hooks, false); err != nil { + if err := installHooks(hooks, false, false); err != nil { t.Fatalf("installHooks() failed: %v", err) } @@ -149,7 +150,7 @@ func TestInstallHooksForce(t *testing.T) { } // Install hooks with force (should not create backup) - if err := installHooks(hooks, true); err != nil { + if err := installHooks(hooks, true, false); err != nil { t.Fatalf("installHooks() failed: %v", err) } @@ -178,7 +179,7 @@ func TestUninstallHooks(t *testing.T) { if err != nil { t.Fatalf("getEmbeddedHooks() failed: %v", err) } - if err := installHooks(hooks, false); err != nil { + if err := installHooks(hooks, false, false); err != nil { t.Fatalf("installHooks() failed: %v", err) } @@ -224,7 +225,7 @@ func TestHooksCheckGitHooks(t *testing.T) { if err != nil { t.Fatalf("getEmbeddedHooks() failed: %v", err) } - if err := installHooks(hooks, false); err != nil { + if err := installHooks(hooks, false, false); err != nil { t.Fatalf("installHooks() failed: %v", err) } @@ -243,3 +244,60 @@ func TestHooksCheckGitHooks(t *testing.T) { } } } + +func TestInstallHooksShared(t *testing.T) { + // Create temp directory + tmpDir := t.TempDir() + + // Change to temp directory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + // Initialize a real git repo (needed for git config command) + if err := exec.Command("git", "init").Run(); err != nil { + t.Skipf("Skipping test: git init failed (git may not be available): %v", err) + } + + // Get embedded hooks + hooks, err := getEmbeddedHooks() + if err != nil { + t.Fatalf("getEmbeddedHooks() failed: %v", err) + } + + // Install hooks in shared mode + if err := installHooks(hooks, false, true); err != nil { + t.Fatalf("installHooks() with shared=true failed: %v", err) + } + + // Verify hooks were installed to .beads-hooks/ + sharedHooksDir := ".beads-hooks" + for hookName := range hooks { + hookPath := filepath.Join(sharedHooksDir, hookName) + if _, err := os.Stat(hookPath); os.IsNotExist(err) { + t.Errorf("Hook %s was not installed to .beads-hooks/", hookName) + } + // Windows does not support POSIX executable bits, so skip the check there. + if runtime.GOOS == "windows" { + continue + } + + info, err := os.Stat(hookPath) + if err != nil { + t.Errorf("Failed to stat %s: %v", hookName, err) + continue + } + if info.Mode()&0111 == 0 { + t.Errorf("Hook %s is not executable", hookName) + } + } + + // Verify hooks were NOT installed to .git/hooks/ + standardHooksDir := filepath.Join(".git", "hooks") + for hookName := range hooks { + hookPath := filepath.Join(standardHooksDir, hookName) + if _, err := os.Stat(hookPath); !os.IsNotExist(err) { + t.Errorf("Hook %s should not be in .git/hooks/ when using --shared", hookName) + } + } +} diff --git a/examples/git-hooks/README.md b/examples/git-hooks/README.md index 803775c9..0ae209c5 100644 --- a/examples/git-hooks/README.md +++ b/examples/git-hooks/README.md @@ -51,6 +51,36 @@ The installer will: - Make them executable - Detect and preserve existing hooks +### Shared Hooks for Teams (New in v0.24.3) + +For teams that need to share hooks across members (especially when using pre-built containers or CI/CD): + +```bash +bd hooks install --shared +``` + +This installs hooks to `.beads-hooks/` (a versioned directory) instead of `.git/hooks/`, and configures git to use them via `git config core.hooksPath .beads-hooks`. + +**Benefits:** +- ✅ Hooks are versioned and can be committed to your repository +- ✅ Team members get hooks automatically when they clone/pull +- ✅ Security teams can scan and audit hook contents before deployment +- ✅ Works with pre-built containers (hooks are already in the repo) +- ✅ Hooks stay in sync when you run `bd hooks install --shared` after upgrades + +**Use cases:** +- Teams building containers in CI that need hooks pre-installed +- Organizations requiring security scanning of all code (including hooks) +- Projects where consistent tooling across team members is critical +- Devcontainer workflows where bd is installed during container build + +After running `bd hooks install --shared`, commit `.beads-hooks/` to your repository: + +```bash +git add .beads-hooks/ +git commit -m "Add bd git hooks for team" +``` + ### Manual Install ```bash