Skip to content

Commit dd37971

Browse files
committed
cleanup pruning
1 parent d79c742 commit dd37971

3 files changed

Lines changed: 126 additions & 10 deletions

File tree

src/main.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ enum Command {
134134
},
135135
/// Open the git-stack state file in an editor for manual editing.
136136
Edit,
137-
/// Restack your active branch and all branches in its related stack.
137+
/// Restack your active branch onto its parent branch.
138138
Restack {
139139
/// The name of the branch to restack.
140140
#[arg(long, short)]
@@ -145,6 +145,9 @@ enum Command {
145145
/// Push any changes up to the remote after restacking.
146146
#[arg(long, short)]
147147
push: bool,
148+
/// Restack all parent branches recursively up to trunk.
149+
#[arg(long, short = 'a', default_value_t = false)]
150+
all_parents: bool,
148151
},
149152
/// Shows the log between the given branch and its parent (git-stack tree) branch.
150153
Log {
@@ -368,6 +371,7 @@ fn inner_main() -> Result<()> {
368371
branch,
369372
fetch,
370373
push,
374+
all_parents,
371375
}) => {
372376
let restack_branch = branch.clone().unwrap_or_else(|| current_branch.clone());
373377
state.try_auto_mount(&git_repo, &repo, &restack_branch)?;
@@ -380,6 +384,7 @@ fn inner_main() -> Result<()> {
380384
current_branch,
381385
fetch,
382386
push,
387+
all_parents,
383388
)
384389
}
385390
Some(Command::Mount { parent_branch }) => {
@@ -969,15 +974,33 @@ fn restack(
969974
orig_branch: String,
970975
fetch: bool,
971976
push: bool,
977+
all_parents: bool,
972978
) -> Result<(), anyhow::Error> {
973979
let restack_branch = restack_branch.unwrap_or(orig_branch.clone());
974980

975981
if fetch {
976982
git_fetch()?;
977983
}
978984

985+
// Ensure target branch exists locally (check it out from remote if needed)
986+
if !git_repo.branch_exists(&restack_branch) {
987+
let remote_ref = format!("{DEFAULT_REMOTE}/{restack_branch}");
988+
if git_repo.ref_exists(&remote_ref) {
989+
run_git(&["checkout", "-b", &restack_branch, &remote_ref])?;
990+
println!(
991+
"Created local branch {} from remote.",
992+
restack_branch.yellow()
993+
);
994+
} else {
995+
bail!(
996+
"Branch {} does not exist locally or on remote.",
997+
restack_branch
998+
);
999+
}
1000+
}
1001+
9791002
// Find starting_branch in the stacks of branches to determine which stack to use.
980-
let plan = state.plan_restack(git_repo, repo, &restack_branch)?;
1003+
let plan = state.plan_restack(git_repo, repo, &restack_branch, all_parents)?;
9811004

9821005
tracing::debug!(?plan, "Restacking branches with plan. Checking out main...");
9831006
git_checkout_main(git_repo, None)?;
@@ -1002,6 +1025,7 @@ fn restack(
10021025
if push
10031026
&& !git_repo.shas_match(&format!("{DEFAULT_REMOTE}/{}", branch.name), &branch.name)
10041027
{
1028+
println!("Pushing branch '{}' to remote...", branch.name);
10051029
run_git(&[
10061030
"push",
10071031
match branch.stack_method {
@@ -1106,7 +1130,7 @@ fn restack(
11061130
"git checkout {} failed",
11071131
restack_branch
11081132
);
1109-
tracing::info!("Done.");
1133+
println!("Done.");
11101134
state.refresh_lkgs(git_repo, repo)?;
11111135

11121136
// Note: PR sync is now handled separately via `git stack sync`

src/state.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,34 @@ impl State {
288288
git_repo: &GitRepo,
289289
repo: &str,
290290
starting_branch: &str,
291+
all_parents: bool,
291292
) -> Result<Vec<RestackStep<'_>>> {
292-
tracing::debug!("Planning restack for {starting_branch}");
293-
let trunk = git_trunk(git_repo)?;
294-
// Find all the descendents of the starting branch.
295-
// Traverse the tree from the starting branch to the root,
293+
tracing::debug!("Planning restack for {starting_branch} (all_parents={all_parents})");
294+
295+
// Single-step mode: only restack the target branch onto its immediate parent
296+
if !all_parents {
297+
let parent = self
298+
.get_parent_branch_of(repo, starting_branch)
299+
.ok_or_else(|| {
300+
anyhow!("Branch {starting_branch} not found in the git-stack tree.")
301+
})?;
302+
303+
let branch = self.get_tree_branch(repo, starting_branch).ok_or_else(|| {
304+
anyhow!("Branch {starting_branch} not found in the git-stack tree.")
305+
})?;
306+
307+
// Resolve parent ref to local or origin/branch
308+
let parent_ref = git_repo
309+
.resolve_branch_ref(&parent.name)
310+
.unwrap_or_else(|| parent.name.clone());
311+
312+
return Ok(vec![RestackStep {
313+
parent: parent_ref,
314+
branch,
315+
}]);
316+
}
317+
318+
// All-parents mode: restack the entire ancestry chain
296319
let mut path: Vec<&Branch> = vec![];
297320
let repo_state = self
298321
.repos

src/sync.rs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ pub struct SyncPlan {
183183
pub local_changes: Vec<LocalChange>,
184184
pub remote_changes: Vec<RemoteChange>,
185185
pub warnings: Vec<String>,
186+
/// Branches that will be unmounted (for checkout logic if user is on one)
187+
pub branches_to_unmount: Vec<String>,
188+
/// Branches safe to delete locally (work preserved on remote)
189+
pub branches_to_delete: Vec<String>,
186190
}
187191

188192
impl SyncPlan {
@@ -712,9 +716,10 @@ fn compute_sync_plan(
712716
}
713717
}
714718

715-
// Detect merged PRs and handle unmounting (pull direction)
719+
// Detect merged/closed PRs and handle unmounting (pull direction)
716720
// This runs regardless of push_only/pull_only since it's about reconciling state
717721
let mut branches_to_unmount: Vec<(String, String)> = Vec::new(); // (branch, repoint_to)
722+
let mut branches_to_delete: Vec<String> = Vec::new();
718723

719724
tracing::debug!(
720725
"Checking for merged PRs. Local branches: {:?}, Closed PRs: {:?}",
@@ -738,14 +743,31 @@ fn compute_sync_plan(
738743
closed_pr.number,
739744
closed_pr.state
740745
);
741-
if closed_pr.state == RemotePrState::Merged {
742-
// This branch's PR was merged - it should be unmounted
746+
747+
// Any closed PR (merged or just closed) should unmount from git-stack
748+
if matches!(closed_pr.state, RemotePrState::Merged | RemotePrState::Closed) {
749+
// This branch's PR was merged/closed - it should be unmounted
743750
// Children should be repointed to this branch's parent
744751
let repoint_to = local_branch
745752
.parent
746753
.clone()
747754
.unwrap_or_else(|| local.trunk.clone());
748755
branches_to_unmount.push((branch_name.clone(), repoint_to));
756+
757+
// Determine if local branch is safe to delete
758+
// Safe if: merged, OR (closed AND remote exists AND local is ancestor of remote)
759+
let safe_to_delete = if closed_pr.state == RemotePrState::Merged {
760+
true
761+
} else {
762+
// Closed but not merged - check if remote has our work
763+
let remote_ref = format!("{}/{}", DEFAULT_REMOTE, branch_name);
764+
git_repo.ref_exists(&remote_ref)
765+
&& git_repo.is_ancestor(branch_name, &remote_ref).unwrap_or(false)
766+
};
767+
768+
if safe_to_delete {
769+
branches_to_delete.push(branch_name.clone());
770+
}
749771
}
750772
}
751773
}
@@ -953,6 +975,11 @@ fn compute_sync_plan(
953975
local_changes,
954976
remote_changes,
955977
warnings,
978+
branches_to_unmount: branches_to_unmount
979+
.iter()
980+
.map(|(name, _)| name.clone())
981+
.collect(),
982+
branches_to_delete,
956983
}
957984
}
958985

@@ -1037,6 +1064,38 @@ fn apply_plan(
10371064
repo_id: &RepoIdentifier,
10381065
plan: &SyncPlan,
10391066
) -> Result<()> {
1067+
// If current branch is being unmounted, checkout a safe ancestor first
1068+
if !plan.branches_to_unmount.is_empty() {
1069+
let current_branch = git_repo.current_branch().unwrap_or_default();
1070+
let unmount_set: HashSet<&str> = plan
1071+
.branches_to_unmount
1072+
.iter()
1073+
.map(|s| s.as_str())
1074+
.collect();
1075+
1076+
if unmount_set.contains(current_branch.as_str()) {
1077+
// Find the first ancestor that's NOT being unmounted
1078+
let trunk = git_trunk(git_repo)?;
1079+
let mut safe_branch = trunk.main_branch.clone();
1080+
let mut current = current_branch.clone();
1081+
1082+
while let Some(parent) = state.get_parent_branch_of(repo, &current) {
1083+
if !unmount_set.contains(parent.name.as_str()) {
1084+
safe_branch = parent.name.clone();
1085+
break;
1086+
}
1087+
current = parent.name.clone();
1088+
}
1089+
1090+
println!(
1091+
"Branch {} was merged. Switching to {}...",
1092+
current_branch.yellow(),
1093+
safe_branch.green()
1094+
);
1095+
run_git(&["checkout", &safe_branch])?;
1096+
}
1097+
}
1098+
10401099
// Apply local changes first (checkout, mount, update pr_number)
10411100
for change in &plan.local_changes {
10421101
apply_local_change(git_repo, state, repo, change)?;
@@ -1053,6 +1112,16 @@ fn apply_plan(
10531112
// Save state again if PR numbers were updated
10541113
state.save_state()?;
10551114

1115+
// Delete local branches that are safe to delete (work preserved on remote)
1116+
for branch_name in &plan.branches_to_delete {
1117+
if git_repo.branch_exists(branch_name) {
1118+
println!("Deleting local branch {}...", branch_name.yellow());
1119+
if let Err(e) = run_git(&["branch", "-D", branch_name]) {
1120+
tracing::warn!("Failed to delete local branch {}: {}", branch_name, e);
1121+
}
1122+
}
1123+
}
1124+
10561125
Ok(())
10571126
}
10581127

0 commit comments

Comments
 (0)