Files
dotfiles/.config/dot/CLAUDE.md

121 lines
6.1 KiB
Markdown

# Dotfiles
This machine's dotfiles are a bare git repo at `~/.dotfiles`, checked out with
`$HOME` as its work-tree. The `dot` fish function wraps that invocation
(`git --git-dir=~/.dotfiles --work-tree=$HOME $argv`, declared with
`--wraps=git`), so every git subcommand works through it: `dot status`,
`dot add`, `dot commit`, `dot push`, etc.
This directory (`~/.config/dot`) holds the `dot` CLI's custom subcommands,
tests, and package lists, but the repo tracks files across `$HOME` — fish
config, git identity, the `dot` function itself, and more. To see everything
tracked, run `dot ls-tree -r --name-only HEAD` from `$HOME` (paths are shown
relative to cwd, so running it from elsewhere silently truncates the list).
## Always add by explicit path
`status.showUntrackedFiles=no` is set locally (see `dot init` below), and
`.gitignore` only excludes `.dotfiles` itself plus OS/editor cruft — it is
**not** a whitelist. That
means virtually everything under `$HOME` reads as untracked, and `git status`
deliberately hides all of it.
**Always run `dot add <specific-path>`.** Never `dot add -A`, `dot add .`, or
any wildcard add — that would try to stage the entire home directory (caches,
secrets, everything).
**Stage automatically after changes.** Once a tracked file is edited, run
`dot add <specific-path>` for it right away rather than waiting to be asked —
one explicit path per changed file, still never a wildcard. This does not
extend to `dot commit` or `dot push`, which still require an explicit
request.
## The dot CLI
### Architecture
`dot` is defined in one file: `~/.config/fish/functions/dot.fish`. It holds
three functions:
- `dot` (`--wraps=git`) — dispatches `init`, `help`, and any file found under
`~/.config/dot/commands/`, otherwise forwards everything to
`git --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 `~/.dotfiles` already 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 to `git init`
- backs up any pre-existing file that checkout would clobber into
`~/.dotfiles-backup/<timestamp>/`, then retries the checkout
- explicitly sets `status.showUntrackedFiles=no` after cloning — this is a
local-only git setting, so a fresh `git clone` never 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.
1. Create `~/.config/dot/commands/<name>.fish` defining a `_dot_<name>`
function.
2. Confirm `dot <name>` dispatches to it. No other wiring is needed —
`~/.config/fish/completions/dot.fish` and `__dot_help` both discover new
command files by globbing that directory, and `--wraps=git` still covers
raw git subcommands.
3. Implement a `help` subcommand: check for `help` as `_dot_<name>`'s first
positional argument before `argparse`, and call a `_dot_<name>_usage`
function that prints usage and every flag. If `_dot_<name>` itself
dispatches to nested subcommands, apply this same check-then-dispatch
pattern at that level too — there's no central `--help` handling in
`dot.fish` to lean on; each level is responsible for its own.
`_dot_<name>_usage` should print its text as a single multi-line
`echo "..."` string (fish preserves literal newlines inside double
quotes) rather than one `echo` per line.
4. Add a row to `~/.gitea/README.md`'s command table for it — one row per
distinct use case, with paths written relative to `$HOME`
(`~/.config/dot/...`), not relative to the README's own location.
5. Add a case to `~/.config/dot/tests/dot.fish` covering it, including its
`help` output, and run `fishtape ~/.config/dot/tests/dot.fish` until 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 calling
`dot`, so tests never touch the real `~/.dotfiles`.
- Build a throwaway bare "remote" fixture with `git init --bare` plus a
seeded commit, and explicitly set its `HEAD`
(`git --git-dir=$remote symbolic-ref HEAD refs/heads/main`). Pushing with
`git push origin HEAD:main` does **not** update the bare repo's `HEAD`
symref — skip this and a clone of the fixture can end up "on a branch yet
to be born."
- Don't use `.gitconfig` as a fake pre-existing "conflict" file in a
fixture — git parses `$HOME/.gitconfig` as 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 `.bashrc` instead.
- `@test "description" <expr> <op> <expected>` mirrors fish's `test` builtin
(`-eq`, `-ne`, `=`, `-e`, `-f`, `-d`, `-n`, `-z`); `-a`/`-o` combinators
aren't supported.