Files
xboct-deploy/docs/plan-git-branch-deploy.md

13 KiB
Raw Permalink Blame History

План: 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'им.
  • Минимум кода, ничего не ломает для существующих конфигов.

Конфиг

[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 задан в конфиге):

  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_branchErr если git падает (например, битый .git)
  • current_branchErr если 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_branchResult<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 repositoriesgit 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. Реализуем итеративно, по мере потребностей.