Во-первых, отметим, что git pull
в основном состоит из двух команд Git.Это означает, что это удобная операция, позволяющая набирать git pull
вместо git fetch
, вводить git .....
.Первая команда всегда git fetch
, а вторая по вашему выбору: по умолчанию git merge
, но вы можете выбрать git rebase
.Чтобы выполнить одну команду, нужно набрать почти столько же, сколько две, когда вы хотите выполнить перебазирование, так что в конце концов это не очень удобно, и я предлагаю использовать отдельные git fetch
и вторую команду, по крайней мере, пока вы не оченьзнаком с Git. 1
Итак, ваш вопрос действительно разрешается проще: Почему в rebase иногда возникают конфликты, которых нет в результате слияния? И есть ответк тому, что на самом деле довольно просто: Rebase - это в основном просто повторный сбор вишни, а сбор вишни - это форма слияния .Поэтому, когда вы объединяетесь, у вас есть одно место, где вы можете получить конфликты.Если вы перебазируете десять коммитов, у вас есть десять мест, где вы можете получить конфликты.Сами конфликты также могут быть разными, но основным фактором здесь является просто масштаб возможностей.
1 В репозиториях с подмодулями git pull
может возвращаться в подмодули, в этом случае это более двух команд, и его удобство становится значительным.Вы также можете настроить git pull
на запуск git rebase
по умолчанию, что сделает его снова доступным даже без субмодулей.Тем не менее, я по-прежнему призываю новых пользователей использовать две отдельные команды - синтаксис git pull
немного странный и немного отличается от почти всех других Git-компонентов, и его слишком легко запутать.Слишком много магии назначено, чтобы вытащить, когда на самом деле все магия от второй команды - и вам нужно изучить слияние, чтобы понять rebase.
Слияние
Хотя реализация полна хитрых мелких поворотов, идея , стоящая за слиянием, проста.Когда мы просим Git объединиться, у нас есть «наша работа» и «их работа».Git должен выяснить, что мы изменили, что они изменили, и объединить эти изменения.
Чтобы сделать это, Git должен найти общую отправную точку,Фиксация - это не набор изменений вообще: это на самом деле снимок.Git может показывать один из этих снимков как отличия от своего непосредственного предшественника, то есть извлекать оба снимка и видеть, что отличается.Поэтому, если мы начали с какого-то коммита с некоторым хеш-идентификатором B
, и они также начали с этого того же коммита:
C--D <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
, тогда Git может сравнитьснимок от B
до наших последних D
и до их последних F
.Что бы ни отличалось в B
-vs- D
, это вещи , мы изменились.Что бы ни отличалось в B
-vs- F
- это вещи , они изменились.Затем Git объединяет изменения, применяет объединенные изменения к снимку из базы слияния B
и фиксирует результат, связывая его не с одним, а с двумя предшественниками:
C--D
/ \
...--A--B G <-- our-branch (HEAD)
\ /
E--F <-- their-branch
Чтобы попасть туда, Git должен запустить:
git diff --find-renames <em>hash-of-B</em> <em>hash-of-D</em>
(что мы изменили) git diff --find-renames <em>hash-of-B</em> <em>hash-of-F</em>
(что они изменили)
Когда Git объединяет эти два различия, могут быть места, где мы и они изменили одинаковые строки того же файла .Если мы не сделаем то же самое изменение для этих строк, Git объявит конфликт и остановит слияние в середине, пока не сделает коммит G
, и заставит насочистить беспорядок и завершить объединение, чтобы создать G
.
Cherry-picking
Идея вишневого пика состоит в том, чтобы copy commit.Чтобы скопировать коммит, мы можем заставить Git превратить его в набор изменений:
git diff --find-renames <em>hash-of-parent</em> <em>hash-of-commit</em>
Затем мы можем взять эти изменения и применить их где-нибудь вручнуюиначе, т. е. для какого-то другого коммита.Например, если у нас есть:
C--D <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
и нам нравится то, что они делали в F
, но пока не хотим самого E
, мы можем посмотреть E
против F
, чтобы увидеть, что они сделали.Мы можем использовать это, чтобы попытаться внести такое же изменение в наш снимок в D
.Затем мы делаем новый коммит - назовем его F'
, что означает копия F
:
C--D--F' <-- our-branch (HEAD)
/
...--A--B
\
E--F <-- their-branch
Но если мы внесли значительные изменения в C
, или онивнесены значительные изменения в E
, может быть трудно получить изменения, которые они внесли с E
-to- F
, в соответствие с тем, что есть в нашем снимке в D
.Чтобы Git помог нам и сделал это копирование автоматически , Git хотел бы знать: что отличается между E
и D
? То есть Git хочет запустить:
git diff --find-renames <em>hash-of-E</em> <em>hash-of-D</em>
(что мы имеем в C
, против E
) git diff --find-renames <em>hash-of-E</em> <em>hash-of-F</em>
(что они изменили в F
)
Но подождите, мы только что видели эту же модель выше, во время git merge
!И на самом деле, это именно то, что Git делает здесь: он использует тот же код , что и git merge
, он просто заставляет базу слияния - которая будет B
для обычного слияния - быть коммитом E
, родитель коммита F
, который мы собираем.Git теперь объединяет наши изменения с их изменениями, применяя объединенный набор изменений к моментальному снимку в базе - в E
- и делая окончательный F'
коммит самостоятельно, но на этот раз как обычный коммит.
Новый коммит также повторно использует коммит сообщение от самого коммита F
, так что новый коммит F'
(который имеет новый идентификатор хеша, отличный от F
)очень напоминает F
: git show
, вероятно, показывает одинаковый или очень похожий список diff для каждого и, конечно, одно и то же сообщение журнала фиксации.
Как и в случае git merge
, этот процесс слияния -то, что я люблю называть сливаться как глагол - может пойти не так.Если что-то идет не так, Git жалуется на конфликт слияния, останавливается с незаконченным слиянием и заставляет вас убрать беспорядок и зафиксировать.Когда вы делаете коммит, Git знает, что вы заканчиваете git cherry-pick
, и в этот момент копирует для вас сообщение о коммите, чтобы сделать F'
.
Повторная выборка повторяется по-черри
Чтобы сделать git rebase <em>target</em>
, в Git:
- перечислены коммиты, которые у вас есть в вашей ветке и которые не достижимы (технический термин: см. Think Like (a) Git из target ;
- обрезает этот список, если необходимо - см. Ниже;
- проверяет коммит target как «отсоединенный HEAD»";
- несколько раз, один коммит за раз, использует
git cherry-pick
для копирования каждого коммита, который есть в списке. 2
После того, как всекоммиты be-copied были успешно скопированы, Git перемещает имя ветви в конец скопированного списка.
Предположим, мы начнем с настройки, аналогичной предыдущей, хотя я перечислюеще несколько коммитов здесь:
C--D--E--F <-- our-branch (HEAD)
/
...--A--B
\
G--H <-- their-branch
Мы запускаем git rebase their-branch
, поэтому Git перечисляет коммиты для копирования: C-D-E-F
, в этом порядке. Затем Git проверяет коммит H
как «отделенная ГОЛОВА»:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch, HEAD
Теперь Git выберет C
, чтобы скопировать его.Если все идет хорошо:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch
\
C' <-- HEAD
Git повторяется для D
, E
и F
.Как только это будет сделано D
и E
, мы находимся в этом состоянии:
C--D--E--F <-- our-branch
/
...--A--B
\
G--H <-- their-branch
\
C'-D'-E' <-- HEAD
После того, как Git завершит копирование F
в F'
, последний шаг перебазирования - вернуть имя our-branch
чтобы указать на последний скопированный коммит и повторно присоединить HEAD
к нему:
C--D--E--F [abandoned]
/
...--A--B
\
G--H <-- their-branch
\
C'-D'-E'-F' <-- our-branch (HEAD)
Каждый вишневый кир выполняет одно трехстороннее слияние, причем основа слияния операции является родительскойиз копируемого коммита, а "наш" коммит - это тот, что на отдельном HEAD
- отметим, что изначально это их коммит H
, и по мере нашего продвижения он становится "их коммитом H
плюс наша работа "со временем.Каждый раз «их» коммит - это наш собственный коммит.Каждый вишневый кир может иметь все обычные конфликты слияния, хотя в большинстве случаев у большинства их нет.
Есть дваo случаи особенно плохие.Один из них, возможно, самый распространенный, - это когда ваши собственные коммиты, например, в списке C-D-E-F
, сами выбирают что-то, что было в цепочке G-H
(которая часто бывает длиннее, чем двакоммиты) - или наоборот, например, возможно H
по существу D'
.
Если бы вы или они смогли сделать этот вишневый отбор легко, без конфликтов, ваша копия, вероятно, выглядит почтиточно так же, или даже на 100% точно, как одна из цепей G-H
.Если это так, Git может распознать, что это является такой копией, и удалить ее из списка «для копирования».В нашем примере, если H
действительно D'
, и Git может это видеть, Git удалит D
из списка, подлежащего копированию, и скопирует только C-E-F
.Но если нет - если, например, им пришлось изменить свою копию D
связкой, чтобы сделать H
- тогда Git будет попытаться скопировать D
и этиизменения почти наверняка будут конфликтовать с их измененными H
.
Если вы объединяете, а не копируете, вы будете сравнивать B
против H
(их) и B
против F
(твой) и вероятность конфликтов, возможно, уменьшена.Даже если есть конфликты, они, вероятно, более очевидны и их легче разрешить.Если конфликты происходят из-за ненужной копии, по моему опыту они выглядят более хитрыми.
Другая распространенная проблема - это когда в вашей цепочке C-D-E-F
последние несколько коммитов были чем-то, что вы делалиспециально для облегчения слияния.То есть кто-то мог сказать что-то вроде: мы изменили подсистему foo, теперь вам нужен третий параметр , и вы добавили третий параметр в F
после того, как вишня выбрала изменение в E
.Вы получите конфликты при копировании C
и D
.Вы можете пропустить копирование E
, потому что оно является выбором вишни, и затем копирование F
не требуется после устранения конфликтов в D
и E
, но это две копии, которыетребуется исправление, которое автоматически отбрасывается, а другое - собственное, ручное отбрасывание.
Итак, в итоге git merge
выполняет одно слияние, но git rebase
выполняет много вишневых пиков, каждый из которыхкоторый - внутренне - слияние, и каждый из которых может привести к конфликтам слияния.Неудивительно, что перебазирование вызывает больше конфликтов!
2 Технически, простой (неинтерактивный) git rebase
часто не использует git cherry-pick
.Вместо этого он, по сути, использует git format-patch ... | git am ...
.Использование git rebase -i
всегда использует git cherry-pick
, а git rebase -m
заставляет неинтерактивный git rebase
использовать git cherry-pick
.Тот факт, что простая перебазировка позволяет избежать этого, в основном является пережитком древнего (до 2008 года или около того, вероятно) Git, до того, как вишневый пикник научился выполнять правильное трехстороннее слияние.
The git am
step использует -3
, поэтому в случае неудачи патча Git «отступит» к трехстороннему слиянию.Результат обычно тот же, но метод format-patch-pipe-to-am никогда не находит переименованные файлы.Это делает стиль форматирования патча быстрее , но не так хорошо.