Prune unused worktrees during install
Assisted-by: Claude Opus 4.6 via pi
change pwqvlkznxrnrqpvzskslttuuoxnzmtpy
commit bec80f251fa311876ee1c9cae0f82ad51d9813bf
author Alpha Chen <alpha@kejadlen.dev>
date
parent nwkmzmqz
diff --git a/src/git.rs b/src/git.rs
index dc49149..fca878e 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
 use std::process::Command;
 
 use color_eyre::eyre::{Context, Result, bail};
+use tracing::info;
 
 /// Derive a bare repo path from a remote URL.
 /// e.g. "https://github.com/user/repo" -> repos_dir/github.com/user/repo.git
@@ -114,3 +115,73 @@ pub fn ensure_worktree(repo_path: &Path, rev: &str) -> Result<PathBuf> {
 
     Ok(wt_path)
 }
+
+/// Remove worktrees that aren't referenced by the lockfile.
+pub fn prune_worktrees(repos_dir: &Path, lockfile: &crate::lockfile::Lockfile) -> Result<()> {
+    use std::collections::{HashMap, HashSet};
+
+    // Build a map: repo_path -> set of used rev prefixes
+    let mut used: HashMap<PathBuf, HashSet<String>> = HashMap::new();
+    for plugin in &lockfile.plugins {
+        let rp = repo_path(repos_dir, &plugin.src);
+        used.entry(rp)
+            .or_default()
+            .insert(plugin.rev[..12].to_string());
+    }
+
+    // Walk all bare repos looking for pinch-worktrees dirs
+    visit_worktree_dirs(repos_dir, &used)?;
+
+    Ok(())
+}
+
+fn visit_worktree_dirs(
+    dir: &Path,
+    used: &std::collections::HashMap<PathBuf, std::collections::HashSet<String>>,
+) -> Result<()> {
+    let entries = match std::fs::read_dir(dir) {
+        Ok(e) => e,
+        Err(_) => return Ok(()),
+    };
+
+    for entry in entries {
+        let entry = entry?;
+        let path = entry.path();
+
+        if !path.is_dir() {
+            continue;
+        }
+
+        if path.file_name().is_some_and(|n| n == "pinch-worktrees") {
+            // This is a worktrees dir inside a bare repo
+            let repo_path = path.parent().unwrap();
+            let used_revs = used.get(repo_path);
+
+            for wt_entry in std::fs::read_dir(&path)? {
+                let wt_entry = wt_entry?;
+                let wt_name = wt_entry.file_name();
+                let wt_name_str = wt_name.to_string_lossy();
+
+                let is_used = used_revs.is_some_and(|revs| revs.contains(wt_name_str.as_ref()));
+
+                if !is_used {
+                    let wt_path = wt_entry.path();
+                    // Remove the git worktree properly
+                    let _ = std::process::Command::new("git")
+                        .args(["worktree", "remove", "--force", &wt_path.to_string_lossy()])
+                        .current_dir(repo_path)
+                        .status();
+                    // If that fails, just remove the directory
+                    if wt_path.exists() {
+                        std::fs::remove_dir_all(&wt_path)?;
+                    }
+                    info!("pruned worktree: {}", wt_path.display());
+                }
+            }
+        } else {
+            visit_worktree_dirs(&path, used)?;
+        }
+    }
+
+    Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index b62878a..85e9a06 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -212,6 +212,9 @@ fn do_install() -> Result<()> {
         }
     }
 
+    // Clean up stale worktrees
+    git::prune_worktrees(&repos_dir, &lockfile)?;
+
     info!("install complete");
     Ok(())
 }