@@ -173,6 +173,8 @@ pub enum RemoteChange {
173173 old_base : String ,
174174 new_base : String ,
175175 } ,
176+ /// Push a branch to remote (used before retargeting to it)
177+ PushBranch { branch : String } ,
176178}
177179
178180// ============== Stage 4: Sync Plan ==============
@@ -658,7 +660,7 @@ fn compute_sync_plan(
658660) -> SyncPlan {
659661 let mut local_changes = Vec :: new ( ) ;
660662 let mut remote_changes = Vec :: new ( ) ;
661- let warnings = Vec :: new ( ) ;
663+ let mut warnings = Vec :: new ( ) ;
662664
663665 // Compute local changes (pull direction)
664666 if !options. push_only {
@@ -699,6 +701,60 @@ fn compute_sync_plan(
699701 }
700702 }
701703
704+ // Ensure all parents are available (transitive closure)
705+ // Keep adding missing parents until no more are needed
706+ loop {
707+ let mut parents_to_add: Vec < ( String , String ) > = Vec :: new ( ) ;
708+
709+ for ( _, parent) in & branches_to_mount {
710+ // Skip if parent is trunk
711+ if parent == & local. trunk {
712+ continue ;
713+ }
714+ // Skip if parent is already in local tree
715+ if local. branches . contains_key ( parent) {
716+ continue ;
717+ }
718+ // Skip if parent is already being mounted
719+ if branches_to_mount. iter ( ) . any ( |( b, _) | b == parent) {
720+ continue ;
721+ }
722+ // Check if parent exists as remote tracking branch
723+ let remote_ref = format ! ( "{}/{}" , DEFAULT_REMOTE , parent) ;
724+ if git_repo. ref_exists ( & remote_ref) {
725+ // Mount missing parent on trunk
726+ parents_to_add. push ( ( parent. clone ( ) , local. trunk . clone ( ) ) ) ;
727+ }
728+ }
729+
730+ if parents_to_add. is_empty ( ) {
731+ break ;
732+ }
733+ branches_to_mount. extend ( parents_to_add) ;
734+ }
735+
736+ // Filter out branches whose parents still can't be resolved
737+ // (parents that don't exist as remote refs and aren't in tree)
738+ let branches_being_mounted: HashSet < String > =
739+ branches_to_mount. iter ( ) . map ( |( b, _) | b. clone ( ) ) . collect ( ) ;
740+
741+ let ( valid, invalid) : ( Vec < _ > , Vec < _ > ) = branches_to_mount
742+ . into_iter ( )
743+ . partition ( |( _, parent) | {
744+ parent == & local. trunk
745+ || local. branches . contains_key ( parent)
746+ || branches_being_mounted. contains ( parent)
747+ } ) ;
748+
749+ for ( branch, parent) in invalid {
750+ warnings. push ( format ! (
751+ "Skipping branch '{}': parent '{}' not available on remote" ,
752+ branch, parent
753+ ) ) ;
754+ }
755+
756+ let branches_to_mount = valid;
757+
702758 // Topologically sort branches to mount (parents before children)
703759 let sorted_branches = topological_sort_branches ( & branches_to_mount, & local. trunk ) ;
704760
@@ -793,6 +849,15 @@ fn compute_sync_plan(
793849 if let Some ( pr) = remote. prs . get ( child_name) {
794850 // PR's old base should be the unmounted branch, new base is repoint_to
795851 if pr. base == * branch_name {
852+ // Check if the new base branch is pushed to remote
853+ let new_base_remote_ref =
854+ format ! ( "{}/{}" , DEFAULT_REMOTE , repoint_to) ;
855+ if !git_repo. ref_exists ( & new_base_remote_ref) {
856+ // Need to push the intermediate branch first
857+ remote_changes. push ( RemoteChange :: PushBranch {
858+ branch : repoint_to. clone ( ) ,
859+ } ) ;
860+ }
796861 remote_changes. push ( RemoteChange :: RetargetPr {
797862 number : pr. number ,
798863 branch : child_name. clone ( ) ,
@@ -842,27 +907,25 @@ fn compute_sync_plan(
842907 match ( remote_pr, & target_branch. expected_pr_base ) {
843908 // PR exists, check if base matches
844909 ( Some ( pr) , Some ( expected_base) ) if pr. base != * expected_base => {
910+ // Check if the new base branch is pushed to remote
911+ let new_base_remote_ref = format ! ( "{}/{}" , DEFAULT_REMOTE , expected_base) ;
912+ if !git_repo. ref_exists ( & new_base_remote_ref) {
913+ // Need to push the intermediate branch first
914+ remote_changes. push ( RemoteChange :: PushBranch {
915+ branch : expected_base. clone ( ) ,
916+ } ) ;
917+ }
845918 remote_changes. push ( RemoteChange :: RetargetPr {
846919 number : pr. number ,
847920 branch : branch_name. clone ( ) ,
848921 old_base : pr. base . clone ( ) ,
849922 new_base : expected_base. clone ( ) ,
850923 } ) ;
851924 }
852- // No open PR but branch is pushed - could create PR
853- // But first check if there's a merged/closed PR
854- ( None , Some ( expected_base) ) => {
855- // Skip if this branch had a merged or closed PR
856- if remote. closed_prs . contains_key ( branch_name) {
857- continue ;
858- }
859- // Generate title from branch name or first commit
860- let title = branch_name. clone ( ) ;
861- remote_changes. push ( RemoteChange :: CreatePr {
862- branch : branch_name. clone ( ) ,
863- base : expected_base. clone ( ) ,
864- title,
865- } ) ;
925+ // No open PR but branch is pushed - don't auto-create PRs
926+ // Users should create PRs explicitly with `git stack pr`
927+ ( None , Some ( _expected_base) ) => {
928+ continue ;
866929 }
867930 _ => { }
868931 }
@@ -1337,6 +1400,15 @@ fn apply_remote_change(
13371400 )
13381401 . map_err ( |e| anyhow ! ( "{}" , e) ) ?;
13391402 }
1403+ RemoteChange :: PushBranch { branch } => {
1404+ println ! ( " Pushing '{}' to remote" , branch. yellow( ) ) ;
1405+ run_git ( & [
1406+ "push" ,
1407+ "-fu" ,
1408+ DEFAULT_REMOTE ,
1409+ & format ! ( "{}:{}" , branch, branch) ,
1410+ ] ) ?;
1411+ }
13401412 }
13411413 Ok ( ( ) )
13421414}
@@ -1422,6 +1494,9 @@ fn print_plan(plan: &SyncPlan, dry_run: bool) {
14221494 new_base. green( )
14231495 ) ;
14241496 }
1497+ RemoteChange :: PushBranch { branch } => {
1498+ println ! ( " - Push '{}' to remote" , branch. yellow( ) ) ;
1499+ }
14251500 }
14261501 }
14271502 }
0 commit comments