Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров34K

Статей на тему много, но, видимо, недостаточно. Последние 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 в рабочий процесс. Может быть, сэкономите кучу времени и нервов, особенно, если проект подразумевает работу с множеством веток, а чистая сборка занимает много времени.

Теги:
Хабы:
Всего голосов 84: ↑84 и ↓0+100
Комментарии77

Публикации

Ближайшие события