Статей на тему много, но, видимо, недостаточно: время от времени слышу от коллег (последние 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
в рабочий процесс. Может быть, сэкономите кучу времени и нервов, особенно, если проект подразумевает работу с множеством веток, а чистая сборка занимает много времени.
Комментарии (57)
ishpartko
02.07.2024 17:10+5Спасибо добрый человек, я как раз пилю гуи для этой цели в компании(надо много линковать+микрофронты), очень облегчит дело
FreeNickname
02.07.2024 17:10+2Эх, знал бы я об этом 2 часа назад) Спасибо!
arturdumchev Автор
02.07.2024 17:10`git stash drop` случайно сделали?)
FreeNickname
02.07.2024 17:10+1Я из тех самых людей, которые stash-ат и меняют ветку) Но там часть зависимостей не в гите, проект довольно старый, и нет уверенности, что они потом нормально подтянутся, и что я ничего не забыл)
Правда, я ещё из тех мерзких людей, которые используют UI (SourceTree), не знаю, умеет ли он в worktree. Ну, есть только один способ проверить)) К тому же, всё равно часть приходится делать в консоли (например, на одном из проектов были теги с аннотациями). Плюс одно действие.
mayorovp
02.07.2024 17:10+2Переходите на винду, у нас есть Git Extensions
А если серьёзно, то, судя по скриншотам, теги с аннотациями SourceTree поддерживает, ради них точно нет смысла возвращаться в терминал. Что же до worktree - многие (но, увы, не все) программы прекрасно работают с ними если открыть заранее созданный worktree как отдельный репозиторий.
qw1
02.07.2024 17:10+2Ну очень не хватает возможности делать несколько копий одной ветки.
Чтобы делать несколько фич параллельно и коммитить только те, которые готовы.arturdumchev Автор
02.07.2024 17:10+1Чем не нравится вариант делать ветку A, а затем отводить от нее ветки А1, А2... Аn (можно с тем же worktree)? И когда они готовы, мержить их в А? Кажется, так проще будет самому не запутаться.
qw1
02.07.2024 17:10Если сделать ветку A от master, то git pull перестанет принимать в неё обновления из master.
Вроде нашёл ключик--force
дляgit worktree add
, буду наблюдать, что из этого получится.encyclopedist
02.07.2024 17:10+6Вы можете вручную задать upstream ветку origin/master для вашей рабочей ветки А:
git branch --set-upstream-to=origin/master
qw1
02.07.2024 17:10Тогда это вообще прекрасно, если пуши будут попадать в master!
FreeNickname
02.07.2024 17:10+1А если и пуши в master, и pull из master-а, то чем это отличается просто от использования локальной ветки master, просто с другим названием? Не совсем понимаю.
qw1
02.07.2024 17:10+3Раньше я делал несколько копий репозитория, в каждой копии пилил какую-то фичу и пушил по мере готовности. worktree позволит иметь один репозиторий, экономя на повторах папки .git, а также fetch можно делать единожды, он затронет все копии.
Playa
02.07.2024 17:10Попробуйте GitButler
qw1
02.07.2024 17:10Посмотрел их презенташку. Мне кажется, мне это не нужно - управлять каждым минимальным изменением в файлах. Это хорошо, когда примеры простенькие - "поменял цвет фона" - изменение в единственной строчке. Если же одно изменение это 15 мест в разных файлах, а другое изменение 20 мест в файлах, частично пересекающихся с первыми, получается какой-то микро-менеджмент изменений.
BearOff
02.07.2024 17:10+10Для справки:
Прыгают древолазы редко, только в случае крайней необходимости, предпочитая передвигаться пешком.
Не способен прыгать с дерева на дерево, поэтому спускается на землю и переходит на другое дерево.
Tony-Sol
02.07.2024 17:10Давненько приглядывался к worktree, но не могу заставить себя попробовать - как-то всегда хватало stash/временный commit/diff > apply
arturdumchev Автор
02.07.2024 17:10Может быть, жизнь заставит, если придется работать над 3-4 задачами условно одновременно. Если в дополнении к этому окажется, что чистая сборка 30 минут (такое тоже было), то тут только копии (worktree) проекта держать.
wl2776
02.07.2024 17:10Надо будет попробовать.
Обучаю нейросети, код храню в git. Стараюсь хранить все эксперименты, один эксперимент - один коммит.
Бывают сложности, если есть очередь из задач на обучение. Код для задачи берётся из рабочего каталога, и может так получиться, что когда она попадёт на исполнение, там заcheckoutен уже не тот коммит.
Пока решаю эту проблему с помощью python executable zip archives
Docker по независящим от меня причинам использовать не могу.
ImagineTables
02.07.2024 17:10+2А в черепахе это есть?
AnyKey80lvl
02.07.2024 17:10+5Черепахи в прыжках по веткам замечены не были
ImagineTables
02.07.2024 17:10+3Как говорится, хочешь что-то узнать — узнай это сам: https://tortoisegit.org/docs/tortoisegit/tgit-dug-worktrees.html
Прыгают. Но низэнько…
aamonster
02.07.2024 17:10+1Хм, я вроде раньше пользовался (сэкономил на clone), но что-то не понравилось (уже не помню, что именно), и вернулся к варианту 3.
Imho зря вы такой длинный текст для такой простой штуки раскатали. Сравнивать с вариантами 1 и 2 (временные ветки и stash) вообще бессмысленно, разработчики, которым они не подходят, и так знают об этом (например, мне надо открывать в xcode два разных воркспейса – для iOS и MacOS, но они имеют общие подпроекты утилит сборки, и XCode этого стерпеть уже не может). А сравнение со вторым клоном очень простое, одного-двух абзацев бы хватило.
arturdumchev Автор
02.07.2024 17:10+5зря вы такой длинный текст
На всякий случай написал, чтобы и новичку было полезно и понятно все. Для пользователей вроде вас оставил во введении предложение сразу перескочить к разделу "Мой вариант использования git worktree".
Сравнивать с вариантами 1 и 2 (временные ветки и stash) вообще бессмысленно, разработчики, которым они не подходят, и так знают об этом
Как я в статье и сказал, последние 10 лет вижу, как люди буквально мучаются от того, что у них все тормозит, лагает, бесятся, что нужно clean build делать. Фразу о том, что человек не может показать код, потому что ему на ветку нужно прыгать, слышал не далее, как месяц назад. Я эту статью написал в том числе для того, чтобы коллегам скинуть. И все коллеги как раз 1 и 2 и используют.
А сравнение со вторым клоном очень простое, одного-двух абзацев бы хватило.
А разве у меня больше?
aamonster
02.07.2024 17:10+4Вспомнил, кстати, что меня раздражало в worktree. Именно невозможность зачекаутить одну ветку в двух местах – что делало мёржи в мастер довольно неудобными. Тут в комментах как раз обсудили, как обойти это ограничение, так что, возможно, при очередной настройке рабочего места попробую вновь.
arturdumchev Автор
02.07.2024 17:10+2В мастер же обычно напрямую никто ничего не пушит. Вместо этого создают ветки от мастера и потому создают ПР в мастер. Можно создать ветку от мастера без чекаута на мастер:
> git checkout -b feature1 master
А если случайно "зачекаутились" от другой ветки, можно просто вернуть ее содержание на мастер:
> git fetch # чтобы потом на свежий мастер обновиться > git reset --hard origin/master
aamonster
02.07.2024 17:10+1Если у вас с гитом работает небольшая команда – обычно людям дают самим мёржить ветки в мастер. А то и одиночные коммиты делать прямо в него.
Да и просто перед созданием ветки – обычно переключаешься на мастер и делаешь pull. Вот тут-то и выяснялось, что переключиться на мастер ты не можешь, потому что утром уже сделал это в другой папке.
ЗЫ: При виде "git reset" до сих пор вздрагиваю, хотя много лет прошло... Мне в своё время пришлось освоить git reflog практически сразу: я как-то в hg привык, что ты не можешь в одно движение разломать себе репу.
arturdumchev Автор
02.07.2024 17:10Понимаю :) Ну вот когда нельзя в мастере ничего менять (у нас нельзя кстати, как и в релизных ветках), а только в ветках, над которыми работаешь локально, "git reset" использовать не страшно.
Я кстати когда над своими проектами работаю, не ставлю никакие запреты, но и
--hard не использую никогда. Если коммит убрать, то спокойно revert, реже rebase. Пусть все будет в истории, не помешает.
aamonster
02.07.2024 17:10Свои изменения потерять тоже жалко :-)
Это теперь я учёный и перед git reset новую ветку делаю, чтобы в случае чего с reflog не возиться.
AskePit
02.07.2024 17:10+1А вот в моем случае, например, worktree скорее дает меньше маневра, нежели второй честно откопированный репозиторий.
Рабочий проект весит больше полутерабайта, которые примерно пополам разделены между
.git
и остальной частью репо. Вот и выходит, что каждый раз, когда мне нужно создатьworktree
, у меня начинается копирование ~300Gb, и это занимает катастрофическое время. Ни о какой скорости и продуктивности для хотфикса и речи нет. Вот буквально вчера попробовал и на n-ой минуте тщательного копирования файликов гитом я просто прервал созданиеworktree
.А так как (если я конечно все правильно понял),
worktree
намертво привязан к одной ветке, то мне сильно дешевле просто иметь второй клон репо, который будет выкачан единожды, но теперь у меня в распоряжении есть два репо, где я могу прыгать между любыми двумя ветками, как бабуин, без ограничений.Да, это занимает уйму места, но что поделать.
arturdumchev Автор
02.07.2024 17:10+1А так как (если я конечно все правильно понял),
worktree
намертво привязан к одной веткеВетку в каждом worktree можно поменять, точно так же как меняешь в копии проекта, которую "склонил". Разница только в том, что папка
.git
будет общей, т.е. только в одном проекте достаточно сделать `git fetch`. И нельзя одну и ту же ветку в держать в основном проекте и в worktree.Кстати, по поводу того, что .git 300gb. Если большая история, можно сэкономить, не держа все изменения за все времена.
> git clone --depth 300 <project url>.
Команда выше заберет только последние 300 коммитов.
AskePit
02.07.2024 17:10+1Что же, значит я действительно не так понял worktree. Попробую улучшить качество жизни сегодня же :)
За хинт с ограниченной историей тоже спасибо, думаю, это должно освободить приличное количество места
AskePit
02.07.2024 17:10+1В общем, спасибо @Deosis и @arturdumchev за наводки. Мои предположения о несменяемости ветки в worktree были неверны. Теперь я тоже перешел на
git worktree
и, добавив дополнительный флаг--ignore-other-worktrees
к алиасу для командыgit checkout
, могу делать все то же, что делал раньше с полноценным клоном репозитория, но с меньшим количеством занятого места
coctic
02.07.2024 17:10+1Проблемы git worktree есть (частные но тем не менее):
Eclipse не умеет с ними работать. Соответственно, если разработка в eclipse - вся работа с git уходит в ком. строку. Вообще вся, даже diff не посмотреть. Eclipse не понимает, что это репозиторий.
vscode devcontainers. Если .devcontainer в репозитории, то в контейнере открывается одно worktree, и там внезапно не оказывается основного репозитория, и работа git тоже ломается. Надо городить костыли с другой точкой монтирования. Я пока не нашел разумного решения.
fishHook
02.07.2024 17:10Я правильно понимаю, что нет какой-то "активной" версии кода когда используем деревья? Я имею в виду, когда я делаю чекаут на ветку, я получаю другую версию кода в той же самой директории. При этом моя ИДЕ остается открытой на том же проекте. А для того чтобы работать в стиле wortree мне надо для каждого дерева создавать проект в ИДЕ и деражть окрытыми несколько версий ИДЕ?
qw1
02.07.2024 17:10+1В этом и суть. Есть несколько "активных" веток. В каждой открыта своя IDE, можно компилировать, запускать, отлаживать сразу одновременно несколько фич. Чтобы не тратить время на восстановление контекста при возврате к предыдущей задаче.
arturdumchev Автор
02.07.2024 17:10Ага, IntellijIDEA пытается помогать, и когда в рамках одного проекта переключаешь ветку, то она открывает те же файлы, которые были открыты. Но все равно она восстанавливает не весь контекст, и может понадобиться clean build делать.
Когда я в Neovim работал на Clojure-проекте, проблем открывать другую папку вообще не было, дело 200 мс. Сейчас на Java/Kotlin с IntellijIDEA. Открыть другой проект подольше, но жить можно.
mayorovp
02.07.2024 17:10Зачем создавать проект в IDE, когда надо просто открыть лежащий в репозитории?
lrrr11
02.07.2024 17:10+1Чем git worktree отличается от того, чтобы вызывать git clone с другим именем папки?
git worktree позволяет централизованно управлять репозиторием. Простыми словами: достаточно вызывать git fetch в любой папке, чтобы обновления были видны во всех.
объяснение плохое и не отражающее сути.
Суть в том, что git хранит содержимое всех файлов в хранилище объектов в папке
.git
, откуда распаковывает их в рабочую папку (worktree
в их терминологии), по умолчанию - в родительскую по отношению к.git
.Команда
git clone
копирует и рабочее дерево, и хранилище объектов.Команда
git worktree
копирует рабочее дерево, а хранилище объектов в целевой папке вместо копирования заменяет файлом со ссылкой на основное, экономя таким образом много места (т.к. хранилище объектов может сильно разрастаться в активных репозиториях. Кстати советую набратьgit gc
, чтобы освободить место)arturdumchev Автор
02.07.2024 17:10+1объяснение плохое и не отражающее сути.
Не соглашусь с вами. Моя статья о том, как и зачем пользовать worktree, а не о деталях реализации.
Пользователя, который держит несколько "clone-ов" проекта, проще убедить фразой "достаточно вызывать git fetch в любой папке, чтобы обновления были видны во всех", чем вашими тремя абзацами, которые он с большей вероятностью пропустит, читая статью по диагонали.
AMDmi3
02.07.2024 17:10+3git diff $(git rev-parse HEAD)^
Зачем такая сложность? Можно
git diff HEAD^
, а вообще простоgit show
arturdumchev Автор
02.07.2024 17:10Вы правы. У меня остался алиас с незапамятных времён. Поправлю в статье.
MarijQA
И всё-таки worktree или wortree?
arturdumchev Автор
Спасибо, поправил.
MarijQA
Пожалуйста, я уж думала, что я чего-то не понимаю))