Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions .bin/dotfiles-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#!/bin/zsh

# Test this dotfiles repo without requiring project-local package manifests or a
# non-bare checkout in the real home directory.
#
# Default mode runs in a disposable normal clone so package-manager state,
# Neovim plugin bootstrap, and other temporary files stay out of the author's
# actual `$HOME`. `--here` runs the same checks in the current checkout, which
# is useful for CI and for explicit local debugging.
#
# Coverage:
# - Markdown formatting with Prettier
# - Lua formatting and lint for the tracked Neovim config
# - Zsh syntax for the shell entrypoints
# - A headless Neovim startup smoke test with isolated XDG state
#
# The script installs the Node-backed Markdown formatter into a temporary
# directory on each run instead of relying on a tracked `package.json` or
# `node_modules`.

set -euo pipefail

script_dir=${0:A:h}
repo_root=${script_dir:h}
run_here=0
assume_yes=0
apply_fixes=0
keep_clone=${DOTFILES_CI_KEEP_CLONE:-0}

usage() {
cat <<'EOF'
Usage:
dotfiles-test [--here] [--fix] [--yes]

Without arguments, run the checks in a disposable normal clone.

Options:
--here Run in the current checkout instead of a temp clone.
--fix Apply fixes where supported before running the remaining checks.
-y, --yes Skip the confirmation prompt for --here.
-h, --help Show this help.
EOF
}

while (( $# > 0 )); do
case "$1" in
--here)
run_here=1
;;
--fix)
apply_fixes=1
;;
-y|--yes)
assume_yes=1
;;
-h|--help)
usage
exit 0
;;
*)
print -u2 "dotfiles-test: unknown argument: $1"
usage >&2
exit 2
;;
esac
shift
done

if [[ -d "$repo_root/.dotfiles" ]]; then
git_cmd=(git --git-dir="$repo_root/.dotfiles" --work-tree="$repo_root")
clone_source=$repo_root/.dotfiles
elif git -C "$repo_root" rev-parse --show-toplevel >/dev/null 2>&1; then
git_cmd=(git -C "$repo_root")
clone_source=$repo_root
else
print -u2 "dotfiles-test: could not find the dotfiles repository from $repo_root"
exit 1
fi

cd "$repo_root"

require_cmd() {
local cmd=$1
local help=${2:-}

if command -v "$cmd" >/dev/null 2>&1; then
return 0
fi

print -u2 "dotfiles-test: missing required command: $cmd"
if [[ -n "$help" ]]; then
print -u2 "dotfiles-test: $help"
fi
exit 127
}

