@@ -549,6 +549,7 @@ func TestSync_SquashMergedBranch_UsesOnto(t *testing.T) {
549549 }
550550
551551 mock := newSyncMock (tmpDir , "b2" )
552+ mock .BranchExistsFn = func (name string ) bool { return true }
552553 // Trunk behind remote to trigger rebase
553554 mock .RevParseFn = func (ref string ) (string , error ) {
554555 if ref == "main" {
@@ -765,8 +766,8 @@ func TestSync_BranchFastForward_WithTrunkUpdate(t *testing.T) {
765766 writeStackFile (t , tmpDir , s )
766767
767768 var updateBranchRefCalls []struct { branch , sha string }
768- var rebaseCalls []rebaseCall
769- var pushCalls []pushCall
769+ var rebaseCalls2 []rebaseCall
770+ var pushCalls2 []pushCall
770771
771772 mock := newSyncMock (tmpDir , "b1" )
772773 // Trunk and b2 both behind remote
@@ -803,15 +804,15 @@ func TestSync_BranchFastForward_WithTrunkUpdate(t *testing.T) {
803804 }
804805 mock .CheckoutBranchFn = func (string ) error { return nil }
805806 mock .RebaseFn = func (base string ) error {
806- rebaseCalls = append (rebaseCalls , rebaseCall {branch : "(rebase)" + base })
807+ rebaseCalls2 = append (rebaseCalls2 , rebaseCall {branch : "(rebase)" + base })
807808 return nil
808809 }
809810 mock .RebaseOntoFn = func (newBase , oldBase , branch string ) error {
810- rebaseCalls = append (rebaseCalls , rebaseCall {newBase , oldBase , branch })
811+ rebaseCalls2 = append (rebaseCalls2 , rebaseCall {newBase , oldBase , branch })
811812 return nil
812813 }
813814 mock .PushFn = func (remote string , branches []string , force , atomic bool ) error {
814- pushCalls = append (pushCalls , pushCall {remote , branches , force , atomic })
815+ pushCalls2 = append (pushCalls2 , pushCall {remote , branches , force , atomic })
815816 return nil
816817 }
817818
@@ -829,7 +830,6 @@ func TestSync_BranchFastForward_WithTrunkUpdate(t *testing.T) {
829830 output := string (errOut )
830831
831832 assert .NoError (t , err )
832-
833833 // Both trunk and b2 should be updated
834834 branchUpdates := make (map [string ]string )
835835 for _ , c := range updateBranchRefCalls {
@@ -839,7 +839,83 @@ func TestSync_BranchFastForward_WithTrunkUpdate(t *testing.T) {
839839 assert .Equal (t , "b2-remote" , branchUpdates ["b2" ], "b2 should be fast-forwarded" )
840840
841841 assert .Contains (t , output , "fast-forwarded" )
842- assert .NotEmpty (t , rebaseCalls , "rebase should occur" )
843- require .Len (t , pushCalls , 1 )
844- assert .True (t , pushCalls [0 ].force , "push should use force after rebase" )
842+ assert .NotEmpty (t , rebaseCalls2 , "rebase should occur" )
843+ require .Len (t , pushCalls2 , 1 )
844+ assert .True (t , pushCalls2 [0 ].force , "push should use force after rebase" )
845+ }
846+
847+ func TestSync_MergedBranchDeletedFromRemote (t * testing.T ) {
848+ s := stack.Stack {
849+ Trunk : stack.BranchRef {Branch : "main" },
850+ Branches : []stack.BranchRef {
851+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 1 , Merged : true }},
852+ {Branch : "b2" },
853+ },
854+ }
855+
856+ tmpDir := t .TempDir ()
857+ writeStackFile (t , tmpDir , s )
858+
859+ var rebaseOntoCalls []rebaseCall
860+
861+ mock := newSyncMock (tmpDir , "b2" )
862+ mock .BranchExistsFn = func (name string ) bool {
863+ // b1 does not exist locally (deleted from remote after merge)
864+ return name != "b1"
865+ }
866+ mock .RevParseMultiFn = func (refs []string ) ([]string , error ) {
867+ shas := make ([]string , len (refs ))
868+ for i , r := range refs {
869+ if r == "b1" {
870+ t .Fatalf ("RevParseMulti should not be called with non-existent branch b1" )
871+ }
872+ if r == "main" {
873+ shas [i ] = "local-sha"
874+ } else if r == "origin/main" {
875+ shas [i ] = "remote-sha"
876+ } else {
877+ shas [i ] = "sha-" + r
878+ }
879+ }
880+ return shas , nil
881+ }
882+ // Trunk behind remote to trigger rebase
883+ mock .RevParseFn = func (ref string ) (string , error ) {
884+ if ref == "main" {
885+ return "local-sha" , nil
886+ }
887+ if ref == "origin/main" {
888+ return "remote-sha" , nil
889+ }
890+ return "sha-" + ref , nil
891+ }
892+ mock .IsAncestorFn = func (a , d string ) (bool , error ) {
893+ return a == "local-sha" && d == "remote-sha" , nil
894+ }
895+ mock .UpdateBranchRefFn = func (string , string ) error { return nil }
896+ mock .CheckoutBranchFn = func (string ) error { return nil }
897+ mock .RebaseOntoFn = func (newBase , oldBase , branch string ) error {
898+ rebaseOntoCalls = append (rebaseOntoCalls , rebaseCall {newBase , oldBase , branch })
899+ return nil
900+ }
901+
902+ restore := git .SetOps (mock )
903+ defer restore ()
904+
905+ cfg , _ , errR := config .NewTestConfig ()
906+ cmd := SyncCmd (cfg )
907+ cmd .SetOut (io .Discard )
908+ cmd .SetErr (io .Discard )
909+ err := cmd .Execute ()
910+
911+ cfg .Err .Close ()
912+ errOut , _ := io .ReadAll (errR )
913+ output := string (errOut )
914+
915+ assert .NoError (t , err )
916+ assert .Contains (t , output , "Skipping b1" )
917+
918+ // Only b2 should be rebased
919+ require .Len (t , rebaseOntoCalls , 1 )
920+ assert .Equal (t , "b2" , rebaseOntoCalls [0 ].branch )
845921}
0 commit comments