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


Возьмём простой пример с интегрированием ветки feature в ветку master. При слиянии мы создаём новый коммит g, который представляет слияние между двумя ветками. График коммита ясно показывает, что произошло, и хорошо видны контуры графика «железнодорожных путей», знакомого нам по более крупным Git-репозиториям.



Пример слияния


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



Пример rebase


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



Пример ускоренного слияния


По сравнению с обычным слиянием мы получаем линейную историю изменений без ответвлений. Я предпочитал использовать rebase перед слиянием из-за улучшения читабельности и думал, что другие руководствуются той же причиной.


Но, как я упоминал, этот подход влечёт за собой ряд неочевидных сложностей.


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


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



Пример неудачного rebase


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


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



Ветка с багом ближе к концу


Вы можете не знать о баге в течение недель после слияния ветки с master. Для обнаружения коммита, с которым был внесён этот баг, вам придётся перелопатить десятки или сотни коммитов. Процесс автоматизируется, если написать скрипт для тестирования на наличие бага и автоматически запускать его во время Git bisect с помощью команды git bisect run <yourtest.sh>.


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



Пример успешного выполнения Git bisect


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



Пример сбойного Git bisect


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


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


Некоторое время назад я применял bisect к нескольким сотням коммитов, чтобы найти баг в системе. Битый коммит находился в середине длинной цепочки коммитов. Она не компилировалась из-за сбойного rebase, выполненного моим коллегой. Этой ошибки можно было легко избежать, и я потратил почти день на её поиск.


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


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


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


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


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


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


Думаю, тщеславие. Rebase — чисто эстетическая операция. Чистенькая история приятна нам как разработчикам, но это не может быть оправдано ни с технической точки зрения, ни с точки зрения функциональности.



