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:
2026-07-06 16:19:06 -04:00
parent 540928f24f
commit 1fd8e7e773
11 changed files with 783 additions and 16 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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).

View File

@@ -0,0 +1,134 @@
function _dot_setup_folders_usage
echo "usage: dot setup folders [--dry-run]
Brings the 8 standard XDG user directories under the project's fixed
short-name convention (Desktop -> .desktop, Documents -> doc, Downloads ->
dwn, Music -> mus, Pictures -> pic, Videos -> vid, Templates/Public ->
.ignoreme). This mapping is fixed and does not depend on
~/.config/user-dirs.dirs, which is a separate, manually tracked dotfile
this command never reads or writes.
Content left behind in a legacy full-named folder (e.g. ~/Documents) by a
fresh XDG-defaults install -- empty or not -- is merged into its short-named
replacement. A nested Pictures/Screenshots folder is renamed to
pic/screenshots as part of the same pass.
An entry that collides by name with something already in the short-named
target is never overwritten: it's skipped, reported, and its legacy folder is
left in place (not removed) even when everything else in it migrated.
--dry-run report what would move and what would be skipped as a
collision, without changing anything on disk
Runs xdg-user-dirs-update once afterwards to notify running apps/portals
(skipped under --dry-run)."
end
function _dot_setup_folders
if test "$argv[1]" = help
_dot_setup_folders_usage
return 0
end
argparse 'dry-run' -- $argv
or return 1
# Fixed legacy-name -> short-name mapping. Deliberately hardcoded rather
# than read from ~/.config/user-dirs.dirs: that file is a separate,
# manually tracked dotfile whose XDG_*_DIR values can drift or go stale
# (or never get edited to the short names at all), and this command's
# own migration logic must not depend on it being correct.
set -l legacy_names Desktop Documents Downloads Music Pictures Videos Templates Public
set -l target_names .desktop doc dwn mus pic vid .ignoreme .ignoreme
for i in (seq (count $legacy_names))
set -l legacy_name $legacy_names[$i]
set -l target_rel $target_names[$i]
set -l target_path $HOME/$target_rel
set -l legacy_path $HOME/$legacy_name
if not set -q _flag_dry_run
mkdir -p $target_path
end
if not test -d $legacy_path
continue
end
# Screenshots is always moved as one atomic unit (renamed to
# lowercase screenshots), so its individual files must never appear
# as separate move/report entries.
set -l screenshots_path $legacy_path/Screenshots
set -l top_level_entries (find $legacy_path -mindepth 1 -maxdepth 1 -not -name Screenshots)
# No-clobber: an entry whose name already exists in the target is
# never moved over. It's collected here and reported below; its
# legacy folder is left in place (not removed) if any collision
# occurred, even though everything else in it migrated successfully.
set -l collisions
set -l movable_entries
set -l screenshots_movable 0
if test -d $screenshots_path
if test -e $target_path/screenshots
set -a collisions $screenshots_path
else
set screenshots_movable 1
end
end
for entry in $top_level_entries
if test -e $target_path/(path basename $entry)
set -a collisions $entry
else
set -a movable_entries $entry
end
end
set -l movable_count (count $movable_entries)
set -l entry_word entries
test $movable_count -eq 1
and set entry_word entry
if set -q _flag_dry_run
if test $screenshots_movable -eq 1
echo "dot setup folders: would move $screenshots_path to $target_path/screenshots"
end
if test $movable_count -gt 0
echo "dot setup folders: would move $movable_count $entry_word from ~/$legacy_name to ~/$target_rel"
end
if test (count $collisions) -gt 0
echo "dot setup folders: ~/$legacy_name has entries already present in ~/$target_rel, would skip (not overwritten):"
for c in $collisions
echo " $c"
end
echo "dot setup folders: ~/$legacy_name would remain in place due to the collision(s) above"
end
continue
end
if test $screenshots_movable -eq 1
mv -n $screenshots_path $target_path/screenshots
echo "dot setup folders: moved $screenshots_path to $target_path/screenshots"
end
if test $movable_count -gt 0
mv -n $movable_entries $target_path/
echo "dot setup folders: moved $movable_count $entry_word from ~/$legacy_name to ~/$target_rel"
end
if test (count $collisions) -gt 0
echo "dot setup folders: ~/$legacy_name has entries already present in ~/$target_rel, skipping (not overwritten):"
for c in $collisions
echo " $c"
end
echo "dot setup folders: leaving ~/$legacy_name in place due to the collision(s) above"
else
rmdir $legacy_path
end
end
if not set -q _flag_dry_run
xdg-user-dirs-update
end
end

