@@ -564,7 +564,12 @@ func TestSync_MergedBranch_UsesOnto(t *testing.T) {
564564 return "default-sha" , nil
565565 }
566566 mock .IsAncestorFn = func (a , d string ) (bool , error ) {
567- return a == "local-sha" && d == "remote-sha" , nil
567+ // Trunk: local is behind remote → triggers fast-forward
568+ if a == "local-sha" && d == "remote-sha" {
569+ return true , nil
570+ }
571+ // For --onto stale-check: old bases are valid ancestors (first-run)
572+ return true , nil
568573 }
569574 mock .UpdateBranchRefFn = func (string , string ) error { return nil }
570575 mock .CheckoutBranchFn = func (string ) error { return nil }
@@ -603,6 +608,93 @@ func TestSync_MergedBranch_UsesOnto(t *testing.T) {
603608 assert .True (t , pushCalls [0 ].force )
604609}
605610
611+ // TestSync_StaleOntoOldBase_FallsBackToMergeBase verifies that when a branch
612+ // was already rebased past the merged branch's tip, sync detects the stale
613+ // ontoOldBase and falls back to merge-base for the correct divergence point.
614+ func TestSync_StaleOntoOldBase_FallsBackToMergeBase (t * testing.T ) {
615+ s := stack.Stack {
616+ Trunk : stack.BranchRef {Branch : "main" },
617+ Branches : []stack.BranchRef {
618+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 1 , Merged : true }},
619+ {Branch : "b2" },
620+ {Branch : "b3" },
621+ },
622+ }
623+
624+ tmpDir := t .TempDir ()
625+ writeStackFile (t , tmpDir , s )
626+
627+ var rebaseOntoCalls []rebaseCall
628+
629+ branchSHAs := map [string ]string {
630+ "b1" : "b1-stale-presquash-sha" ,
631+ "b2" : "b2-on-main-sha" ,
632+ "b3" : "b3-on-b2-sha" ,
633+ }
634+
635+ mock := newSyncMock (tmpDir , "b2" )
636+ mock .BranchExistsFn = func (name string ) bool { return true }
637+ mock .RevParseFn = func (ref string ) (string , error ) {
638+ if ref == "main" {
639+ return "local-sha" , nil
640+ }
641+ if ref == "origin/main" {
642+ return "remote-sha" , nil
643+ }
644+ if sha , ok := branchSHAs [ref ]; ok {
645+ return sha , nil
646+ }
647+ return "default-sha" , nil
648+ }
649+ mock .IsAncestorFn = func (a , d string ) (bool , error ) {
650+ // Trunk: local is behind remote
651+ if a == "local-sha" && d == "remote-sha" {
652+ return true , nil
653+ }
654+ // b1's stale SHA is NOT an ancestor of b2 (already rebased)
655+ if a == "b1-stale-presquash-sha" {
656+ return false , nil
657+ }
658+ return true , nil
659+ }
660+ mock .MergeBaseFn = func (a , b string ) (string , error ) {
661+ if a == "main" && b == "b2" {
662+ return "main-b2-mergebase" , nil
663+ }
664+ return "default-mergebase" , nil
665+ }
666+ mock .UpdateBranchRefFn = func (string , string ) error { return nil }
667+ mock .CheckoutBranchFn = func (string ) error { return nil }
668+ mock .RebaseOntoFn = func (newBase , oldBase , branch string ) error {
669+ rebaseOntoCalls = append (rebaseOntoCalls , rebaseCall {newBase , oldBase , branch })
670+ return nil
671+ }
672+ mock .PushFn = func (string , []string , bool , bool ) error { return nil }
673+
674+ restore := git .SetOps (mock )
675+ defer restore ()
676+
677+ cfg , _ , _ := config .NewTestConfig ()
678+ cmd := SyncCmd (cfg )
679+ cmd .SetOut (io .Discard )
680+ cmd .SetErr (io .Discard )
681+ err := cmd .Execute ()
682+
683+ cfg .Out .Close ()
684+ cfg .Err .Close ()
685+
686+ assert .NoError (t , err )
687+ require .Len (t , rebaseOntoCalls , 2 )
688+
689+ // b2: stale ontoOldBase → falls back to merge-base(main, b2)
690+ assert .Equal (t , rebaseCall {"main" , "main-b2-mergebase" , "b2" }, rebaseOntoCalls [0 ],
691+ "b2 should use merge-base as oldBase when ontoOldBase is stale" )
692+
693+ // b3: b2's SHA is a valid ancestor → uses it directly
694+ assert .Equal (t , rebaseCall {"b2" , "b2-on-main-sha" , "b3" }, rebaseOntoCalls [1 ],
695+ "b3 should use b2's original SHA as oldBase" )
696+ }
697+
606698// TestSync_PushFailureAfterRebase verifies that when push fails after a
607699// successful rebase, the command does not return a fatal error — only a
608700// warning is printed about the push failure.
@@ -890,7 +982,12 @@ func TestSync_MergedBranchDeletedFromRemote(t *testing.T) {
890982 return "sha-" + ref , nil
891983 }
892984 mock .IsAncestorFn = func (a , d string ) (bool , error ) {
893- return a == "local-sha" && d == "remote-sha" , nil
985+ // Trunk FF check
986+ if a == "local-sha" && d == "remote-sha" {
987+ return true , nil
988+ }
989+ // For --onto stale-check: old bases are valid ancestors (first-run)
990+ return true , nil
894991 }
895992 mock .UpdateBranchRefFn = func (string , string ) error { return nil }
896993 mock .CheckoutBranchFn = func (string ) error { return nil }
0 commit comments