После использования Git в течение нескольких лет я постепенно использовал все более и более продвинутые команды. Вскоре после того, как я познакомился с Git rebase, я быстро включил его в свою работу. Те, кто знаком с ребейзингом, знают, насколько это мощный инструмент и насколько заманчиво использовать его постоянно. Однако вскоре я обнаружил, что перебазирование сопряжено с некоторыми трудностями, которые не очевидны, когда вы впервые начинаете это делать. Прежде чем представить их, вспомним разницу между merge (слиянием) и rebase (перебазированием).

Рассмотрим простой пример: нужно интегрировать ветку feature с веткой master. Делая merge, мы создаем новый коммит g, представляющий собой слияние этих двух ветвей. Граф коммитов четко показывает, что произошло, и мы видим контуры «железнодорожной колеи» (train-track), знакомые по более крупным Git-репо.

В качестве альтернативы, мы можем сделать rebase перед слиянием. Коммиты удаляются, а ветвь feature сбрасывается в master, после чего коммиты снова применяются поверх feature. Различия этих повторно примененных коммитов обычно идентичны их оригинальным аналогам, но они имеют разные родительские коммиты и, следовательно, разные ключи SHA-1.

Теперь мы изменили базовый коммит ветки feature с b на c, буквально произведя ребейз. Слияние ветки  feature с master теперь является fast-forward merge (слиянием с ускоренной перемоткой вперед), поскольку все коммиты на объекте являются прямыми потомками master.

В сравнении с подходом слияния результирующая история является линейной без расходящихся ветвей. Повышенная читаемость — причина, по которой я предпочитал сначала делать ребейз ветки перед мерджем. Думаю, что это пригодится и другим разработчикам.

Однако у этого подхода есть не очевидные проблемы.

Рассмотрим случай, когда зависимость, которая все еще используется во feature, была удалена в ветке master. Когда feature перебазируется на master, первый повторный коммит нарушит ваш билд, но пока нет конфликтов слияния, процесс перебазировки будет продолжаться непрерывно. Ошибка первого коммита будет присутствовать во всех последующих коммитах, что приведет к цепочке прерванных коммитов.

Эта ошибка обнаруживается только после завершения процесса ребазирования, и обычно ее исправляют, применяя новый коммит-bugfix g сверху.

 

Пример неудачной перебазировки


Однако, если вы получите конфликты во время перебазирования, Git приостановит конфликтующий коммит, что позволит вам исправить конфликт перед продолжением. Решение конфликтов в середине перебазирования длинной цепочки коммитов часто сбивает с толку, трудно понять правильно, и является еще одним источником потенциальных ошибок.

Выявление ошибок является особенно проблематичным, когда это происходит во время перебазирования. Таким образом, новые ошибки появляются, когда вы переписываете историю, и они могут скрывать подлинные ошибки, которые были введены, когда история была впервые написана. В частности, это затруднит использование Git bisect, возможно, самого мощного средства отладки в наборе инструментов Git. В качестве примера рассмотрим следующую feature ветку. Допустим, мы обнаружили ошибки в конце ветки.

Ветка с ошибками, появившимися ближе к концу


Вы можете обнаружить эту ошибку только через несколько недель после того, как ветка была объединена с мастером. Чтобы найти коммит, который привел к ошибке, вам, возможно, придется поискать десятки или сотни коммитов. Этот процесс можно автоматизировать, написав скрипт, который проверяет наличие ошибки, и автоматически запустив его через Git bisect, используя команду git bisect run <yourtest.sh>.

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

Пример успешного git bisect


С другой стороны, если мы обнаружили дополнительные прерванные коммиты во время ребейза (здесь, d и e), то при bisect возникнет проблема. В этом случае мы надеемся, что Git идентифицирует коммит f как плохой, но вместо этого он ошибочно идентифицирует d, поскольку d содержит некоторую другую ошибку, которая нарушает тест.

Неудачный git bisect


Эта проблема больше, чем может показаться на первый взгляд.

Почему мы вообще используем Git? Потому что это наш самый важный инструмент для отслеживания источника ошибок в нашем коде. Git — это наша сеть безопасности. Опровергая это, мы придаем этому меньший приоритет в пользу стремления к линейной истории.

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

Итак, как мы можем избежать этих цепочек нарушенных коммитов во время ребазинга? Один из подходов может состоять в том, чтобы завершить процесс ребазирования, протестировать код для выявления любых ошибок и вернуться в историю, чтобы исправить ошибки, в которых они были обнаружены. Для этого мы могли бы использовать интерактивный rebase.

Другой подход заключается в том, чтобы сделать Git-паузу на каждом этапе процесса ребазирования, проверять наличие ошибок и исправлять их непосредственно перед продолжением.

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

Есть — Git merge. Это простой одношаговый процесс, в котором все конфликты разрешаются в одном коммите. Результирующий коммит слияния четко обозначает точку интеграции между нашими ветвями, а наша история отображает, что на самом деле произошло и когда это произошло.

Важность сохранения вашей истории не следует недооценивать. Опровергая, вы лжете себе и своей команде. Вы делаете вид, что коммиты были написаны сегодня, когда они были написаны вчера, на основе другого коммита. Вы вынули коммиты из их первоначального контекста, скрывая то, что на самом деле произошло. Можете ли вы быть уверены, что код работает? Можете ли вы быть уверены, что сообщения о коммитах все еще имеют смысл? Вы можете верить в то, что вы очищаете и проясняете свою историю, но результат вполне может быть противоположным.

Невозможно сказать, какие ошибки и проблемы возникнут в будущем в вашем коде. Однако вы можете быть уверены, что реальная история будет более полезной, чем переписанная (или фальшивая).

Что заставляет людей делать ребейз веток? 

Я пришел к выводу, что речь идет о тщеславии. Перебазирование — это чисто эстетическая операция. Очевидно, чистая история привлекает нас как разработчиков, но это не может быть оправдано с технической или функциональной точки зрения.

Нелинейная Git история

 


Графики нелинейной истории, «железнодорожные пути», могут быть пугающими, но нет причин бояться их. Есть много великолепных инструментов, которые могут анализировать и визуализировать сложную историю Git, как на основе GUI, так и на основе CLI. Эти графики содержат ценную информацию о том, что произошло и когда это произошло, и мы ничего не получаем, линеаризуя это.

Git создан для нелинейной истории и поощряет ее. Если это вас отталкивает, вам лучше использовать более простую VCS, которая поддерживает только линейную историю.

Я думаю, вы должны сохранить правдивость своей истории. Освойте инструменты для ее анализа и не поддавайтесь искушению переписать ее. Награды за переписывание минимальны, но риски велики. Вы будете благодарить меня в следующий раз, когда будете делать bisect истории, чтобы отследить скрытую ошибку.

Перевел на русский: Александр Худоев