run_checks_here() {
local work_root=$1

cd "$work_root"

require_cmd git
require_cmd node "Install Node 22 via mise, for example: mise use -g node@22"
require_cmd npm "Install Node 22 via mise, for example: mise use -g node@22"
require_cmd nvim "Install Neovim, for example: brew install neovim"
require_cmd stylua "Install Stylua, for example: brew install stylua"
require_cmd luacheck "Install Luacheck, for example: brew install luacheck"
require_cmd zsh "Install zsh, for example: brew install zsh"

local tmp_root
tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test.XXXXXX")

local tracked_md=()
local markdown_path
for markdown_path in "${(@0)$("${git_cmd[@]}" ls-files -z -- '*.md')}"; do
if [[ -n "$markdown_path" && ! -L "$work_root/$markdown_path" ]]; then
tracked_md+=("$markdown_path")
fi
done

if (( ${#tracked_md[@]} > 0 )); then
npm install \
--prefix "$tmp_root/npm-tools" \
--no-package-lock \
--no-save \
prettier@3 >/dev/null

if (( apply_fixes == 1 )); then
"$tmp_root/npm-tools/node_modules/.bin/prettier" --list-different --write -- "${tracked_md[@]}"
else
"$tmp_root/npm-tools/node_modules/.bin/prettier" --list-different -- "${tracked_md[@]}"
fi
fi

if (( apply_fixes == 1 )); then
stylua .vim/init.lua .vim/lua
else
stylua --check .vim/init.lua .vim/lua
fi
luacheck --config .vim/.luacheckrc .vim/init.lua .vim/lua
zsh -n .zshenv .zshrc

mkdir -p \
"$tmp_root/xdg-cache" \
"$tmp_root/xdg-data" \
"$tmp_root/xdg-state"

local nvim_log="$tmp_root/nvim.log"
if ! env \
HOME="$work_root" \
XDG_CACHE_HOME="$tmp_root/xdg-cache" \
XDG_DATA_HOME="$tmp_root/xdg-data" \
XDG_STATE_HOME="$tmp_root/xdg-state" \
nvim --headless "+Lazy! restore" "+qa" >"$nvim_log" 2>&1; then
cat "$nvim_log"
rm -rf "$tmp_root"
exit 1
fi

rm -rf "$tmp_root"
}

run_checks_in_temp_clone() {
require_cmd git
require_cmd rsync "Install rsync, for example: brew install rsync"

local tmp_root
tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test-clone.XXXXXX")

cleanup_clone() {
if [[ "$keep_clone" == "1" ]]; then
print "dotfiles-test: kept temp clone at $tmp_root/repo"
return 0
fi

rm -rf "$tmp_root"
}

git clone "$clone_source" "$tmp_root/repo" >/dev/null

local overlay_files=("${(@0)$("${git_cmd[@]}" ls-files -z)}")
overlay_files=("${(@)overlay_files:#}")
overlay_files=("${(@)overlay_files:#(#m)(?)}")
local extra_dir
for extra_dir in .bin .github; do
if [[ ! -d "$repo_root/$extra_dir" ]]; then
continue
fi

local extra_files=("${(@0)$("${git_cmd[@]}" ls-files -z --others --exclude-standard -- "$extra_dir")}")
overlay_files+=("${extra_files[@]}")
done

local existing_overlay_files=()
local overlay_file
for overlay_file in "${overlay_files[@]}"; do
if [[ -e "$repo_root/$overlay_file" || -L "$repo_root/$overlay_file" ]]; then
existing_overlay_files+=("$overlay_file")
fi
done

if (( ${#existing_overlay_files[@]} > 0 )); then
rsync -a --files-from=<(printf '%s\0' "${existing_overlay_files[@]}") --from0 "$repo_root"/ "$tmp_root/repo"/
fi

local deleted_files=("${(@0)$("${git_cmd[@]}" ls-files -z --deleted)}")
if (( ${#deleted_files[@]} > 0 )); then
(cd "$tmp_root/repo" && rm -f -- "${deleted_files[@]}")
fi

local nested_args=(--here --yes)
if (( apply_fixes == 1 )); then
nested_args+=(--fix)
fi

(cd "$tmp_root/repo" && ./.bin/dotfiles-test "${nested_args[@]}")
cleanup_clone
}

confirm_here_run() {
if (( assume_yes == 1 )); then
return 0
fi

local prompt="dotfiles-test: run checks in the current checkout and allow temporary files under this checkout? [y/N] "
local reply

if [[ -t 0 ]]; then
read -r "?$prompt" reply
else
print -u2 "$prompt"
read -r reply || return 1
fi

[[ "$reply" == [Yy] || "$reply" == [Yy][Ee][Ss] ]]
}

if (( run_here == 1 )); then
if ! confirm_here_run; then
print -u2 "dotfiles-test: aborted"
exit 1
fi

run_checks_here "$repo_root"
else
run_checks_in_temp_clone
fi
6 changes: 3 additions & 3 deletions .docs/GIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ Get latest
</td>
<td>

- ⚠️ This is destructive, for a couple reasons.
- ⚠️ This is destructive, for a couple reasons.
1. It defaults to rebasing. I think
[a clean history is best. However, the backspace key does carry risk.](https://blog.izs.me/2012/12/git-rebase/)
To get out of trouble, `git rebase --abort` or reference `git reflog`.
1. It cleans up deleted branches, e.g. a merged upstream pull request. A
clean local repo is best. However, if you have local, unpushed commits on
that _other_ branch, the command could drop your changes.
- If you want to be safer, drop down to `git fetch` and `git merge`, which
will modify only your current branch, and without rewriting history.
- If you want to be safer, drop down to `git fetch` and `git merge`, which will
modify only your current branch, and without rewriting history.

</td>
</tr>
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
pull_request:
push:
branches:
- master

jobs:
ci:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install --yes neovim luarocks zsh

- uses: taiki-e/install-action@v2
with:
tool: stylua

- name: Install Luacheck
run: sudo luarocks install luacheck

- name: Run dotfiles tests
env:
HOME: ${{ github.workspace }}
XDG_CACHE_HOME: ${{ runner.temp }}/xdg-cache
XDG_DATA_HOME: ${{ runner.temp }}/xdg-data
XDG_STATE_HOME: ${{ runner.temp }}/xdg-state
run: ./.bin/dotfiles-test --here --yes
7 changes: 0 additions & 7 deletions .remarkrc

This file was deleted.

1 change: 1 addition & 0 deletions .vim/.luacheckrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
globals = {
"Snacks",
"vim",
}
2 changes: 1 addition & 1 deletion .vim/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ require("lazy").setup({
})

-- Load remaining VimScript config
vim.cmd('source ~/.vim/vimrc')
vim.cmd("source ~/.vim/vimrc")
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,6 @@ dotfiles config --local status.showUntrackedFiles no
dotfiles checkout
```

### Machine-Local Commit Identity

Set in `.mise.local.toml`:

```toml
# $HOME/.mise.local.toml
[env]
GIT_AUTHOR_EMAIL = "you@company.com"
GIT_COMMITTER_EMAIL = "you@company.com"
JJ_EMAIL = "you@company.com"
```

Read more about per-machine settings in
[.docs/MACHINE_LOCAL_CONFIGURATION.md](./.docs/MACHINE_LOCAL_CONFIGURATION.md).

### If zsh is not the default shell

```zsh
Expand Down Expand Up @@ -64,6 +49,31 @@ lifecycle for you][the best way to store your dotfiles]. Same interface as
`git`. No extra, bespoke tool. The repo layout stays in sync with how the files
are used.

### Machine-Local Commit Identity

Set in `.mise.local.toml`:

```toml
# $HOME/.mise.local.toml
[env]
GIT_AUTHOR_EMAIL = "you@company.com"
GIT_COMMITTER_EMAIL = "you@company.com"
JJ_EMAIL = "you@company.com"
```

Read more about per-machine settings in
[.docs/MACHINE_LOCAL_CONFIGURATION.md](./.docs/MACHINE_LOCAL_CONFIGURATION.md).

### Test

The test command is globally added to `$PATH`: `$HOME/.bin/dotfiles-test`.

```zsh
dotfiles-test # Run tests in a disposable normal clone
dotfiles-test --here # Run tests in the current checkout, with confirmation
dotfiles-test --here --fix # Apply available fixes in the current checkout, with confirmation
```

### Advanced Usage

See [the docs folder](./.docs/).
Expand Down
Loading