Пример нелинейной истории


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


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


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

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


  1. Fesor
    23.10.2017 15:11

    Я считаю, что лучше сохранять достоверность истории.

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


    Но нет никаких причин бояться их.

    Trunk-based development?


    1. poxvuibr
      23.10.2017 15:14

      то есть вся проблема решается запретом на пуш с форсом в мастер, нет?

      Нет, это вообще не решает проблему. Допустим на локальном компьютере 10 коммитов. Программист делаем rebase на мастере и 5 из этих коммитов становятся поломаны. В 11 коммите он чинит поломку и делает push. force при этом пуше не нужен, всё пройдёт хорошо и битые коммиты окажутся в условно главном репозитории.


      если в ремоут все будет как если бы мы этим самым ребейзом никогда не пользовались?

      Не будет коммитов, которые явно ломают сборку?


      Trunk-based development?

      Это вы к чему? Так выглядит одна ветка, если в неё мёржили другие. Это не обязательно master.


    1. Smerig
      23.10.2017 15:18
      +1

      вот это дело. Как раз как у нас. В бранчах делай ребейз от мастера сколько душе угодно, а в мастер фича идет через git merge --squash. И никаких конфликтов, т.к. ветка с фичей постоянно ребейзилась с мастером.


      1. poxvuibr
        23.10.2017 15:22

        А bisect потом работает? git может понять, что все коммиты из фича ветки есть в мастере и ветках, отбранченных от мастера?


        1. Smerig
          23.10.2017 15:25
          +1

          может навлеку на себя гнев, но я ни разу не пользовался bisect. Даже не в курсе, что это.

          git может понять, что все коммиты из фича ветки есть в мастере
          А ему это не надо. фича мержится в мастер с параметром --squash, это значит, что будет все одним коммитом. А бисект вы можете посмотреть в ветке, если нужны конкретные коммиты фичи.


  1. poxvuibr
    23.10.2017 15:11

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


    git pull --rebase


  1. acmnu
    23.10.2017 15:29

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

    Что-то я не понял чем вам здесь merge поможет?


    1. Falstaff
      23.10.2017 15:41

      Автор, думаю, имеет в виду, что если мёржить master в feature при необходимости (вместо rebase), то не будет лишних сломанных коммитов в истории. То есть при rebase может внезапно появиться коммит из серии "… а тут мы исправляем баг, из-за которого десять предыдущих коммитов даже не компилируются, потому что был rebase, но его тут не видно". Если же делать merge, то в явном виде будет сломанный merge commit и фикс сразу за ним (хотя история будет более кучерявая, это да).


    1. MonkAlex
      23.10.2017 15:43

      Самим фактом своего существования. Можно будет восстановить логически обе ветки и понять что и где пошло не так.

      Другое дело, не представляю, как работает по смерженным веткам bisect.


      1. Falstaff
        23.10.2017 15:45

        Я думаю, что эта проблема как раз не стоит, поскольку даже после rebase всё равно можно сделать merge --no-ff. Автор как-то всё вместе подаёт, но в целом это независимые вещи, можно делать rebase и иметь явные слияния.


        1. sumanai
          23.10.2017 15:47

          После rebase коммиты feature ветки внезапно могут стать сломанными, и именно от этого предостерегает автор статьи.


          1. Falstaff
            23.10.2017 15:49

            Это уже другой вопрос, я в ответе повыше об этом написал. Автор до кучи ещё добавляет проблему линейной истории (и MonkAlex как раз об этом пишет, так что тут я отвечаю как раз на эту проблему — которая в общем-то не относится к проблеме сломанных коммитов).


          1. Yeah
            23.10.2017 16:15

            Что значит "могут быть"? Конфликты будут ровно теми же самыми. Разница лишь в том, что при rebase конфликты разрешаются по-коммитно и соответственно — проще (практика "разделяй и властвуй"), а при merge — всё кучей. Как по мне, то при merge можно наделать не меньше, а даже больше ошибок.


  1. johnfound
    23.10.2017 15:33

    Так ведь Ричард Хипп написал fossil в том числе и чтобы избавится от rebase. Он однажды сказал, что в git историю пишут победители, а в fossil история такая, какая случилась в реальности.


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


    1. staticlab
      23.10.2017 15:45

      Один из вариантов: когда разработчик в первом коммите что-то убрал или перенёс, а во втором вернул всё как было. Тогда после squash эти изменения исчезнут, а если оставить как есть, то при git blame такие строки будут показаны изменёнными этим разработчиком. Хотя, конечно, это напрямую не связано с ребейзом на новый коммит.


      1. sumanai
        23.10.2017 15:48

        Иногда и эта информация может быть полезна, особенно если у коммита возврата будет внятное описание того, почему так было сделано.


    1. lair
      23.10.2017 15:52

      И зачем вообще писать историю проекта если эта история будет причесана и раскрашена?

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


      1. Smerig
        23.10.2017 16:06

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


      1. johnfound
        23.10.2017 16:08

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

        История, она пишется чтобы читать ее в будущем. Как можно знать кому и что будет интересно в будущем?


        1. lair
          23.10.2017 16:11

          Как можно знать кому и что будет интересно в будущем?

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


  1. Squash
    23.10.2017 15:47

    С удивлением узнал что много кто пользуется rebase. Согласен с автором, что если все делать через merge — то потом просматривая историю явно видно где какие ветки создавались и как они между собой мержились. Для этого ведь собственно репозиторием и пользуемся.


    1. Smerig
      23.10.2017 16:07

      у Вас даже ник говорящий )) кто мешает мержить ветку в мастер одним коммитом с ключом --squash?


  1. Aquahawk
    23.10.2017 15:59

    использую hg, и всегда использую merge и никогда rebase, и почти никогда graft. И более того всегда коммичу перед пуллом и если есть разветвление делаю отдельный merge. И кстати при merge делаю всегда в два этапа, сначала девелоп на фичу, потом прогон тестов, иногда ручное тестирование, потом фичу на девелоп.


    1. Aquahawk
      23.10.2017 16:08

      Более того, я никогда не храню коммиты локально и не понимаю в чём проблема их пушить. Сиди в ветке, пуш что хочешь, потом один мердж в мастере и история ровно такая как она есть, и ветку видно и мерж видно.


  1. Yeah
    23.10.2017 16:10
    +2

    Для всех адептов merge, вместо тысячи слов:


    image


    1. Yeah
      23.10.2017 16:21
      +1

      Еще немного картинок

      image
      image
      image
      image