View File

@@ -0,0 +1,35 @@
function _dot_setup_usage
echo "usage: dot setup [<task>]
Tasks:
folders bring the 8 standard XDG user directories under the short-name convention
help show this message
Run 'dot setup <task> help' for details on a specific task.
With no task given, runs every setup task."
end
function _dot_setup
if test "$argv[1]" = help
_dot_setup_usage
return 0
end
set -l helper_dir (status dirname)
source $helper_dir/folders.fish
if test -z "$argv[1]"
_dot_setup_folders
return $status
end
switch $argv[1]
case folders
_dot_setup_folders $argv[2..-1]
return $status
case '*'
_dot_setup_usage
return 1
end
end

View File

@@ -568,3 +568,360 @@ set -l complete_output (python3 $HOME/.config/dot/commands/kde/kde.py complete)
# --- dot help / dot help discovers dot kde ---
set -l help_with_kde (dot help)
@test "dot help lists the kde subcommand" (string match -q '*kde*' -- $help_with_kde; echo $status) -eq 0
# --- dot setup folders ---
# The legacy->short-name mapping is fixed in the command itself, not read
# from ~/.config/user-dirs.dirs (a separate, manually tracked dotfile this
# command never reads or writes), so no scenario below needs to seed one.
# xdg-user-dirs-update is faked out via a PATH-prepended bin that logs each
# invocation, exactly mirroring dot install's fake sudo/pacman.
set -l fake_bin_xdg (mktemp -d)
echo '#!/bin/sh
echo "$@" >>"$XDG_UPDATE_LOG"
exit 0' >$fake_bin_xdg/xdg-user-dirs-update
chmod +x $fake_bin_xdg/xdg-user-dirs-update
set -gx PATH $fake_bin_xdg $PATH
# --- a fresh migration: all 8 legacy folders present and empty, including a
# nested empty Pictures/Screenshots ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Desktop $HOME/Documents $HOME/Downloads $HOME/Music $HOME/Pictures/Screenshots $HOME/Videos $HOME/Templates $HOME/Public
set -gx XDG_UPDATE_LOG (mktemp)
dot setup folders >/dev/null 2>&1
set -l fresh_folders_status $status
@test "dot setup folders succeeds on a fresh scratch HOME" $fresh_folders_status -eq 0
@test "Desktop is renamed to .desktop" -d $HOME/.desktop
@test "Documents is renamed to doc" -d $HOME/doc
@test "Downloads is renamed to dwn" -d $HOME/dwn
@test "Music is renamed to mus" -d $HOME/mus
@test "Pictures is renamed to pic" -d $HOME/pic
@test "Videos is renamed to vid" -d $HOME/vid
@test "Templates and Public both merge into .ignoreme" -d $HOME/.ignoreme
@test "the nested Screenshots folder is renamed to pic/screenshots" -d $HOME/pic/screenshots
@test "the legacy Desktop folder no longer exists" (test -e $HOME/Desktop; and echo yes; or echo no) = no
@test "the legacy Documents folder no longer exists" (test -e $HOME/Documents; and echo yes; or echo no) = no
@test "the legacy Pictures folder no longer exists" (test -e $HOME/Pictures; and echo yes; or echo no) = no
@test "xdg-user-dirs-update is invoked exactly once" (cat $XDG_UPDATE_LOG | count) -eq 1
# --- re-running after a clean migration is a no-op ---
dot setup folders >/dev/null 2>&1
set -l rerun_status $status
@test "re-running dot setup folders succeeds" $rerun_status -eq 0
@test "re-running leaves the short-named folders in place" -d $HOME/pic/screenshots
@test "re-running does not recreate any legacy folder" (test -e $HOME/Pictures; and echo yes; or echo no) = no
# --- the short-name mapping is fixed regardless of what (if anything)
# ~/.config/user-dirs.dirs declares -- this is the exact real-world bug
# that motivated dropping the dependency: a stale, never-updated
# user-dirs.dirs (still pointing XDG_DOCUMENTS_DIR at ~/Documents itself)
# must not make the target collide with the legacy folder ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/.config
echo 'XDG_DESKTOP_DIR="$HOME/Desktop"
XDG_DOWNLOAD_DIR="$HOME/Downloads"
XDG_TEMPLATES_DIR="$HOME/"
XDG_PUBLICSHARE_DIR="$HOME/"
XDG_DOCUMENTS_DIR="$HOME/Documents"
XDG_MUSIC_DIR="$HOME/"
XDG_PICTURES_DIR="$HOME/Pictures"
XDG_VIDEOS_DIR="$HOME/Videos"' >$HOME/.config/user-dirs.dirs
mkdir -p $HOME/Documents
echo real-content >$HOME/Documents/report.txt
set -gx XDG_UPDATE_LOG (mktemp)
dot setup folders >/dev/null 2>&1
@test "a stale user-dirs.dirs pointing at the legacy folder itself doesn't confuse the migration" (cat $HOME/doc/report.txt) = real-content
@test "the legacy folder is still removed despite the stale user-dirs.dirs" (test -e $HOME/Documents; and echo yes; or echo no) = no
@test "the stale user-dirs.dirs file itself is left byte-for-byte untouched" (string match -q '*XDG_DOCUMENTS_DIR="$HOME/Documents"*' -- (cat $HOME/.config/user-dirs.dirs); echo $status) -eq 0
# --- dot setup folders works even when user-dirs.dirs doesn't exist at all ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Documents
echo real-content >$HOME/Documents/report.txt
set -gx XDG_UPDATE_LOG (mktemp)
set -l no_user_dirs_status
dot setup folders >/dev/null 2>&1
set no_user_dirs_status $status
@test "dot setup folders succeeds with no user-dirs.dirs present at all" $no_user_dirs_status -eq 0
@test "migration still happens with no user-dirs.dirs present at all" (cat $HOME/doc/report.txt) = real-content
@test "no user-dirs.dirs is created as a side effect" (test -e $HOME/.config/user-dirs.dirs; and echo yes; or echo no) = no
# --- bare `dot setup` (no task given) runs folders as part of running everything ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Desktop $HOME/Documents $HOME/Downloads $HOME/Music $HOME/Pictures $HOME/Videos $HOME/Templates $HOME/Public
set -gx XDG_UPDATE_LOG (mktemp)
dot setup >/dev/null 2>&1
set -l bare_setup_status $status
@test "bare dot setup succeeds" $bare_setup_status -eq 0
@test "bare dot setup runs the folders task" -d $HOME/.desktop
@test "bare dot setup also merges Pictures into pic" -d $HOME/pic
# --- a non-empty legacy folder merges unconditionally, no flag needed --
# an unrelated empty legacy folder migrates in the same run ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Documents $HOME/Desktop
echo real-content >$HOME/Documents/report.txt
set -gx XDG_UPDATE_LOG (mktemp)
set -l nonempty_output (dot setup folders 2>&1)
set -l nonempty_status $status
@test "dot setup folders succeeds when a legacy folder has content" $nonempty_status -eq 0
@test "a non-empty legacy folder's content is migrated by default" (cat $HOME/doc/report.txt) = real-content
@test "the now-empty legacy folder is removed" (test -e $HOME/Documents; and echo yes; or echo no) = no
@test "prints a message about what was moved" (string match -q '*Documents*' -- $nonempty_output; echo $status) -eq 0
@test "an unrelated empty legacy folder still migrates in the same run" (test -e $HOME/Desktop; and echo yes; or echo no) = no
# --- a legacy folder containing only a stray dotfile still migrates by
# default -- there's no separate empty-vs-non-empty gate to trip ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Downloads
touch $HOME/Downloads/.directory
set -gx XDG_UPDATE_LOG (mktemp)
dot setup folders >/dev/null 2>&1
@test "a legacy folder holding only a stray dotfile is migrated by default" -e $HOME/dwn/.directory
@test "the legacy folder holding only a stray dotfile is removed" (test -e $HOME/Downloads; and echo yes; or echo no) = no
# --- a nested empty Screenshots folder migrates alongside unrelated real
# content in the same Pictures folder, all in the same default run ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Pictures/Screenshots
echo vacation-photo >$HOME/Pictures/vacation.jpg
set -gx XDG_UPDATE_LOG (mktemp)
dot setup folders >/dev/null 2>&1
set -l pictures_with_content_status $status
@test "dot setup folders succeeds when Pictures has unrelated content" $pictures_with_content_status -eq 0
@test "the unrelated file in Pictures is migrated into pic" (cat $HOME/pic/vacation.jpg) = vacation-photo
@test "the nested Screenshots folder is renamed to pic/screenshots" -d $HOME/pic/screenshots
@test "the now-empty Pictures folder is removed" (test -e $HOME/Pictures; and echo yes; or echo no) = no
# --- a Screenshots folder that itself holds real content migrates by
# default too, renamed to pic/screenshots, even when the rest of
# Pictures is empty ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Pictures/Screenshots
echo shot-content >$HOME/Pictures/Screenshots/shot.png
set -gx XDG_UPDATE_LOG (mktemp)
dot setup folders >/dev/null 2>&1
@test "a non-empty Screenshots folder migrates by default, renamed to pic/screenshots" (cat $HOME/pic/screenshots/shot.png) = shot-content
@test "the now-empty Pictures folder is removed after migrating Screenshots" (test -e $HOME/Pictures; and echo yes; or echo no) = no
# --- a filename collision between a legacy folder and its already-populated
# short-named target is skipped (not overwritten), reported, and leaves
# the legacy folder in place -- even when another non-colliding file in
# the same folder is merged successfully ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Documents $HOME/doc
echo legacy-content >$HOME/Documents/report.txt
echo legacy-only >$HOME/Documents/notes.txt
echo target-content >$HOME/doc/report.txt
set -gx XDG_UPDATE_LOG (mktemp)
set -l collision_output (dot setup folders 2>&1)
set -l collision_status $status
@test "dot setup folders still succeeds when a collision occurs" $collision_status -eq 0
@test "the colliding target file is preserved byte-for-byte" (cat $HOME/doc/report.txt) = target-content
@test "the colliding legacy file is left in place, untouched" (cat $HOME/Documents/report.txt) = legacy-content
@test "the collision is reported" (string match -q '*report.txt*' -- $collision_output; echo $status) -eq 0
@test "the legacy Documents folder is left in place due to the collision" -d $HOME/Documents
@test "a non-colliding file in the same folder is still merged" (cat $HOME/doc/notes.txt) = legacy-only
@test "the merged non-colliding file no longer sits in the legacy folder" (test -e $HOME/Documents/notes.txt; and echo yes; or echo no) = no
# --- re-running after a collision was reported: the skipped file isn't
# lost, and the already-migrated file isn't moved again ---
dot setup folders >/dev/null 2>&1
@test "re-running after a collision still preserves the target file" (cat $HOME/doc/report.txt) = target-content
@test "re-running after a collision still leaves the legacy file in place" (cat $HOME/Documents/report.txt) = legacy-content
@test "re-running after a collision does not resurrect the already-migrated file in the legacy folder" (test -e $HOME/Documents/notes.txt; and echo yes; or echo no) = no
# --- a collision on the nested Screenshots unit is skipped, reported, and
# leaves Pictures in place, even though Pictures also holds other
# content unrelated to the collision ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Pictures/Screenshots $HOME/pic/screenshots
echo legacy-shot >$HOME/Pictures/Screenshots/shot.png
echo target-shot >$HOME/pic/screenshots/shot.png
set -gx XDG_UPDATE_LOG (mktemp)
set -l screenshots_collision_output (dot setup folders 2>&1)
@test "a Screenshots collision preserves the existing target screenshot" (cat $HOME/pic/screenshots/shot.png) = target-shot
@test "a Screenshots collision leaves the legacy Screenshots folder in place" (cat $HOME/Pictures/Screenshots/shot.png) = legacy-shot
@test "the Screenshots collision is reported" (string match -q '*Screenshots*' -- $screenshots_collision_output; echo $status) -eq 0
@test "Pictures itself is left in place due to the Screenshots collision" -d $HOME/Pictures
# --- the same Screenshots-collision handling also holds when Pictures has
# nothing else in it besides the colliding Screenshots folder ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Pictures/Screenshots $HOME/pic/screenshots
echo target-shot >$HOME/pic/screenshots/shot.png
set -gx XDG_UPDATE_LOG (mktemp)
set -l bare_screenshots_collision_output (dot setup folders 2>&1)
set -l bare_screenshots_collision_status $status
@test "a Screenshots-only collision still succeeds" $bare_screenshots_collision_status -eq 0
@test "a Screenshots-only collision preserves the existing target screenshot" (cat $HOME/pic/screenshots/shot.png) = target-shot
@test "a Screenshots-only collision leaves the empty legacy Screenshots folder in place" -d $HOME/Pictures/Screenshots
@test "a Screenshots-only collision leaves Pictures itself in place" -d $HOME/Pictures
@test "the Screenshots-only collision is reported" (string match -q '*Screenshots*' -- $bare_screenshots_collision_output; echo $status) -eq 0
# --- --dry-run reports what would move/skip without touching the
# filesystem: no mkdir, no mv/rmdir, no xdg-user-dirs-update ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Documents $HOME/Pictures/Screenshots
echo real-content >$HOME/Documents/report.txt
set -gx XDG_UPDATE_LOG (mktemp)
set -l dryrun_output (dot setup folders --dry-run 2>&1)
set -l dryrun_status $status
@test "dot setup folders --dry-run succeeds" $dryrun_status -eq 0
@test "--dry-run reports the entry it would move" (string match -q '*Documents*' -- $dryrun_output; echo $status) -eq 0
@test "--dry-run reports the Screenshots folder it would move" (string match -q '*Screenshots*' -- $dryrun_output; echo $status) -eq 0
@test "--dry-run leaves the legacy Documents folder's content untouched" (cat $HOME/Documents/report.txt) = real-content
@test "--dry-run does not remove the legacy Documents folder" -d $HOME/Documents
@test "--dry-run does not create the short-named target folder" (test -e $HOME/doc; and echo yes; or echo no) = no
@test "--dry-run does not rename the nested Screenshots folder" -d $HOME/Pictures/Screenshots
@test "--dry-run never invokes xdg-user-dirs-update" (test -s $XDG_UPDATE_LOG; and echo yes; or echo no) = no
# --- --dry-run also reports a would-be collision without touching
# either side, and doesn't move the non-colliding entry either ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Documents $HOME/doc
echo legacy-content >$HOME/Documents/report.txt
echo legacy-only >$HOME/Documents/notes.txt
echo target-content >$HOME/doc/report.txt
set -gx XDG_UPDATE_LOG (mktemp)
set -l dryrun_collision_output (dot setup folders --dry-run 2>&1)
@test "--dry-run reports the would-be collision" (string match -q '*report.txt*' -- $dryrun_collision_output; echo $status) -eq 0
@test "--dry-run leaves the colliding target file untouched" (cat $HOME/doc/report.txt) = target-content
@test "--dry-run leaves the colliding legacy file untouched" (cat $HOME/Documents/report.txt) = legacy-content
@test "--dry-run does not move the non-colliding file either" -e $HOME/Documents/notes.txt
# --- a legacy folder with nothing to move produces no --dry-run output ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Desktop
set -gx XDG_UPDATE_LOG (mktemp)
set -l dryrun_empty_output (dot setup folders --dry-run 2>&1)
@test "--dry-run is silent for a legacy folder with nothing to move" -z "$dryrun_empty_output"
@test "--dry-run leaves an empty legacy folder in place" -d $HOME/Desktop
# --- the removed --yes flag now fails fast as an unknown option ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
set -gx XDG_UPDATE_LOG (mktemp)
dot setup folders --yes >/dev/null 2>&1
set -l old_yes_status $status
@test "dot setup folders --yes now fails as an unknown option" $old_yes_status -ne 0
# --- help prints usage and makes no filesystem changes ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/setup
cp $commands_dir/setup/setup.fish $HOME/.config/dot/commands/setup/setup.fish
cp $commands_dir/setup/folders.fish $HOME/.config/dot/commands/setup/folders.fish
mkdir -p $HOME/Documents $HOME/Desktop
set -gx XDG_UPDATE_LOG (mktemp)
set -l setup_help_output (dot setup help)
set -l setup_help_status $status
set -l folders_help_output (dot setup folders help)
set -l folders_help_status $status
set -l xdg_called_for_help (test -s $XDG_UPDATE_LOG; and echo yes; or echo no)
@test "dot setup help succeeds" $setup_help_status -eq 0
@test "dot setup help mentions folders" (string match -q '*folders*' -- $setup_help_output; echo $status) -eq 0
@test "dot setup folders help succeeds" $folders_help_status -eq 0
@test "dot setup folders help mentions the short-name convention" (string match -q '*.desktop*' -- $folders_help_output; echo $status) -eq 0
@test "dot setup folders help documents --dry-run" (string match -q '*--dry-run*' -- $folders_help_output; echo $status) -eq 0
@test "neither help invocation ever calls xdg-user-dirs-update" $xdg_called_for_help = no
@test "dot setup help leaves the legacy Documents folder untouched" -d $HOME/Documents
@test "dot setup folders help leaves the legacy Desktop folder untouched" -d $HOME/Desktop
@test "help does not create any short-named target folder" (test -e $HOME/doc; and echo yes; or echo no) = no
# --- dot help discovers dot setup ---
set -l help_with_setup (dot help)
@test "dot help lists the setup subcommand" (string match -q '*setup*' -- $help_with_setup; echo $status) -eq 0