feat(story-200): auto-prune worktrees when stories are archived
- Add `prune_worktree_sync` to worktree.rs: removes a story's worktree if it exists, delegating to `remove_worktree_sync` (best-effort, failures logged internally) - Update `sweep_done_to_archived` to accept `git_root` and call `prune_worktree_sync` after promoting a story from 5_done to 6_archived - Add Part 2 to the sweep: scan 6_archived and prune any stale worktrees for stories already there (catches items archived before this feature) - All worktree removal failures are logged but never block file moves - Add 5 new tests: prune noop, prune real worktree, sweep-on-promote, sweep-stale-archived, sweep-not-blocked-by-removal-failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -166,6 +166,21 @@ fn configure_sparse_checkout(_wt_path: &Path) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the git worktree for a story if it exists, deriving the path and
|
||||
/// branch name deterministically from `project_root` and `story_id`.
|
||||
///
|
||||
/// Returns `Ok(())` if the worktree was removed or did not exist.
|
||||
/// Removal is best-effort: `remove_worktree_sync` logs failures internally
|
||||
/// but always returns `Ok`.
|
||||
pub fn prune_worktree_sync(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let wt_path = worktree_path(project_root, story_id);
|
||||
if !wt_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let branch = branch_name(story_id);
|
||||
remove_worktree_sync(project_root, &wt_path, &branch)
|
||||
}
|
||||
|
||||
/// Remove a git worktree and its branch.
|
||||
pub async fn remove_worktree(
|
||||
project_root: &Path,
|
||||
@@ -653,6 +668,33 @@ mod tests {
|
||||
assert!(result.is_ok(), "Expected removal to succeed: {:?}", result.err());
|
||||
}
|
||||
|
||||
// ── prune_worktree_sync ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prune_worktree_sync_noop_when_no_worktree_dir() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
// No worktree directory exists — must return Ok without touching git.
|
||||
let result = prune_worktree_sync(tmp.path(), "42_story_nonexistent");
|
||||
assert!(result.is_ok(), "Expected Ok when worktree dir absent: {:?}", result.err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_worktree_sync_removes_real_worktree() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let project_root = tmp.path().join("my-project");
|
||||
fs::create_dir_all(&project_root).unwrap();
|
||||
init_git_repo(&project_root);
|
||||
|
||||
let story_id = "55_story_prune_test";
|
||||
let wt_path = worktree_path(&project_root, story_id);
|
||||
create_worktree_sync(&project_root, &wt_path, &format!("feature/story-{story_id}")).unwrap();
|
||||
assert!(wt_path.exists(), "worktree dir should exist before prune");
|
||||
|
||||
let result = prune_worktree_sync(&project_root, story_id);
|
||||
assert!(result.is_ok(), "prune_worktree_sync must return Ok: {:?}", result.err());
|
||||
assert!(!wt_path.exists(), "worktree dir should be gone after prune");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_worktree_succeeds_despite_setup_failure() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user