138 lines
7.0 KiB
Markdown
138 lines
7.0 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).
|
|
|
|
For an agent driving this through separate tool calls: `cd ~` in one call does
|
|
not reliably carry over to the next, since each call may reset to the
|
|
project's working directory. Always `cd "$HOME"` and run the `ls-tree` (or any
|
|
other cwd-sensitive `dot`/`git` command) in that *same* call — e.g.
|
|
`cd "$HOME" && dot ls-tree -r --name-only HEAD` — rather than trusting a prior
|
|
`cd` to have stuck. Getting this wrong silently narrows the listing to
|
|
whatever the leftover cwd happens to be, which reads as "this file isn't
|
|
tracked" when it actually is.
|
|
|
|
## 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 `~/.github/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.
|
|
|
|
## Keybindings
|
|
|
|
Whenever a keybind is added, changed, or removed in *any* config on this
|
|
machine (tmux, KDE, neovim, fish, whatever), add or update its row in
|
|
[`~/.github/keybindings.md`](../../.github/keybindings.md) in the same
|
|
change. That file is the single reference for every keybind across tools —
|
|
it drifts the moment a bind changes somewhere without a matching edit there.
|