diff --git a/home/.gitconfig b/home/.gitconfig.dolan similarity index 100% rename from home/.gitconfig rename to home/.gitconfig.dolan diff --git a/home/.zshenv b/home/.zshenv.dolan similarity index 100% rename from home/.zshenv rename to home/.zshenv.dolan diff --git a/home/.zshrc b/home/.zshrc.dolan similarity index 100% rename from home/.zshrc rename to home/.zshrc.dolan diff --git a/home/bin/dol b/home/bin/dol index 0ddda8d..7543e66 100755 --- a/home/bin/dol +++ b/home/bin/dol @@ -117,6 +117,83 @@ parse_common_flags() { done } +managed_block_start="# >>> ~/.dotfiles/install.sh >>>" +managed_block_end="# <<< ~/.dotfiles/install.sh <<<" + +ensure_local_dotfile() { + local target="$1" + + if [[ -d "${target}" ]]; then + echo "Cannot manage ${target}: it is a directory." + return 1 + fi + + # Older installs symlinked these files directly; replace with a fresh local file. + if [[ -L "${target}" ]]; then + echo " Replacing symlinked $(basename "${target}") with a local file..." + rm -f "${target}" + : > "${target}" + return 0 + fi + + if [[ ! -e "${target}" ]]; then + : > "${target}" + fi +} + +strip_managed_block() { + local source_file="$1" + local destination_file="$2" + + awk -v start="${managed_block_start}" -v end="${managed_block_end}" ' + $0 == start { + in_block=1 + next + } + in_block && $0 == end { + in_block=0 + next + } + !in_block { + print + } + ' "${source_file}" > "${destination_file}" +} + +prepend_managed_block() { + local target="$1" + shift + local block_lines=("$@") + local stripped_file + local line + + ensure_local_dotfile "${target}" + + stripped_file="$(mktemp "${TMPDIR:-/tmp}/dotfiles-install.XXXXXX")" + strip_managed_block "${target}" "${stripped_file}" + + { + printf '%s\n' "${managed_block_start}" + for line in "${block_lines[@]}"; do + printf '%s\n' "${line}" + done + printf '%s\n' "${managed_block_end}" + if [[ -s "${stripped_file}" ]]; then + printf '\n' + cat "${stripped_file}" + fi + } > "${target}" + + rm -f "${stripped_file}" +} + +configure_managed_dotfiles() { + echo "Configuring managed include blocks..." + prepend_managed_block "${HOME}/.zshrc" "[ -f \"\$HOME/.zshrc.dolan\" ] && . \"\$HOME/.zshrc.dolan\"" + prepend_managed_block "${HOME}/.zshenv" "[ -f \"\$HOME/.zshenv.dolan\" ] && . \"\$HOME/.zshenv.dolan\"" + prepend_managed_block "${HOME}/.gitconfig" '[include]' ' path = ~/.gitconfig.dolan' +} + run_dotfiles_install() { local home_src local home_dst @@ -165,6 +242,8 @@ run_dotfiles_install() { ln -sf "${file}" "${dest_file}" done + configure_managed_dotfiles + os_name="$(uname)" if [[ "${os_name}" == "Darwin" ]]; then echo "Installing Mac OS Software..." diff --git a/tests/install.bats b/tests/install.bats new file mode 100644 index 0000000..dd97bc0 --- /dev/null +++ b/tests/install.bats @@ -0,0 +1,156 @@ +#!/usr/bin/env bats + +repo_root="$(git rev-parse --show-toplevel)" + +setup() { + tmp_home="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-install-test.XXXXXX")" + fake_bin="${tmp_home}/fake-bin" + mkdir -p "${fake_bin}" "${tmp_home}/.oh-my-zsh" + + cat > "${fake_bin}/uname" <<'EOF' +#!/bin/sh +echo "Linux" +EOF + chmod +x "${fake_bin}/uname" + + real_git="$(command -v git)" + cat > "${fake_bin}/git" </dev/null 2>&1; then + install_command=( + "$(command -v zsh)" + "${repo_root}/install.sh" + ) + else + install_command=( + "$(command -v bash)" + "${repo_root}/home/bin/dol" + install + ) + fi +} + +teardown() { + rm -rf "${tmp_home}" +} + +@test "fresh install prepends managed include blocks" { + run env "${install_env[@]}" "${install_command[@]}" + [ "${status}" -eq 0 ] + + [ -L "${tmp_home}/.zshrc.dolan" ] + [ -L "${tmp_home}/.zshenv.dolan" ] + [ -L "${tmp_home}/.gitconfig.dolan" ] + + [ -f "${tmp_home}/.zshrc" ] + [ ! -L "${tmp_home}/.zshrc" ] + [ -f "${tmp_home}/.zshenv" ] + [ ! -L "${tmp_home}/.zshenv" ] + [ -f "${tmp_home}/.gitconfig" ] + [ ! -L "${tmp_home}/.gitconfig" ] + + run sed -n '1,3p' "${tmp_home}/.zshrc" + [ "${status}" -eq 0 ] + [ "${lines[0]}" = "# >>> ~/.dotfiles/install.sh >>>" ] + [ "${lines[1]}" = "[ -f \"\$HOME/.zshrc.dolan\" ] && . \"\$HOME/.zshrc.dolan\"" ] + [ "${lines[2]}" = "# <<< ~/.dotfiles/install.sh <<<" ] + + run sed -n '1,3p' "${tmp_home}/.zshenv" + [ "${status}" -eq 0 ] + [ "${lines[0]}" = "# >>> ~/.dotfiles/install.sh >>>" ] + [ "${lines[1]}" = "[ -f \"\$HOME/.zshenv.dolan\" ] && . \"\$HOME/.zshenv.dolan\"" ] + [ "${lines[2]}" = "# <<< ~/.dotfiles/install.sh <<<" ] + + run sed -n '1,4p' "${tmp_home}/.gitconfig" + [ "${status}" -eq 0 ] + [ "${lines[0]}" = "# >>> ~/.dotfiles/install.sh >>>" ] + [ "${lines[1]}" = "[include]" ] + [ "${lines[2]}" = " path = ~/.gitconfig.dolan" ] + [ "${lines[3]}" = "# <<< ~/.dotfiles/install.sh <<<" ] +} + +@test "existing file contents are preserved under prepended block" { + cat > "${tmp_home}/.zshrc" <<'EOF' +export KEEP_THIS_LINE=1 +EOF + + run env "${install_env[@]}" "${install_command[@]}" + [ "${status}" -eq 0 ] + + run tail -n 1 "${tmp_home}/.zshrc" + [ "${status}" -eq 0 ] + [ "${output}" = "export KEEP_THIS_LINE=1" ] +} + +@test "stale managed blocks are replaced with current include content" { + cat > "${tmp_home}/.zshrc" <<'EOF' +# >>> ~/.dotfiles/install.sh >>> +[ -f "$HOME/.old-zshrc" ] && . "$HOME/.old-zshrc" +# <<< ~/.dotfiles/install.sh <<< + +export STILL_HERE=1 +EOF + + run env "${install_env[@]}" "${install_command[@]}" + [ "${status}" -eq 0 ] + + run grep -c '^# >>> ~/.dotfiles/install.sh >>>$' "${tmp_home}/.zshrc" + [ "${status}" -eq 0 ] + [ "${output}" -eq 1 ] + + run grep -c 'old-zshrc' "${tmp_home}/.zshrc" + [ "${status}" -eq 1 ] + + run sed -n '1,3p' "${tmp_home}/.zshrc" + [ "${status}" -eq 0 ] + [ "${lines[1]}" = "[ -f \"\$HOME/.zshrc.dolan\" ] && . \"\$HOME/.zshrc.dolan\"" ] +} + +@test "existing symlinked dotfiles are unlinked and replaced with local files" { + ln -s "${repo_root}/home/.zshrc.dolan" "${tmp_home}/.zshrc" + ln -s "${repo_root}/home/.zshenv.dolan" "${tmp_home}/.zshenv" + ln -s "${repo_root}/home/.gitconfig.dolan" "${tmp_home}/.gitconfig" + + run env "${install_env[@]}" "${install_command[@]}" + [ "${status}" -eq 0 ] + + [ -f "${tmp_home}/.zshrc" ] + [ ! -L "${tmp_home}/.zshrc" ] + [ -f "${tmp_home}/.zshenv" ] + [ ! -L "${tmp_home}/.zshenv" ] + [ -f "${tmp_home}/.gitconfig" ] + [ ! -L "${tmp_home}/.gitconfig" ] +} + +@test "re-running install does not duplicate managed blocks" { + run env "${install_env[@]}" "${install_command[@]}" + [ "${status}" -eq 0 ] + + run env "${install_env[@]}" "${install_command[@]}" + [ "${status}" -eq 0 ] + + run grep -c '^# >>> ~/.dotfiles/install.sh >>>$' "${tmp_home}/.zshrc" + [ "${status}" -eq 0 ] + [ "${output}" -eq 1 ] + + run grep -c '^# >>> ~/.dotfiles/install.sh >>>$' "${tmp_home}/.zshenv" + [ "${status}" -eq 0 ] + [ "${output}" -eq 1 ] + + run grep -c '^# >>> ~/.dotfiles/install.sh >>>$' "${tmp_home}/.gitconfig" + [ "${status}" -eq 0 ] + [ "${output}" -eq 1 ] +} diff --git a/tests/lint.bats b/tests/lint.bats index fa59662..faf84bc 100644 --- a/tests/lint.bats +++ b/tests/lint.bats @@ -5,6 +5,12 @@ repo_root="$(git rev-parse --show-toplevel)" check_script() { local file="$1" local shebang + + if [[ "${file}" == *.bats ]]; then + bats --count "${file}" >/dev/null + return 0 + fi + shebang="$(head -n1 "${file}")" if [[ "${shebang}" == *zsh* ]] && command -v zsh >/dev/null 2>&1; then zsh -n "${file}" @@ -31,6 +37,13 @@ done < <(find "${repo_root}/tests" -name '*.bats' -type f || true) scripts+=("${bats_tests[@]}") +# De-duplicate script paths in case a Bats test contains heredocs with shebangs. +unique_scripts=() +while IFS= read -r line; do + unique_scripts+=("${line}") +done < <(printf '%s\n' "${scripts[@]}" | awk 'NF && !seen[$0]++') +scripts=("${unique_scripts[@]}") + for script in "${scripts[@]}"; do bats_test_function --description "${script}" -- check_script "${script}" done