feat: add Git worktree compatibility (PR #478)

Adds comprehensive Git worktree support for beads issue tracking:

Core changes:
- New internal/git/gitdir.go package for worktree detection
- GetGitDir() returns proper .git location (main repo, not worktree)
- Updated all hooks to use git.GetGitDir() instead of local helper
- BeadsDir() now prioritizes main repository's .beads directory

Features:
- Hooks auto-install in main repo when run from worktree
- Shared .beads directory across all worktrees
- Config option no-install-hooks to disable auto-install
- New bd worktree subcommand for diagnostics

Documentation:
- New docs/WORKTREES.md with setup instructions
- Updated CHANGELOG.md and AGENT_INSTRUCTIONS.md

Testing:
- Updated tests to use exported git.GetGitDir()
- Added worktree detection tests

Co-authored-by: Claude <noreply@anthropic.com>
Closes: #478
This commit is contained in:
matt wilkie
2025-12-13 10:40:40 -08:00
committed by Steve Yegge
parent de7b511765
commit e01b7412d9
64 changed files with 1895 additions and 3708 deletions

View File

@@ -3,6 +3,7 @@ package daemon
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -208,8 +209,12 @@ func discoverDaemon(socketPath string) DaemonInfo {
// FindDaemonByWorkspace finds a daemon serving a specific workspace
func FindDaemonByWorkspace(workspacePath string) (*DaemonInfo, error) {
// First try the socket in the workspace itself
socketPath := filepath.Join(workspacePath, ".beads", "bd.sock")
// Determine the correct .beads directory location
// For worktrees, .beads is in the main repository root, not the worktree
beadsDir := findBeadsDirForWorkspace(workspacePath)
// First try the socket in the determined .beads directory
socketPath := filepath.Join(beadsDir, "bd.sock")
if _, err := os.Stat(socketPath); err == nil {
daemon := discoverDaemon(socketPath)
if daemon.Alive {
@@ -232,6 +237,46 @@ func FindDaemonByWorkspace(workspacePath string) (*DaemonInfo, error) {
return nil, fmt.Errorf("no daemon found for workspace: %s", workspacePath)
}
// findBeadsDirForWorkspace determines the correct .beads directory for a workspace
// For worktrees, this is the main repository root; for regular repos, it's the workspace itself
func findBeadsDirForWorkspace(workspacePath string) string {
// Change to the workspace directory to check if it's a worktree
originalDir, err := os.Getwd()
if err != nil {
return filepath.Join(workspacePath, ".beads") // fallback
}
defer func() {
_ = os.Chdir(originalDir) // restore original directory
}()
if err := os.Chdir(workspacePath); err != nil {
return filepath.Join(workspacePath, ".beads") // fallback
}
// Check if we're in a git worktree
cmd := exec.Command("git", "rev-parse", "--git-dir", "--git-common-dir")
output, err := cmd.Output()
if err != nil {
return filepath.Join(workspacePath, ".beads") // fallback
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) >= 2 {
gitDir := strings.TrimSpace(lines[0])
commonDir := strings.TrimSpace(lines[1])
// If git-dir != git-common-dir, we're in a worktree
if gitDir != commonDir {
// Worktree: .beads is in main repo root (parent of git-common-dir)
mainRepoRoot := filepath.Dir(commonDir)
return filepath.Join(mainRepoRoot, ".beads")
}
}
// Regular repository: .beads is in the workspace
return filepath.Join(workspacePath, ".beads")
}
// checkDaemonErrorFile checks for a daemon-error file in the .beads directory
func checkDaemonErrorFile(socketPath string) string {
// Socket path is typically .beads/bd.sock, so get the parent dir