dotcli: Initial config

dotcli is my utility script for everything dotfiles.

A bootstrap command is included in the README.
A test suite and the necessary scaffolding for extending
dotcli is also included.
This commit is contained in:
2026-07-04 00:43:58 -04:00
parent 4a0e382494
commit 6ad0f109df
10 changed files with 428 additions and 2 deletions

View File

View File

@@ -0,0 +1,72 @@
# Fixture: a fake bare "remote" repo with tracked dotfiles, shared read-only
# across every case below. dot init only ever clones from it, never mutates it.
set -l remote (mktemp -d)/dotfiles.git
git init -q --bare $remote
set -l seed (mktemp -d)
pushd $seed
git init -q -b main
git config user.email test@dot.fish
git config user.name dot-tests
mkdir -p .config/fish/functions
echo 'echo tracked-bashrc' >.bashrc
echo 'echo hi' >.config/fish/functions/greet.fish
git add -A
git commit -qm seed >/dev/null
git remote add origin $remote
git push -q origin HEAD:main >/dev/null 2>&1
popd
git --git-dir=$remote symbolic-ref HEAD refs/heads/main
# --- fresh bootstrap, no conflicts ---
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
set -l fresh_status $status
@test "dot init succeeds against a clean HOME" $fresh_status -eq 0
@test "clones the bare repo to ~/.dotfiles" -e $HOME/.dotfiles
@test "checks out tracked files onto HOME" -e $HOME/.bashrc
@test "checked-out file has the repo's content" (cat $HOME/.bashrc) = "echo tracked-bashrc"
@test "disables status.showUntrackedFiles" (git --git-dir=$HOME/.dotfiles config --local status.showuntrackedfiles) = no
dot init --url $remote >/dev/null 2>&1
set -l repeat_status $status
@test "re-running dot init refuses when already initialized" $repeat_status -eq 1
set -l passthrough_status (dot status >/dev/null 2>&1; echo $status)
@test "git passthrough still works (dot status)" $passthrough_status -eq 0
# --- a pre-existing conflicting file gets backed up, not clobbered ---
set -gx HOME (mktemp -d)
echo 'pre-existing-content' >$HOME/.bashrc
dot init --url $remote >/dev/null 2>&1
set -l conflict_status $status
@test "dot init still succeeds with a conflicting file present" $conflict_status -eq 0
@test "conflicting file ends up with the tracked content" (cat $HOME/.bashrc) = "echo tracked-bashrc"
@test "a backup directory was created" -d $HOME/.dotfiles-backup
@test "the pre-existing content was preserved in the backup" (cat $HOME/.dotfiles-backup/*/.bashrc) = "pre-existing-content"
# --- an unreachable URL never falls back to creating an empty repo ---
set -gx HOME (mktemp -d)
dot init --url /nonexistent/path.git >/dev/null 2>&1
set -l bad_url_status $status
set -l dotfiles_exists (test -e $HOME/.dotfiles; and echo yes; or echo no)
@test "dot init fails on an unreachable URL" $bad_url_status -eq 1
@test "no .dotfiles directory is left behind on failure" $dotfiles_exists = no
# --- dispatches to files under ~/.config/dot/commands/ without polluting
# the fish function namespace: the file only defines _dot_<name>, which
# only becomes known to fish once dot sources it on demand.
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands
set -l marker (mktemp)
echo "function _dot_mark
echo marked >$marker
end" >$HOME/.config/dot/commands/mark.fish
dot mark >/dev/null 2>&1
@test "dispatches to a command file under ~/.config/dot/commands/" (cat $marker) = marked

View File

@@ -0,0 +1,6 @@
function __dot_custom_subcommands
echo init
path basename $HOME/.config/dot/commands/*.fish 2>/dev/null | path change-extension ''
end
complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)"

View File

@@ -0,0 +1,2 @@
complete --command fishtape --short v --long version --description "Print version"
complete --command fishtape --short h --long help --description "Print help"

View File

@@ -0,0 +1 @@
jorgebucaran/fishtape

View File

@@ -1,4 +1,89 @@
function dot --wraps=git --description 'Manage dotfiles via a bare repo checked out over $HOME'
git --git-dir=$HOME/.dotfiles --work-tree=$HOME $argv
set -l dotfiles_dir $HOME/.dotfiles
if test "$argv[1]" = init
set -e argv[1]
__dot_init $dotfiles_dir $argv
return $status
end
set -l commands_dir $HOME/.config/dot/commands
set -l command_file $commands_dir/$argv[1].fish
if test -n "$argv[1]" -a -f "$command_file"
source $command_file
_dot_$argv[1] $argv[2..-1]
return $status
end
git --git-dir=$dotfiles_dir --work-tree=$HOME $argv
end
# Kept inline (not a separate autoloaded function file) because this is the
# only subcommand that must work before the dotfiles repo has been cloned.
function __dot_init
set -l dotfiles_dir $argv[1]
set -e argv[1]
argparse 'url=' -- $argv
or return 1
set -l url $_flag_url
test -n "$url"; or set url ssh://gitea@git.alexion.dev:2022/alexion/dotfiles.git
if test -e $dotfiles_dir
echo "dot init: $dotfiles_dir already exists, refusing to re-initialize" >&2
return 1
end
git clone --bare $url $dotfiles_dir
or begin
echo "dot init: failed to clone $url" >&2
return 1
end
git --git-dir=$dotfiles_dir config status.showUntrackedFiles no
set -l checkout_output (git --git-dir=$dotfiles_dir --work-tree=$HOME checkout 2>&1)
set -l checkout_status $status
if test $checkout_status -ne 0
set -l conflicts
set -l in_block 0
for line in $checkout_output
if test $in_block -eq 1
if string match -rq '^\s' -- $line
set -a conflicts (string trim -- $line)
continue
else
set in_block 0
end
end
string match -q '*would be overwritten by checkout:*' -- $line
and set in_block 1
end
if test (count $conflicts) -eq 0
echo "dot init: checkout failed and no recoverable conflicts were found:" >&2
printf '%s\n' $checkout_output >&2
return 1
end
set -l backup_dir $HOME/.dotfiles-backup/(date +%Y%m%dT%H%M%S)
for f in $conflicts
mkdir -p (path dirname $backup_dir/$f)
mv $HOME/$f $backup_dir/$f
echo "dot init: backed up ~/$f to $backup_dir/$f"
end
git --git-dir=$dotfiles_dir --work-tree=$HOME checkout
or begin
echo "dot init: checkout still failing after backing up conflicts, aborting" >&2
return 1
end
end
echo "dot init: bootstrapped $dotfiles_dir from $url"
end
complete -c dot -w git

View File

@@ -0,0 +1,116 @@
function fishtape --description "Test scripts, functions, and plugins in Fish"
switch "$argv"
case -v --version
echo "fishtape, version 3.0.1"
case "" -h --help
echo "Usage: fishtape <files ...> Run test files"
echo "Options:"
echo " -v or --version Print version"
echo " -h or --help Print this help message"
case \*
set --local files (realpath $argv)
for file in $files
if test ! -f $file
echo "fishtape: Invalid file or file not found: \"$file\"" >&2
return 1
end
end
set --local operators -{n,z,b,c,d,e,f,g,G,k,L,O,p,r,s,S,t,u,w,x}
set --local expectations \
"a non-zero length string" \
"a zero length string" \
"a block device" \
"a character device" \
"a directory" \
"an existing file" \
"a regular file" \
"a file with the set-group-ID bit set" \
"a file with same group ID as the current user" \
"a file with the sticky bit set" \
"a symbolic link" \
"a file owned by the current user" \
"a named pipe" \
"a file marked as readable" \
"a file of size greater than zero" \
"a socket" \
"a terminal tty file descriptor" \
"a file with the set-user-ID bit set" \
"a file marked as writable" \
"a file marked as executable"
set --universal _fishtape_test_number 0
set --universal _fishtape_test_passed 0
set --universal _fishtape_test_failed 0
function @echo
echo "# $argv"
end
function @test --argument-names name --inherit-variable operators --inherit-variable expectations
set --erase argv[1]
set --query argv[2] || set --append argv ""
set _fishtape_test_number (math $_fishtape_test_number + 1)
if test $argv
set _fishtape_test_passed (math $_fishtape_test_passed + 1)
echo "ok $_fishtape_test_number $name"
else
if test $argv[1] = "!"
set operator "! "
set expected "not "
set --erase argv[1]
end
if set --query argv[3]
set operator "$operator"$argv[2]
set expected (string escape -- $argv[3])
set actual (string escape -- $argv[1])
else
set operator "$operator"$argv[1]
set expected "$expected"$expectations[(contains --index -- $argv[1] $operators)]
set actual (string escape -- $argv[2])
end
set _fishtape_test_failed (math $_fishtape_test_failed + 1)
status print-stack-trace |
string replace --filter --regex -- "\s+called on line (\d+) of file (.+)" '$2:$1' |
read --local at
echo "not ok $_fishtape_test_number $name"
echo " ---"
echo " operator: $operator"
echo " expected: $expected"
echo " actual: $actual"
echo " at: $at"
echo " ..."
end
end
echo TAP version 13
for file in $files
fish --init-command=(functions @echo | string collect) --init-command=(functions @test | string collect) $file
end
echo
echo "1..$_fishtape_test_number"
echo "# pass $_fishtape_test_passed"
test $_fishtape_test_failed -eq 0 &&
echo "# ok" ||
echo "# fail $_fishtape_test_failed"
functions --erase @echo @test
set --local failed $_fishtape_test_failed
set --erase _fishtape_test_number
set --erase _fishtape_test_passed
set --erase _fishtape_test_failed
test $failed -eq 0
end
end