Skip to content

Commit 08a7f7b

Browse files
committed
fix for rev-parse error when rebasing over deleted branches
1 parent 688c1c4 commit 08a7f7b

File tree

2 files changed

+69
-4
lines changed

2 files changed

+69
-4
lines changed

cmd/rebase.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,15 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
172172
// Sync PR state before rebase so we can detect merged PRs.
173173
syncStackPRs(cfg, s)
174174

175-
branchNames := make([]string, len(s.Branches))
176-
for i, b := range s.Branches {
177-
branchNames[i] = b.Branch
175+
branchNames := make([]string, 0, len(s.Branches))
176+
for _, b := range s.Branches {
177+
// Merged branches that no longer exist locally have no ref to
178+
// resolve. They are always skipped during rebase, but we must
179+
// also exclude them here to avoid a rev-parse error.
180+
if b.IsMerged() && !git.BranchExists(b.Branch) {
181+
continue
182+
}
183+
branchNames = append(branchNames, b.Branch)
178184
}
179185
originalRefs, err := git.RevParseMap(branchNames)
180186
if err != nil {

cmd/rebase_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func TestRebase_SquashMergedBranch_UsesOnto(t *testing.T) {
133133
}
134134

135135
mock := newRebaseMock(tmpDir, "b2")
136+
mock.BranchExistsFn = func(name string) bool { return true }
136137
mock.RevParseFn = func(ref string) (string, error) {
137138
if sha, ok := branchSHAs[ref]; ok {
138139
return sha, nil
@@ -197,6 +198,7 @@ func TestRebase_OntoPropagatesToSubsequentBranches(t *testing.T) {
197198
}
198199

199200
mock := newRebaseMock(tmpDir, "b3")
201+
mock.BranchExistsFn = func(name string) bool { return true }
200202
mock.RevParseFn = func(ref string) (string, error) {
201203
if sha, ok := branchSHAs[ref]; ok {
202204
return sha, nil
@@ -927,7 +929,6 @@ func TestRebase_FastForwardsBranchFromRemote(t *testing.T) {
927929
output := string(errOut)
928930

929931
assert.NoError(t, err)
930-
931932
// b1 should be fast-forwarded to remote SHA
932933
require.Len(t, updateBranchRefCalls, 1, "should fast-forward b1 via UpdateBranchRef")
933934
assert.Equal(t, "b1", updateBranchRefCalls[0].branch)
@@ -1040,3 +1041,61 @@ func TestRebase_BranchDiverged_NoFF(t *testing.T) {
10401041
assert.NoError(t, err)
10411042
assert.Equal(t, 0, updateBranchRefCalls, "no FF when branches have diverged")
10421043
}
1044+
1045+
func TestRebase_SkipsMergedBranchesNotExistingLocally(t *testing.T) {
1046+
// Simulates a stack where b1 is merged and its branch was auto-deleted
1047+
// from the remote, so it doesn't exist locally.
1048+
s := stack.Stack{
1049+
Trunk: stack.BranchRef{Branch: "main"},
1050+
Branches: []stack.BranchRef{
1051+
{Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 42, Merged: true}},
1052+
{Branch: "b2"},
1053+
},
1054+
}
1055+
1056+
tmpDir := t.TempDir()
1057+
writeStackFile(t, tmpDir, s)
1058+
1059+
var rebaseCalls []rebaseCall
1060+
1061+
mock := newRebaseMock(tmpDir, "b2")
1062+
mock.BranchExistsFn = func(name string) bool {
1063+
// b1 does not exist locally (deleted from remote after merge)
1064+
return name != "b1"
1065+
}
1066+
mock.RevParseMultiFn = func(refs []string) ([]string, error) {
1067+
// Only resolve refs that exist — b1 should not be in the list
1068+
shas := make([]string, len(refs))
1069+
for i, r := range refs {
1070+
if r == "b1" {
1071+
t.Fatalf("RevParseMulti should not be called with non-existent branch b1")
1072+
}
1073+
shas[i] = "sha-" + r
1074+
}
1075+
return shas, nil
1076+
}
1077+
mock.RebaseOntoFn = func(newBase, oldBase, branch string) error {
1078+
rebaseCalls = append(rebaseCalls, rebaseCall{newBase, oldBase, branch})
1079+
return nil
1080+
}
1081+
1082+
restore := git.SetOps(mock)
1083+
defer restore()
1084+
1085+
cfg, _, errR := config.NewTestConfig()
1086+
cmd := RebaseCmd(cfg)
1087+
cmd.SetOut(io.Discard)
1088+
cmd.SetErr(io.Discard)
1089+
err := cmd.Execute()
1090+
1091+
cfg.Err.Close()
1092+
errOut, _ := io.ReadAll(errR)
1093+
output := string(errOut)
1094+
1095+
assert.NoError(t, err)
1096+
assert.Contains(t, output, "Skipping b1")
1097+
1098+
// Only b2 should be rebased
1099+
require.Len(t, rebaseCalls, 1)
1100+
assert.Equal(t, "b2", rebaseCalls[0].branch)
1101+
}

0 commit comments

Comments
 (0)