# План: Deploy разных git веток из одной папки ## Проблема Сейчас проекты идентифицируются по `workdir` — два проекта в одной папке не работают корректно. Нет интеграции с git — нельзя деплоить конкретную ветку или удостовериться, что деплоится та ветка, которая задумана. ## Статус / подход **Итеративная разработка.** Реализуем минимальный полезный шаг, пользуемся утилитой вживую, и только по мере реальных потребностей добавляем следующее. Всё, что **не** реализовано сейчас (worktree, stash, `.env` sync и прочие edge cases), вынесено в отдельный файл `docs/git-branch-roadmap.md` — там детальная проработка, но **только для обсуждения**, на будущее. Сюда смотрим, когда понадобится следующий шаг. --- ## Шаг 1 (РЕАЛИЗУЕМ сейчас): поле `branch` + проверка совпадения при деплое ### Решение Добавить опциональное поле `branch` в проект. Поведение при деплое: - `branch` **не задан** → деплоим как раньше (полная совместимость, ничего не проверяем). - `branch` **задан**: - определить текущую git ветку в `workdir`; - текущая **совпадает** с `branch` → деплоим напрямую; - текущая **не совпадает** → **abort** с внятным сообщением. Никаких worktree, stash, авто-переключений. Только безопасная проверка перед тем, как что-то улетит на прод. > В подсказках пользователю используем `git switch`. ### Почему именно так - Устраняет главный риск: деплой не той ветки, чем задумано (например, забыли переключиться с `main` на `hotfix`). - Нулевая вероятность потери данных — мы ничего не двигаем, не переключаем, не stash'им. - Минимум кода, ничего не ломает для существующих конфигов. ### Конфиг ```toml [projects.my-app] workdir = 'C:\projects\my-app' branch = "staging" ``` Поле `branch` опционально. Без него — работает как раньше. ### UX ```bash 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>` — вызывает `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` задан в конфиге): 1. Вызвать `current_branch(workdir)` и обработать ошибку. 2. `Ok(None)` (не git repo) → abort с текстом: «project X has branch Y set, но workdir не git-репозиторий». 3. `Err` (git не установлен) → abort с текстом: «project X has branch Y set, но `git` не найден в PATH. Установите git: https://git-scm.com/downloads». 4. `Err` (битый репо) → abort с текстом: «project X: git repo повреждён (bit .git directory)». 5. `"HEAD"` (detached) → abort с отдельным текстом: «project X в detached HEAD, ожидается ветка Y; вернись на ветку `git switch -c Y`». 6. Имя ≠ `branch` → abort с текстом: «project X на ветке A, ожидается B; переключись `git switch B`». 7. Имя == `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 repo - `current_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) 1. `src/git.rs` — новый модуль (`current_branch` → `Result>`) 2. `src/config.rs` — поле `branch` + валидация непустой строки в `Config::validate` 3. `src/deploy.rs` — проверка ветки в начале `deploy()` (обработка `None` / detached / несовпадения) 4. Тесты (в `git.rs` — на реальных репо с коммитом; в `config.rs` — на непустой `branch`) 5. `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`. Реализуем итеративно, по мере потребностей.