На протяжении многих лет использования git я выявил для себя одну серьезную проблему, которую этот инструмент не дает возможности разрешить удобным способом. Я назвал эту проблему “проблемой длинного прыжка”. Суть такова: у вас есть старая залежавшаяся ветка, которая отстает от main-ветки на сотни и тысячи коммитов. Задача — обновить эту старую ветку до свежего main. Все усугубляется тем, что у вас тяжеловесный репозиторий: гигабайты, десятки или сотни гигабайт.
Обычный алгоритм для обновления такой ветки:
-
прыгаем на старую ветку
-
мержим в нее свежий origin/main
-
пушим
Если вы в начале выполнения этих действий находитесь на какой-нибудь свежей ветке, например на main, то для вас выполнение этих операций означает два прыжка: один прыжок из актуальной версии репозитория в устаревшую версию, а потом, после того как вы совершите мерж, — это обратный прыжок от старого кода к новому. Если разрыв между старой версией репозитория и новой составляет много гигабайт и вы не можете похвастаться большой скоростью скачивания с git-сервера, то вы ощутите три волны боли:
-
боль предвкушения
-
боль прыжка в протухший репозиторий
-
боль обратного прыжка
Если я вас не убедил
Мой нынешний проект возводит неприятные последствия от таких прыжков в куб из-за своей специфики.
Во-первых, мы говорим об объемах репозитория, который перевалил за терабайт. Папка .git/lfs занимает 90% репозитория.
Во-вторых, периодически случаются эпизоды, когда все наши актуальные ветки в одночасье протухают на тысячи коммитов из-за огромного стороннего мержа, который обновляет половину проекта. Ситуация не то чтобы частая, но она случается. И когда она случается, нам всем неизбежно приходится сталкиваться с проблемой, о которой я пишу. То есть недостаточно быть хорошим мальчиком и держать все свои ветки в актуальном состоянии — даже это в какой-то момент не спасет тебя.
Приходится адаптироваться и изобретать самые разные способы, как обойти эту проблему с наименьшими затратами времени. И у меня с коллегами накопился не один способ, как можно обновить протухшую ветку, не особо страдая или страдая лишь только чуть-чуть.
Git**b Update
Если есть возможность, первое, что нужно сделать — попытаться провернуть всю операцию не своими руками, а на стороне сервера. Обычно он делает это быстро и эффективно. Но здесь мы упираемся в возможности вашей конкретной платформы.
Счастливчики те, кто работает в GitHub, поскольку у них есть все возможности совершить мерж руками платформы, не выходя из Web-интерфейса. В частности на странице PR’а есть опция Update branch, которая по умолчанию совершает git merge. Для желающих ребейзнуться есть возможность сделать это через Update with rebase.
В GitLab прямой аналог кнопки Update branch мне неизвестен. Точнее опция обновления ветки есть, но она безальтернативно делает ребейз — Rebase source branch. И это ребейз без права выбрать не-ребейз.
В обоих платформах перед обновлением ветки нужно разрешить конфликты. Причем это можно сделать тут же, в Web-интерфейсе. Однако, не во всех случаях платформа готова дать вам решить конфликты через Web-IDE — в тяжелых случаях сервис откажет вам в такой возможности и попросит сделать все локально. И это будет тупик.
Недостатки подхода:
-
Подходит для несложных случаев
-
Если вы не на GitHub, есть вероятность, что вы останетесь один на один с опцией сделать ребейз. А возможно, ваша платформа и вовсе не предоставляет таких возможностей, как удаленный мерж или ребейз
-
Ограниченные возможности комфортно решить конфликты — нельзя скомпилировать и запустить результат; а в сложных случаях — большие файлы, бинарные файлы, нетривиальные переименования, большое количество файлов — платформа может вам отказать и предложить решить все на вашей собственной машине
-
Я знаю один МР, который Web UI в принципе не мог адекватно переварить из-за обилия изменений, поэтому он не мог показать ничего — ни diff, ни кнопку Merge, ни опции
Update branch / Rebase source branch— просто часами крутит спиннер и обещает, что скоро все покажет. Это тоже тупик. Возможно, можно было сделать что-то через API, но я не спец
Обратный PR/MR
Если площадка не дает прямой возможности обновить ветку так, как вы хотите, вы можете попытаться сымитировать это через фиктивный PR/MR, который “вывернут наизнанку” — он будет таргетить не feature -> main, а main -> feature. Далее, если у вас есть права и на работе в целом не против таких грязных мувов, вы на месте можете порешать конфликты (если они решаемы, если нет — тупик) и замержить main в вашу ветку.
Для GitHub этот метод, вероятно, неактуален, поскольку Update branch делает все то же самое, но более прямым путем. А в GitLab’е он вполне имеет смысл, если вы сильно против ребейза.
Недостатки подхода:
-
Недостатки предыдущего подхода
-
Замусоривание платформы фиктивными MRами, которые пачками будут отлегаться во вкладке Merged
-
Если у вас недостаточно прав на нажатие кнопки Merge, вам придется попросить старшего
Обратный локальный мерж
Аналог предыдущего подхода, но локальный. Мы снова мержим feature в main вместо мержа main в feature — ведь если результат одинаковый, то какая разница? Разница, конечно, как мы увидим, есть, но результат самого мержа и правда обычно одинаковый. Этот подход, внезапно, сильно дешевле прямого подхода, когда мы прыгаем на feature и обратно — как раз по той причине, что мы в этом случае не совершаем ни один из этих прыжков, а просто занимаемся локальным мержем. Мерж тоже может занимать время и ресурсы, но обычно они несравнимы с полноценными прыжками туда/обратно, поскольку прыжок зачастую подразумевает выкачку “лишнего шума”, порой много-гигабайтного.
# уходим в detached stategit switch --detach origin/main# мержим ветку в detached HEADgit merge -n feature# решаем конфликты# НЕ КОММИТИМ# ретаргетим ветку содержимым detached HEADgit switch -C feature# вот теперь коммитимgit commit# форсим обновление ветки тем, что мы наделалиgit push --force-with-lease origin feature# возвращаемся из detached state куда-нибудьgit switch main
Недостатки подхода:
-
Форс ветки. Несмотря на то, что мы начинали c нормального мержа, закончили мы на форсировании ветки
и все испортили. В каких-то ситуациях форс может быть неприменим в реалиях вашей работы.
В остальном я считаю этот подход вполне себе адекватным и полноценным. Именно из-за практически полного отсутствия ограничений.
Feature-patch
Сперва нужно добыть патч с изменениями вашей ветки относительно таргета. Это можно сделать кучей способов:
-
Если у вас GitHub или GitLab, то к URL с PR или MR можно добавить
.patch, и вы попадете на страницу с текстом патча, который можно скопировать -
Набрать в Git Bash команду
git fetch origin feature git format-patch --stdout origin/main..origin/feature > feature.patch -
You name it
Я хочу сразу предостеречь: не перепутайте .patch и .diff — нам нужен .patch! В теории .diff тоже может подойти, но он не умеет в нормальное разрешение конфликтов и стирает информацию о коммитах и их метаданных вроде сообщений, дат и т.д.
После того, как достали патч:
# уходим в detached stategit switch --detach origin/main# применяем патчgit am feature.patch# форсим обновление ветки тем, что мы наделалиgit branch -f feature HEADgit push --force-with-lease origin feature# возвращаемся из detached state куда-нибудьgit switch main
Если на стадии применения патча, у вас появились конфликты, решаете их как при обычном мерже, затем запускаете git am --continue.
Фактически эту ручной rebase, что-то в духе git rebase --onto origin/main <old-main> feature. Вот только настоящие локальные rebase и merge на тяжелом репозитории могут частично восстанавливать состояние старой ветки на вашей рабочей машине, что может стриггерить нелегкие возвраты в прошлое. В готовом патче эта работа уже проделана, так что теоретически этот способ быстрее предыдущего.
Недостатки подхода:
-
Может быть ситуация, когда вам не так просто достать патч
-
Все еще форс
Sparse-checkout
Наверное, самый правильный с точки зрения сохранения истории и потребления ресурсов вариант. Но насколько он правильный, настолько же он и муторный. Во первых, мы вступаем на территорию advanced git и будем использовать sparse-checkout. Во-вторых, мы будем клонировать новый репо. Но вы не бойтесь — мы склонируем пустышку.
В-третьих, прежде чем приступить к работе, сперва вам придется узнать все пути к папкам, которые были затронуты вашей веткой.
Наглядный пример:
.├── assets├── src│ ├── a│ ├── b│ │ ├── ba│ │ │ └── touched-I.cpp│ │ ├── bb│ │ ├── bc│ │ └── bd│ │ └── touched-II.cpp│ ├── c│ ├── d│ │ └── touched-II.cpp│ └── e└── third-party
Чем более детальный список папок вы получите, тем меньше вам придется выкачивать. Идеальный вариант — это когда вы вычислите через diff папки:
-
src/b/ba/ -
src/b/bd/ -
src/d/
Но в целом можно и широкими мазками: если вы знаете, что вся ваша работа была проведена в папке src, и неважно, что там тысячи файлов в подпапках, которые вы не трогали — они все весят считанные мегабайты, и вы готовы их скачать — то пожалуйста.
То есть и так сгодится:
-
src/b/ -
src/d/
Да и просто src/ сойдет при условии, что вы не боитесь скачивать всю src/.
После взятия списка папок создаем временную папку и выполняем следующее:
# клонируем временный репо не забирая ничего и входя в spapse-checkout режимgit clone --filter=blob:none --sparse <YOUR_REPO>.git# идем на свою веткуgit checkout feature# самое муторное: через пробел указываем все пути, которые мы потрогали в веткеgit sparse-checkout set src/b/ba/ src/b/bd/ src/d/# запускаем мержgit merge origin/main# правим конфликты. в git status будут огромные полотна в индексе - с этим ничего не сделать# правим, коммитим, пушим# удаляем временный репо
Как видите, мы сделали честный мерж ветки, обойдя стороной выкачивание всего репозитория. Фактически мы обошлись выкачиванием только того, что нам действительно необходимо для мержа — на стадии git sparse-checkout set гит выкачает все по указанным путям.
При мержах бывает один хитрый случай, когда в main-ветке переименована папка, родительская для файлов, которые мы меняли в нашей ветке. Например, в main папка b переименована в bigB. А в вашей старой ветке она все еще b. Решение есть — в git sparse-checkout set нужно перечислить обе версии путей:
git sparse-checkout set src/b/ba/ src/b/bd/ src/bigB/ba/ src/bigB/bd/ src/d/
Это сработает.
Если вы адепт worktrees и хотите использовать worktree вместо клонирования репозитория, то набор команд будет немного иной:
# создаем worktreegit worktree add --no-checkout ../feature-update feature# заходим в негоcd ../feature-update# инициируем sparse-checkoutgit sparse-checkout init --conegit sparse-checkout set src/b/ba/ src/b/bd/ src/d/# дальше обычный мерж
Но будет один неприятный нюанс — так как git worktree add нельзя скомбинировать с созданием sparse-режима, ваш индекс нещадно заполонится тьмой файлов, и вам придется чистить это самостоятельно. git clone обходит эту проблему стороной за счет флага --sparse, аналога которому у git worktree add нет.
Недостатки подхода:
Очевидны. Свистопляска со списком папок может оказаться непосильно мучительной, а порой и вовсе невозможной, если изменений много. Я на такой случай навайбодил python-скрипт, который читает diff к ветке и выводит мне готовую строку для git sparse-checkout set.
Скрипт
import sysfrom pathlib import PurePosixPathfrom collections import defaultdictdef parse_diff(lines):paths = set()for line in lines: line = line.rstrip("\n") if line.startswith("+++ ") or line.startswith("--- "): path = line[4:] if path.startswith("a/") or path.startswith("b/"): path = path[2:] if path != "/dev/null": paths.add(path)return pathsdef build_dir_stats(paths):stats = defaultdict(int)for path in paths: parts = PurePosixPath(path).parts for i in range(1, len(parts)): stats["/".join(parts[:i])] += 1return statsdef make_sparse_paths(paths):dirs = set()for path in paths: parts = PurePosixPath(path).parts dirs.add("/".join(parts[:-1]))result = []for d in sorted(dirs): # remove parents if a child directory already covers them if not any( child.startswith(d + "/") for child in dirs if child != d ): result.append(d)return resultif name == “main”:with open(sys.argv[1], encoding=“utf-8”) as f:paths = parse_diff(f)sparse = make_sparse_paths(paths)print(‘git sparse-checkout set’, end=" “) for path in sparse: print(path, end=” ")
Послесловие
Спасибо моим коллегам за вклад в эту нелегкую тему ценой собственных нервных клеток
ссылка на оригинал статьи https://habr.com/ru/articles/1063614/