feat(install): конфликтующие файлы бэкапятся перед stow для всех пакетов

- Зачем:
  - конфликт с уже существующим конфигом приходилось разбирать руками:
    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
This commit is contained in:
ddadmin committed 2026-07-26 17:53:30 +03:00
1 parent 6610adeac5
commit c6d7e7eeb0
2 files changed
+83 -16

No files matched your search

+45 -15
View File
@@ -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