Files
beads/cmd/bd/worktree.go
Charles P. Cross a69e94a958 Auto-disable daemon in git worktrees for safety (#567)
* feat: auto-disable daemon in git worktrees for safety

Implement worktree daemon compatibility as proposed in the analysis.
The daemon is now automatically disabled when running in a git worktree
unless sync-branch is configured.

Git worktrees share the same .beads directory, and the daemon commits
to whatever branch its working directory has checked out. This causes
commits to go to the wrong branch when using daemon in worktrees.

- Add shouldDisableDaemonForWorktree() helper that checks:
  1. If current directory is a git worktree (via git rev-parse)
  2. If sync-branch is configured (env var or config.yaml)
- Modify shouldAutoStartDaemon() to call the helper
- Modify daemon connection logic in main.go to skip connection
- Add FallbackWorktreeSafety constant for daemon status reporting
- Update warnWorktreeDaemon() to skip warning when sync-branch configured

- In worktree WITHOUT sync-branch: daemon auto-disabled, direct mode used
- In worktree WITH sync-branch: daemon enabled (commits go to dedicated branch)
- In regular repo: no change (daemon works as before)

- Added comprehensive unit tests for shouldDisableDaemonForWorktree()
- Added integration tests for shouldAutoStartDaemon() in worktree contexts
- Manual E2E testing verified correct behavior

- Updated WORKTREES.md with new automatic safety behavior
- Updated DAEMON.md with Git Worktrees section

* feat: check database config for sync-branch in worktree safety logic

Previously, the worktree daemon safety check only looked at:
- BEADS_SYNC_BRANCH environment variable
- sync-branch in config.yaml

This meant users who configured sync-branch via `bd config set sync-branch`
(which stores in the database) would still have daemon disabled in worktrees.

Now the check also reads sync.branch from the database config table,
making daemon work in worktrees when sync-branch is configured via any method.

Changes:
- Add IsConfiguredWithDB() function that checks env, config.yaml, AND database
- Add findBeadsDB() to locate database (worktree-aware via git-common-dir)
- Add getMainRepoRoot() helper using git rev-parse
- Add getConfigFromDB() for lightweight database reads
- Update shouldDisableDaemonForWorktree() to use IsConfiguredWithDB()
- Update warnWorktreeDaemon() to use IsConfiguredWithDB()
- Add test case for database config path

* refactor: use existing beads.FindDatabasePath() instead of duplicating code

Remove duplicate getMainRepoRoot() and findBeadsDB() functions from
syncbranch.go and use the existing beads.FindDatabasePath() which is
already worktree-aware.

Changes:
- Replace custom findBeadsDB() with beads.FindDatabasePath()
- Remove duplicate getMainRepoRoot() (git.GetMainRepoRoot() exists)
- Remove unused imports (exec, strings, filepath)
- Clean up debug logging in tests

---------

Co-authored-by: Charles P. Cross <cpdata@users.noreply.github.com>
2025-12-16 00:06:19 -08:00

175 lines
7.8 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/git"
"github.com/steveyegge/beads/internal/syncbranch"
)
// isGitWorktree detects if the current directory is in a git worktree.
// This is a wrapper around git.IsWorktree() for CLI-layer compatibility.
func isGitWorktree() bool {
return git.IsWorktree()
}
// shouldDisableDaemonForWorktree returns true if daemon should be disabled
// due to being in a git worktree without sync-branch configured.
//
// The daemon is unsafe in worktrees because all worktrees share the same
// .beads directory, and the daemon commits to whatever branch its working
// directory has checked out - which can cause commits to go to the wrong branch.
//
// However, when sync-branch is configured, the daemon commits to a dedicated
// branch (e.g., "beads-metadata") using an internal worktree, so the user's
// current branch is never affected. This makes daemon mode safe in worktrees.
//
// Returns:
// - true: Disable daemon (in worktree without sync-branch)
// - false: Allow daemon (not in worktree, or sync-branch is configured)
func shouldDisableDaemonForWorktree() bool {
// If not in a worktree, daemon is safe
if !isGitWorktree() {
return false
}
// In a worktree - check if sync-branch is configured
// IsConfiguredWithDB checks env var, config.yaml, AND database config
if syncbranch.IsConfiguredWithDB("") {
// Sync-branch is configured, daemon is safe (commits go to dedicated branch)
return false
}
// In worktree without sync-branch - daemon is unsafe, disable it
return true
}
// gitRevParse runs git rev-parse with the given flag and returns the trimmed output.
// This is a helper for CLI utilities that need git command execution.
func gitRevParse(flag string) string {
out, err := exec.Command("git", "rev-parse", flag).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
// getWorktreeGitDir returns the .git directory path for a worktree
// Returns empty string if not in a git repo or not a worktree
func getWorktreeGitDir() string {
gitDir, err := git.GetGitDir()
if err != nil {
return ""
}
return gitDir
}
// warnWorktreeDaemon prints a warning if using daemon with worktrees without sync-branch.
// Call this only when daemon mode is actually active (connected).
//
// With the new worktree safety logic, this warning should rarely appear because:
// - Daemon is auto-disabled in worktrees without sync-branch
// - When sync-branch is configured, daemon is safe (commits go to dedicated branch)
//
// This warning is kept as a safety net for edge cases where daemon might still
// be connected in a worktree (e.g., daemon started in main repo, then user cd's to worktree).
func warnWorktreeDaemon(dbPathForWarning string) {
if !isGitWorktree() {
return
}
// If sync-branch is configured, daemon is safe in worktrees - no warning needed
if syncbranch.IsConfiguredWithDB("") {
return
}
gitDir := getWorktreeGitDir()
beadsDir := filepath.Dir(dbPathForWarning)
if beadsDir == "." || beadsDir == "" {
beadsDir = dbPathForWarning
}
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "╔══════════════════════════════════════════════════════════════════════════╗")
fmt.Fprintln(os.Stderr, "║ WARNING: Git worktree detected with daemon mode ║")
fmt.Fprintln(os.Stderr, "╠══════════════════════════════════════════════════════════════════════════╣")
fmt.Fprintln(os.Stderr, "║ Git worktrees share the same .beads directory, which can cause the ║")
fmt.Fprintln(os.Stderr, "║ daemon to commit/push to the wrong branch. ║")
fmt.Fprintln(os.Stderr, "║ ║")
fmt.Fprintf(os.Stderr, "║ Shared database: %-55s ║\n", truncateForBox(beadsDir, 55))
fmt.Fprintf(os.Stderr, "║ Worktree git dir: %-54s ║\n", truncateForBox(gitDir, 54))
fmt.Fprintln(os.Stderr, "║ ║")
fmt.Fprintln(os.Stderr, "║ RECOMMENDED SOLUTIONS: ║")
fmt.Fprintln(os.Stderr, "║ 1. Configure sync-branch: bd config set sync-branch beads-metadata ║")
fmt.Fprintln(os.Stderr, "║ 2. Use --no-daemon flag: bd --no-daemon <command> ║")
fmt.Fprintln(os.Stderr, "║ 3. Disable daemon mode: export BEADS_NO_DAEMON=1 ║")
fmt.Fprintln(os.Stderr, "╚══════════════════════════════════════════════════════════════════════════╝")
fmt.Fprintln(os.Stderr)
}
// truncateForBox truncates a path to fit in the warning box
func truncateForBox(path string, maxLen int) string {
if len(path) <= maxLen {
return path
}
// Truncate with ellipsis
return "..." + path[len(path)-(maxLen-3):]
}
// warnMultipleDatabases prints a warning if multiple .beads databases exist
// in the directory hierarchy, to prevent confusion and database pollution
func warnMultipleDatabases(currentDB string) {
databases := beads.FindAllDatabases()
if len(databases) <= 1 {
return // Only one database found, no warning needed
}
// Find which database is active
activeIdx := -1
for i, db := range databases {
if db.Path == currentDB {
activeIdx = i
break
}
}
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "╔══════════════════════════════════════════════════════════════════════════╗")
fmt.Fprintf(os.Stderr, "║ WARNING: %d beads databases detected in directory hierarchy ║\n", len(databases))
fmt.Fprintln(os.Stderr, "╠══════════════════════════════════════════════════════════════════════════╣")
fmt.Fprintln(os.Stderr, "║ Multiple databases can cause confusion and database pollution. ║")
fmt.Fprintln(os.Stderr, "║ ║")
for i, db := range databases {
isActive := (i == activeIdx)
issueInfo := ""
if db.IssueCount >= 0 {
issueInfo = fmt.Sprintf(" (%d issues)", db.IssueCount)
}
marker := " "
if isActive {
marker = "▶"
}
line := fmt.Sprintf("%s %s%s", marker, db.BeadsDir, issueInfo)
fmt.Fprintf(os.Stderr, "║ %-72s ║\n", truncateForBox(line, 72))
}
fmt.Fprintln(os.Stderr, "║ ║")
if activeIdx == 0 {
fmt.Fprintln(os.Stderr, "║ Currently using the closest database (▶). This is usually correct. ║")
} else {
fmt.Fprintln(os.Stderr, "║ WARNING: Not using the closest database! Check your BEADS_DB setting. ║")
}
fmt.Fprintln(os.Stderr, "║ ║")
fmt.Fprintln(os.Stderr, "║ RECOMMENDED: Consolidate or remove unused databases to avoid confusion. ║")
fmt.Fprintln(os.Stderr, "╚══════════════════════════════════════════════════════════════════════════╝")
fmt.Fprintln(os.Stderr)
}