fix: Validate sync-branch at config-time and runtime (closes #1166) (#1168)

* fix(config): validate sync-branch at config time

Add sync-branch validation to validateYamlConfigValue() to reject
main/master at config time, preventing the validation bypass in GH#1166.

- Add case for sync-branch and sync.branch keys
- Inline validation logic to avoid import cycle (config <-> syncbranch)
- Add unit tests for rejection (main/master) and acceptance (valid names)

Part of: #1166

* fix(sync): add runtime guard for sync-branch == current-branch

Add dynamic runtime check before worktree operations to catch cases
where sync-branch matches the current branch. This provides defense
in depth for manual YAML edits, pre-fix configs, or non-main/master
branch names (trunk, develop, production, etc.).

- Check IsSyncBranchSameAsCurrent() after hasSyncBranchConfig is set
- Position check BEFORE worktree entry (CWD changes inside worktree)
- Add integration test TestSync_FailsWhenOnSyncBranch

Part of: #1166

* docs: note main/master restriction in sync-branch FAQ

Clarifies that git worktrees cannot checkout the same branch in
multiple locations, so main/master cannot be used as sync branch.
This commit is contained in:
Peter Chanthamynavong
2026-01-19 10:11:06 -08:00
committed by GitHub
parent 4fffdb7fae
commit 2cbf3a5497
5 changed files with 148 additions and 1 deletions

View File

@@ -297,6 +297,13 @@ func validateYamlConfigValue(key, value string) error {
if depth < 1 {
return fmt.Errorf("hierarchy.max-depth must be at least 1, got %d", depth)
}
case "sync-branch", "sync.branch":
// GH#1166: Validate sync branch name at config time
// Note: Cannot import syncbranch due to import cycle, so inline the validation.
// This mirrors syncbranch.ValidateSyncBranchName() logic.
if value == "main" || value == "master" {
return fmt.Errorf("cannot use '%s' as sync branch: git worktrees prevent checking out the same branch in multiple locations. Use a dedicated branch like 'beads-sync' instead", value)
}
}
return nil
}

View File

@@ -382,3 +382,53 @@ func TestValidateYamlConfigValue_OtherKeys(t *testing.T) {
t.Errorf("unexpected error for routing.mode: %v", err)
}
}
// TestValidateYamlConfigValue_SyncBranch_RejectsMain tests that main/master are rejected as sync branch (GH#1166)
func TestValidateYamlConfigValue_SyncBranch_RejectsMain(t *testing.T) {
tests := []struct {
name string
key string
value string
}{
{"sync-branch main", "sync-branch", "main"},
{"sync-branch master", "sync-branch", "master"},
{"sync.branch main", "sync.branch", "main"},
{"sync.branch master", "sync.branch", "master"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateYamlConfigValue(tt.key, tt.value)
if err == nil {
t.Errorf("expected error for %s=%s, got nil", tt.key, tt.value)
}
if err != nil && !strings.Contains(err.Error(), "cannot use") {
t.Errorf("expected 'cannot use' error, got: %v", err)
}
})
}
}
// TestValidateYamlConfigValue_SyncBranch_AcceptsValid tests that valid branch names are accepted (GH#1166)
func TestValidateYamlConfigValue_SyncBranch_AcceptsValid(t *testing.T) {
tests := []struct {
name string
key string
value string
}{
{"sync-branch beads-sync", "sync-branch", "beads-sync"},
{"sync-branch feature/test", "sync-branch", "feature/test"},
{"sync.branch beads-sync", "sync.branch", "beads-sync"},
{"sync.branch develop", "sync.branch", "develop"},
{"sync-branch empty", "sync-branch", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateYamlConfigValue(tt.key, tt.value)
if err != nil {
t.Errorf("unexpected error for %s=%s: %v", tt.key, tt.value, err)
}
})
}
}