Почему git cherry-pick вызывает меньше конфликтов, чем git rebase?

Я часто перебазирую. Изредка rebase является особенно проблематичным (много конфликтов слияния), и мое решение в таких случаях заключается в cherry-pick индивид принимает на себя главную ветвь. Я делаю это потому, что почти каждый раз, когда я делаю, количество конфликтов значительно меньше.

Мой вопрос: почему это так?

Почему возникает меньше конфликтов слияния, когда я cherry-pick чем когда я rebase?

В моей ментальной модели rebase и cherry-pick делают то же самое.

Пример перебазирования

A-B-C (master)
   \
    D-E (next)

git checkout next
git rebase master

производит

A-B-C (master)
     \
      D`-E` (next)

а потом

git checkout master
git merge next

производит

A-B-C-D`-E` (master)

Пример выбора вишни

A-B-C (master)
   \
    D-E (next)

git checkout master 
git cherry-pick D E

производит

A-B-C-D`-E` (master)

Насколько я понимаю, конечный результат один и тот же. (D и E теперь находятся на главном сервере с чистой (прямой) историей коммитов.)

Почему последний (сбор вишни) вызовет меньше конфликтов слияний, чем первый (перебазирование)?

ОБНОВЛЕНИЕ ОБНОВЛЕНИЕ ОБНОВЛЕНИЕ

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

Скажем, у меня есть следующее (обратите внимание на дополнительную ветку)

A-B-C (master)
   \
    D-E (next)
       \
        F-G (other-next)

И тогда я делаю следующее

git checkout next
git rebase master
git checkout master
git merge next

Я заканчиваю со следующим

A-B-C-D`-E` (master)
   \ \
    \ D`-E` (next)
     \
      D-E
         \
          F-G (other-next)

Отсюда я либо перебазирую, либо черешню

Пример перебазирования

git checkout other-next
git rebase master 

производит

A-B-C-D`-E`-F`-G` (master)

Пример сбора вишни

git checkout master
git cherry-pick F G

дает тот же результат

A-B-C-D`-E`-F`-G` (master)

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

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

1 ответ

Решение

Обновленный ответ (см. Обновление в вопросе)

Я думаю, что то, что здесь происходит, связано с выбором коммитов для копирования.

Давайте отметим, а затем отложим в сторону тот факт, что git rebase может использовать либо git cherry-pick, или же git format-patch а также git am, чтобы скопировать некоторые коммиты. В большинстве случаев git cherry-pick а также git am должен достичь тех же результатов. (The git rebase документация специально вызывает переименования файлов в исходном коде как проблему для метода cherry-pick, по умолчанию git am метод для неинтерактивного перебазирования. См. Также различные замечания в скобках в оригинальном ответе ниже и комментарии.)

Здесь важно учитывать, какие коммиты нужно скопировать. В ручном методе вы сначала вручную копируете коммиты D а также E в D' а также E' потом копируешь вручную F а также G в F' а также G', Это минимальный объем работы и это то, что мы хотим; единственным недостатком здесь является то, что мы должны делать фиксацию вручную.

Когда вы используете команду:

git checkout <branch> && git rebase <upstream>

вы заставляете Git автоматизировать процесс поиска коммитов для копирования. Это замечательно, когда Git делает это правильно, но не если Git делает это неправильно.

Так как же Git выбирает эти коммиты? Простой, но несколько неправильный ответ в этом предложении (из той же документации):

Все изменения, сделанные коммитами в текущей ветке, но не в , сохраняются во временную область. Это тот же набор коммитов, который будет показан git log <upstream>..HEAD; или git log 'fork_point'..HEAD, если --fork-point активен (см. описание на --fork-point ниже); или git log HEAD если --root опция указана.

--fork-point усложнение является несколько новым, так как git 2.something, но в данном случае оно не "активно", потому что вы указали <upstream> аргумент так и не уточнил --fork-point, Настоящий <upstream> является master оба раза.

Теперь, если вы на самом деле запустить каждый git log--oneline чтобы было лучше):

git checkout next && git log --oneline master..HEAD

а также:

git checkout other-next && git log --oneline master..HEAD

вы увидите, что первый перечисляет коммиты D а также E - отлично! - но второй перечисляет D, E, F, а также G, Ой, D а также E происходят дважды!

Дело в том, что это иногда работает. Ну, я сказал "несколько неправильно" выше. Вот что делает это неправильно, всего два абзаца ниже предыдущей цитаты:

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

Обратите внимание, что HEAD..<upstream> вот обратная сторона <upstream>..HEAD в git log команды, которые мы только что выполнили, где мы видели D -через- G,

При первом ребазе коммитов в git log HEAD..master, поэтому нет коммитов, которые могли бы быть пропущены. Это хорошо, потому что нет коммитов для пропуска: мы копируем E а также F в E' а также F' и это только то, что мы хотим.

