@@ -567,6 +567,115 @@ describe("rebase autostash + conflict + undo", () => {
567567 } ) ) ;
568568} ) ;
569569
570+ // ── merged-new-work + dirty tree ────────────────────────────────
571+
572+ /**
573+ * Simulate a merge on the bare remote: clone, merge source into target, push.
574+ */
575+ async function simulateMerge (
576+ env : { testDir : string ; originDir : string } ,
577+ repo : string ,
578+ source : string ,
579+ target : string ,
580+ ) {
581+ const bareDir = join ( env . originDir , `${ repo } .git` ) ;
582+ const tmpClone = join ( env . testDir , `tmp-merge-${ repo } -${ Date . now ( ) } ` ) ;
583+ await git ( env . testDir , [ "clone" , bareDir , tmpClone ] ) ;
584+ await git ( tmpClone , [ "checkout" , target ] ) ;
585+ await git ( tmpClone , [ "merge" , `origin/${ source } ` ] ) ;
586+ await git ( tmpClone , [ "push" ] ) ;
587+ }
588+
589+ describe ( "merged-new-work with dirty tree" , ( ) => {
590+ /** Advance main via a temp clone (avoids stale canonical repo after simulateMerge). */
591+ async function advanceMainViaClone (
592+ env : { testDir : string ; originDir : string } ,
593+ repo : string ,
594+ file : string ,
595+ content : string ,
596+ ) {
597+ const tmpClone = join ( env . testDir , `tmp-advance-${ repo } -${ Date . now ( ) } ` ) ;
598+ await git ( env . testDir , [ "clone" , join ( env . originDir , `${ repo } .git` ) , tmpClone ] ) ;
599+ await git ( tmpClone , [ "checkout" , "main" ] ) ;
600+ await write ( join ( tmpClone , file ) , content ) ;
601+ await git ( tmpClone , [ "add" , file ] ) ;
602+ await git ( tmpClone , [ "commit" , "-m" , `advance main: ${ file } ` ] ) ;
603+ await git ( tmpClone , [ "push" ] ) ;
604+ }
605+
606+ test ( "merged-new-work with dirty tree and no autostash skips (not a false conflict)" , ( ) =>
607+ withEnv ( async ( env ) => {
608+ await arb ( env , [ "create" , "my-feature" , "repo-a" , "--base" , "main" ] ) ;
609+ const ws = join ( env . projectDir , "my-feature" ) ;
610+ const wt = join ( ws , "repo-a" ) ;
611+
612+ // Make a commit on feature and push so remote knows about the branch
613+ await write ( join ( wt , "feature.txt" ) , "feature" ) ;
614+ await git ( wt , [ "add" , "feature.txt" ] ) ;
615+ await git ( wt , [ "commit" , "-m" , "feature" ] ) ;
616+ await git ( wt , [ "push" , "-u" , "origin" , "my-feature" ] ) ;
617+
618+ // Simulate merge of feature branch into main on the remote
619+ await simulateMerge ( env , "repo-a" , "my-feature" , "main" ) ;
620+
621+ // Add new commit after the merge (the "new work")
622+ await write ( join ( wt , "new-feature.txt" ) , "new feature" ) ;
623+ await git ( wt , [ "add" , "new-feature.txt" ] ) ;
624+ await git ( wt , [ "commit" , "-m" , "new feature work" ] ) ;
625+
626+ // Advance main separately so there's behind count
627+ await advanceMainViaClone ( env , "repo-a" , "main-advance.txt" , "main advance" ) ;
628+
629+ // Create dirty changes (unstaged modified file)
630+ await write ( join ( wt , "feature.txt" ) , "dirty modification" ) ;
631+
632+ // Rebase without --autostash — should skip with "uncommitted changes", not report false conflict
633+ const r = await arb ( env , [ "rebase" , "--yes" ] , { cwd : ws } ) ;
634+ const output = r . stderr ;
635+ expect ( output ) . toContain ( "uncommitted changes" ) ;
636+ expect ( output ) . not . toContain ( "conflict" ) ;
637+ } ) ) ;
638+
639+ test ( "merged-new-work with dirty tree and --autostash succeeds" , ( ) =>
640+ withEnv ( async ( env ) => {
641+ await arb ( env , [ "create" , "my-feature" , "repo-a" , "--base" , "main" ] ) ;
642+ const ws = join ( env . projectDir , "my-feature" ) ;
643+ const wt = join ( ws , "repo-a" ) ;
644+
645+ // Make a commit on feature and push
646+ await write ( join ( wt , "feature.txt" ) , "feature" ) ;
647+ await git ( wt , [ "add" , "feature.txt" ] ) ;
648+ await git ( wt , [ "commit" , "-m" , "feature" ] ) ;
649+ await git ( wt , [ "push" , "-u" , "origin" , "my-feature" ] ) ;
650+
651+ // Simulate merge of feature branch into main on the remote
652+ await simulateMerge ( env , "repo-a" , "my-feature" , "main" ) ;
653+
654+ // Add new commit after the merge
655+ await write ( join ( wt , "new-feature.txt" ) , "new feature" ) ;
656+ await git ( wt , [ "add" , "new-feature.txt" ] ) ;
657+ await git ( wt , [ "commit" , "-m" , "new feature work" ] ) ;
658+
659+ // Advance main separately
660+ await advanceMainViaClone ( env , "repo-a" , "main-advance.txt" , "main advance" ) ;
661+
662+ // Create dirty changes on a file not touched by rebased commits
663+ await write ( join ( wt , "dirty.txt" ) , "uncommitted work" ) ;
664+ await git ( wt , [ "add" , "dirty.txt" ] ) ;
665+
666+ // Rebase with --autostash — should succeed
667+ const r = await arb ( env , [ "rebase" , "--yes" , "--autostash" ] , { cwd : ws } ) ;
668+ if ( r . exitCode !== 0 ) {
669+ throw new Error ( `arb rebase failed (exit ${ r . exitCode } ):\nstderr: ${ r . stderr } \nstdout: ${ r . stdout } ` ) ;
670+ }
671+ expect ( r . stderr ) . toContain ( "Rebased 1 repo" ) ;
672+ expect ( r . stderr ) . not . toContain ( "conflicted" ) ;
673+
674+ // Dirty file should be preserved
675+ expect ( existsSync ( join ( wt , "dirty.txt" ) ) ) . toBe ( true ) ;
676+ } ) ) ;
677+ } ) ;
678+
570679// ── continue-then-undo end-to-end ────────────────────────────────
571680
572681describe ( "rebase continue then undo" , ( ) => {
0 commit comments