Статей на тему много, но, видимо, недостаточно: время от времени слышу от коллег (последние 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 diff $(git rev-parse HEAD)^!

Команда выше — то же самое, что руками достать хеш коммита из git log, а затем посмотреть его содержание с командой

> git diff 460802e4e3b4e070b5fc831c582a2d23aa4dbce5^!

Далее можем сделать "uncommit" командой git reset HEAD~1 --soft, либо добавить изменения в имеющийся коммит, изменив ему имя:

> git add -A
> git commit -m 'Fix all the release bugs, but introduce more' --ammend

Решение с `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).

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

В заключение

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

Комментарии (12)


  1. MarijQA
    02.07.2024 17:10

    И всё-таки worktree или wortree?


    1. arturdumchev Автор
      02.07.2024 17:10
      +1

      Спасибо, поправил.


      1. MarijQA
        02.07.2024 17:10

        Пожалуйста, я уж думала, что я чего-то не понимаю))


  1. ishpartko
    02.07.2024 17:10

    Спасибо добрый человек, я как раз пилю гуи для этой цели в компании(надо много линковать+микрофронты), очень облегчит дело


    1. arturdumchev Автор
      02.07.2024 17:10

      Спасибо за обратную связь! Уверен, с worktree будет удобно


  1. FreeNickname
    02.07.2024 17:10

    Эх, знал бы я об этом 2 часа назад) Спасибо!


    1. arturdumchev Автор
      02.07.2024 17:10

      `git stash drop` случайно сделали?)


      1. FreeNickname
        02.07.2024 17:10

        Я из тех самых людей, которые stash-ат и меняют ветку) Но там часть зависимостей не в гите, проект довольно старый, и нет уверенности, что они потом нормально подтянутся, и что я ничего не забыл)

        Правда, я ещё из тех мерзких людей, которые используют UI (SourceTree), не знаю, умеет ли он в worktree. Ну, есть только один способ проверить)) К тому же, всё равно часть приходится делать в консоли (например, на одном из проектов были теги с аннотациями). Плюс одно действие.


  1. dim_bug
    02.07.2024 17:10

    Дерево норм тема.


  1. qw1
    02.07.2024 17:10

    Ну очень не хватает возможности делать несколько копий одной ветки.
    Чтобы делать несколько фич параллельно и коммитить только те, которые готовы.


    1. arturdumchev Автор
      02.07.2024 17:10

      Чем не нравится вариант делать ветку A, а затем отводить от нее ветки А1, А2... Аn (можно с тем же worktree)? И когда они готовы, мержить их в А? Кажется, так проще будет самому не запутаться.


      1. qw1
        02.07.2024 17:10

        Если сделать ветку A от master, то git pull перестанет принимать в неё обновления из master.
        Вроде нашёл ключик --force для git worktree add, буду наблюдать, что из этого получится.