Pull to refresh

Git. Скачем между ветками как древесные лягушки

Level of difficultyEasy
Reading time6 min
Views27K

Статей на тему много, но, видимо, недостаточно: время от времени слышу от коллег (последние 10 лет, в 4-х разных компаниях):

  • «Не могу пошарить экран с кодом, у меня другая ветка сейчас».

  • «Не хочу переключать ветку, придется запускать кодогенерацию, у меня сбросятся build-файлы, потом это опять пересобирать!»

  • «Стаскивать ветку для просмотра ПР? Это же неудобно, надо "стэшить" изменения, ветку переключать».

  • «А я “склонировал“ 3 копии проекта, `git clone` to the rescue!»

Что-то из вышеперечисленного я не слышал, но добавил для драматизма. Если по какой-то из фраз вы узнали себя, предлагаю ознакомиться со статьей, может быть, найдете что-то полезное.

Почему "древесные лягушки"? Всего лишь совпадение по слову “Tree“ в “Tree frogs“ и git worktree, о котором пойдет речь.

И последнее, если и так знаете про git worktree, предлагаю сразу перейти к разделу "Мой вариант использования git worktree".

О проблемах и “неправильных“ решениях

Во введении к статье было уже все сказано, но для формализма распишу еще раз.

Проблемы:

  1. Потеря текущих изменений кода при смене ветки;

  2. Потеря временных файлов кодогенерации/компиляции при смене ветки.

Решения:

  1. Временный коммит и смена ветки. Решает проблему 1;

  2. git stash и смена ветки. Решает проблему 1, но можно потеряться в стешах, если не давать им имена;

  3. git clone проекта в другую папку. Решает 1 и 2;

  4. 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). Опять же, не хочется тратить время на временные коммиты и потерю файлов кодогенерации.

Я всегда держу несколько папок с проектом по принадлежности к релизным веткам:

  1. master (основная ветка, новые релизы у нас отводятся от нее);

  2. release3 (новая релизная ветка — следующий релиз);

  3. release2 (предыдущая релизная ветка — релиз в процессе);

  4. release1 (самый старенький, удаляю его после того, как релиз2 “зарелизится“);

  5. 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

В общем-то, проблем никаких нет

Но могут быть мелкие неудобства:

  1. Лишнее место на диске.

  2. Нельзя "зачекаутить" одну и ту же ветку в двух worktree.

  3. Нельзя удалить ветку, если на нее смотрит какой-то из worktree. Гит об этом скажет, и тут просто надо удалить этот worktree (git worktree remove <path>).

  4. Могут быть проблемы в функционале недоделанных инструментов. Например, я когда-то отказывался от neovim-плагина neogit, потому что были баги в worktree (Github Issues: 1, 2). В комментарии к статье писали о проблемах с Eclipse и vscode devcontainers.

  5. Если используете `git submodule`, то в каждой папке worktree придется обновлять их отдельно. Обходные пути есть.

Если знаете о других проблемах, напишите в комментариях или в личку, дополню статью.

В заключение

Попробуйте включить git worktree в рабочий процесс. Может быть, сэкономите кучу времени и нервов, особенно, если проект подразумевает работу с множеством веток, а чистая сборка занимает много времени.

Tags:
Hubs:
Total votes 84: ↑84 and ↓0+100
Comments71

Articles