Skip to content

Commit b366231

Browse files
committed
add cleanup command
1 parent 6315f9a commit b366231

File tree

2 files changed

+208
-1
lines changed

2 files changed

+208
-1
lines changed

src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ enum Command {
101101
/// The name of the branch to delete.
102102
branch_name: String,
103103
},
104+
/// Clean up branches from the git-stack tree that no longer exist locally.
105+
Cleanup {
106+
/// Show what would be cleaned up without actually removing anything.
107+
#[arg(long, short, default_value_t = false)]
108+
dry_run: bool,
109+
/// Clean up all trees in the config, removing invalid repos and cleaning branches.
110+
#[arg(long, short, default_value_t = false)]
111+
all: bool,
112+
},
104113
}
105114

106115
fn main() {
@@ -172,6 +181,9 @@ fn inner_main() -> Result<()> {
172181
}
173182
Some(Command::Status { fetch }) => status(state, &repo, &current_branch, fetch),
174183
Some(Command::Delete { branch_name }) => state.delete_branch(&repo, &branch_name),
184+
Some(Command::Cleanup { dry_run, all }) => {
185+
state.cleanup_missing_branches(&repo, dry_run, all)
186+
}
175187
Some(Command::Diff { branch }) => diff(state, &repo, &branch.unwrap_or(current_branch)),
176188
Some(Command::Log { branch }) => show_log(state, &repo, &branch.unwrap_or(current_branch)),
177189
Some(Command::Note { edit, branch }) => {

src/state.rs

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub enum StackMethod {
3636
Merge,
3737
}
3838

39-
#[derive(Debug, Serialize, Deserialize)]
39+
#[derive(Debug, Clone, Serialize, Deserialize)]
4040
pub struct Branch {
4141
/// The name of the branch or ref.
4242
pub name: String,
@@ -239,6 +239,161 @@ impl State {
239239
Ok(())
240240
}
241241

242+
pub(crate) fn cleanup_missing_branches(
243+
&mut self,
244+
repo: &str,
245+
dry_run: bool,
246+
all: bool,
247+
) -> Result<()> {
248+
if all {
249+
self.cleanup_all_trees(dry_run)
250+
} else {
251+
self.cleanup_single_tree(repo, dry_run)
252+
}
253+
}
254+
255+
fn cleanup_single_tree(&mut self, repo: &str, dry_run: bool) -> Result<()> {
256+
let Some(tree) = self.trees.get_mut(repo) else {
257+
println!("No stack tree found for repo {}", repo.yellow());
258+
return Ok(());
259+
};
260+
261+
let mut removed_branches = Vec::new();
262+
let mut remounted_branches = Vec::new();
263+
264+
// Recursively cleanup the tree
265+
cleanup_tree_recursive(tree, &mut removed_branches, &mut remounted_branches);
266+
267+
if removed_branches.is_empty() {
268+
println!("No missing branches found. Tree is clean.");
269+
return Ok(());
270+
}
271+
272+
// Print summary
273+
println!("Cleanup summary for {}:", repo.yellow());
274+
println!();
275+
println!("Removed branches (no longer exist locally):");
276+
for branch_name in &removed_branches {
277+
println!(" - {}", branch_name.red());
278+
}
279+
280+
if !remounted_branches.is_empty() {
281+
println!();
282+
println!("Re-mounted branches (moved to grandparent):");
283+
for (branch_name, new_parent) in &remounted_branches {
284+
println!(
285+
" - {} {} {}",
286+
branch_name.yellow(),
287+
"→".truecolor(90, 90, 90),
288+
new_parent.green()
289+
);
290+
}
291+
}
292+
293+
if dry_run {
294+
println!();
295+
println!("{}", "Dry run mode: no changes were saved.".bright_blue());
296+
} else {
297+
self.save_state()?;
298+
println!();
299+
println!("{}", "Changes saved.".green());
300+
}
301+
302+
Ok(())
303+
}
304+
305+
fn cleanup_all_trees(&mut self, dry_run: bool) -> Result<()> {
306+
let mut repos_to_remove = Vec::new();
307+
let mut total_removed_branches = 0;
308+
let mut total_remounted_branches = 0;
309+
310+
// Collect all repo paths first to avoid borrow checker issues
311+
let repo_paths: Vec<String> = self.trees.keys().cloned().collect();
312+
313+
println!("Scanning {} repositories...", repo_paths.len());
314+
println!();
315+
316+
for repo_path in &repo_paths {
317+
// Check if the directory exists
318+
if !std::path::Path::new(repo_path).exists() {
319+
println!(
320+
"{}: {}",
321+
repo_path.yellow(),
322+
"directory does not exist".red()
323+
);
324+
repos_to_remove.push(repo_path.clone());
325+
continue;
326+
}
327+
328+
// Check if git works in this directory
329+
let original_dir = std::env::current_dir()?;
330+
let git_works = std::env::set_current_dir(repo_path).is_ok()
331+
&& run_git(&["rev-parse", "--git-dir"]).is_ok();
332+
333+
// Always restore the original directory
334+
std::env::set_current_dir(original_dir)?;
335+
336+
if !git_works {
337+
println!("{}: {}", repo_path.yellow(), "git is not working".red());
338+
repos_to_remove.push(repo_path.clone());
339+
continue;
340+
}
341+
342+
// Change to the repo directory and clean up branches
343+
let original_dir = std::env::current_dir()?;
344+
std::env::set_current_dir(repo_path)?;
345+
346+
let tree = self.trees.get_mut(repo_path).unwrap();
347+
let mut removed_branches = Vec::new();
348+
let mut remounted_branches = Vec::new();
349+
350+
cleanup_tree_recursive(tree, &mut removed_branches, &mut remounted_branches);
351+
352+
std::env::set_current_dir(original_dir)?;
353+
354+
if !removed_branches.is_empty() || !remounted_branches.is_empty() {
355+
println!(
356+
"{}: cleaned up {} branches, re-mounted {}",
357+
repo_path.yellow(),
358+
removed_branches.len().to_string().red(),
359+
remounted_branches.len().to_string().green()
360+
);
361+
total_removed_branches += removed_branches.len();
362+
total_remounted_branches += remounted_branches.len();
363+
} else {
364+
println!("{}: {}", repo_path.yellow(), "clean".green());
365+
}
366+
}
367+
368+
// Remove invalid repos
369+
if !repos_to_remove.is_empty() {
370+
println!();
371+
println!("Removing {} invalid repositories:", repos_to_remove.len());
372+
for repo_path in &repos_to_remove {
373+
println!(" - {}", repo_path.red());
374+
self.trees.remove(repo_path);
375+
}
376+
}
377+
378+
println!();
379+
println!("Summary:");
380+
println!(" Repositories scanned: {}", repo_paths.len());
381+
println!(" Invalid repositories removed: {}", repos_to_remove.len());
382+
println!(" Branches removed: {}", total_removed_branches);
383+
println!(" Branches re-mounted: {}", total_remounted_branches);
384+
385+
if dry_run {
386+
println!();
387+
println!("{}", "Dry run mode: no changes were saved.".bright_blue());
388+
} else {
389+
self.save_state()?;
390+
println!();
391+
println!("{}", "Changes saved.".green());
392+
}
393+
394+
Ok(())
395+
}
396+
242397
pub(crate) fn ensure_trunk(&mut self, repo: &str) -> Result<GitTrunk> {
243398
let trunk = git_trunk()?;
244399
// The branch might not exist in git, let's create it, and add it to the tree.
@@ -521,6 +676,46 @@ fn is_branch_mentioned_in_tree(branch_name: &str, branch: &Branch) -> bool {
521676
}
522677
false
523678
}
679+
680+
/// Recursively cleans up missing branches from the tree.
681+
/// Returns the number of branches cleaned up at this level.
682+
fn cleanup_tree_recursive(
683+
branch: &mut Branch,
684+
removed_branches: &mut Vec<String>,
685+
remounted_branches: &mut Vec<(String, String)>,
686+
) {
687+
// First, recursively process all children
688+
for child in &mut branch.branches {
689+
cleanup_tree_recursive(child, removed_branches, remounted_branches);
690+
}
691+
692+
// Collect branches to remove and their children to adopt
693+
let mut branches_to_adopt: Vec<Branch> = Vec::new();
694+
let mut indices_to_remove = Vec::new();
695+
696+
for (index, child) in branch.branches.iter().enumerate() {
697+
if !git_branch_exists(&child.name) {
698+
// This branch doesn't exist locally, mark it for removal
699+
removed_branches.push(child.name.clone());
700+
701+
// Collect its children to be adopted by the current branch
702+
for grandchild in &child.branches {
703+
branches_to_adopt.push(grandchild.clone());
704+
remounted_branches.push((grandchild.name.clone(), branch.name.clone()));
705+
}
706+
707+
indices_to_remove.push(index);
708+
}
709+
}
710+
711+
// Remove missing branches (in reverse order to maintain indices)
712+
for &index in indices_to_remove.iter().rev() {
713+
branch.branches.remove(index);
714+
}
715+
716+
// Add adopted branches
717+
branch.branches.extend(branches_to_adopt);
718+
}
524719
fn find_stack_with_branch<'a>(
525720
stacks: &'a mut [Vec<String>],
526721
current_branch: &str,

0 commit comments

Comments
 (0)