Знакомая ситуация. Ты по локоть в фиче: десяток правок, половина в рабочем дереве, половина в индексе, тесты наполовину переписаны, ничего пока не коммитится потому что состояние сырое. И тут прилетает: на проде упал платёжный модуль, чини сейчас же, ветка hotfix/payments от main. Что делаешь? Классика - git stash или мусорный коммит "wip", потом git switch hotfix, фикс, потом обратно и git stash pop с молитвой что ничего не конфликтнёт. А ещё IDE переиндексирует весь проект дважды, контейнеры пересоберутся, кеш сборки протухнет. Полчаса возни вокруг пятиминутного фикса.
Второй вариант, который многие используют годами - сделать второй git clone в соседнюю папку. Работает, но это отдельная копия: своя история, свои объекты, свой .git на гигабайт. Чтобы стянуть из неё свежие коммиты обратно, надо настраивать remote на локальную папку или гонять через сервер. И диск тает.
git worktree - это третий путь, и он почти всегда правильный. Команда даёт тебе второй (третий, пятый) рабочий каталог, привязанный к тому же самому .git. Один репозиторий - несколько рабочих деревьев. В каждом дереве своя ветка, свой HEAD, свой индекс, свои незакоммиченные правки. А вот хранилище объектов (.git/objects), refs, конфиг, стек reflog и удалённые ветки - общие. Получается параллельная работа git без второго клона и без stash-плясок: текущую фичу не трогаешь вообще, она спокойно лежит в своём дереве, а hotfix чинишь в соседнем. Рабочее дерево git перестаёт быть единственным.

