У каждого разработчика была такая пятница: посреди большого feature-простыни на сорок файлов прилетает срочный баг с прода, и рабочая копия разукрашена наполовину написанным кодом, который коммитить рано, а терять жалко. Классический ответ - git stash, переключение, фикс, возврат, pop и надежда на то, что конфликты при восстановлении не съедят вечер. Git worktree предлагает другой сценарий: рядом с основным каталогом проекта появляется второй, третий, хоть пятый рабочий каталог с собственным состоянием файлов, и каждый смотрит на свою ветку. Ничего не откладывается, ничего не переключается, hotfix живёт в отдельной папке, пока feature спокойно дозревает в основной.
Что такое worktree и зачем он когда stash уже надоел
Механизм простой. Один репозиторий, одно хранилище объектов, один .git, но несколько рабочих деревьев, то есть несколько каталогов с честными файлами на диске. Каждое дерево привязано к своей ветке и имеет собственный индекс, собственный HEAD и собственные незакоммиченные изменения. Переключение контекста сводится к cd в соседний каталог, а не к акробатике со stash.
Разница со stash принципиальная. Stash прячет изменения внутрь одного дерева, и пока они спрятаны, их не видно ни в редакторе, ни в тестах, ни в запущенном приложении. Worktree держит оба состояния живыми одновременно: в одном окне терминала идёт сборка feature-ветки, в другом правится hotfix, и они никак не мешают друг другу, потому что файлы физически разные. По сути это дешёвый способ иметь несколько чекнутых версий проекта без полного клонирования репозитория, которое для большого проекта означает минуты копирования и гигабайты истории вдвойне.
Создание дополнительного дерева и разбор вывода worktree list
Основная команда создания выглядит так:
git worktree add ../project-hotfix hotfix-payment
Первый аргумент - путь нового каталога, принято складывать такие каталоги рядом с основным проектом, чтобы не путаться. Второй - ветка, которая туда будет помещена; если ветки не существует, добавляют флаг -b, и ветка создастся от текущего HEAD. Git отвечает коротко: Preparing worktree (checking out 'hotfix-payment') и HEAD is now at abc1234, после чего в ../project-hotfix лежит полноценная рабочая копия.
Проверить, что появилось, помогает git worktree list:
/home/dev/project abc1234 [main]
/home/dev/project-hotfix def5678 [hotfix-payment]
Колонки читаются прямо: путь дерева, SHA коммита, на котором оно стоит, и ветка в квадратных скобках. Если дерево находится в отсоединённом состоянии HEAD, - например, его создавали по тегу, - в скобках вместо ветки будет написано detached. С флагом --porcelain та же команда выдаёт машиночитаемый формат, удобный для скриптов: пары worktree / HEAD / branch по одной на строку, и по ним легко строить автоматическую уборку.
Небольшое, но ценное наблюдение из практики: worktree неожиданно хорошо дисциплинирует сам по себе. Когда каждый контекст - это видимый каталог на диске с понятным именем, количество одновременно ведущихся задач становится физически осязаемым, и лишние ветки закрываются быстрее, просто потому что они буквально занимают место перед глазами.
Устройство внутри файла git и общего хранилища объектов
Заглянув в дополнительное дерево, любопытный обнаружит вместо каталога .git обычный текстовый файл с тем же именем:
cat ../project-hotfix/.git
# gitdir: /home/dev/project/.git/worktrees/project-hotfix
Это указатель. Настоящие служебные данные дерева лежат внутри основного репозитория, в подкаталоге .git/worktrees/<имя>: там собственные HEAD, index, ORIG_HEAD и прочая механика. А вот объекты, refs и вся история общие. Отсюда следуют два приятных последствия и одно важное предостережение.
Приятно первое: создание дерева стоит копейки, потому что история не дублируется, на диск пишутся только файлы рабочей копии. Приятно второе: коммит, сделанный в дополнительном дереве, мгновенно виден в основном через git log, потому что refs общие. Предостережение касается переноса на внешний диск: каталог с файлом-указателем нельзя просто скопировать на флешку отдельно, путь gitdir туда абсолютный, и после смены точки монтирования дерево придётся починить через git worktree repair, передав ему новые пути.
Реальные сценарии hotfix ревью и параллельная сборка
Отдельного абзаца заслуживает перемещение готового дерева. Каталог рабочей копии законно хочется перетащить в другое место диска, например с системного раздела на большой data-том. Ручной mv сломает связность, потому что служебный каталог в .git/worktrees хранит путь к дереву. Правильный инструмент - git worktree move ../project-hotfix /mnt/data/project-hotfix: команда переносит каталог и поправляет ссылки с обеих сторон. Если же каталог уже перенесён руками и git ругается на битую ссылку, спасает упомянутый выше git worktree repair с новым путём, и вся связь восстанавливается без потери состояния.
Сценарий срочного исправления выглядит в боевой рутине так:
- git worktree add ../project-hotfix -b hotfix-payment main создаёт дерево и ветку от стабильного main;
- в новом каталоге правится код, запускаются тесты, делается коммит и push;
- пока идёт ревью, основное дерево продолжает пахать над feature без единого прерывания;
- после слияния исправления ветка удаляется, а дерево убирается через git worktree remove ../project-hotfix.
Второй сценарий - ревью чужой ветки. Вместо переключения своего рабочего состояния туда-сюда создаётся временное дерево на ветку коллеги: git worktree add ../review-igor origin/igor-feature --track не нужен, достаточно git worktree add ../review-igor igor-feature, и код смотрится вживую, со сборкой и запуском. Третий сценарий любят те, кто гоняет долгие компиляции: два дерева с двумя версиями собираются параллельно, и сравнение артефактов становится механическим diff результатов, а не марафоном переключений.
Ограничение одна ветка одно дерево и работа блокировок
Git ревниво следит за тем, чтобы одна и та же ветка не была выписана в двух деревьях сразу. Попытка сделать git worktree add ../second-copy main при уже занятой main обрывается ошибкой fatal: 'main' is already checked out at '/home/dev/project'. Логика защитная: два дерева с одной веткой означали бы двойное владение ref-ом, и гонки при коммитах были бы неизбежны.
Изредка обход всё же нужен, и для этого есть ключ --force, но пользоваться им стоит осознанно, понимая, что оба дерева будут писать в одну ветку. Аккуратная альтернатива - git worktree add с флагом --detach, тогда второе дерево получает голый HEAD без ветки, и ничьи права не страдают.
Отдельный инструмент - git worktree lock. Он вешает замок на дерево целиком, обычно с пояснением: git worktree lock --reason "на внешнем диске, не трогать до понедельника" ../project-hotfix. Замок в первую очередь защищает от автоматической уборки: запертое дерево не удалится ни prune, ни руками через remove, пока lock не снят командой unlock. Для деревьев, лежащих на сетевых и съёмных носителях, это обязательная дисциплина, потому что иначе одна неудачная неделя отпуска превращает долгоживущее дерево в бесхозный мусор.
Есть и менее очевидный бонус общего хранилища: bisect. Длинная бинарная прогонка по истории в поисках сломавшего коммита обычно блокирует рабочую копию надолго, а с отдельным деревом вся процедура уходит в фон: git worktree add ../bisect-run -b bisect-tmp, затем в новом каталоге классический цикл bisect start, good, bad, а основная копия продолжает служить текущей работе. Когда виновник найден, временное дерево удаляется одной командой, и рабочий индекс за всю историю расследования не испытал ни одного переключения.
Стоит упомянуть и настройку конфигурации per-worktree. По умолчанию config общий для всех деревьев, что логично: пользователь и пульты одни на всех. Но если включить extensions.worktreeConfig командой git config extensions.worktreeConfig true, часть настроек начинает жить в файле config.worktree конкретного дерева. Это удобно для локальных экспериментов: например, в отладочном дереве можно поднять детальность логов сборки или сменить core.hooksPath на каталог с более строгими хуками, не трогая основную копию. Возможность редко используется, а зря: она решает класс ситуаций, где эксперимент требует не только другого кода, но и другого поведения инструментария.
Подводные камни node_modules сборочные кэши и индексы IDE
Главная ловушка новичка - забыть, что дерево копирует только версионируемые файлы. Всё, что лежит в .gitignore, в новое дерево не попадает: ни vendor, ни node_modules, ни target, ни виртуальные окружения. Открываешь свежий каталог, запускаешь приложение, и оно падает с грустью про отсутствующие зависимости. Лечение очевидное, но рутинное: npm install, pip install или composer install приходится выполнять в каждом дереве заново, и на больших проектах это минуты. Некоторые команды автоматизируют это коротким скриптом, который после worktree add сам дёргает установщик пакетов.
Вторая ловушка - сборочные кэши и индексы. Каждое дерево со своим выделенным кэшем артефактов дублирует гигабайты, а с общим кэшем по абсолютным путям внезапно получаются чужие объектники под своими исходниками. Правило здесь простое: пути кэшей должны зависеть от расположения дерева, и большинство современных систем сборки так и поступают по умолчанию. IDE тоже добавляют шума: каждое дерево воспринимается редактором как отдельный проект со своим индексом, и горячие клавиши навигации прыгают только внутри открытого каталога. После пары недель жизни с worktree вырабатывается привычка держать в редакторе ровно те окна, чьи деревья сейчас в работе.
Уборка за собой remove prune и профилактика забытых деревьев
Деревья имеют свойство множиться незаметно. Сегодня hotfix, завтра ревью, послезавтра эксперимент, и через месяц git worktree list напоминает кладбище каталогов, о назначении которых остались смутные воспоминания. Порядок наводят двумя командами.
git worktree remove ../project-hotfix удаляет дерево с диска, но сначала проверяет, что в нём нет незакоммиченных изменений и неотправленных наверх коммитов. Если мусор есть, команда откажется, и правильно сделает: -f сносит всё без вопросов, и этот флаг лучше набирать только тогда, когда содержимое точно не нужно. Вторая команда, git worktree prune, занимается осиротевшими записями: если каталог стёрли руками через rm -rf, в .git/worktrees остаются служебные записи о несуществующем дереве, и prune вычищает их. В коллективной практике хорошо работает банальная договорённость: дерево считается временным с момента создания, к имени каталога прибавляется дата или номер задачи, а раз в неделю список деревьев сверяется с реально живыми задачами. Является ли это бюрократией - вопрос спорный, но именно эта мелочная дисциплина отличает команду, которая пользуется worktree годами, от команды, которая один раз нашла диск, забитый сорока забытыми копиями проекта, и вернулась к stash.
Мониторить забытые деревья помогает простая арифметика: git worktree list выводится рядом с git branch --sort=-committerdate, и ветки, у которых деревья старше пары недель, попадают в список на вынос. Некоторые команды вешают эту связку в weekly-скрипт, который рассылает виновникам вежливые напоминания. Звучит настороженно, зато диск сервера сборок перестаёт расти как на дрожжах, а hotfix-каталоги перестают переживать свои ветки на месяцы.
Финальная мысль проста. Worktree не заменяет stash полностью: отложить на десять минут крошечную правку по-прежнему удобнее классикой. Но всё, что живёт дольше перекурса, - hotfix, ревью, сборка второй версии - заслуживает отдельного каталога. Один раз привыкнув, что контексты разложены по папкам, а не по слоям стэша отложенных изменений, назад возвращаться не хочется совсем.