Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions cmd/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
skarim marked this conversation as resolved.
}
originalRefs, err := git.RevParseMap(branchNames)
if err != nil {
Expand Down
61 changes: 60 additions & 1 deletion cmd/rebase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Comment thread
skarim marked this conversation as resolved.

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)
}