From c6d7e7eeb024c1be5091f8dcf27c2b79e5dd3c94 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 26 Jul 2026 17:53:30 +0300 Subject: [PATCH] =?UTF-8?q?feat(install):=20=D0=BA=D0=BE=D0=BD=D1=84=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D1=82=D1=83=D1=8E=D1=89=D0=B8=D0=B5=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D1=8B=20=D0=B1=D1=8D=D0=BA=D0=B0=D0=BF=D1=8F=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20stow=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=BF=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - конфликт с уже существующим конфигом приходилось разбирать руками: stow печатал "All operations aborted" и откатывал все пакеты сразу. - Что: - backup_conflicting_kitty_files заменена на общую backup_conflicting_stow_targets по списку AVAILABLE_PACKAGES. - обход повторяет логику stow: отсутствующая цель и каталоги-симлинки пропускаются вместе с поддеревом, поэтому свёрнутые пакеты (~/.codex, ~/.vscode-server) не обходятся и не бэкапятся. - в фикстуру тестов добавлен пакет wezterm, добавлены тесты на конфликт в HOME и на неприкосновенность содержимого свёрнутого пакета. - Проверка: - bash tests/test_install.sh --- install.sh | 60 ++++++++++++++++++++++++++++++++----------- tests/test_install.sh | 39 +++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/install.sh b/install.sh index caa3eeb..72e1220 100755 --- a/install.sh +++ b/install.sh @@ -169,12 +169,31 @@ migrate_claude_stow_symlink() { fi } -# Убрать с пути чужие файлы конфигурации kitty: kitty создаёт их сам, и без -# бэкапа stow отказывается ставить весь набор пакетов ("All operations -# aborted"), а не только kitty. -backup_conflicting_kitty_files() { - local src_dir="$DOTFILES_DIR/kitty/.config/kitty" - local dst_dir="$HOME/.config/kitty" +# Убрать с пути чужие файлы, которые конфликтуют со stow-пакетами. +# +# Программы создают свои конфиги сами (kitty.conf, .wezterm.lua), и при +# конфликте stow не просто пропускает пакет, а печатает "All operations +# aborted" и откатывает весь набор: install.sh падает по pipefail, не дойдя +# до настройки claude, opencode и .bashrc. Раньше это чинилось руками. +# +# Обход повторяет логику stow, а не перебирает файлы подряд: если каталога в +# HOME нет, конфликта быть не может и всё поддерево пропускается. Это заодно +# отсекает свёрнутые пакеты (~/.codex, ~/.vscode-server) с их гигабайтами +# рантайм-данных. +backup_conflicting_stow_targets() { + local pkg="" + + for pkg in "$@"; do + backup_conflicting_tree "$DOTFILES_DIR/$pkg" "$HOME" "" + done +} + +# Рекурсивный обход одного уровня: src_dir — каталог пакета, dst_dir — его +# соответствие в HOME, rel — путь для сообщений. +backup_conflicting_tree() { + local src_dir="$1" + local dst_dir="$2" + local rel="$3" local src="" local dst="" local base="" @@ -182,18 +201,30 @@ backup_conflicting_kitty_files() { local dst_real="" [ -d "$src_dir" ] || return 0 - [ -d "$dst_dir" ] || return 0 - [ ! -L "$dst_dir" ] || return 0 - for src in "$src_dir"/*; do - [ -e "$src" ] || continue + for src in "$src_dir"/* "$src_dir"/.[!.]*; do + [ -e "$src" ] || [ -L "$src" ] || continue base="$(basename "$src")" dst="$dst_dir/$base" - # -L отдельно от -e: битый симлинк для stow тоже конфликт. + # Цели нет — stow создаст симлинк или свернёт каталог, конфликта нет. [ -e "$dst" ] || [ -L "$dst" ] || continue - # Симлинк на наш же файл — это результат прошлого прогона, не трогаем. + if [ -d "$src" ] && [ ! -L "$src" ]; then + # Каталог: симлинк в цели трогать нельзя — stow спускается внутрь, + # так работают и свёрнутые пакеты, и посторонний симлинк ~/.config. + [ ! -L "$dst" ] || continue + # Два настоящих каталога stow объединяет, конфликт может быть глубже. + if [ -d "$dst" ]; then + backup_conflicting_tree "$src" "$dst" "$rel/$base" + continue + fi + # Файл на месте каталога — stow каталог поверх него не создаст. + backup_existing_path "$dst" "~$rel/$base" + continue + fi + + # Файл: симлинк на наш же файл — результат прошлого прогона. if [ -L "$dst" ]; then src_real="$(readlink -f "$src" 2>/dev/null || true)" dst_real="$(readlink -f "$dst" 2>/dev/null || true)" @@ -202,7 +233,7 @@ backup_conflicting_kitty_files() { fi fi - backup_existing_path "$dst" "~/.config/kitty/$base" + backup_existing_path "$dst" "~$rel/$base" done } @@ -378,9 +409,8 @@ if [ -d "$HOME/.config" ] && [ ! -L "$HOME/.config" ] \ mkdir -p "$HOME/.config/kitty" fi -backup_conflicting_kitty_files - if [ "${#AVAILABLE_PACKAGES[@]}" -gt 0 ]; then + backup_conflicting_stow_targets "${AVAILABLE_PACKAGES[@]}" stow_package "${AVAILABLE_PACKAGES[@]}" fi diff --git a/tests/test_install.sh b/tests/test_install.sh index b511527..8873121 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -37,7 +37,7 @@ setup_fixture() { # zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают # stow конфликтами, поэтому в тестовое дерево не попадает. git -C "$REPO_ROOT" ls-files -z --cached -- \ - install.sh bash tmux vim zed kitty vscode agents opencode codex gemini \ + install.sh bash tmux vim zed kitty wezterm vscode agents opencode codex gemini \ claude proxy-client scripts \ | tar -C "$REPO_ROOT" --null -T - -cf - \ | tar -xf - -C "$repo_root" @@ -200,6 +200,41 @@ test_rerun_does_not_backup_managed_kitty_config() { [ -z "$backup" ] || fail "Rerun must not create backups of its own symlinks: $backup" } +test_existing_wezterm_config_is_backed_up() { + local fixture repo_root home_dir backup + fixture="$(setup_fixture)" + repo_root="$fixture/repo" + home_dir="$fixture/home" + mkdir -p "$home_dir" + # Тот же конфликт, но у пакета с файлом прямо в HOME, а не в ~/.config. + printf '%s\n' 'local wezterm = require("wezterm")' > "$home_dir/.wezterm.lua" + + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" + + [ -L "$home_dir/.wezterm.lua" ] || fail "Expected managed .wezterm.lua symlink" + backup="$(find "$home_dir" -maxdepth 1 -name '.wezterm.lua.bak.*' -print -quit)" + [ -n "$backup" ] || fail "Expected backup of the pre-existing .wezterm.lua" + assert_file_contains "$backup" 'require("wezterm")' +} + +test_folded_package_runtime_files_are_not_backed_up() { + local fixture repo_root home_dir backup + fixture="$(setup_fixture)" + repo_root="$fixture/repo" + home_dir="$fixture/home" + mkdir -p "$home_dir" + + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" + # ~/.codex свёрнут в симлинк на пакет: файлы внутри — уже наши, не конфликт. + printf '%s\n' '{}' > "$home_dir/.codex/auth.json" + + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out" + + backup="$(find "$repo_root" "$home_dir" -name '*.bak.*' -print -quit)" + [ -z "$backup" ] || fail "Folded package contents must not be backed up: $backup" + assert_file_contains "$home_dir/.codex/auth.json" "{}" +} + test_migrates_legacy_zed_config_symlink() { local fixture repo_root home_dir legacy_config output mode fixture="$(setup_fixture)" @@ -599,6 +634,8 @@ main() { test_existing_kitty_config_is_backed_up_and_does_not_abort_stow test_foreign_kitty_config_symlink_is_backed_up test_rerun_does_not_backup_managed_kitty_config + test_existing_wezterm_config_is_backed_up + test_folded_package_runtime_files_are_not_backed_up test_migrates_legacy_zed_config_symlink test_stops_legacy_migration_when_listing_fails test_does_not_migrate_unrelated_config_symlink