diff --git a/backlog.md b/backlog.md
new file mode 100644
index 0000000..4fb2c9a
--- /dev/null
+++ b/backlog.md
@@ -0,0 +1,14 @@
+# Backlog
+
+## Validate config команда
+
+Добавить команду `xd validate` — проверка конфигурации на ошибки и потенциальные проблемы.
+
+### Что проверять
+
+- Одинаковые имена проектов (сейчас serde откажет, но стоит выводить понятное сообщение)
+- Одинаковые `workdir` + `branch` комбинации у разных проектов
+- Дублирующиеся проекты с одинаковыми deploy путями (одна папка, одна ветка, разные имена)
+- Ветки, указанные в конфиге, которые не существуют в git репозитории
+- Reachability серверов (resolve host, проверить SSH config)
+- Sync target пути, которые выглядят подозрительно
diff --git a/docs/plan-git-branch-deploy.md b/docs/plan-git-branch-deploy.md
new file mode 100644
index 0000000..b377dc7
--- /dev/null
+++ b/docs/plan-git-branch-deploy.md
@@ -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>` — вызывает `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`. Реализуем итеративно, по мере потребностей.
diff --git a/docs/roadmap-git-branch-roadmap.md b/docs/roadmap-git-branch-roadmap.md
new file mode 100644
index 0000000..f5a844b
--- /dev/null
+++ b/docs/roadmap-git-branch-roadmap.md
@@ -0,0 +1,587 @@
+# Roadmap: git-интеграция деплоя (обсуждение, на будущее)
+
+> Этот файл содержит детальную проработку будущих возможностей git-интеграции.
+> Это **НЕ план к реализации** — это материал для обсуждения и осмысления.
+> Реализуем итеративно: по мере использования утилиты и реальных потребностей.
+>
+> Текущий реализованный шаг — поле `branch` + проверка совпадения при деплое (см. `git-branch-deploy-plan.md`).
+
+---
+
+## Общая картина
+
+Сейчас реализован только минимальный шаг (проверка ветки). Дальше, по мере необходимости, можно добавлять:
+
+- **A. Deploy конкретной ветки** через `git worktree` (когда target != текущей).
+- **B. `.env` и другие untracked файлы** в worktree (отдельный механизм, не связан со stash).
+- **C. CLI `--branch`** флаг (переопределение target).
+- **D. Auto-stash** uncommitted changes (по умолчанию — не нужен).
+- **E. Lock-файл** против concurrent deploys.
+- **F. Прочие edge cases.**
+
+Ниже — детали каждого, чтобы при выборе не начинать с нуля.
+
+---
+
+## UX (будущее)
+
+### Команды
+
+```bash
+cd C:\projects\my-app
+
+# Авто-определение по текущей git ветке (если N проектов в папке)
+xd deploy
+
+# Переопределение ветки через флаг
+xd deploy --branch staging
+xd deploy --branch hotfix
+
+# Явное указание имени проекта
+xd deploy my-app-production
+
+# С auto-stash при uncommitted изменениях
+xd deploy --branch staging --auto-stash
+
+# С сохранением worktree после деплоя (для отладки)
+xd deploy --branch staging --keep-worktree
+```
+
+### Матрица поведения
+
+| Ситуация | `xd deploy` | `xd deploy --branch X` |
+| -------------------------------------- | --------------------------------------------- | --------------------------------- |
+| 1 проект в папке, без `branch` | деплоит текущую ветку | deploys X через worktree |
+| 1 проект в папке, с `branch` | деплоит branch из конфига | deploys X через worktree |
+| N проектов в папке, все с `branch` | деплоит тот, чей `branch` = текущая git ветка | deploys X |
+| Uncommitted + нет `--auto-stash` | abort | abort |
+| Uncommitted + `--auto-stash` | — | stash → worktree → deploy → pop |
+| Branch не существует | — | abort с ошибкой |
+| Workdir не git repo, `branch` задан | abort | abort |
+| Workdir не git repo, `branch` не задан | деплоит как раньше | — |
+| Detached HEAD | abort: "checkout branch first" | abort: "checkout branch first" |
+| Worktree уже существует (остаток) | — | пересоздать (remove + create) |
+| Stash pop — конфликт | — | stash сохранён, инструкция по pop |
+
+### Цепочка определения "что деплоить"
+
+```txt
+1. Имя проекта на CLI? → используем его
+2. Нет имени? → find_project_by_cwd():
+ a. 1 проект с этим workdir? → он
+ b. N проектов? → текущая git ветка = branch в конфиге?
+ → нашли? → деплоим
+ → не нашли? → ошибка: "укажите ветку или имя проекта"
+3. --branch X на CLI? → переопределяет target branch
+4. branch в конфиге? → target branch
+5. Ни того ни другого? → деплоим текущую ветку (без worktree)
+```
+
+### Worktree trigger
+
+```txt
+target branch == текущая git branch → деплоим из workdir напрямую
+target branch != текущая git branch → worktree → деплой → cleanup
+```
+
+---
+
+## A. Worktree для deploy конкретной ветки
+
+Когда `branch` != текущая ветка — не abort, а собрать ветку через `git worktree` (изолированно, вне репо), задеплоить, почистить.
+
+### Структура worktrees (вне репо)
+
+Все worktree-деплои хранятся **вне** репозитория, на том же уровне, что и сам репо:
+
+```txt
+repo/
+ .git/
+ src/...
+ deploy.toml
+repo/../.xd-worktrees/ ← общая площадка xd-деплоев (вне репо)
+ / ← имя папки, в которой лежит репо (изоляция от одноимённых репо в разных местах)
+ / ← изоляция разных проектов из одного workdir
+ staging/ ← worktree для ветки staging
+ main/ ← worktree для ветки main
+```
+
+Правила:
+
+- Worktree создаётся как `/.xd-worktrees///`
+- Итоговый путь запрашиваем у git / берём из `worktree list --porcelain`, не строим вручную из имени ветки
+- `.gitignore` не нужен — main worktree всегда чистый, статус/CI/линтеры не видят мусор
+- Если удалить репо — worktree-ветки остаются отдельно и не ломают ничего
+
+### Cleanup при ошибках (WorktreeGuard)
+
+Rust не имеет `finally`. Используем RAII — структуру `WorktreeGuard`, которая удаляет worktree в `Drop`:
+
+```rust
+struct WorktreeGuard {
+ path: Option,
+ repo_path: PathBuf,
+}
+
+impl Drop for WorktreeGuard {
+ fn drop(&mut self) {
+ if let Some(path) = self.path.take() {
+ // Игнорируем ошибку — не можем сделать ничего если cleanup fails
+ let _ = git::remove_worktree(&path);
+ }
+ }
+}
+
+impl WorktreeGuard {
+ fn new(worktree_path: PathBuf, repo_path: PathBuf) -> Self {
+ Self { path: Some(worktree_path), repo_path }
+ }
+
+ fn keep(mut self) { self.path = None; } // --keep-worktree
+}
+```
+
+Использование в deploy:
+
+```rust
+let guard = if need_worktree {
+ Some(WorktreeGuard::new(worktree_path, project.workdir.clone()))
+} else {
+ None
+};
+
+// ... deploy logic ...
+// Guard автоматически удалит worktree при выходе из scope (включая panic/error)
+
+if let Some(g) = guard {
+ if opts.keep_worktree { g.keep(); }
+}
+```
+
+### Новая логика в deploy (Phase 0.5 — перед local commands)
+
+```txt
+1. Определить effective_branch:
+ - target_branch (из CLI --branch)
+ - или project.branch (из конфига)
+ - или None (деплоим текущую ветку, без worktree)
+
+2. Если effective_branch задан:
+ a. Проверить что workdir существует (fs::metadata)
+ b. Проверить что workdir — git репозиторий (git rev-parse --git-dir)
+ c. Проверить что workdir/cwd не внутри .xd-worktrees
+ d. Проверить что ветка существует (git::branch_exists)
+ → Если local: ok
+ → Если только remote: git::fetch_branch + создать tracking worktree
+ → Если не существует: abort
+ e. Получить текущую ветку (git::current_branch)
+ → Если "HEAD" (detached): abort
+ f. Если текущая == target:
+ workdir = project.workdir (без worktree)
+ g. Если текущая != target:
+ i. Проверить uncommitted (git::has_uncommitted)
+ → если есть + нет auto_stash → abort
+ → если есть + auto_stash + detached HEAD → abort
+ → если есть + auto_stash + ветка → git::stash (с проверкой существующего auto-stash)
+ ii. Проверить worktree не существует (git::worktree_list_branch)
+ → если существует: git::remove_worktree
+ iii. Создать worktree (git::create_worktree) → вне репо, путь из worktree list
+ iv. Если есть submodules: git::submodule_update
+ v. workdir = worktree_path
+ vi. Создать DeployLock (для защиты от concurrent deploys)
+
+3. Phase 1-4: работают с workdir (обычный или worktree)
+
+4. Cleanup (через Drop — WorktreeGuard + DeployLock):
+ - WorktreeGuard::drop → git::remove_worktree (если path Some)
+ - DeployLock::drop → удалить lock file
+ - Если stash был сделан:
+ → Попытаться git::stash_pop (из оригинального workdir)
+ → Если конфликт: вывести инструкцию, stash остаётся
+```
+
+---
+
+## B. `.env` и другие untracked файлы в worktree
+
+`.env` обычно лежит в `.gitignore` и просто **не существует для git-веток**. При создании worktree из другого branch'а `.env` туда не попадёт → локальный build может упасть.
+
+**Это отдельный механизм, не связанный со stash.** При создании worktree — копировать набор untracked конфиг-файлов из основного workdir в worktree.
+
+```toml
+[projects.my-app]
+workdir = 'C:\projects\my-app'
+branch = "staging"
+sync_worktree = [".env", ".env.local"]
+```
+
+Почему это не конфликтует со stash:
+
+| Механизм | Что делает | Связь со stash |
+| ------------------ | --------------------------------------- | -------------- |
+| `.env` копирование | copy untracked конфиг-файлов в worktree | нет |
+| Uncommitted stash | tracked-файлы с изменениями разработки | да |
+
+Безопасность:
+
+- `.env` не в git → копируется как есть, ничего не обновляется.
+- worktree удаляется после деплоя → копия исчезает.
+- Оригинальный `.env` не трогается.
+- Если файла нет → просто пропускаем (build может работать и без него).
+
+---
+
+## C. CLI `--branch` флаг
+
+```bash
+xd deploy --branch staging # переопределить target branch
+xd deploy --branch hotfix
+```
+
+Поле `--branch` переопределяет `project.branch`. Если оба заданы и различаются → предупреждение (не abort):
+
+```txt
+"Overriding project branch '{config_branch}' with CLI branch '{cli_branch}'"
+```
+
+---
+
+## D. Auto-stash uncommitted changes (по умолчанию — НЕ надо)
+
+Uncommitted changes — признак активной разработки. Это **другой контекст**, несовместимый с деплоем другой ветки из этой же папки. Дефолт — **abort при uncommitted**, без авто-stash. `--auto-stash` — опциональный escape-hatch для редких случаев.
+
+Связанные ограничения (чтобы не потерять данные):
+
+- **Не stash'ить в detached HEAD** — stash в detached HEAD не привязан к ветке; при сбое между stash и pop изменения теряют ветку-контекст. Detached HEAD + uncommitted → всегда abort, даже с `--auto-stash`.
+- **Проверять существующий auto-stash** перед новым (`git stash list --grep="xd-auto-stash"`) → abort, если найден, с инструкцией pop/drop.
+- **При конфликте pop** — stash остаётся в stash list, выводим инструкцию, deploy считается успешным.
+- **Stash искать по message, не по индексу** — чтобы индексы не путались между несколькими деплоями.
+- **Stash push** должен включать untracked (`--include-untracked`), иначе новые файлы не застешатся.
+- **Перед pop** проверять, что не detached HEAD.
+
+---
+
+## E. Lock-файл против concurrent deploys
+
+Защита от двух параллельных деплоев одного проекта.
+
+```txt
+Lock file: {temp_dir}/xd/deploy-.lock
+ → Windows: %TEMP%\xd\deploy-.lock
+ → Unix: /tmp/xd/deploy-.lock
+
+→ При начале деплоя: попробовать создать lock file (create_new(true))
+→ Если lock существует:
+ → Проверить PID процесса из lock
+ → Если процесс жив: abort "Deploy already running (PID {pid})"
+ → Если процесс мёртв (stale lock): удалить lock, продолжить
+→ При завершении (включая error paths): удалить lock через Drop
+```
+
+```rust
+struct DeployLock { path: PathBuf }
+impl Drop for DeployLock { fn drop(&mut self) { fs::remove_file(&self.path).ok(); } }
+```
+
+Важно: lock должен создаваться **до** проверки uncommitted и stash, иначе два разных branch-деплоя одного workdir могут гоняться друг через друга (race).
+
+Известный edge case: PID может быть переиспользован после crash → лучше дополнительно хранить timestamp/hostname или использовать файловый лок (`fs2::File::lock_exclusive`).
+
+---
+
+## F. Прочие edge cases (по мере надобности)
+
+### Detached HEAD
+
+`git rev-parse --abbrev-ref HEAD` в detached HEAD возвращает `HEAD`, а не имя ветки:
+
+```txt
+current_branch == "HEAD" →
+ → Если target_branch задан: OK, используем worktree
+ → Если target_branch не задан: abort с ошибкой
+ "You are in detached HEAD state.
+ Run 'git checkout ' or specify --branch "
+```
+
+### Workdir не существует / является файлом
+
+```txt
+fs::canonicalize(workdir) fails →
+ → Abort: "Project workdir does not exist: {path}"
+
+fs::metadata(workdir) → is_file() == true →
+ → Abort: "Workdir is a file, not a directory: {path}"
+```
+
+### Remote-only ветки
+
+```txt
+branch_exists (local) = false, branch_exists (remote) = true →
+ → git fetch origin
+ → git worktree add --track origin/ (создаст tracking branch)
+ → Если fetch fails: abort "Cannot fetch branch '{branch}' from origin"
+```
+
+### Network offline при remote-only branch
+
+```txt
+git fetch origin → ошибка (network, DNS, timeout) →
+ → Abort: "Cannot fetch branch '{branch}' from origin.
+ Check your network connection or deploy from a local branch."
+```
+
+### Shallow clone
+
+```txt
+git rev-parse --is-shallow-repository → true →
+ → Если target branch != текущая:
+ → git worktree add может упасть (нет полной истории)
+ → Попробовать с --depth 1
+ → Если и это падает:
+ → abort "Shallow clone detected, cannot create worktree for branch '{branch}'.
+ Run 'git fetch --unshallow' first, or deploy from current branch."
+```
+
+### Submodules
+
+```txt
+При создании worktree:
+ → Проверить наличие .gitmodules в workdir
+ → Если есть: после worktree create → git submodule update --init --recursive
+ → Если submodule update падает:
+ → warning "Submodule init failed in worktree, deploy continues"
+ → Если git не установлен: warning, deploy продолжается
+```
+
+### Git LFS
+
+```txt
+Если worktree создана для проекта с LFS:
+ → После checkout worktree: git lfs pull (если .gitattributes существует)
+ → Если git-lfs не установлен: warning, но не abort (deploy продолжается)
+```
+
+### Workdir внутри .xd-worktrees (рекурсия)
+
+```txt
+workdir (или cwd при авто-определении) содержит "/.xd-worktrees/" сегмент →
+ → Abort: "Workdir is inside an .xd-worktrees directory: {path}
+ Deploying from within a worktree is not supported."
+```
+
+Если не проверить → xd может создать worktree внутри уже работающего worktree → вложенность, деплой не туда.
+
+### Sanitization ветки — НЕ НУЖЕН
+
+```txt
+Имена веток и пути worktree НЕ санитайзим вручную:
+ → git сам валидирует имена через `git check-ref-format` (запрещает * ? < > | " : , . .. и т.д.)
+ → `git worktree add ` сам вернёт ошибку при невалидном пути
+ → Читаем фактический путь из вывода `git worktree list --porcelain`, не конструируем сами
+ → Если git вернул ошибку — пробрасываем её as-is (abort с текстом git)
+```
+
+Ветки типа `feature/foo-bar` → worktree path: `.xd-worktrees//feature/foo-bar` — git сам сделает поддиректории.
+
+### Worktree уже существует (остаток)
+
+```txt
+→ Проверить existence через git::worktree_list или fs::metadata
+→ Если существует:
+ → git worktree remove --force (даже если пустой)
+ → Создать заново
+→ Если remove fails (locked, busy):
+ → На Windows: попробовать 2-3 раза с задержкой (файлы могут быть заняты антивирусом)
+ → Если всё ещё fails: abort с инструкцией "закрой процессы в worktree и повтори"
+```
+
+### Git worktree locked
+
+```txt
+git worktree remove --force → exit code != 0, output содержит "locked" →
+ → Abort: "Worktree '{path}' is locked by git.
+ Run 'git worktree unlock {path}' manually, then retry."
+```
+
+### Multiple worktrees одной ветки
+
+```txt
+git worktree list показывает 2+ worktrees для одной target branch →
+ → Abort: "Branch '{branch}' already has {n} worktrees ({paths}).
+ Remove extras with: git worktree remove "
+```
+
+### Stash — существует другой auto-stash
+
+```txt
+При stash (до worktree):
+ → git stash list --grep="xd-auto-stash" — проверить наличие предыдущего auto-stash
+ → Если найден:
+ → abort "Previous auto-stash exists (stash@{N}).
+ Run 'git stash pop stash@{N}' or 'git stash drop stash@{N}' first."
+ → При stash pop: искать stash по message, не по индексу
+```
+
+---
+
+## Сигнатуры git-функций (для справки при будущей реализации)
+
+| Функция | Сигнатура | Описание |
+| ---------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
+| `current_branch` | `(workdir: &Path) -> Result` | `git rev-parse --abbrev-ref HEAD` — вернёт "HEAD" если detached |
+| `is_detached_head` | `(workdir: &Path) -> Result` | `current_branch() == "HEAD"` |
+| `has_uncommitted` | `(workdir: &Path) -> Result` | `git status --porcelain` — не пусто = есть изменения |
+| `branch_exists` | `(workdir: &Path, branch: &str) -> Result` | `git rev-parse --verify `, fallback на `git ls-remote origin ` |
+| `fetch_branch` | `(workdir: &Path, branch: &str) -> Result<()>` | `git fetch origin ` — для remote-only веток |
+| `worktree_list` | `(workdir: &Path) -> Result>` | `git worktree list --porcelain` — возвращает пути worktrees |
+| `create_worktree` | `(workdir: &Path, project: &str, branch: &str) -> Result` | `git worktree add /.xd-worktrees/// `; путь берётся из `worktree list --porcelain` |
+| `remove_worktree` | `(path: &Path) -> Result<()>` | `git worktree remove --force` |
+| `stash` | `(workdir: &Path) -> Result` | `git stash push -m "xd-auto-stash"`, возвращает true если что-то застешилось |
+| `stash_pop` | `(workdir: &Path) -> Result<()>` | `git stash pop` — может вернуть ошибку при конфликте |
+| `is_worktree_clean` | `(path: &Path) -> Result` | Проверяет что worktree пуст (нет untracked/modified файлов) |
+| `is_shallow` | `(workdir: &Path) -> Result` | `git rev-parse --is-shallow-repository` |
+| `worktree_list_branch` | `(workdir: &Path, branch: &str) -> Result>` | Фильтрует worktree_list по ветке |
+| `has_gitmodules` | `(workdir: &Path) -> bool` | fs::exists(workdir.join(".gitmodules")) |
+| `submodule_update` | `(workdir: &Path, worktree: &Path) -> Result<()>` | `git submodule update --init --recursive` в worktree |
+| `has_locked_worktree` | `(output: &str) -> bool` | Проверяет stderr/out на наличие "locked" |
+| `stash_find_xd` | `(workdir: &Path) -> Result>` | `git stash list --grep="xd-auto-stash"` → индекс или None |
+
+---
+
+## Проектирование find-логики (будущее, для справки)
+
+```rust
+/// Все проекты с workdir == cwd
+pub fn find_projects_by_cwd<'a>(cfg: &'a Config, cwd: &Path) -> Vec<&'a str> {
+ let current = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
+ cfg.projects.iter()
+ .filter(|(_, p)| canonicalized(&p.workdir) == current)
+ .map(|(name, _)| name.as_str())
+ .collect()
+}
+
+/// Выбор проекта: 1 проект → он, N проектов → по branch
+pub fn find_project_by_cwd<'a>(
+ cfg: &'a Config,
+ cwd: &Path,
+ cli_branch: Option<&str>,
+) -> Result<&'a str> {
+ let matches = find_projects_by_cwd(cfg, cwd);
+ match matches.len() {
+ 0 => bail!("no project found in {}", cwd.display()),
+ 1 => Ok(matches[0]),
+ _ => {
+ // N проектов — нужна ветка для различения
+ let target = cli_branch.or_else(|| {
+ git::current_branch(cwd).ok().as_deref()
+ });
+ match target {
+ Some(branch) => {
+ let found = matches.iter().find(|name| {
+ cfg.projects[*name].branch.as_deref() == Some(branch)
+ });
+ match found {
+ Some(name) => Ok(name),
+ None => bail!(
+ "no project with branch '{branch}' in {}. Available: {}",
+ cwd.display(),
+ matches.iter()
+ .filter_map(|n| cfg.projects[*n].branch.as_deref())
+ .collect::>()
+ .join(", ")
+ ),
+ }
+ }
+ None => bail!(
+ "multiple projects in {} — specify --project or --branch. Found: {}",
+ cwd.display(),
+ matches.join(", ")
+ ),
+ }
+ }
+ }
+}
+```
+
+---
+
+## CLI-флаги (будущее, для справки)
+
+```rust
+#[derive(Args, Debug)]
+pub struct DeployArgs {
+ pub project_name: Option,
+
+ /// Override target branch (uses worktree if not on this branch)
+ #[arg(short, long)]
+ pub branch: Option,
+
+ /// Auto-stash uncommitted changes instead of aborting
+ #[arg(long)]
+ pub auto_stash: bool,
+
+ /// Keep worktree after deploy (for debugging)
+ #[arg(long)]
+ pub keep_worktree: bool,
+}
+```
+
+---
+
+## Тесты (будущее, для справки)
+
+### `src/git.rs`
+
+- `current_branch` возвращает имя ветки
+- `current_branch` возвращает "HEAD" при detached HEAD
+- `has_uncommitted` определяет чистый/грязный repo
+- `branch_exists` находит существующую ветку
+- `branch_exists` не находит несуществующую ветку
+- `branch_exists` находит remote-only ветку
+- `fetch_branch` скачивает remote ветку
+- `worktree_list` возвращает список worktrees
+- `create_worktree` создаёт и возвращает путь
+- `create_worktree` создаёт поддиректории для веток с `/`
+- `remove_worktree` удаляет worktree
+- `is_worktree_clean` определяет чистый/грязный worktree
+- `create_worktree` создаёт worktree вне репо (в .xd-worktrees рядом)
+- `is_shallow` определяет shallow/non-shallow clone
+- `worktree_list_branch` фильтрует worktrees по ветке
+- `stash_find_xd` находит существующий xd-auto-stash
+- `stash_find_xd` не находит если нет auto-stash
+- `has_locked_worktree` определяет locked текст
+- `submodule_update` инициализирует submodules в worktree
+
+### `src/config.rs`
+
+- `find_projects_by_cwd` находит все проекты с одинаковым workdir
+- `find_project_by_cwd` с 1 проектом — возвращает его
+- `find_project_by_cwd` с N проектами + branch — находит нужный
+- `find_project_by_cwd` с N проектами без branch — ошибка
+- `find_project_by_cwd` с несуществующим workdir — abort
+- Парсинг конфига с `branch` полем
+
+### `src/deploy.rs`
+
+- Dry-run показывает worktree шаги
+- Dry-run показывает "worktree exists, will be recreated"
+- Deploy с worktree создаёт/удаляет worktree
+- Deploy без worktree (текущая ветка) — напрямую из workdir
+- Auto-stash при uncommitted + --auto-stash
+- Abort при uncommitted без --auto-stash
+- Worktree guard удаляет worktree при ошибке
+- Worktree guard НЕ удаляет worktree при --keep-worktree
+- Stash pop при конфликте — stash остаётся, выводится инструкция
+- Deploy lock блокирует повторный deploy
+- Deploy lock удаляется после завершения
+- Abort при shallow clone + другая ветка
+- Warning при submodules + submodule update fails
+- Abort при workdir = файл
+- Abort при workdir внутри .xd-worktrees
+- Abort при non-valid имени ветки (ошибка пробрасывается от git as-is)
+- Abort при существующем auto-stash в stash list
+- Abort при locked worktree
+- Предупреждение при CLI --branch переопределяет project.branch
+- Abort при detached HEAD + uncommitted (даже с --auto-stash)
+- Abort при multiple worktrees одной ветки
+- Worktree создаётся вне репо (repo/../.xd-worktrees//)