Несмотря на распространённость операции git cherry-pick (копирование коммитов) в Git, обычно это не самое лучшее решение. Иногда это меньшее из двух зол, но я ещё не видел ситуации, где оно было бы целесообразно.
Это первая часть из серии статей, которые начинаются объяснением почему копирование это плохо, продолжаются рассказом почему это ужасно, а затем описывают как получить тот же результат, применяя слияние (merge). Я покажу как применить эту технику в случае, когда вам нужно сделать слияние со старыми коммитами (retroactive merge) и когда вы хотите исправить копирование на слияние пока не случилось чего-нибудь плохого.
В копировании задействованы две ветки: донорская (откуда берётся коммит) и принимающая (куда он копируется). Давайте назовём их соответственно master и feature. Для простоты предположим, что копируемый коммит содержит изменение только в одной строке единственного файла. На данной диаграмме каждый коммит помечен содержанием этой строки, а штрихованная стрелка означает само копирование (операцию git cherry-pick
).
Имейте ввиду, что штрихованная стрелка не существует физически в репозитории, она только у нас в головах чтобы отслеживать хронологию.
Есть какой-то общий предок А с коммитом строки "apple". Далее ветки расходятся, коммит F1 лежит в ветке feature, а M1 в master. Эти коммиты не затрагивают рассматриваемую строку, поэтому они всё ещё помечены "apple". Затем мы фиксируем F2 в ветку feature, меняющую содержимое нашей строки на "berry", а затем копируем F2 в ветку master с названием M2.
Пока ничего необычного не происходит.
Время идёт, дерево репозитория обогащается новыми коммитами:
Мы зафиксировали М3 в ветку master и F3 в feature. Оба коммита обходят стороной нашу строку, поэтому она всё ещё равна "berry".
Пришло время слить feature в master. Так как строка одна и та же в обоих ветках, конфликтов не происходит, результат слияния всё ещё "berry".
Это лучший случай, но происходит он редко, особенно в репозиториях, над которыми активно работают много людей.
Давайте рассмотрим другой вариант. После копирования F2 мы фиксируем М3 в master и F3 в feature, но на этот раз F3 меняет нашу строку на "cherry". Такое может быть если программист, работающий над веткой feature, нашёл улучшение в коде, или менеджмент резко потребовал перевести весь проект на "cherry". Какова бы ни была причина, теперь дерево репозитория выглядит вот так:
На этот раз при слиянии feature в master происходит конфликт. Основание трехстороннего слияния (three-way merge) содержит "apple", входящая ветка feature содержит "cherry", а текущая — "berry".
<<<<<<<<<< HEAD (master)
berry
||||||||| merged common ancestors
apple
=========
cherry
>>>>>>>>>> feature
Конфликт возникает потому, что скопированные изменения затёрлись новыми, а сама информация о копировании не сохранилась. Вспомним, что штрихованная стрелка только у нас в голове.
Можно понадеяться на программиста, разрешающего конфликт, что он помнит историю изменений, или спросит команду чтобы понять что верным решением будет принять изменения из feature.
В случае только двух веток шансы на корректное разрешение довольно хорошие (коллизий не так много, программист не сильно устал и т.п). Но что будет если взять три ветки?
Снова начинаем с коммита А, где наша строка равна "apple". Сразу создаём ветку victim на основе A и фиксируем изменения в V1, не охватывающие нашу строку. Из V1 создаём третью ветку feature с той же историей: коммит F1 не охватывает рассматриваемую строку, поэтому пока она везде равна "apple". Тем временем в ветке master появляется новый коммит М1, который тоже не трогает нашу строку.
Продолжаем веселье. В ветке feature фиксируем изменение нашей строки на "berry" как F2, и копируем его в master под именем M2. Затем снова меняем нашу строку уже на "cherry" в feature и фиксируем это как F3. В master появляется новый коммит М3, который не трогает нашу строку, поэтому в master она пока равна "berry".
Тем временем ветка victim и знать не знает про наши "шуры-муры" с копированием из feature в master. В ней фиксируются два новых изменения V2 и V3, оставляющих нашу строку девственно равной "apple".
Всё хорошее должно когда-то заканчиваться, ветка feature сливается в victim, производя на свет фиксацию V4 с нашей строкой, равной "cherry" благодаря наследию из feature.
Расплачиваться за "непотребство" с копированием придётся ветке victim, когда в неё сливают master. Бум! Впервые возникает конфликт: "благородное" изменение F2 встречает своего клонированного двойника M2. Бедняга программист, разрешающий эту коллизию, не имеет понятия о копировании, к тому же он скорее всего уже устал от других (обоснованных) конфликтов, поэтому вряд ли сможет корректно разрешить и этот.
Вкратце, проблема: при git cherry-pick
в дереве появляются две копии одного коммита. Если хотя бы одна из его строк поменяется до слияния её копий, то возникнет невынужденная коллизия. Причём это может произойти и через неделю, и через год. Это значит, что тот, кто будет её разрешать, может попросту не иметь ресурсов для принятия правильного решения (не он копировал, команда полностью поменялась и проч).
Однако, вся эта Санта-Барбара может стать ещё хуже если конфликта не произойдёт!
Почему? Читайте в следующей части.