fix: prune empty directories left behind by switch/checkout/merge/pull
muse#95. muse switch (and checkout, merge, pull, rebase, cherry-pick, reset, revert, shelf -- every caller of apply_manifest, plus checkout's own separate incremental-delta path) correctly removes files absent from the target branch's manifest, but never pruned the directories those files lived in. A directory whose only file was removed persisted as an empty, untracked leftover indistinguishable from a directory a human just created and hasn't staged yet.
Confirmed broader blast radius per the ticket's own follow-up comment: not just cross-branch switch -- the identical leftover appears from a plain fast-forward along a single branch's own history, since fast- forward also goes through apply_manifest.
Git has no analogous state inconsistency here (it never tracked the directory as an entity, so a stray empty dir is pure filesystem noise, not a tracked-state problem). Muse directories are real, content- addressed tracked objects, so a leftover empty one is a genuine correctness gap, not cosmetic.
Fix: apply_manifest (muse/core/workdir.py) now prunes any directory left empty by file removal, deepest-first, stopping at the first directory that still has content, is explicitly protected via the new target_directories parameter, or is the repo root. checkout.py's separate _checkout_snapshot incremental-delta path (used by switch) gets the same treatment via the same shared helper, using the target snapshot's own directories list (already loaded there) as protection.
Threaded target_directories through every apply_manifest call site that has a target manifest at hand -- merge (fast-forward, octopus, all three merge-strategy branches, abort, partial-conflict), pull (fast-forward, three-way), rebase, cherry-pick, reset. Deliberately derives the protected-directories set from directories_from_manifest(target_manifest) everywhere, NOT from a loaded SnapshotRecord.directories, even at call sites where one was already in scope -- found live that a plain `muse commit` leaves its own snapshot's directories field stale (still lists a directory whose last file that same commit just removed), which would have made target_directories wrongly protect exactly the directories this fix exists to prune. directories_from_manifest derives fresh from the file manifest every time and can't go stale this way. (The underlying muse-commit stale-directories-field bug is real and worth its own ticket -- not fixed here, scoped out.)
Tests: new tests/test_apply_manifest_dir_pruning.py, 7 tests -- unit coverage for apply_manifest's pruning (single and multi-level empty directories, an intentionally-protected directory surviving, a directory with other surviving files left alone, a directory with an untracked human file left alone) plus two full CLI integration tests: the ticket's exact switch repro, and a same-repo fast-forward-merge reproduction of the follow-up comment's broader blast radius. All green. Fixed one pre-existing test (test_cmd_cherry_pick_hardening.py) whose apply_manifest tracking mock didn't accept the new keyword argument.
Regression suite across every apply_manifest/checkout caller (workdir integrity, commit workdir preservation, switch, detached-head switch, checkout, merge, octopus merge, pull, clone, rebase, cherry-pick, reset, revert, shelf): 870 passed, zero regressions.
Verified against the real CLI (muse-dev, not just pytest): both the ticket's exact repro and the fast-forward blast-radius scenario, confirmed fixed live -- the previously-leftover directories no longer exist on disk after switch/merge, and muse status reports no scaffold leftovers.
Semantic Changes
36 symbols
Files Changed
+1
~9
1213 in snapshot
0 comments
muse hub commit comment sha256:1f1fca3c6e64bf11f2bfded0e1f03ee4955e297af4d6b5138a2b1362e31f8187 --body "your comment"
No comments yet. Be the first to start the discussion.