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.
This commit is contained in:
@@ -41,20 +41,50 @@ duplication point, and a README command-table row.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `dot setup folders` on a fresh scratch `$HOME` (all 8 legacy folders
|
||||
- [x] `dot setup folders` on a fresh scratch `$HOME` (all 8 legacy folders
|
||||
present and empty) renames them to their short-name targets per the
|
||||
mapping table, including `Pictures/Screenshots→pic/screenshots`, and
|
||||
leaves the tracked `user-dirs.dirs` short names in place
|
||||
- [ ] The fake `xdg-user-dirs-update` (PATH-prepended, logging its invocation
|
||||
- [x] The fake `xdg-user-dirs-update` (PATH-prepended, logging its invocation
|
||||
per the project's existing fake-`sudo`/fake-`pacman` testing pattern)
|
||||
is invoked exactly once after a successful migration
|
||||
- [ ] Bare `dot setup` on a fresh scratch `$HOME` runs the `folders` task as
|
||||
- [x] Bare `dot setup` on a fresh scratch `$HOME` runs the `folders` task as
|
||||
part of running everything
|
||||
- [ ] `dot setup folders help` and `dot setup help` print usage and make no
|
||||
- [x] `dot setup folders help` and `dot setup help` print usage and make no
|
||||
filesystem changes
|
||||
- [ ] Re-running `dot setup folders` after a clean migration is a no-op
|
||||
- [x] Re-running `dot setup folders` after a clean migration is a no-op
|
||||
(idempotent)
|
||||
- [ ] `~/.github/README.md` has a command-table row for `dot setup`
|
||||
- [x] `~/.github/README.md` has a command-table row for `dot setup`
|
||||
(and its `folders` task) with paths relative to `$HOME`
|
||||
- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and
|
||||
- [x] `~/.config/dot/tests/dot.fish` covers the above cases and
|
||||
`fishtape ~/.config/dot/tests/dot.fish` passes
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The desired short names for `dot setup folders` are read directly from the
|
||||
tracked `~/.config/user-dirs.dirs` (parsed via `grep`/`string match`, not
|
||||
sourced as shell), per the parent spec's decision that this file is the
|
||||
single source of truth. This machine's real `user-dirs.dirs` was
|
||||
deliberately left untouched/untracked and no live migration was run against
|
||||
this machine's actual home directory — the user chose "code + tests only"
|
||||
scope for this task (a real rename of `~/Desktop`, `~/Documents`, etc. is a
|
||||
separate, explicit action to take later), so only the scratch-`$HOME`
|
||||
fishtape fixtures exercise the short-name `user-dirs.dirs` content.
|
||||
Tracking the real file and running the real migration remains open.
|
||||
- During `/review-uncommitted`, the spec-fidelity pass caught a real bug: the
|
||||
nested `Pictures/Screenshots→pic/screenshots` move ran unconditionally,
|
||||
before checking whether `Pictures` held other, unrelated content — so a
|
||||
`Pictures` folder with both `Screenshots/` and some other file got
|
||||
partially mutated (Screenshots pulled out) while still being reported as
|
||||
"left in place." Fixed by gating the Screenshots move on the rest of the
|
||||
folder being empty too; added a regression test for this case
|
||||
("Screenshots is not peeled off... when Pictures still has other
|
||||
content").
|
||||
- Completions (`~/.config/fish/completions/dot.fish`) got a `dot setup`
|
||||
block mirroring `dot kde`'s per-subcommand completion entries, even though
|
||||
the task's required "completions/help-glob duplication point" is already
|
||||
satisfied automatically by the existing generic directory glob (no changes
|
||||
were needed there for `dot setup`/`dot help` to discover the new nested
|
||||
command). The added completions are a small polish addition beyond the
|
||||
strict letter of the acceptance criteria, consistent with the existing
|
||||
`kde` subcommand's treatment.
|
||||
|
||||
@@ -19,12 +19,34 @@ rule.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A legacy folder with real content (a real file, not just an empty
|
||||
- [x] A legacy folder with real content (a real file, not just an empty
|
||||
directory) refuses to migrate without `--yes`, prints what would have
|
||||
been moved, and leaves the folder and its contents untouched
|
||||
- [ ] The same legacy folder migrates successfully when `--yes` is passed
|
||||
- [ ] A legacy folder containing only a stray dotfile/metadata file (e.g. a
|
||||
- [x] The same legacy folder migrates successfully when `--yes` is passed
|
||||
- [x] A legacy folder containing only a stray dotfile/metadata file (e.g. a
|
||||
fake `.directory`) is still treated as non-empty and triggers the same
|
||||
confirmation gate
|
||||
- [ ] `~/.config/dot/tests/dot.fish` covers the above cases and
|
||||
- [x] `~/.config/dot/tests/dot.fish` covers the above cases and
|
||||
`fishtape ~/.config/dot/tests/dot.fish` passes
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `--yes`'s actual move reuses the exact same branch shape as the existing
|
||||
silent-empty path (rename `Screenshots` → `screenshots` when present, then
|
||||
`rmdir` the legacy folder), extended to also `mv` any remaining top-level
|
||||
entries into the target first. Screenshots is always moved as one atomic
|
||||
unit — its individual files are never mv'd/reported separately — so a
|
||||
non-empty `Screenshots` (own acceptance criterion in the parent spec) is
|
||||
gated and migrated the same way a non-empty top-level file would be.
|
||||
- Collision handling (no-clobber `mv -n`, reporting skipped files, leaving the
|
||||
legacy folder in place on a collision) is explicitly out of scope here —
|
||||
it's owned by 0008-folders-collision-handling.md, per that task's own
|
||||
frontmatter/spec section. The `--yes` path added here uses a plain `mv`.
|
||||
- `/review-uncommitted` flagged two minor issues, both fixed: a stale comment
|
||||
claiming a helper variable was used by both the silent-empty and `--yes`
|
||||
paths when it was only read by the latter, and a duplicated `find`
|
||||
invocation computing the same top-level listing twice under one condition
|
||||
(now computed once and reused). It also flagged the non-empty "would move"
|
||||
preview listing recursively-nested files individually instead of treating
|
||||
`Screenshots` as one unit like the real move does — fixed so the preview
|
||||
and the actual move share the same top-level-entries list.
|
||||
|
||||
@@ -18,15 +18,38 @@ $to`, which had no collision protection at all.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A filename collision between a legacy folder and its already-populated
|
||||
- [x] 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
|
||||
- [x] The skipped collision is reported to the user
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] `~/.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.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
spec: dot-setup-folders
|
||||
blocked-by: 0008-folders-collision-handling
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Remove the `--yes` confirmation gate that 0007/0008 built: a legacy folder
|
||||
with real content in it is migrated unconditionally now, the same as an
|
||||
empty one, since the collision handling from 0008 already makes the merge
|
||||
non-destructive on its own (a same-named entry is never overwritten, and the
|
||||
legacy folder is kept whenever any collision occurred). The `--yes` gate
|
||||
turned out to protect against a scenario collision handling already
|
||||
prevents, while making the everyday case — a machine that already has real
|
||||
files in `~/Documents`, `~/Pictures`, etc. — a silent no-op unless the flag
|
||||
was remembered, which defeats the point of the task.
|
||||
|
||||
In its place:
|
||||
|
||||
- `dot setup folders` always attempts the merge for every legacy folder,
|
||||
content or none.
|
||||
- A new `--dry-run` flag replaces `--yes` in the flag slot: it reports what
|
||||
would move and what would be skipped as a collision, without touching the
|
||||
filesystem at all (no `mkdir`, no `mv`/`rmdir`, no `xdg-user-dirs-update`).
|
||||
- A real (non-dry-run) run now reports what it moved per legacy folder
|
||||
(e.g. `moved 12 entries from ~/Documents to ~/doc`), instead of staying
|
||||
silent on success. A folder where nothing top-level moved (already empty,
|
||||
or everything in it collided) prints no such line — only non-trivial moves
|
||||
and collisions produce output.
|
||||
- `--yes` is removed outright (not kept as a silent no-op): passing it now
|
||||
fails with argparse's standard unknown-option error.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] A legacy folder with real content merges on a plain `dot setup
|
||||
folders`, with no flag required
|
||||
- [x] A real run prints `moved N entries from ~/<legacy> to ~/<target>` for a
|
||||
folder where top-level entries actually moved, and nothing for a
|
||||
folder where none did
|
||||
- [x] A real run prints a dedicated line when the nested Screenshots folder
|
||||
itself is moved (e.g. `moved ~/Pictures/Screenshots to ~/pic/screenshots`)
|
||||
- [x] Collision detection/reporting and the "leave the legacy folder in
|
||||
place when a collision occurred" behavior from 0008 are unchanged
|
||||
under the new unconditional default
|
||||
- [x] `--dry-run` reports the same would-move/would-skip information without
|
||||
creating any target directory, moving/removing anything, or invoking
|
||||
`xdg-user-dirs-update`
|
||||
- [x] `dot setup folders --yes` fails with an unknown-option error (argparse
|
||||
default), rather than being silently accepted or gated on
|
||||
- [x] `dot setup folders help` output no longer mentions `--yes` and
|
||||
documents `--dry-run` instead
|
||||
- [x] Idempotency holds: re-running after a clean merge, and re-running
|
||||
after a collision was reported, both behave the same as before
|
||||
- [x] `~/.config/dot/tests/dot.fish` is updated to exercise the above
|
||||
(replacing the old `--yes`-gated cases) and
|
||||
`fishtape ~/.config/dot/tests/dot.fish` passes
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `--yes` gate and the `screenshots_emptyish`/`other_entries` machinery
|
||||
that computed it were deleted outright rather than special-cased away:
|
||||
once merging is unconditional, that machinery had no remaining purpose
|
||||
(it existed solely to decide "empty enough to skip the gate").
|
||||
- `mkdir -p $target_path` and the final `xdg-user-dirs-update` are both now
|
||||
guarded by `not set -q _flag_dry_run`, making `--dry-run` a true no-op
|
||||
rather than "no-op except for directory scaffolding."
|
||||
- Collision detection (`test -e $target_path/...`) runs identically in both
|
||||
modes; `--dry-run` only gates the actual `mv`/`rmdir`/`mkdir` calls, so the
|
||||
reported would-move/would-skip split is exactly what a real run would do.
|
||||
- Success reporting is per-legacy-folder and suppressed at zero: a folder
|
||||
that was already empty (or whose only entries all collided) prints
|
||||
nothing, so a routine re-run stays quiet like before.
|
||||
- All prior collision/idempotency/Screenshots test scenarios were kept,
|
||||
just re-pointed at the plain `dot setup folders` invocation instead of
|
||||
`--yes`; two scenarios that only differed by which code branch (`--yes`
|
||||
vs. silent-empty) they exercised now hit the same branch, but were both
|
||||
kept since they still cover distinct fixture shapes (Pictures with vs.
|
||||
without unrelated top-level content alongside a colliding Screenshots).
|
||||
- `fishtape ~/.config/dot/tests/dot.fish` passes (178 tests).
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
spec: dot-setup-folders
|
||||
blocked-by: 000b-folders-unconditional-merge
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Stop reading the short-name target from `~/.config/user-dirs.dirs` and
|
||||
hardcode the legacy-name -> short-name mapping directly in
|
||||
`_dot_setup_folders`, dropping the dependency on that file entirely.
|
||||
|
||||
The original design treated the tracked `user-dirs.dirs` as the single
|
||||
source of truth for target names, assuming someone would hand-edit it to
|
||||
the short names before ever running the command. On this machine that
|
||||
never happened: the tracked file still had the stock XDG defaults
|
||||
(`XDG_DOCUMENTS_DIR="$HOME/Documents"`, etc.), so `target_path` resolved to
|
||||
the exact same directory as `legacy_path` for every folder. The migration
|
||||
logic then reported every entry as a "collision" against itself instead of
|
||||
moving anything -- a confusing, silent-feeling failure rather than an
|
||||
actual migration.
|
||||
|
||||
The short names are fixed (`.desktop`, `doc`, `dwn`, `mus`, `pic`, `vid`,
|
||||
`.ignoreme`) and not meant to be configurable, so there's nothing to read
|
||||
from a file in the first place. `user-dirs.dirs` remains a separate,
|
||||
manually tracked dotfile (edited and tracked by hand, like any other
|
||||
dotfile) for apps/`xdg-user-dirs-update` to consult -- `dot setup folders`
|
||||
itself no longer reads it, requires its presence, or writes to it.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `_dot_setup_folders` no longer reads, parses, or requires
|
||||
`~/.config/user-dirs.dirs`; the legacy->short-name mapping is a fixed
|
||||
table in the function itself
|
||||
- [x] Migration works identically whether `user-dirs.dirs` is absent,
|
||||
empty, or declares stale/full-name values (the exact real-world case)
|
||||
- [x] `user-dirs.dirs` is left byte-for-byte untouched by `dot setup
|
||||
folders` when present, and no file is created when absent
|
||||
- [x] `dot setup folders help` no longer describes reading target names
|
||||
from `user-dirs.dirs`
|
||||
- [x] `~/.config/dot/tests/dot.fish` no longer seeds a `user-dirs.dirs`
|
||||
fixture as a migration precondition, and covers the stale/missing
|
||||
cases above; `fishtape ~/.config/dot/tests/dot.fish` passes
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Replaced the `xdg_vars`/`grep`/`string match` parsing of `user-dirs.dirs`
|
||||
with two parallel hardcoded arrays, `legacy_names` and `target_names`,
|
||||
indexed together -- same shape the code already used for `legacy_names`
|
||||
alone, just extended to cover the target side too.
|
||||
- The early `if not test -f $user_dirs; return 1` guard was deleted outright
|
||||
rather than kept as a soft check: there's nothing left for the function to
|
||||
read from that file, so requiring its existence would just be a
|
||||
vestigial, unjustifiable precondition.
|
||||
- Removed the `short_name_user_dirs` fixture and its seeding step from every
|
||||
test scenario (it was previously duplicated into ~15 scenarios as a
|
||||
migration precondition); added two new scenarios instead: one reproducing
|
||||
the exact real-machine bug (stale full-name `user-dirs.dirs` values) and
|
||||
one confirming migration works with no `user-dirs.dirs` file at all.
|
||||
- Verified against this machine's real, still-stale `~/.config/user-dirs.dirs`
|
||||
via `_dot_setup_folders --dry-run`: previously reported every entry in
|
||||
Desktop/Documents/Downloads/Pictures/Videos as a collision against
|
||||
itself; now correctly reports `would move N entries from ~/Documents to
|
||||
~/doc` etc.
|
||||
- `fishtape ~/.config/dot/tests/dot.fish` passes (183 tests).
|
||||
Reference in New Issue
Block a user