Skip to content

Commit fb0fd4f

Browse files
authored
fix(sync): skip branches checked out in other worktrees instead of aborting (#71)
1 parent a2f292d commit fb0fd4f

2 files changed

Lines changed: 97 additions & 12 deletions

File tree

cmd/sync.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -426,24 +426,20 @@ func runSync(gitClient git.GitClient, githubClient github.GitHubClient, syncRemo
426426
currentWorktreePath = ""
427427
}
428428

429+
worktreeSkipSet := make(map[string]string)
429430
for _, branch := range sorted {
430431
if worktreePath, inWorktree := worktrees[branch.Name]; inWorktree {
431-
// Only error if we're NOT already in this worktree
432432
if currentWorktreePath != worktreePath {
433-
return fmt.Errorf(
434-
"cannot sync: branch '%s' is checked out in worktree at %s\n\n"+
435-
"To sync this stack:\n"+
436-
" 1. cd %s\n"+
437-
" 2. stack sync\n\n"+
438-
"Or remove the worktree: git worktree remove %s",
439-
branch.Name,
440-
worktreePath,
441-
worktreePath,
442-
worktreePath,
443-
)
433+
worktreeSkipSet[branch.Name] = worktreePath
444434
}
445435
}
446436
}
437+
if len(worktreeSkipSet) > 0 {
438+
for name, path := range worktreeSkipSet {
439+
fmt.Fprintf(os.Stderr, "%s Skipping %s (checked out in worktree at %s)\n", ui.WarningIcon(), ui.Branch(name), path)
440+
}
441+
fmt.Println()
442+
}
447443

448444
// Collect all branches we need PR info for (stack branches + their parents)
449445
prBranchSet := make(map[string]bool)
@@ -489,6 +485,12 @@ func runSync(gitClient git.GitClient, githubClient github.GitHubClient, syncRemo
489485
for i, branch := range sorted {
490486
progress := ui.Progress(i+1, len(sorted))
491487

488+
// Skip branches checked out in other worktrees
489+
if worktreePath, skip := worktreeSkipSet[branch.Name]; skip {
490+
fmt.Printf("%s Skipping %s (checked out in %s)\n\n", progress, ui.Branch(branch.Name), worktreePath)
491+
continue
492+
}
493+
492494
// Check if this branch has a merged PR - if so, remove from stack tracking
493495
if pr, exists := prCache[branch.Name]; exists && pr.State == "MERGED" {
494496
fmt.Printf("%s Skipping %s (PR #%d is %s)...\n", progress, ui.Branch(branch.Name), pr.Number, ui.PRState(pr.State))

cmd/sync_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,3 +1017,86 @@ func TestDetermineSyncRemote(t *testing.T) {
10171017
assert.Equal(t, "origin", result)
10181018
})
10191019
}
1020+
1021+
func TestRunSyncSkipsWorktreeBranches(t *testing.T) {
1022+
testutil.SetupTest()
1023+
defer testutil.TeardownTest()
1024+
1025+
t.Run("skips branch checked out in another worktree", func(t *testing.T) {
1026+
mockGit := new(testutil.MockGitClient)
1027+
mockGH := new(testutil.MockGitHubClient)
1028+
1029+
// Setup: no existing sync state
1030+
mockGit.On("GetConfig", "stack.sync.stashed").Return("")
1031+
mockGit.On("GetConfig", "stack.sync.originalBranch").Return("")
1032+
mockGit.On("GetCurrentBranch").Return("feature-c", nil)
1033+
mockGit.On("SetConfig", "stack.sync.originalBranch", "feature-c").Return(nil)
1034+
mockGit.On("IsWorkingTreeClean").Return(true, nil)
1035+
mockGit.On("GetConfig", "branch.feature-c.stackparent").Return("feature-b")
1036+
mockGit.On("GetConfig", "stack.baseBranch").Return("").Maybe()
1037+
mockGit.On("GetDefaultBranch").Return("main").Maybe()
1038+
1039+
stackParents := map[string]string{
1040+
"feature-a": "main",
1041+
"feature-b": "feature-a",
1042+
"feature-c": "feature-b",
1043+
}
1044+
mockGit.On("GetAllStackParents").Return(stackParents, nil).Maybe()
1045+
1046+
// Parallel operations
1047+
mockGit.On("FetchRemote", "origin").Return(nil)
1048+
mockGH.On("GetPRsForBranches", mock.Anything).Return(make(map[string]*github.PRInfo))
1049+
1050+
// feature-b is in another worktree
1051+
mockGit.On("GetWorktreeBranches").Return(map[string]string{
1052+
"feature-b": "/other/worktree",
1053+
}, nil)
1054+
mockGit.On("GetCurrentWorktreePath").Return("/Users/test/repo", nil)
1055+
1056+
mockGit.On("GetRemoteBranchesSet").Return(map[string]bool{
1057+
"main": true,
1058+
"feature-a": true,
1059+
"feature-b": true,
1060+
"feature-c": true,
1061+
})
1062+
1063+
// Process feature-a (not skipped)
1064+
mockGit.On("CheckoutBranch", "feature-a").Return(nil)
1065+
mockGit.On("GetCommitHash", "feature-a").Return("aaa111", nil)
1066+
mockGit.On("GetCommitHash", "origin/feature-a").Return("aaa111", nil)
1067+
mockGit.On("FetchBranchFromRemote", "origin", "main").Return(nil)
1068+
mockGit.On("GetUniqueCommitsByPatch", "origin/main", "feature-a").Return([]string{"aaa111"}, nil)
1069+
mockGit.On("GetMergeBase", "feature-a", "origin/main").Return("main123", nil)
1070+
mockGit.On("GetCommitHash", "origin/main").Return("main123", nil)
1071+
mockGit.On("Rebase", "origin/main").Return(nil)
1072+
mockGit.On("FetchBranch", "feature-a").Return(nil)
1073+
mockGit.On("PushWithExpectedRemote", "feature-a", "aaa111").Return(nil)
1074+
1075+
// feature-b is SKIPPED (in another worktree) - no checkout/rebase/push calls
1076+
1077+
// Process feature-c (not skipped)
1078+
mockGit.On("CheckoutBranch", "feature-c").Return(nil)
1079+
mockGit.On("GetCommitHash", "feature-c").Return("ccc333", nil)
1080+
mockGit.On("GetCommitHash", "origin/feature-c").Return("ccc333", nil)
1081+
mockGit.On("GetUniqueCommitsByPatch", "feature-b", "feature-c").Return([]string{"ccc333"}, nil)
1082+
mockGit.On("GetMergeBase", "feature-c", "feature-b").Return("bbb222", nil)
1083+
mockGit.On("GetCommitHash", "feature-b").Return("bbb222", nil)
1084+
mockGit.On("Rebase", "feature-b").Return(nil)
1085+
mockGit.On("FetchBranch", "feature-c").Return(nil)
1086+
mockGit.On("PushWithExpectedRemote", "feature-c", "ccc333").Return(nil)
1087+
1088+
// Return to original branch
1089+
mockGit.On("CheckoutBranch", "feature-c").Return(nil)
1090+
// Clean up sync state
1091+
mockGit.On("UnsetConfig", "stack.sync.stashed").Return(nil)
1092+
mockGit.On("UnsetConfig", "stack.sync.originalBranch").Return(nil)
1093+
1094+
err := runSync(mockGit, mockGH, "origin")
1095+
1096+
assert.NoError(t, err)
1097+
// Verify feature-b was never checked out
1098+
mockGit.AssertNotCalled(t, "CheckoutBranch", "feature-b")
1099+
mockGit.AssertExpectations(t)
1100+
mockGH.AssertExpectations(t)
1101+
})
1102+
}

0 commit comments

Comments
 (0)