* 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>
192 lines
5.8 KiB
Go
192 lines
5.8 KiB
Go
package syncbranch
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
|
|
"github.com/steveyegge/beads/internal/beads"
|
|
"github.com/steveyegge/beads/internal/config"
|
|
"github.com/steveyegge/beads/internal/storage"
|
|
|
|
// Import SQLite driver (same as used by storage/sqlite)
|
|
_ "github.com/ncruces/go-sqlite3/driver"
|
|
_ "github.com/ncruces/go-sqlite3/embed"
|
|
)
|
|
|
|
const (
|
|
// ConfigKey is the database config key for sync branch
|
|
ConfigKey = "sync.branch"
|
|
|
|
// ConfigYAMLKey is the config.yaml key for sync branch
|
|
ConfigYAMLKey = "sync-branch"
|
|
|
|
// EnvVar is the environment variable for sync branch
|
|
EnvVar = "BEADS_SYNC_BRANCH"
|
|
)
|
|
|
|
// branchNamePattern validates git branch names
|
|
// Based on git-check-ref-format rules
|
|
var branchNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._/-]*[a-zA-Z0-9]$`)
|
|
|
|
// ValidateBranchName checks if a branch name is valid according to git rules
|
|
func ValidateBranchName(name string) error {
|
|
if name == "" {
|
|
return nil // Empty is valid (means use current branch)
|
|
}
|
|
|
|
// Basic length check
|
|
if len(name) > 255 {
|
|
return fmt.Errorf("branch name too long (max 255 characters)")
|
|
}
|
|
|
|
// Check pattern
|
|
if !branchNamePattern.MatchString(name) {
|
|
return fmt.Errorf("invalid branch name: must start and end with alphanumeric, can contain .-_/ in middle")
|
|
}
|
|
|
|
// Disallow certain patterns
|
|
if name == "HEAD" || name == "." || name == ".." {
|
|
return fmt.Errorf("invalid branch name: %s is reserved", name)
|
|
}
|
|
|
|
// No consecutive dots
|
|
if regexp.MustCompile(`\.\.`).MatchString(name) {
|
|
return fmt.Errorf("invalid branch name: cannot contain '..'")
|
|
}
|
|
|
|
// No leading/trailing slashes
|
|
if name[0] == '/' || name[len(name)-1] == '/' {
|
|
return fmt.Errorf("invalid branch name: cannot start or end with '/'")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves the sync branch configuration with the following precedence:
|
|
// 1. BEADS_SYNC_BRANCH environment variable
|
|
// 2. sync-branch from config.yaml (version controlled, shared across clones)
|
|
// 3. sync.branch from database config (legacy, for backward compatibility)
|
|
// 4. Empty string (meaning use current branch)
|
|
func Get(ctx context.Context, store storage.Storage) (string, error) {
|
|
// Check environment variable first (highest priority)
|
|
if envBranch := os.Getenv(EnvVar); envBranch != "" {
|
|
if err := ValidateBranchName(envBranch); err != nil {
|
|
return "", fmt.Errorf("invalid %s: %w", EnvVar, err)
|
|
}
|
|
return envBranch, nil
|
|
}
|
|
|
|
// Check config.yaml (version controlled, shared across clones)
|
|
// This is the recommended way to configure sync branch for teams
|
|
if yamlBranch := config.GetString(ConfigYAMLKey); yamlBranch != "" {
|
|
if err := ValidateBranchName(yamlBranch); err != nil {
|
|
return "", fmt.Errorf("invalid %s in config.yaml: %w", ConfigYAMLKey, err)
|
|
}
|
|
return yamlBranch, nil
|
|
}
|
|
|
|
// Check database config (legacy, for backward compatibility)
|
|
dbBranch, err := store.GetConfig(ctx, ConfigKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get %s from config: %w", ConfigKey, err)
|
|
}
|
|
|
|
if dbBranch != "" {
|
|
if err := ValidateBranchName(dbBranch); err != nil {
|
|
return "", fmt.Errorf("invalid %s in database: %w", ConfigKey, err)
|
|
}
|
|
}
|
|
|
|
return dbBranch, nil
|
|
}
|
|
|
|
// GetFromYAML retrieves sync branch from config.yaml only (no database lookup).
|
|
// This is useful for hooks and checks that need to know if sync-branch is configured
|
|
// in the version-controlled config without database access.
|
|
func GetFromYAML() string {
|
|
// Check environment variable first
|
|
if envBranch := os.Getenv(EnvVar); envBranch != "" {
|
|
return envBranch
|
|
}
|
|
return config.GetString(ConfigYAMLKey)
|
|
}
|
|
|
|
// IsConfigured returns true if sync-branch is configured in config.yaml or env var.
|
|
// This is a fast check that doesn't require database access.
|
|
func IsConfigured() bool {
|
|
return GetFromYAML() != ""
|
|
}
|
|
|
|
// IsConfiguredWithDB returns true if sync-branch is configured in any source:
|
|
// 1. BEADS_SYNC_BRANCH environment variable
|
|
// 2. sync-branch in config.yaml
|
|
// 3. sync.branch in database config
|
|
//
|
|
// The dbPath parameter should be the path to the beads.db file.
|
|
// If dbPath is empty, it will use beads.FindDatabasePath() to locate the database.
|
|
// This function is safe to call even if the database doesn't exist (returns false in that case).
|
|
func IsConfiguredWithDB(dbPath string) bool {
|
|
// First check env var and config.yaml (fast path)
|
|
if GetFromYAML() != "" {
|
|
return true
|
|
}
|
|
|
|
// Try to read from database
|
|
if dbPath == "" {
|
|
// Use existing beads.FindDatabasePath() which is worktree-aware
|
|
dbPath = beads.FindDatabasePath()
|
|
if dbPath == "" {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Read sync.branch from database config table
|
|
branch := getConfigFromDB(dbPath, ConfigKey)
|
|
return branch != ""
|
|
}
|
|
|
|
// getConfigFromDB reads a config value directly from the database file.
|
|
// This is a lightweight read that doesn't require the full storage layer.
|
|
// Returns empty string if the database doesn't exist or the key is not found.
|
|
func getConfigFromDB(dbPath string, key string) string {
|
|
// Check if database exists
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
return ""
|
|
}
|
|
|
|
// Open database in read-only mode
|
|
// Use file: prefix as required by ncruces/go-sqlite3 driver
|
|
connStr := fmt.Sprintf("file:%s?mode=ro", dbPath)
|
|
db, err := sql.Open("sqlite3", connStr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer db.Close()
|
|
|
|
// Query the config table
|
|
var value string
|
|
err = db.QueryRow(`SELECT value FROM config WHERE key = ?`, key).Scan(&value)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
// Set stores the sync branch configuration in the database
|
|
func Set(ctx context.Context, store storage.Storage, branch string) error {
|
|
if err := ValidateBranchName(branch); err != nil {
|
|
return err
|
|
}
|
|
|
|
return store.SetConfig(ctx, ConfigKey, branch)
|
|
}
|
|
|
|
// Unset removes the sync branch configuration from the database
|
|
func Unset(ctx context.Context, store storage.Storage) error {
|
|
return store.DeleteConfig(ctx, ConfigKey)
|
|
}
|