sync.branch config was lost on clone because it was only stored in the database (which is gitignored). Now syncbranch.Get() checks: 1. BEADS_SYNC_BRANCH env var (highest) 2. config.yaml sync-branch (tracked, persists across clones) 3. Database config (local override) 4. Empty (use current branch) Changes: - Update syncbranch.Get() to check config.yaml - Update config.yaml template with sync-branch option - Set sync-branch: beads-sync in this repo config.yaml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
110 lines
3.2 KiB
Go
110 lines
3.2 KiB
Go
package syncbranch
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
|
|
"github.com/steveyegge/beads/internal/config"
|
|
"github.com/steveyegge/beads/internal/storage"
|
|
)
|
|
|
|
const (
|
|
// ConfigKey is the database config key for sync branch
|
|
ConfigKey = "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 (tracked in git, persists across clones)
|
|
// 3. sync.branch from database config (local override)
|
|
// 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 (tracked in git, persists across clones)
|
|
// This is the recommended way to configure sync branch for teams
|
|
if yamlBranch := config.GetString("sync-branch"); yamlBranch != "" {
|
|
if err := ValidateBranchName(yamlBranch); err != nil {
|
|
return "", fmt.Errorf("invalid sync-branch in config.yaml: %w", err)
|
|
}
|
|
return yamlBranch, nil
|
|
}
|
|
|
|
// Check database config (local override, doesn't persist across clones)
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|