588 lines
30 KiB
Markdown
588 lines
30 KiB
Markdown
# 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-деплоев (вне репо)
|
||
<parent-dir>/ ← имя папки, в которой лежит репо (изоляция от одноимённых репо в разных местах)
|
||
<project-name>/ ← изоляция разных проектов из одного workdir
|
||
staging/ ← worktree для ветки staging
|
||
main/ ← worktree для ветки main
|
||
```
|
||
|
||
Правила:
|
||
|
||
- Worktree создаётся как `<repo/.git/../..>/.xd-worktrees/<parent-dir>/<project-name>/<branch>`
|
||
- Итоговый путь запрашиваем у git / берём из `worktree list --porcelain`, не строим вручную из имени ветки
|
||
- `.gitignore` не нужен — main worktree всегда чистый, статус/CI/линтеры не видят мусор
|
||
- Если удалить репо — worktree-ветки остаются отдельно и не ломают ничего
|
||
|
||
### Cleanup при ошибках (WorktreeGuard)
|
||
|
||
Rust не имеет `finally`. Используем RAII — структуру `WorktreeGuard`, которая удаляет worktree в `Drop`:
|
||
|
||
```rust
|
||
struct WorktreeGuard {
|
||
path: Option<PathBuf>,
|
||
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-<project-name>.lock
|
||
→ Windows: %TEMP%\xd\deploy-<project-name>.lock
|
||
→ Unix: /tmp/xd/deploy-<project-name>.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 <branch>' or specify --branch <name>"
|
||
```
|
||
|
||
### 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 <branch>
|
||
→ git worktree add --track origin/<branch> (создаст tracking branch)
|
||
→ Если fetch fails: abort "Cannot fetch branch '{branch}' from origin"
|
||
```
|
||
|
||
### Network offline при remote-only branch
|
||
|
||
```txt
|
||
git fetch origin <branch> → ошибка (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 <path>` сам вернёт ошибку при невалидном пути
|
||
→ Читаем фактический путь из вывода `git worktree list --porcelain`, не конструируем сами
|
||
→ Если git вернул ошибку — пробрасываем её as-is (abort с текстом git)
|
||
```
|
||
|
||
Ветки типа `feature/foo-bar` → worktree path: `.xd-worktrees/<project>/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 <path>"
|
||
```
|
||
|
||
### 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<String>` | `git rev-parse --abbrev-ref HEAD` — вернёт "HEAD" если detached |
|
||
| `is_detached_head` | `(workdir: &Path) -> Result<bool>` | `current_branch() == "HEAD"` |
|
||
| `has_uncommitted` | `(workdir: &Path) -> Result<bool>` | `git status --porcelain` — не пусто = есть изменения |
|
||
| `branch_exists` | `(workdir: &Path, branch: &str) -> Result<bool>` | `git rev-parse --verify <branch>`, fallback на `git ls-remote origin <branch>` |
|
||
| `fetch_branch` | `(workdir: &Path, branch: &str) -> Result<()>` | `git fetch origin <branch>` — для remote-only веток |
|
||
| `worktree_list` | `(workdir: &Path) -> Result<Vec<PathBuf>>` | `git worktree list --porcelain` — возвращает пути worktrees |
|
||
| `create_worktree` | `(workdir: &Path, project: &str, branch: &str) -> Result<PathBuf>` | `git worktree add <repo_parent>/.xd-worktrees/<parent-dir>/<project>/<branch> <branch>`; путь берётся из `worktree list --porcelain` |
|
||
| `remove_worktree` | `(path: &Path) -> Result<()>` | `git worktree remove <path> --force` |
|
||
| `stash` | `(workdir: &Path) -> Result<bool>` | `git stash push -m "xd-auto-stash"`, возвращает true если что-то застешилось |
|
||
| `stash_pop` | `(workdir: &Path) -> Result<()>` | `git stash pop` — может вернуть ошибку при конфликте |
|
||
| `is_worktree_clean` | `(path: &Path) -> Result<bool>` | Проверяет что worktree пуст (нет untracked/modified файлов) |
|
||
| `is_shallow` | `(workdir: &Path) -> Result<bool>` | `git rev-parse --is-shallow-repository` |
|
||
| `worktree_list_branch` | `(workdir: &Path, branch: &str) -> Result<Vec<PathBuf>>` | Фильтрует 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<Option<usize>>` | `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::<Vec<_>>()
|
||
.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<String>,
|
||
|
||
/// Override target branch (uses worktree if not on this branch)
|
||
#[arg(short, long)]
|
||
pub branch: Option<String>,
|
||
|
||
/// 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/<project>/<branch>)
|