Несмотря на распространённость операции git cherry-pick (копирование коммитов) в Git, обычно это не самое лучшее решение. Иногда это меньшее из двух зол, но я ещё не видел ситуации, где оно было бы целесообразно.
Это первая часть из серии статей, которые начинаются объяснением почему копирование это плохо, продолжаются рассказом почему это ужасно, а затем описывают как получить тот же результат, применяя слияние (merge). Я покажу как применить эту технику в случае, когда вам нужно сделать слияние со старыми коммитами (retroactive merge) и когда вы хотите исправить копирование на слияние пока не случилось чего-нибудь плохого.
В копировании задействованы две ветки: донорская (откуда берётся коммит) и принимающая (куда он копируется). Давайте назовём их соответственно master и feature. Для простоты предположим, что копируемый коммит содержит изменение только в одной строке единственного файла. На данной диаграмме каждый коммит помечен содержанием этой строки, а штрихованная стрелка означает само копирование (операцию git cherry-pick
).
Внимание! Все стрелки перевёрнуты! A <- B означает коммит B следует за коммитом А.
Имейте ввиду, что штрихованная стрелка не существует физически в репозитории, она только у нас в головах чтобы отслеживать хронологию.
Есть какой-то общий предок А с коммитом строки "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
в дереве появляются две копии одного коммита. Если хотя бы одна из его строк поменяется до слияния её копий, то возникнет невынужденная коллизия. Причём это может произойти и через неделю, и через год. Это значит, что тот, кто будет её разрешать, может попросту не иметь ресурсов для принятия правильного решения (не он копировал, команда полностью поменялась и проч).
Однако, вся эта Санта-Барбара может стать ещё хуже если конфликта не произойдёт!
Почему? Читайте в следующей части.
amarao
Скажите, для проектов какого размера действует ваша рекомендация? Насколько я знаю, в openstack'е, например (в котором >10k user faced изменений на версию — и мы не про коммиты, мы про release notes), там вот, в openstack'е как раз очень приветствуют cherry-pick, потому что он приносит в бранч только то, что нужно, и ничего лишнего.
egryaznov Автор
Возможно они не сливают потом ветки, в которые копируют коммиты. Вся суть статьи в том, что если смешивать эти две операции, то возникнут проблемы. Если даже целые VCS, основанные на модели патчей, это Darcs, pijul. Последний так вообще претендует на математическую корректность, основываясь на теории категорий
amarao
Да. Мастер не вливается в stable, потому что мастер бежит очень быстро, а стабильные бранчи держатся долго.