From 2ecb978fc05d708db6daceb92cd8c4df38bd40f5 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 13 Apr 2026 18:21:51 -0400 Subject: [PATCH] fix for rev-parse error during sync --- cmd/sync.go | 14 ++++++--- cmd/sync_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/cmd/sync.go b/cmd/sync.go index 019059a..8ab4fbf 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -125,10 +125,16 @@ func runSync(cfg *config.Config, opts *syncOptions) error { // Sync PR state to detect merged PRs before rebasing. syncStackPRs(cfg, s) - // Save original refs so we can restore on conflict - branchNames := make([]string, len(s.Branches)) - for i, b := range s.Branches { - branchNames[i] = b.Branch + // Save original refs so we can restore on conflict. + // 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. + branchNames := make([]string, 0, len(s.Branches)) + for _, b := range s.Branches { + if b.IsMerged() && !git.BranchExists(b.Branch) { + continue + } + branchNames = append(branchNames, b.Branch) } originalRefs, _ := git.RevParseMap(branchNames) diff --git a/cmd/sync_test.go b/cmd/sync_test.go index a842ae4..f3ed052 100644 --- a/cmd/sync_test.go +++ b/cmd/sync_test.go @@ -535,6 +535,7 @@ func TestSync_SquashMergedBranch_UsesOnto(t *testing.T) { } mock := newSyncMock(tmpDir, "b2") + mock.BranchExistsFn = func(name string) bool { return true } // Trunk behind remote to trigger rebase mock.RevParseFn = func(ref string) (string, error) { if ref == "main" { @@ -647,3 +648,79 @@ func TestSync_PushFailureAfterRebase(t *testing.T) { assert.True(t, pushCalls[0].force, "push after rebase should use force") assert.Contains(t, output, "Push failed") } + +func TestSync_MergedBranchDeletedFromRemote(t *testing.T) { + s := stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{ + {Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 1, Merged: true}}, + {Branch: "b2"}, + }, + } + + tmpDir := t.TempDir() + writeStackFile(t, tmpDir, s) + + var rebaseOntoCalls []rebaseCall + + mock := newSyncMock(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) { + 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") + } + if r == "main" { + shas[i] = "local-sha" + } else if r == "origin/main" { + shas[i] = "remote-sha" + } else { + shas[i] = "sha-" + r + } + } + return shas, nil + } + // Trunk behind remote to trigger rebase + mock.RevParseFn = func(ref string) (string, error) { + if ref == "main" { + return "local-sha", nil + } + if ref == "origin/main" { + return "remote-sha", nil + } + return "sha-" + ref, nil + } + mock.IsAncestorFn = func(a, d string) (bool, error) { + return a == "local-sha" && d == "remote-sha", nil + } + mock.UpdateBranchRefFn = func(string, string) error { return nil } + mock.CheckoutBranchFn = func(string) error { return nil } + mock.RebaseOntoFn = func(newBase, oldBase, branch string) error { + rebaseOntoCalls = append(rebaseOntoCalls, rebaseCall{newBase, oldBase, branch}) + return nil + } + + restore := git.SetOps(mock) + defer restore() + + cfg, _, errR := config.NewTestConfig() + cmd := SyncCmd(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, rebaseOntoCalls, 1) + assert.Equal(t, "b2", rebaseOntoCalls[0].branch) +}