From fba004c3e80cbcd5ff2c41b6cba00ad3b7e7722b Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 13 Apr 2026 15:33:11 -0400 Subject: [PATCH] fix for rev-parse error when rebasing over deleted branches --- cmd/rebase.go | 12 ++++++--- cmd/rebase_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/cmd/rebase.go b/cmd/rebase.go index 0e4ee48..0d0ef41 100644 --- a/cmd/rebase.go +++ b/cmd/rebase.go @@ -172,9 +172,15 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error { // Sync PR state before rebase so we can detect merged PRs. syncStackPRs(cfg, s) - branchNames := make([]string, len(s.Branches)) - for i, b := range s.Branches { - branchNames[i] = b.Branch + branchNames := make([]string, 0, len(s.Branches)) + for _, b := range s.Branches { + // Merged branches that no longer exist locally have no ref to + // resolve. They are always skipped during rebase, but we must + // also exclude them here to avoid a rev-parse error. + if b.IsMerged() && !git.BranchExists(b.Branch) { + continue + } + branchNames = append(branchNames, b.Branch) } originalRefs, err := git.RevParseMap(branchNames) if err != nil { diff --git a/cmd/rebase_test.go b/cmd/rebase_test.go index f72eef7..47255cf 100644 --- a/cmd/rebase_test.go +++ b/cmd/rebase_test.go @@ -133,6 +133,7 @@ func TestRebase_SquashMergedBranch_UsesOnto(t *testing.T) { } mock := newRebaseMock(tmpDir, "b2") + mock.BranchExistsFn = func(name string) bool { return true } mock.RevParseFn = func(ref string) (string, error) { if sha, ok := branchSHAs[ref]; ok { return sha, nil @@ -197,6 +198,7 @@ func TestRebase_OntoPropagatesToSubsequentBranches(t *testing.T) { } mock := newRebaseMock(tmpDir, "b3") + mock.BranchExistsFn = func(name string) bool { return true } mock.RevParseFn = func(ref string) (string, error) { if sha, ok := branchSHAs[ref]; ok { return sha, nil @@ -927,7 +929,6 @@ func TestRebase_FastForwardsBranchFromRemote(t *testing.T) { output := string(errOut) assert.NoError(t, err) - // b1 should be fast-forwarded to remote SHA require.Len(t, updateBranchRefCalls, 1, "should fast-forward b1 via UpdateBranchRef") assert.Equal(t, "b1", updateBranchRefCalls[0].branch) @@ -1040,3 +1041,61 @@ func TestRebase_BranchDiverged_NoFF(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 0, updateBranchRefCalls, "no FF when branches have diverged") } + +func TestRebase_SkipsMergedBranchesNotExistingLocally(t *testing.T) { + // Simulates a stack where b1 is merged and its branch was auto-deleted + // from the remote, so it doesn't exist locally. + s := stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{ + {Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 42, Merged: true}}, + {Branch: "b2"}, + }, + } + + tmpDir := t.TempDir() + writeStackFile(t, tmpDir, s) + + var rebaseCalls []rebaseCall + + mock := newRebaseMock(tmpDir, "b2") + mock.BranchExistsFn = func(name string) bool { + // b1 does not exist locally (deleted from remote after merge) + return name != "b1" + } + mock.RevParseMultiFn = func(refs []string) ([]string, error) { + // Only resolve refs that exist — b1 should not be in the list + shas := make([]string, len(refs)) + for i, r := range refs { + if r == "b1" { + t.Fatalf("RevParseMulti should not be called with non-existent branch b1") + } + shas[i] = "sha-" + r + } + return shas, nil + } + mock.RebaseOntoFn = func(newBase, oldBase, branch string) error { + rebaseCalls = append(rebaseCalls, rebaseCall{newBase, oldBase, branch}) + return nil + } + + restore := git.SetOps(mock) + defer restore() + + cfg, _, errR := config.NewTestConfig() + cmd := RebaseCmd(cfg) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + err := cmd.Execute() + + cfg.Err.Close() + errOut, _ := io.ReadAll(errR) + output := string(errOut) + + assert.NoError(t, err) + assert.Contains(t, output, "Skipping b1") + + // Only b2 should be rebased + require.Len(t, rebaseCalls, 1) + assert.Equal(t, "b2", rebaseCalls[0].branch) +}