После использования GIT в течение нескольких лет я постепенно начал использовать все более совершенные команды GIT во время моего ежедневного рабочего процесса. Вскоре после того, как я открыл GIT rebase, я быстро включил его в этот ежедневный процесс. Те, кто знаком с перемещением, знают, насколько мощным инструментом оно является и как же хочется использовать его постоянно. Тем не менее, вскоре я обнаружил, что перемещение ведет за собой некоторые проблемы, которые не кажутся очевидными в момент, когда вы начинаете использовать команду. Прежде чем рассказать о них, я быстро разберу различия между перемещением и слиянием - GIT rebase и merge соответственно.
Давайте сначала рассмотрим простой пример, где вы хотите интегрировать побочную ветвь с главной. При объединении, мы создаем новый коммит g
, который представляет собой слияние между двумя ветвями. График фиксации изменений ясно показывает, что произошло. Можно увидеть контуры графа «железная дорога», знакомого по более крупным GIT-репозиториям.
В качестве альтернативы мы можем до слияния применить GIT rebase - перемещение. Коммиты удаляются, и побочная ветвь (feature) сбрасывается до главной (master), после чего коммиты повторно применяются поверх побочной ветви (feature). Дифференциалы этих повторно применяемых коммитов обычно идентичны их оригинальным аналогам, но они имеют разные материнские версии и, следовательно, разные ключи SHA-1.
Теперь мы изменили базовые коммиты в feature
от b
до c
, буквально переместив их значения. Слияние master
и feature
теперь происходит ускоренно, потому что все коммиты на feature
- прямые потомки master
.
По сравнению с методом слияния получившаяся история линейна и не имеет расходящихся ветвей. Улучшенная читаемость была причиной, по которой я предпочитал перемещать ветви до слияния. Думаю, GIT rebase из-за этого и понравилась другим разработчикам.
Однако этот подход имеет некоторые проблемы, которые могут быть не очевидны.
Рассмотрим случай, когда зависимость, которая все еще используется в feature
, была удалена в master
. Когда feature
перемещается на master
, первый повторный коммит прерывает сборку. Если же конфликтов слияния не возникнет, процесс перемещения будет продолжаться непрерывно. Ошибка от первого коммита останется во всех последующих коммитах, что приведет к цепочке «сломанных» коммитов.
Эта ошибка обнаруживается только после завершения процесса перемещения и обычно исправляется путем применения новой коммита-багфикса g
.
Однако, если вы столкнулись с конфликтами во время перемещения, GIT остановится на конфликтующем коммите, что позволит вам исправить ошибку, а затем продолжить. Решение проблем в середине перемещения длинной цепи коммитов часто сбивает с толку. Здесь трудно не ошибиться, а потому этот этап - еще один источник потенциальных проблем.
Допущение ошибок гораздо проблемнее, когда происходит во время выполнения GIT rebase - перемещения. Из-за этого при перезаписи истории появляются новые ошибки, и они могут маскировать настоящие баги, которые были введены, когда история была написана изначально. В частности, это затруднит использование GIT bisect - возможно, самого мощного инструмента отладки в наборе инструментов GIT. В качестве примера рассмотрим следующую ветвь feature. Предположим, мы ввели ошибку в конец ветки.
Вы можете найти этот баг только спустя несколько недель после объединения ветви с master
. Чтобы найти коммит, который ввел баг, придется обыскивать десятки или сотни коммитов. Этот процесс можно автоматизировать, написав скрипт, который проверяет наличие бага и запустить его автоматически через GIT bisect, используя команду git bisect run <yourtest.sh>
.
Bisect выполнит дихотомический поиск по истории, идентифицируя коммит, который добавил баг. В приведенном ниже примере ему удается найти первый ошибочный коммит, так как все «поломанные» коммиты содержат баг, который мы ищем.
С другой стороны, если бы мы ввели дополнительные ломаные коммиты во время перезагрузки (здесь - d
и e
), Bisect столкнулся бы с проблемой. В этом случае мы надеемся, что GIT идентифицирует коммит f
как плохой, но он ошибочно идентифицирует d
, так как в нем содержится какая-то другая ошибка, прерывающая тест.
Эта проблема крупнее, чем может показаться сначала.
Почему мы вообще используем GIT? Потому что это наш самый важный инструмент для отслеживания источника ошибок в нашем коде. GIT - наша подстраховка. При расширении мы меньше учитываем это, желая получить линейную историю.
Некоторое время назад мне пришлось проводить дихотомический поиск по нескольку сотен коммитов, чтобы выявить ошибку в нашей системе. Неисправный коммит находился в середине длинной цепочки коммитов, которая не скомпилировалась из-за ошибочного перемещения, которое провел коллега. Эта ненужная и полностью предотвращаемая ошибка привела к тому, что я потратил почти целый день на отслеживание коммитов.
Итак, как мы можем избежать появления этих цепочек «ломаных» коммитов во время перемещения? Один из подходов может заключаться в том, чтобы завершить процесс перемещения, протестировать код, чтобы выявить ошибки, и вернуться в историю, чтобы исправить ошибки там, где они были введены. С этой целью мы могли бы использовать интерактивное перемещение.
Другой подход - приостанавливать GIT на каждом этапе процесса перемещения, протестировать любые ошибки и исправить их непосредственно перед продолжением работы.
Это громоздкий и подверженный ошибкам процесс, а единственная причина для его применения - желание добиться линейной истории. Есть ли более простой и удобный способ?
Есть - GIT merge. Это простой, одноэтапный процесс, в котором все конфликты разрешаются в одном коммите. Получающаяся в результате слияния коммитов ясно указывает точку интеграции между нашими ветвями, а история показывает, что и когда произошло.
Не следует недооценивать важность сохранения истории. При перемещении вы лжете себе и своей команде. Вы делаете вид, что коммиты были написаны сегодня, когда они были фактически написаны вчера с основной на другой коммит. Вы вытащили коммиты из первоначального контекста, замаскировав то, что на самом деле произошло. Можете ли вы быть уверены, что код будет работать? Можете ли вы быть уверены, что сообщения коммитов все еще имеют смысл? Вы можете думать, что вы очищаете и уточняете свою историю, но результат вполне может быть обратным.
Невозможно сказать, какие ошибки и проблемы будущее принесет в вашу базу кода. Однако вы можете быть уверены, что настоящая история будет полезнее переписанной (или фальшивой).
Почему люди перемещают ветви?
Я пришел к выводу, что все дело в тщеславии. Перемещение - это чисто эстетическая операция. По-видимому, чистая история нравится нам, разработчикам, но она не может быть оправдана с технической или функциональной точки зрения.
Графики нелинейной истории, похожие на «железнодорожные пути», могут быть пугающими. Они такими казались по началу и мне, но нет причин бояться их. Существует много великолепных инструментов, которые могут анализировать и визуализировать сложную историю GIT, основываясь как на графическом интерфейсе, так и на интерфейсе командной строки. Эти графики содержат ценную информацию о том, что и когда произошло, и мы ничего не получаем при линеаризации.
GIT предназначен для нелинейной истории, и он поощряет ее применение. Если это вас пугает, вам лучше использовать более простой VCS, который поддерживает только линейную историю.
Я думаю, история должна оставаться истинной. Анализируйте ее инструментами и не поддавайтесь соблазну переписать ее. Награды за переписывание минимальны, а вот риски велики. Вы поблагодарите меня в следующий раз, когда будете проводить дихотомический поиск по своей истории, чтобы выследить спрятавшийся баг.