Files
dotfiles/.config/dot/.claude/tasks/0008-folders-collision-handling.md
alexion 1fd8e7e773 dotcli: add dot setup folders (XDG short-name migration)
Adds a dot setup subcommand family (idempotent, re-runnable machine-setup
tasks) with a folders task that brings the 8 standard XDG user
directories under a fixed short-name convention (Desktop -> .desktop,
Documents -> doc, Downloads -> dwn, Music -> mus, Pictures -> pic,
Videos -> vid, Templates/Public -> .ignoreme), hardcoded rather than read
from ~/.config/user-dirs.dirs -- that file is a separate, manually
tracked dotfile whose values can drift or go stale, and the migration
must not depend on it being correct. The nested Pictures/Screenshots
folder is renamed to pic/screenshots in the same pass.

Content left behind in a legacy full-named folder by a fresh XDG-defaults
install -- empty or not -- is merged into its short-named replacement.
A same-named collision between a legacy folder and its target is never
overwritten: it is skipped, reported, and the legacy folder is left in
place rather than deleted while something in it could not be merged.
--dry-run previews what would move/skip without touching the filesystem.
xdg-user-dirs-update runs once afterward to notify running apps/portals.
2026-07-06 16:31:35 -04:00

2.9 KiB

spec, blocked-by
spec blocked-by
dot-setup-folders 0007-folders-non-empty-confirmation

What to build

Make the --yes-confirmed merge from the prior slice collision-safe: when a legacy folder and its short-named target both contain an entry with the same name, use no-clobber move semantics so the target's existing file is never silently overwritten. Report which files were skipped due to a collision, and leave the legacy folder in place (don't remove it) whenever any collision occurred during that folder's migration, rather than deleting a folder that still holds something that couldn't be merged.

This closes the gap left by the old bash setup_folders's naive mv $from/* $to, which had no collision protection at all.

Acceptance criteria

  • A filename collision between a legacy folder and its already-populated short-named target is skipped, not overwritten (the target's existing file is preserved byte-for-byte)
  • The skipped collision is reported to the user
  • The legacy folder is left in place (not removed) when a collision occurred, even though --yes was given and other non-colliding files in it were moved
  • Re-running dot setup folders after a collision was reported and left in place behaves consistently (doesn't lose the previously-skipped file, doesn't re-move already-migrated files)
  • ~/.config/dot/tests/dot.fish covers the above cases and fishtape ~/.config/dot/tests/dot.fish passes

Implementation Notes

  • The two prior branches (silent-empty merge vs. --yes-confirmed merge) were unified into one if test (count $other_entries) -eq 0; or set -q _flag_yes branch, since the collision-detection/no-clobber logic is identical either way. This has one side effect beyond the letter of the acceptance criteria (which frame collision handling around the --yes path): a legacy folder that's otherwise "empty" except for an emptyish nested Screenshots dir now also gets collision-checked against an already-populated pic/screenshots on the silent, no---yes path. This closes the same unguarded-mv gap the spec calls out as the motivating problem (the old code's silent-path mv $screenshots_path $target_path/screenshots had no collision protection at all either), so it was kept rather than special-cased away. Covered by its own test ("a silent-path Screenshots collision ...").
  • Collision detection is a pre-check (test -e $target_path/...) before an actual mv -n, rather than relying on mv -n's exit code alone, so each colliding entry can be individually identified and reported by path.
  • /review-uncommitted (risk: Medium, standards: 0 hard violations, spec: 0 missing/wrong requirements) raised no changes needed; the one scope note it flagged (the silent-path Screenshots case above) was a deliberate, judged-correct decision rather than an oversight.