3.7 KiB
The dot CLI
Architecture
dot is defined in one file: ~/.config/fish/functions/dot.fish. It holds
three functions:
dot(--wraps=git) — dispatchesinit,help, and any file found under~/.config/dot/commands/, otherwise forwards everything togit --git-dir=~/.dotfiles --work-tree=$HOME $argv(full passthrough).__dot_init— the bootstrap logic, inlined in the same file rather than autoloaded separately, because it's the one subcommand that must work before the dotfiles repo has ever been cloned onto a machine.__dot_help— prints usage: the built-in commands plus whatever is currently found under~/.config/dot/commands/, generated by globbing that directory rather than a hardcoded list, so it can't drift from reality.
__dot_help's glob over ~/.config/dot/commands/*.fish is duplicated in
~/.config/fish/completions/dot.fish's __dot_custom_subcommands rather than
shared: fish only autoloads a function from a file named after that function,
so a helper defined inside dot.fish would be undefined if tab-completion
ran before dot had ever been sourced in the session. Keep both copies in
sync when the listing logic changes.
dot init:
- refuses to run if
~/.dotfilesalready exists (no re-init support) - clones the bare repo from
--url(default: the hardcoded Gitea remote) — if the clone fails, it errors out; it never falls back togit init - backs up any pre-existing file that checkout would clobber into
~/.dotfiles-backup/<timestamp>/, then retries the checkout - explicitly sets
status.showUntrackedFiles=noafter cloning — this is a local-only git setting, so a freshgit clonenever carries it over
Adding a subcommand
Beyond init, dot looks for ~/.config/dot/commands/<name>.fish, sources
it, and calls _dot_<name>. These files are deliberately kept out of
~/.config/fish/functions/ (fish's autoload path) so they never become
independently invokable top-level commands or clutter tab-completion outside
of dot itself.
- Create
~/.config/dot/commands/<name>.fishdefining a_dot_<name>function. - Confirm
dot <name>dispatches to it. No other wiring is needed —~/.config/fish/completions/dot.fishand__dot_helpboth discover new command files by globbing that directory, and--wraps=gitstill covers raw git subcommands. - Add a case to
~/.config/dot/tests/dot.fishcovering it and runfishtape ~/.config/dot/tests/dot.fishuntil it passes.
Testing
Tests live at ~/.config/dot/tests/dot.fish, run with
fishtape ~/.config/dot/tests/dot.fish. Fishtape is installed via Fisher
(fisher install jorgebucaran/fishtape) and tracked in
~/.config/fish/fish_plugins — a real, restorable dependency for developing
dot, but never required just to use it.
- Each scenario overrides
$HOME(set -gx HOME (mktemp -d)) before callingdot, so tests never touch the real~/.dotfiles. - Build a throwaway bare "remote" fixture with
git init --bareplus a seeded commit, and explicitly set itsHEAD(git --git-dir=$remote symbolic-ref HEAD refs/heads/main). Pushing withgit push origin HEAD:maindoes not update the bare repo'sHEADsymref — skip this and a clone of the fixture can end up "on a branch yet to be born." - Don't use
.gitconfigas a fake pre-existing "conflict" file in a fixture — git parses$HOME/.gitconfigas its own global config on every invocation, and garbage content there spams "key does not contain a section" errors that drown out the real assertion. Use a harmless file like.bashrcinstead. @test "description" <expr> <op> <expected>mirrors fish'stestbuiltin (-eq,-ne,=,-e,-f,-d,-n,-z);-a/-ocombinators aren't supported.