Skip to content

Commit cad414d

Browse files
committed
fix(sync): handle missing parents, push before retarget, disable auto-PR
- auto-mount missing parent branches from remote during sync - push intermediate branches before retargeting PRs to them - remove auto-PR creation; users must use `git stack pr` explicitly
1 parent 75b70bf commit cad414d

2 files changed

Lines changed: 93 additions & 18 deletions

File tree

src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ enum Command {
189189
/// Clean up branches from the git-stack tree that no longer exist locally.
190190
Cleanup {
191191
/// Show what would be cleaned up without actually removing anything.
192-
#[arg(long, short, default_value_t = false)]
192+
#[arg(long, short = 'n', default_value_t = false)]
193193
dry_run: bool,
194194
/// Clean up all trees in the config, removing invalid repos and cleaning branches.
195195
#[arg(long, short, default_value_t = false)]
@@ -230,7 +230,7 @@ enum Command {
230230
#[arg(long, conflicts_with = "push")]
231231
pull: bool,
232232
/// Show what would be done without making changes
233-
#[arg(long)]
233+
#[arg(long, short = 'n')]
234234
dry_run: bool,
235235
},
236236
}
@@ -266,7 +266,7 @@ enum PrAction {
266266
#[arg(long, short)]
267267
all: bool,
268268
/// Show what would be done without making changes
269-
#[arg(long)]
269+
#[arg(long, short = 'n')]
270270
dry_run: bool,
271271
},
272272
}

src/sync.rs

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)