feat: add plan and backlog
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# План: 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<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` задан в конфиге):
|
||||
|
||||
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<Option<String>>`)
|
||||
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`. Реализуем итеративно, по мере потребностей.
|
||||
Reference in New Issue
Block a user