diff --git a/.config/dot/commands/.gitkeep b/.config/dot/commands/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.config/dot/commands/install.fish b/.config/dot/commands/install.fish new file mode 100644 index 0000000..f5d3156 --- /dev/null +++ b/.config/dot/commands/install.fish @@ -0,0 +1,48 @@ +function _dot_install + argparse 'restore' 'no-sync' -- $argv + or return 1 + + set -l list_dir $HOME/.config/dot/packages + set -l list_file $list_dir/pacman + set -l packages + + if set -q _flag_restore + if test (count $argv) -gt 0 + echo "dot install: --restore cannot be combined with package names" >&2 + return 1 + end + + if not test -s $list_file + echo "dot install: no package list found at $list_file" >&2 + return 1 + end + + set packages (cat $list_file) + else + if test (count $argv) -eq 0 + echo "dot install: no packages given (use --restore to reinstall from the list)" >&2 + return 1 + end + + set packages $argv + end + + if not set -q _flag_no_sync + sudo pacman -Sy + or return 1 + end + + sudo pacman -S --needed $packages + or return 1 + + if set -q _flag_restore + return 0 + end + + mkdir -p $list_dir + test -f $list_file + or touch $list_file + + printf '%s\n' $packages >>$list_file + sort -u -o $list_file $list_file +end diff --git a/.config/dot/packages/pacman b/.config/dot/packages/pacman new file mode 100644 index 0000000..b80ce06 --- /dev/null +++ b/.config/dot/packages/pacman @@ -0,0 +1 @@ +tmux diff --git a/.config/dot/tests/dot.fish b/.config/dot/tests/dot.fish index 364fb85..f558148 100644 --- a/.config/dot/tests/dot.fish +++ b/.config/dot/tests/dot.fish @@ -1,3 +1,5 @@ +set -l commands_dir (path resolve (status dirname)/../commands) + # 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 @@ -89,3 +91,140 @@ end" >$HOME/.config/dot/commands/mark.fish set -l help_with_custom (dot help) @test "dot help lists custom commands found under ~/.config/dot/commands/" (string match -q '*mark*' -- $help_with_custom; echo $status) -eq 0 + +# --- dot install --- +# pacman and sudo are faked out via a bin dir prepended to PATH: sudo just +# execs its arguments, and pacman logs each invocation to $PACMAN_LOG (one +# line per call) and fails only when asked to install a package literally +# named "failpkg", so tests can force the failure path without touching the +# real package manager. +set -l fake_bin (mktemp -d) +echo '#!/bin/sh +exec "$@"' >$fake_bin/sudo +chmod +x $fake_bin/sudo + +echo '#!/bin/sh +echo "$@" >>"$PACMAN_LOG" +for arg in "$@"; do + if [ "$arg" = failpkg ]; then + exit 1 + fi +done +exit 0' >$fake_bin/pacman +chmod +x $fake_bin/pacman + +set -gx PATH $fake_bin $PATH + +# --- a successful install records the packages, sorted and deduplicated --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +set -gx PACMAN_LOG (mktemp) + +dot install zeta alpha >/dev/null 2>&1 +set -l first_install_status $status +set -l list_file $HOME/.config/dot/packages/pacman +set -l synced_by_default (string match -q '*-Sy*' -- (cat $PACMAN_LOG); and echo yes; or echo no) +set -l installed_named (string match -q '*-S --needed zeta alpha*' -- (cat $PACMAN_LOG); and echo yes; or echo no) + +@test "dot install succeeds for real packages" $first_install_status -eq 0 +@test "dot install syncs the database by default" $synced_by_default = yes +@test "dot install passes packages to pacman -S --needed" $installed_named = yes +@test "installed packages are recorded, sorted" (cat $list_file | string collect) = "alpha +zeta" + +dot install beta >/dev/null 2>&1 +@test "a later install merges into the existing list, still sorted" (cat $list_file | string collect) = "alpha +beta +zeta" + +dot install alpha >/dev/null 2>&1 +@test "re-installing an already-recorded package does not duplicate it" (cat $list_file | string collect) = "alpha +beta +zeta" + +# --- --no-sync skips the database refresh --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +set -gx PACMAN_LOG (mktemp) + +dot install --no-sync somepkg >/dev/null 2>&1 +set -l synced_with_no_sync (string match -q '*-Sy*' -- (cat $PACMAN_LOG); and echo yes; or echo no) +@test "--no-sync skips pacman -Sy" $synced_with_no_sync = no + +# --- a failed pacman run records nothing --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +set -gx PACMAN_LOG (mktemp) + +dot install failpkg >/dev/null 2>&1 +set -l failed_install_status $status +set -l list_exists_after_failure (test -e $HOME/.config/dot/packages/pacman; and echo yes; or echo no) + +@test "dot install fails when pacman fails" $failed_install_status -ne 0 +@test "a failed install leaves no package list behind" $list_exists_after_failure = no + +# --- no packages and no --restore is a usage error --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +set -gx PACMAN_LOG (mktemp) + +dot install >/dev/null 2>&1 +set -l no_args_status $status +set -l pacman_called_no_args (test -s $PACMAN_LOG; and echo yes; or echo no) + +@test "dot install with no arguments and no --restore fails" $no_args_status -ne 0 +@test "dot install with no arguments never calls pacman" $pacman_called_no_args = no + +# --- --restore reinstalls everything from the list without rewriting it --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +mkdir -p $HOME/.config/dot/packages +printf 'alpha\nbeta\n' >$HOME/.config/dot/packages/pacman +set -gx PACMAN_LOG (mktemp) + +dot install --restore >/dev/null 2>&1 +set -l restore_status $status +set -l restored_named (string match -q '*-S --needed alpha beta*' -- (cat $PACMAN_LOG); and echo yes; or echo no) + +@test "dot install --restore succeeds" $restore_status -eq 0 +@test "--restore installs every package from the list" $restored_named = yes +@test "--restore does not rewrite the list" (cat $HOME/.config/dot/packages/pacman | string collect) = "alpha +beta" + +# --- --restore with no list yet is an error --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +set -gx PACMAN_LOG (mktemp) + +dot install --restore >/dev/null 2>&1 +set -l restore_no_list_status $status + +@test "--restore fails when no package list exists yet" $restore_no_list_status -ne 0 + +# --- --restore and explicit packages are mutually exclusive --- +set -gx HOME (mktemp -d) +dot init --url $remote >/dev/null 2>&1 +mkdir -p $HOME/.config/dot/commands +cp $commands_dir/install.fish $HOME/.config/dot/commands/install.fish +mkdir -p $HOME/.config/dot/packages +printf 'alpha\n' >$HOME/.config/dot/packages/pacman +set -gx PACMAN_LOG (mktemp) + +dot install --restore extra >/dev/null 2>&1 +set -l restore_conflict_status $status +set -l pacman_called_conflict (test -s $PACMAN_LOG; and echo yes; or echo no) + +@test "--restore combined with package names fails" $restore_conflict_status -ne 0 +@test "--restore combined with package names never calls pacman" $pacman_called_conflict = no diff --git a/.config/fish/completions/dot.fish b/.config/fish/completions/dot.fish index ada98a8..0233670 100644 --- a/.config/fish/completions/dot.fish +++ b/.config/fish/completions/dot.fish @@ -5,3 +5,8 @@ function __dot_custom_subcommands end complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)" + +# --- dot install --- +complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l restore" -l restore -d "reinstall every package from the saved list" +complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l no-sync" -l no-sync -d "skip the pacman -Sy database refresh" +complete -c dot -n "__fish_seen_subcommand_from install; and not __fish_seen_argument -l restore" -f -a "(__fish_print_pacman_packages)"