Статей на тему много, но, видимо, недостаточно: время от времени слышу от коллег (последние 10 лет, в 4-х разных компаниях):
«Не могу пошарить экран с кодом, у меня другая ветка сейчас».
«Не хочу переключать ветку, придется запускать кодогенерацию, у меня сбросятся build-файлы, потом это опять пересобирать!»
«Стаскивать ветку для просмотра ПР? Это же неудобно, надо "стэшить" изменения, ветку переключать».
«А я “склонировал“ 3 копии проекта, `git clone` to the rescue!»
Что-то из вышеперечисленного я не слышал, но добавил для драматизма. Если по какой-то из фраз вы узнали себя, предлагаю ознакомиться со статьей, может быть, найдете что-то полезное.
Почему "древесные лягушки"? Всего лишь совпадение по слову “Tree“ в “Tree frogs“ и git worktree
, о котором пойдет речь.
И последнее, если и так знаете про git worktree
, предлагаю сразу перейти к разделу "Мой вариант использования git worktree".
О проблемах и “неправильных“ решениях
Во введении к статье было уже все сказано, но для формализма распишу еще раз.
Проблемы:
Потеря текущих изменений кода при смене ветки;
Потеря временных файлов кодогенерации/компиляции при смене ветки.
Решения:
Временный коммит и смена ветки. Решает проблему 1;
git stash
и смена ветки. Решает проблему 1, но можно потеряться в стешах, если не давать им имена;git clone
проекта в другую папку. Решает 1 и 2;git worktree
проекта в другую папку. Решает 1 и 2.
Решение 3 содержит новые проблемы. Придется в каждом клоне проекта дублировать .git файлы — держать каждый проект в актуальном состоянии, вызывать git fetch
для каждого, и т.д.
Решение 4 — то, которым я пользуюсь, и статья, по-существу, об этом.
Подробнее о “неправильных“ решениях
Оба решения (1 и 2 из раздела выше) подразумевают смену веток. Это может быть git checkout
или git switch
— попался коммент, рассказывающий о разнице подробнее, не хочу дублироваться.
Называю решения “неправильными“, имея в виду, что есть решение лучше. Лучше тем, что позволяет не терять файлы кодогенерации, кеши и прочие оптимизации систем сборки проекта. Слово “неправильные“ беру в кавычки, потому что не всегда все однозначно, и иногда `git stash` — лучшее решение, об этом будет ниже.
Решение с временным коммитом
Удобно делать коммит, а не stash
, чтобы случайно не потерять изменения. У меня были случаи, когда вместо git stash pop
вызывал git stash drop.
Не смертельно, но повозиться с reflog
придется.
Сперва коммитим все изменения
> git add -A && git commit -m 'tmp commit'
Затем переключаемся на другую ветку с git checkout <branch name>
. Когда вернемся на изначальную ветку, нам может быть интересно, какие изменения были в 'tmp commit'.
> git show
Далее можем сделать "uncommit" командой git reset HEAD~1 --soft
, либо добавить изменения в имеющийся коммит, изменив ему имя:
> git add -A
> git commit -m 'Fix all the release bugs, but introduce more' --amend
Решение с `git stash`
Все то же самое, только вместо коммита используем stash, который специально предназначен для хранения временных изменений.
Пример использования stash без имени с удалением из стека:
> git stash
# переключается на другую ветку
> git checkout some-branch
# делаем что-то, что нужно в some-branch, и возвращаемся обратно
> git checkout -
# возвращаем то, что у нас было
> git stash pop
Пример с использованием имени в stash, поиск по имени, применением без удаления из стека (может быть нужно, чтобы не запутаться, когда пользуемся stash часто):
# создаем стеш с именем
> git stash push -m "trying to make something work"
# переключается на другую ветку
> git switch some-branch
# делаем что-то, что нужно в some-branch, и возвращаемся обратно
> git switch -
# смотрим стек стешей, копируем нужный
> git stash list
# смотрим его содержание, чтобы убедиться, что этот тот самый
> git show stash@{0}
# применяем stash, не удаляя его из стека
> git stash apply
Когда “неправильные“ решения — правильные?
Если вы работаете один, большую часть времени в одной ветке, сами делаете релизные ветки, редко между ними переключаетесь, то оба решения прекрасно подходят.
Если у в проекте нет кодогенерации (или в конкретном случае она не понадобится) или чистая сборка проекта занимает считанные секунды, — оба решения также хороши.
Иногда временные коммиты удобно запушить, чтобы точно ничего не потерять или позволить себе или кому-то другому продолжить работать с другой машины.
Если же кодогенерация занимает несколько минут, а вам надо активно делать коммиты в разные ветки, для которых каждый раз нужен clean build, то удобно использовать git worktree
.
О git worktree
Команда простая, как-то услышал о ней от коллег, прочёл пример в доках, как разработчик быстро переключается, ничего в своей ветке не меняя, фиксит проблему, удаляет копию и возвращается в изначальную папку с проектом — и с тех пор пользуюсь каждый день (немного по-другому, но об этом позже).
— Что делает команда?
— Создает копию проекта. Копия смотрит на указанную ветку. Ветку можно поменять.
— Чем git worktree
отличается от того, чтобы вызывать git clone
с другим именем папки?
—git worktree
позволяет централизованно управлять репозиторием. Простыми словами: достаточно вызывать git fetch
в любой папке, чтобы обновления были видны во всех.
Пример использования:
# переходим в папку с проектом
> cd ~/project
# создаем 2 копии для двух релизных веток
> git worktree add ../release1 release-branch1
> git worktree add ../release2 release-branch2
# проверяем, что все создалось
> git worktree list
~/project d5e92f1 [master]
~/release1 9d77097 [release-branch1]
~/release2 8b2f312 [release-branch2]
Теперь в папках будут лежать копии проекта с соответствующей веткой.
Ок, а что насчет
git clone --reference <project path> --dissociate
?Вкратце: с
git clone --reference
проще выстрелить себе в ногу, т.к. основной проект не знает о том, что какой-то клон меняет его файлы. Написана статья и про другие проблемы: git clone --reference Considered Harmful.
Мой вариант использования git worktree
О проблемах уже писал выше, поэтому спрячу.
Проблемы на текущем проекте, которые решаю с `git worktree`
На проекте часто приходится работать с тремя ветками, для каждой из которых нужна кодогенерация. Если сменить release-branch1 на release-branch2 или master, то нужно запустить clean build, который сломается с какой-то вероятностью, и нужно будет руками удалять build-папки или править что-то еще. Если не сломается, все равно придется ждать минут 5.
Кроме релизных есть ветки, где работаю над задачами, которые могут “черипикаться“ в другие ветки, несовместимые по файлам кодогенерации. Если не запускать кодогенерацию, IntellijIDEA подсветит часть файлов проекта красным. Иногда ничего страшно, да и тесты все равно пройдут на CI, но бывает, что нужно это запускать и дебажить.
Иногда хочется посмотреть ветку ПР локально и даже запустить, потому что так эффективнее и надежнее (IMHO). Опять же, не хочется тратить время на временные коммиты и потерю файлов кодогенерации.
Я всегда держу несколько папок с проектом по принадлежности к релизным веткам:
master
(основная ветка, новые релизы у нас отводятся от нее);release3
(новая релизная ветка — следующий релиз);release2
(предыдущая релизная ветка — релиз в процессе);release1
(самый старенький, удаляю его после того, какрелиз2
“зарелизится“);master
копия (в мастер всегда больше всего PR-ов, поэтому удобно иметь клон).
В текущем процессе, которого придерживается команда, любой ПР может быть в одну из вышеуказанных веток, поэтому когда мне нужно посмотреть что-то локально или сделать ПР самому, я открываю проект в папке с соответствующей веткой и начинаю работать оттуда. Это позволяет не терять время на кодогенерации.
Пример
Делаю свою задачу в мастер, у меня открыт проект в папке master. Просят быстро сделать хотфикс в release2. Открываю проект в папке release2 и создаю там ветку: git checkout -b hotfix release2
. Можно будет сразу запустить проект, минуя clean build. Не нужно суетиться, пряча свои текущие изменения в stash.
В случаях, когда нужно скакать между двумя ветками, которые относятся к одному релизу, могу временно создать еще один git-worktree:
> git worktree add -b release1-2 ../release1-2 release-branch1
# сделать и запушить нужный мне фикс, а когда буду уверен, что папка больше не нужна,
# удалить папку, чтобы не копить мусор
> git worktree remove ../release1-2
Либо сделать обычный git stash
и переключиться тут же. Последний предпочитаю, когда действие разовое, а git worktree
— когда понятно, что ветка будет использоваться несколько раз, например, при релизе хотфикса. Но повторюсь, главное — не тратить время на кодогенерацию и прочие проблемы, возникающие при смене далеких друг от друга веток.
Проблемы при использовании git worktree
В общем-то, проблем никаких нет
Но могут быть мелкие неудобства:
Лишнее место на диске.
Нельзя "зачекаутить" одну и ту же ветку в двух
worktree
.Нельзя удалить ветку, если на нее смотрит какой-то из
worktree
. Гит об этом скажет, и тут просто надо удалить этотworktree
(git worktree remove <path>
).Могут быть проблемы в функционале недоделанных инструментов. Например, я когда-то отказывался от neovim-плагина neogit, потому что были баги в
worktree
(Github Issues: 1, 2). В комментарии к статье писали о проблемах с Eclipse и vscode devcontainers.Если используете `git submodule`, то в каждой папке
worktree
придется обновлять их отдельно. Обходные пути есть.
Если знаете о других проблемах, напишите в комментариях или в личку, дополню статью.
В заключение
Попробуйте включить git worktree
в рабочий процесс. Может быть, сэкономите кучу времени и нервов, особенно, если проект подразумевает работу с множеством веток, а чистая сборка занимает много времени.