Для второй перебазировки, которая происходит после первой перебазировки, git log HEAD..master покажет вам коммиты E' а также F': две копии, которые мы только что сделали. Они потенциально пропущены: они являются кандидатами на рассмотрение пропуска.

"Потенциально пропущено" не "действительно пропущено"

Так как же Git решает, какие коммиты он должен пропустить? Ответ в git patch-id хотя это на самом деле осуществляется непосредственно в git rev-list Это очень модная и сложная команда. Однако ни один из них не описывает это ужасно хорошо, отчасти потому, что это трудно описать. Вот моя попытка в любом случае.:-)

Что Git делает здесь, так это просматривает различия, после удаления идентифицирующих номеров строк, в случае, если патчи идут в несколько разных местах (из-за более ранних патчей, перемещающих строки вверх и вниз в файлах). Он использует те же приемы, что и с файлами, - превращая содержимое файла в уникальные хэши - чтобы превратить каждый коммит в "идентификатор патча". Идентификатор коммита - это уникальный хеш, который идентифицирует один конкретный коммит и всегда тот же один конкретный коммит. Идентификатор исправления - это другой (но все же уникальный для некоторого содержимого) хеш-идентификатор, который всегда идентифицирует "одно и то же" исправление, т. Е. Что-то, что удаляет и добавляет одни и те же элементы различий, даже если оно удаляет и добавляет их из разных местах.

Вычислив ID патча для каждого коммита, Git может сказать: "Ага, коммит D и совершить D' иметь такой же патч-ID! Я должен пропустить копирование D так как D' вероятно, результат копирования D "Это может сделать то же самое для E против E', Это часто работает, но не D всякий раз, когда копия из D в D' требуется ручное вмешательство (устранение конфликтов слияния), и оно также не E всякий раз, когда копия из E в E' требуется ручное вмешательство.

Умный ребаз

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

В данном конкретном случае - копирование D через G - это на самом деле довольно легко, и вы можете сделать это вручную с помощью:

$ git checkout -q other-next && git rebase master
[here rebase copies D, E, F, and G, perhaps with your assistance]

с последующим:

$ git checkout next
[here git checks out "next", so that HEAD is ref: refs/heads/next
 and refs/heads/next points to original commit E]
$ git reset --hard other-next~2

Это работает, потому что other-next имена совершают G' чей родитель F' чей родитель в свою очередь E' и именно здесь мы хотим next В точку. поскольку HEAD относится к отрасли next, git reset подстраивается refs/heads/next указать, чтобы совершить E' и мы закончили.

В более сложных случаях коммиты, которые нужно скопировать точно один раз, не все четко линейны:

                A1-A2-A3  <-- featureA
               /
...--o--o--o--o--o--o--o   <-- master
         \
          *--*--B3-B4-B5   <-- featureB
              \
               C3-C4       <-- featureC

Если мы хотим "перебазировать" все три функции, мы можем перебазировать featureA независимо от двух других - ни один из трех A коммиты зависят от чего-либо "неосновного", кроме ранее A совершает, но скопировать пять B совершает и четыре C коммиты, мы должны скопировать два * коммиты, которые оба B а также C, но скопируйте их только один раз, а затем скопируйте оставшиеся три и два коммита (соответственно) на кончик скопированного коммита.

(Можно было бы написать такой "умный перебаз", но правильно интегрировать его в Git, чтобы git status действительно понимает это, значительно сложнее.)


Оригинальный ответ

Я хотел бы увидеть воспроизводимый пример. В большинстве случаев ваша модель "в голове" должна работать. Хотя есть один известный особый случай.

Интерактивный ребаз или добавление -m или же --merge равнине git rebase на самом деле использует git cherry-pick в то время как неинтерактивная перебазировка по умолчанию использует git format-patch а также git am вместо. Последнее не так хорошо для обнаружения переименования. В частности, если в восходящем потоке имеется переименование файла, 1 --merge Можно ожидать, что rebase будет вести себя по-другому (обычно лучше).

(Также обратите внимание, что оба вида rebase - как патч-ориентированная, так и версия на основе cherry-pick - будут пропускать коммиты, которые git patch-id -идентичен коммитам уже в апстриме, через git rev-list --left-only --cherry-pick HEAD...<upstream> или эквивалент. Смотрите документацию для git rev-list в частности, раздел о --cherry-mark а также --left-right что, я думаю, делает это более понятным. Это должно быть одинаково для обоих видов перебазирования; если вы собираете вишню вручную, это будет зависеть от вас.)


1 Точнее, git diff --find-renames Нужно верить, что там есть переименование. Обычно он верит в это, если таковой существует, но, поскольку он обнаруживает их путем сравнения деревьев, это не идеально.

Другие вопросы по тегам