Как это устроено внутри: общий object store и линкованные деревья
Чтобы понимать грабли, надо понимать механику. Когда ты делаешь git worktree add, Git не копирует репозиторий. Он создаёт новую папку с файлами проекта и кладёт туда не каталог .git, а файл .git - одну строчку-указатель.
Код: Выделить всё
$ git worktree add ../hotfix hotfix/payments
Preparing worktree (checking out 'hotfix/payments')
HEAD is now at 9c4f1ab Fix invoice rounding
$ cat ../hotfix/.git
gitdir: /home/dev/project/.git/worktrees/hotfix
Код: Выделить всё
$ ls /home/dev/project/.git/worktrees/hotfix
HEAD commondir gitdir index ORIG_HEAD logs
$ cat /home/dev/project/.git/worktrees/hotfix/commondir
../..
$ cat /home/dev/project/.git/worktrees/hotfix/gitdir
/home/dev/project/hotfix/.git
Можно проверить руками, что refs общие, а HEAD - нет:
Код: Выделить всё
$ git rev-parse --git-path HEAD
/home/dev/project/.git/worktrees/hotfix/HEAD
$ git rev-parse --git-path refs/heads/main
/home/dev/project/.git/refs/heads/main
Практика: git worktree add и реальный hotfix без потери прогресса
Разберём тот самый сценарий по шагам. Ты в основном дереве, в ветке feature/cart, куча несохранённого. Прилетает баг. Не коммитим, не стэшим - просто отращиваем второе дерево:
Код: Выделить всё
$ git status -s
M src/cart/Basket.php
A src/cart/Promo.php
?? notes-todo.txt
$ git worktree add -b hotfix/payments ../proj-hotfix origin/main
Preparing worktree (new branch 'hotfix/payments')
branch 'hotfix/payments' set up to track 'origin/main'.
HEAD is now at 9c4f1ab Fix invoice rounding
Код: Выделить всё
$ cd ../proj-hotfix
$ git switch -c ... # уже не нужно, мы на hotfix/payments
$ git status -s
$ # правишь src/payment/Gateway.php
$ git commit -am "hotfix: retry failed charge on gateway timeout"
$ git push -u origin hotfix/payments
Код: Выделить всё
$ cd ../project
$ git status -s
M src/cart/Basket.php
A src/cart/Promo.php
?? notes-todo.txt
Код: Выделить всё
$ git worktree remove ../proj-hotfix
Смотрим, что у нас понаращено - git worktree list:
Код: Выделить всё
$ git worktree list
/home/dev/project a17be20 [feature/cart]
/home/dev/project-review 4f9c1de [pr-481]
/home/dev/project-release 88a0c12 (detached HEAD)
Полезные сценарии параллельной работы git
worktree расцветает там, где надо держать несколько веток одновременно git и не терять контекст.
- Hotfix без потери прогресса - разобрали выше, главный кейс.
- Ревью PR в отдельном дереве. Коллега прислал PR, надо реально запустить его ветку, а не читать диф в браузере. git worktree add ../review pr-481 - и у тебя живой каталог с его кодом, можно гонять тесты, кликать руками, а твоя работа стоит нетронутой в основном дереве.
- Долгий тест или сборка. Прогон интеграционных тестов на 40 минут запускаешь в отдельном дереве на нужной ветке - и продолжаешь кодить в основном, не дожидаясь. Два процесса не дерутся за одни и те же файлы.
- Сборка релиза по тегу. git worktree add --detach ../release v3.2.0 даёт чистое дерево ровно на теге, в detached HEAD, без создания мусорной ветки. Собрал артефакт - снёс дерево.
- Сравнение двух версий бок о бок. Старая и новая ветка открыты в двух окнах редактора одновременно - удобно для рефакторинга и портирования.
Управление: list, lock, move, prune
lock - страховка от случайного prune. Если дерево лежит на съёмном диске или сетевой шаре, которая не всегда смонтирована, Git может посчитать его "пропавшим" и вычистить служебную запись. Запираем:
Код: Выделить всё
$ git worktree lock --reason "USB-диск, монтирую по средам" ../proj-usb
$ git worktree unlock ../proj-usb
move - переехать каталогом правильно. Не двигай дерево через mv: сломаешь обратную ссылку gitdir. Делай:
Код: Выделить всё
$ git worktree move ../proj-hotfix ../archive/proj-hotfix
prune - сборка мусора по деревьям. Если ты всё-таки снёс каталог дерева через rm -rf, в .git/worktrees останется висячая запись. Чистим:
Код: Выделить всё
$ git worktree prune --dry-run --verbose
Removing worktrees/proj-hotfix: gitdir file points to non-existent location
$ git worktree prune
Типичные грабли
- Одну ветку нельзя открыть в двух деревьях. Это главное ограничение. Git защищает тебя от двух деревьев с одной веткой - иначе два рабочих процесса дёргали бы один указатель ветки и поймали бы рассинхрон.
Решения: открой другую ветку, или заведи новую (-b), или возьми detached HEAD на тот же коммит через --detach, если ветка не нужна.
Код: Выделить всё
$ git worktree add ../second feature/cart fatal: 'feature/cart' is already checked out at '/home/dev/project' - rm -rf вместо remove оставляет висячую запись. Лечится prune, но лучше всегда remove.
- mv вместо move рвёт ссылку gitdir, дерево "теряется". Лечится repair или move заранее.
- Один stash на все деревья - это миф. reflog общий, но stash в обычном виде применяется к текущему дереву; не рассчитывай ловить состояние одного дерева в другом через stash.
- Submodule и ignored-файлы не переезжают сами. Новое дерево чистое: node_modules, .env, vendor - всё надо поставить заново. Это не баг, это отдельный каталог.
- IDE и build-кеш. Каждое дерево - отдельный проект для IDE; индексируется отдельно. Это и плюс (параллельность), и минус (память).
- Создай песочницу и пару коммитов:
Код: Выделить всё
git init -b main wt-lab && cd wt-lab echo v1 > app.txt && git add app.txt && git commit -m "init" git switch -c feature && echo work > app.txt && git commit -am "feature wip" - Не коммить остальное - сэмулируй "сырое" состояние: echo dirty >> app.txt (не коммить).
- Отрасти hotfix-дерево от main, не трогая feature:
Код: Выделить всё
git worktree add -b hotfix ../wt-hotfix main - Зайди в ../wt-hotfix, сделай фикс и коммит. Вернись - убедись, что dirty в app.txt на месте (git status -s).
- Посмотри устройство: cat ../wt-hotfix/.git и ls .git/worktrees/hotfix. Найди commondir и gitdir.
- Попробуй открыть main вторым деревом - поймай fatal. Затем сделай git worktree list, git worktree remove ../wt-hotfix, git worktree prune --dry-run.
- Что лежит в файле .git линкованного дерева и куда он указывает?
- Какие части состояния у деревьев свои, а какие общие? Где физически хранятся объекты коммита, сделанного в линкованном дереве?
- Почему Git запрещает открыть одну ветку в двух деревьях и как это обойти для сборки по коммиту?
- Чем remove отличается от rm -rf и зачем тогда нужен prune?
git worktree add даёт второе рабочее дерево на тот же .git: общий object store, отдельные HEAD и индекс, мгновенный обмен коммитами без второго клона и без stash. Это штатный инструмент для hotfix без потери прогресса, ревью PR, долгих тестов и сборки релизов. Помни одно жёсткое правило (одна ветка - одно дерево) и убирай за собой через remove, lock, move и prune, а не через сырые mv и rm. Освоишь - и переключения веток ради "быстро глянуть" уйдут из твоей рутины почти полностью.