13 KiB
План: Deploy разных git веток из одной папки
Проблема
Сейчас проекты идентифицируются по workdir — два проекта в одной папке не работают корректно. Нет интеграции с git — нельзя деплоить конкретную ветку или удостовериться, что деплоится та ветка, которая задумана.
Статус / подход
Итеративная разработка. Реализуем минимальный полезный шаг, пользуемся утилитой вживую, и только по мере реальных потребностей добавляем следующее.
Всё, что не реализовано сейчас (worktree, stash, .env sync и прочие edge cases), вынесено в отдельный файл docs/git-branch-roadmap.md — там детальная проработка, но только для обсуждения, на будущее. Сюда смотрим, когда понадобится следующий шаг.
Шаг 1 (РЕАЛИЗУЕМ сейчас): поле branch + проверка совпадения при деплое
Решение
Добавить опциональное поле branch в проект. Поведение при деплое:
branchне задан → деплоим как раньше (полная совместимость, ничего не проверяем).branchзадан:- определить текущую git ветку в
workdir; - текущая совпадает с
branch→ деплоим напрямую; - текущая не совпадает → abort с внятным сообщением.
- определить текущую git ветку в
Никаких worktree, stash, авто-переключений. Только безопасная проверка перед тем, как что-то улетит на прод.
В подсказках пользователю используем
git switch.
Почему именно так
- Устраняет главный риск: деплой не той ветки, чем задумано (например, забыли переключиться с
mainнаhotfix). - Нулевая вероятность потери данных — мы ничего не двигаем, не переключаем, не stash'им.
- Минимум кода, ничего не ломает для существующих конфигов.
Конфиг
[projects.my-app]
workdir = 'C:\projects\my-app'
branch = "staging"
Поле branch опционально. Без него — работает как раньше.
UX
cd C:\projects\my-app # сейчас на ветке main, в конфиге branch = "staging"
xd deploy
# → error: project 'my-app' is on branch 'main', expected 'staging'.
# Switch branches: git switch staging
# Or deploy a different project.
Изменения по файлам
1. src/config.rs — поле branch (+валидация)
Добавить в struct Project опциональное строковое поле branch.
В Config::validate (рядом с остальными проверками проекта) добавить: если branch задан — отклонить пустую/пробельную строку (.trim().is_empty()). Также .trim() при сравнении в deploy() — чтобы branch = " staging " в конфиге корректно матчился с staging из git.
Note: валидацию «workdir — git repo» здесь не делаем (это рантайм-проверка, зависит от состояния машины, а не от конфига) — она в deploy(). В validate только синтаксическая проверка ветки.
2. src/git.rs — НОВЫЙ МОДУЛЬ
Обёртка над git CLI через std::process::Command.
Одна функция current_branch(workdir) -> Result<Option<String>> — вызывает git rev-parse --abbrev-ref HEAD и возвращает:
Ok(Some(имя))— обычная ветка;Ok(Some("HEAD"))— detached HEAD;Ok(None)— не git repo (не внутри рабочего дерева);Err— git реально упал (битый.gitилиgitне установлен в PATH).
Почему
Option, а не только имя:Ok(None)= не git-репозиторий — это не ошибка сама по себе (её осмысляет вызывающий), аErr= git действительно сломался. Так семантика однозначна и не путает detached ("HEAD").
gitне установлен:Command::new("git")упадёт с ошибкой ОС ( NotFound), которая попадёт вErr. Это должно отличаться отNone— см. п.2 в deploy() ниже.
3. src/deploy.rs — проверка ветки перед деплоем
В самом начале deploy() — строго до ветки dry_run и до раннего выхода «nothing to do», чтобы dry-run тоже валидировал ветку. Алгоритм (только если branch задан в конфиге):
- Вызвать
current_branch(workdir)и обработать ошибку. Ok(None)(не git repo) → abort с текстом: «project X has branch Y set, но workdir не git-репозиторий».Err(git не установлен) → abort с текстом: «project X has branch Y set, ноgitне найден в PATH. Установите git: https://git-scm.com/downloads».Err(битый репо) → abort с текстом: «project X: git repo повреждён (bit .git directory)»."HEAD"(detached) → abort с отдельным текстом: «project X в detached HEAD, ожидается ветка Y; вернись на веткуgit switch -c Y».- Имя ≠
branch→ abort с текстом: «project X на ветке A, ожидается B; переключисьgit switch B». - Имя ==
branch→ строка[Git] on branch 'Y' ✓и продолжаем.
Ключевое поведение:
- detached → abort с точной подсказкой (
git switch -c), т.к.git checkoutв detached просто создаст новую ветку; Ok(None)(не git repo) → abort с понятным текстом;- git не установлен → отдельный abort с подсказкой установки (а не generic "git failed");
git rev-parseработает и из подпапки репо (git поднимается вверх), поэтомуworkdirне обязан быть корнем — два проекта в одной папке увидят одну ветку (корректно).
4. src/main.rs — без изменений (проверка внутри deploy)
5. src/cli.rs — без изменений
Тесты
src/git.rs
current_branchвозвращаетSome(имя_ветки)в git репоcurrent_branchвозвращаетSome("HEAD")при detached HEAD (git switch --detach)current_branchвозвращаетOk(None)если не git repocurrent_branch—Errеслиgitпадает (например, битый.git)current_branch—Errеслиgitне установлен в PATH (模拟: невалидный путь к git)
Грабль с тестами — пустой репо.
git rev-parse --abbrev-ref HEADна свежеинициализированном репо без коммитов (unborn HEAD) не даёт стабильной ветки (зависит отinit.defaultBranch/версии git). Поэтому все тесты с репозиторием должны:git init -b test+ создать минимум один коммит до проверки. Существующийtmpdir()(config.rs) git не инициализирует — новый хелпер нужен отдельный (например,git_repo(tag)вgit.rs, который делает init/commit и чистит за собой).Зависимость: тесты требуют наличия
gitв PATH окружения. Для этого проекта приемлемо (git и так нужен для работы), но это новая зависимость для CI/машины — зафиксировать явно.
src/deploy.rs
- Deploy с
branch== текущая ветка → продолжается - Deploy с
branch!= текущая ветка → abort с текстом ошибки - Deploy без
branch→ ничего не проверяет (полная совместимость) - Deploy с
branch, workdir не git repo → abort (сообщение «not a git repository») - Deploy с
branch, git не установлен в PATH → abort (сообщение «git not found» + ссылка на установку) - Deploy с
branch, в detached HEAD → abort (сообщение «detached HEAD state») - Deploy с
branch, пустая строка → ошибка валидации конфига - Deploy с
branch, пробелы в начале/конце (" staging ") → trim работает, ветка матчится корректно - Dry-run с
branchвсё ещё печатает план (после успешной проверки ветки)
Зависимости
Новых crate не требуется — git вызывается через std::process::Command.
Порядок реализации (шаг 1)
src/git.rs— новый модуль (current_branch→Result<Option<String>>)src/config.rs— полеbranch+ валидация непустой строки вConfig::validatesrc/deploy.rs— проверка ветки в началеdeploy()(обработкаNone/ detached / несовпадения)- Тесты (в
git.rs— на реальных репо с коммитом; вconfig.rs— на непустойbranch) cargo test+cargo clippy
Сложности / риски (что нельзя сломать)
- Обратная совместимость — главное правило: ветка проверяется только если
branchзадан в конфиге. Безbranchповедениеdeploy()идентично текущему.deny_unknown_fieldsтребует добавить поле в struct (сделано), старые конфиги не затрагиваются. - Порядок в
deploy()— проверка ветки строго в начале, доdry_runи «nothing to do»: dry-run обязан валидировать ветку. workdirкак подпапка репо — работает, т.к. git поднимается вверх; это фича (не баг), в т.ч. для двух проектов в одном репо.- Empty repo в тестах — без коммита ветки нестабильны; в тестах всегда делать init + commit.
- Новая зависимость тестов от
gitв PATH. - Detached HEAD — отдельное сообщение; не путать с несовпадением веток.
Known Limitations
- TOCTOU (check-then-act) — проверка ветки и реальный deploy разделены по времени. Ветка может измениться между проверкой и rsync. Это фундаментальное ограничение подхода (plan-based guard, не lock). В реальности маловероятно, но документируем явно. Если станет проблемой — следующий шаг (worktree / lock).
- Bare repositories —
git rev-parse --abbrev-ref HEADработает в bare repos, но поведение может быть неожиданным (ветка HEAD в bare repo может не совпадать с ожидаемой). Bare repos не являются целевым кейсом для шага 1. Если понадобится — отдельная проработка. - Submodules — если
workdirуказывает на subdirectory внутри submodule'а,git rev-parseвернёт ветку submodule'а, а не родительского репо. Это корректное поведение git, но может удивить пользователя. Решение: если проект — submodule, branch в конфиге должен указывать ветку submodule'а.
Дальнейшие шаги
Детальная проработка будущих возможностей — в docs/git-branch-roadmap.md. Реализуем итеративно, по мере потребностей.