feat(config): add BD_NO_INSTALL_HOOKS environment variable to disable git hook installation (#500)
* feat(config): add no-install-hooks config to disable git hook installation Add `no-install-hooks` boolean config that prevents git hook installation during `bd init`. This can be set via: - Environment variable: BD_NO_INSTALL_HOOKS=1 - Global config: ~/.config/bd/config.yaml with `no-install-hooks: true` - Local config: .beads/config.yaml with `no-install-hooks: true` The existing `--skip-hooks` flag continues to work and takes precedence. Default behavior unchanged: hooks install by default. * docs: add no-install-hooks to configuration documentation - Add no-install-hooks to Supported Settings table in CONFIG.md - Add example in config file section - Add "Disabling Hook Installation" section to GIT_INTEGRATION.md with examples for flag, env var, and config file methods
This commit is contained in:
@@ -388,8 +388,8 @@ With --stealth: configures global git settings for invisible beads usage:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're in a git repo and hooks aren't installed
|
// Check if we're in a git repo and hooks aren't installed
|
||||||
// Install by default unless --skip-hooks is passed
|
// Install by default unless --skip-hooks is passed or no-install-hooks config is set
|
||||||
if !skipHooks && isGitRepo() && !hooksInstalled() {
|
if !skipHooks && !config.GetBool("no-install-hooks") && isGitRepo() && !hooksInstalled() {
|
||||||
if err := installGitHooks(); err != nil && !quiet {
|
if err := installGitHooks(); err != nil && !quiet {
|
||||||
yellow := color.New(color.FgYellow).SprintFunc()
|
yellow := color.New(color.FgYellow).SprintFunc()
|
||||||
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", yellow("⚠"), err)
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", yellow("⚠"), err)
|
||||||
|
|||||||
@@ -1041,3 +1041,188 @@ func TestSetupClaudeSettings_NoExistingFile(t *testing.T) {
|
|||||||
t.Error("File should contain bd onboard prompt")
|
t.Error("File should contain bd onboard prompt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestInitSkipHooksWithEnvVar verifies BD_NO_INSTALL_HOOKS=1 prevents hook installation
|
||||||
|
func TestInitSkipHooksWithEnvVar(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Initialize git repo first (hooks only install in git repos)
|
||||||
|
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
|
||||||
|
t.Fatalf("Failed to init git: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable to skip hooks
|
||||||
|
os.Setenv("BD_NO_INSTALL_HOOKS", "1")
|
||||||
|
defer os.Unsetenv("BD_NO_INSTALL_HOOKS")
|
||||||
|
|
||||||
|
// Run bd init
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify .beads directory was created
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
t.Error(".beads directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify git hooks were NOT installed
|
||||||
|
preCommitHook := filepath.Join(tmpDir, ".git", "hooks", "pre-commit")
|
||||||
|
if _, err := os.Stat(preCommitHook); err == nil {
|
||||||
|
t.Error("pre-commit hook should NOT be installed when BD_NO_INSTALL_HOOKS=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
postMergeHook := filepath.Join(tmpDir, ".git", "hooks", "post-merge")
|
||||||
|
if _, err := os.Stat(postMergeHook); err == nil {
|
||||||
|
t.Error("post-merge hook should NOT be installed when BD_NO_INSTALL_HOOKS=1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitSkipHooksWithConfigFile verifies no-install-hooks: true in config prevents hook installation
|
||||||
|
func TestInitSkipHooksWithConfigFile(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Initialize git repo first
|
||||||
|
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
|
||||||
|
t.Fatalf("Failed to init git: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global config directory with no-install-hooks: true
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get user config dir: %v", err)
|
||||||
|
}
|
||||||
|
bdConfigDir := filepath.Join(configDir, "bd")
|
||||||
|
if err := os.MkdirAll(bdConfigDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create config dir: %v", err)
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(bdConfigDir, "config.yaml")
|
||||||
|
|
||||||
|
// Backup existing config if present
|
||||||
|
existingConfig, existingErr := os.ReadFile(configPath)
|
||||||
|
defer func() {
|
||||||
|
if existingErr == nil {
|
||||||
|
os.WriteFile(configPath, existingConfig, 0644)
|
||||||
|
} else {
|
||||||
|
os.Remove(configPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Write test config
|
||||||
|
if err := os.WriteFile(configPath, []byte("no-install-hooks: true\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run bd init
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify .beads directory was created
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
t.Error(".beads directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify git hooks were NOT installed
|
||||||
|
preCommitHook := filepath.Join(tmpDir, ".git", "hooks", "pre-commit")
|
||||||
|
if _, err := os.Stat(preCommitHook); err == nil {
|
||||||
|
t.Error("pre-commit hook should NOT be installed when no-install-hooks: true in config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitSkipHooksFlag verifies --skip-hooks flag still works (backward compat)
|
||||||
|
func TestInitSkipHooksFlag(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("skip-hooks", "false")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Initialize git repo first
|
||||||
|
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
|
||||||
|
t.Fatalf("Failed to init git: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run bd init with --skip-hooks
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet", "--skip-hooks"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify .beads directory was created
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
t.Error(".beads directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify git hooks were NOT installed
|
||||||
|
preCommitHook := filepath.Join(tmpDir, ".git", "hooks", "pre-commit")
|
||||||
|
if _, err := os.Stat(preCommitHook); err == nil {
|
||||||
|
t.Error("pre-commit hook should NOT be installed when --skip-hooks is passed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitDefaultInstallsHooks verifies default behavior installs hooks
|
||||||
|
func TestInitDefaultInstallsHooks(t *testing.T) {
|
||||||
|
// Reset global state
|
||||||
|
origDBPath := dbPath
|
||||||
|
defer func() { dbPath = origDBPath }()
|
||||||
|
dbPath = ""
|
||||||
|
|
||||||
|
// Reset Cobra flags
|
||||||
|
initCmd.Flags().Set("skip-hooks", "false")
|
||||||
|
|
||||||
|
// Clear any env var that might affect hooks
|
||||||
|
os.Unsetenv("BD_NO_INSTALL_HOOKS")
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Initialize git repo first
|
||||||
|
if err := runCommandInDir(tmpDir, "git", "init"); err != nil {
|
||||||
|
t.Fatalf("Failed to init git: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run bd init without any hook-skipping options
|
||||||
|
rootCmd.SetArgs([]string{"init", "--prefix", "test", "--quiet"})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify .beads directory was created
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
t.Error(".beads directory was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify git hooks WERE installed (default behavior)
|
||||||
|
preCommitHook := filepath.Join(tmpDir, ".git", "hooks", "pre-commit")
|
||||||
|
if _, err := os.Stat(preCommitHook); os.IsNotExist(err) {
|
||||||
|
t.Error("pre-commit hook SHOULD be installed by default")
|
||||||
|
}
|
||||||
|
|
||||||
|
postMergeHook := filepath.Join(tmpDir, ".git", "hooks", "post-merge")
|
||||||
|
if _, err := os.Stat(postMergeHook); os.IsNotExist(err) {
|
||||||
|
t.Error("post-merge hook SHOULD be installed by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Tool-level settings you can configure:
|
|||||||
| `no-daemon` | `--no-daemon` | `BD_NO_DAEMON` | `false` | Force direct mode, bypass daemon |
|
| `no-daemon` | `--no-daemon` | `BD_NO_DAEMON` | `false` | Force direct mode, bypass daemon |
|
||||||
| `no-auto-flush` | `--no-auto-flush` | `BD_NO_AUTO_FLUSH` | `false` | Disable auto JSONL export |
|
| `no-auto-flush` | `--no-auto-flush` | `BD_NO_AUTO_FLUSH` | `false` | Disable auto JSONL export |
|
||||||
| `no-auto-import` | `--no-auto-import` | `BD_NO_AUTO_IMPORT` | `false` | Disable auto JSONL import |
|
| `no-auto-import` | `--no-auto-import` | `BD_NO_AUTO_IMPORT` | `false` | Disable auto JSONL import |
|
||||||
|
| `no-install-hooks` | `--skip-hooks` | `BD_NO_INSTALL_HOOKS` | `false` | Skip git hook installation during `bd init` |
|
||||||
| `db` | `--db` | `BD_DB` | (auto-discover) | Database path |
|
| `db` | `--db` | `BD_DB` | (auto-discover) | Database path |
|
||||||
| `actor` | `--actor` | `BD_ACTOR` | `$USER` | Actor name for audit trail |
|
| `actor` | `--actor` | `BD_ACTOR` | `$USER` | Actor name for audit trail |
|
||||||
| `flush-debounce` | - | `BEADS_FLUSH_DEBOUNCE` | `5s` | Debounce time for auto-flush |
|
| `flush-debounce` | - | `BEADS_FLUSH_DEBOUNCE` | `5s` | Debounce time for auto-flush |
|
||||||
@@ -59,6 +60,10 @@ flush-debounce: 10s
|
|||||||
# Auto-start daemon (default true)
|
# Auto-start daemon (default true)
|
||||||
auto-start-daemon: true
|
auto-start-daemon: true
|
||||||
|
|
||||||
|
# Disable git hook installation during bd init
|
||||||
|
# (useful if you manage hooks with pre-commit, husky, etc.)
|
||||||
|
no-install-hooks: true
|
||||||
|
|
||||||
# Daemon log rotation settings
|
# Daemon log rotation settings
|
||||||
daemon-log-max-size: 50 # MB per file (default 50)
|
daemon-log-max-size: 50 # MB per file (default 50)
|
||||||
daemon-log-max-backups: 7 # Number of old logs to keep (default 7)
|
daemon-log-max-backups: 7 # Number of old logs to keep (default 7)
|
||||||
|
|||||||
@@ -251,11 +251,33 @@ See [PROTECTED_BRANCHES.md](PROTECTED_BRANCHES.md) for complete setup guide, tro
|
|||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
Git hooks are installed automatically during `bd init`. To install manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# One-time setup in each beads workspace
|
# One-time setup in each beads workspace
|
||||||
./examples/git-hooks/install.sh
|
./examples/git-hooks/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Disabling Hook Installation
|
||||||
|
|
||||||
|
If you prefer to manage git hooks separately (e.g., using pre-commit, husky, or your own hook manager), you can disable automatic hook installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via command-line flag
|
||||||
|
bd init --skip-hooks
|
||||||
|
|
||||||
|
# Via environment variable (useful for CI/scripts)
|
||||||
|
BD_NO_INSTALL_HOOKS=1 bd init
|
||||||
|
|
||||||
|
# Via global config (~/.config/bd/config.yaml)
|
||||||
|
no-install-hooks: true
|
||||||
|
|
||||||
|
# Via project config (.beads/config.yaml)
|
||||||
|
no-install-hooks: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Precedence:** `--skip-hooks` flag > `BD_NO_INSTALL_HOOKS` env var > config file > default (install hooks)
|
||||||
|
|
||||||
### What Gets Installed
|
### What Gets Installed
|
||||||
|
|
||||||
**pre-commit hook:**
|
**pre-commit hook:**
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ func Initialize() error {
|
|||||||
v.SetDefault("db", "")
|
v.SetDefault("db", "")
|
||||||
v.SetDefault("actor", "")
|
v.SetDefault("actor", "")
|
||||||
v.SetDefault("issue-prefix", "")
|
v.SetDefault("issue-prefix", "")
|
||||||
|
v.SetDefault("no-install-hooks", false)
|
||||||
|
|
||||||
// Additional environment variables (not prefixed with BD_)
|
// Additional environment variables (not prefixed with BD_)
|
||||||
// These are bound explicitly for backward compatibility
|
// These are bound explicitly for backward compatibility
|
||||||
|
|||||||
Reference in New Issue
Block a user