Это довольно долго, поэтому не стесняйтесь пропускать уже известные разделы (или прокручивать до конца).Каждый раздел содержит информацию о настройке, чтобы объяснить, что происходит или что мы делаем, в более поздних.
Знакомство с битами
Позвольте мне начать с перерисовки этого графика (который я считаюэто своего рода частичный граф, но он содержит ключевые фиксации, которые нам нужны), как я предпочитаю:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-------------o----M2---M3--------R2 <---- branch-T1
\ \ /
F0--fc1---fc2---M1 <------------------- branch-F
Здесь имена ветвей равны branch-S
, branch-T1
,и branch-F
, и эти имена в настоящее время идентифицируют коммиты, чьи идентификаторы хеша являются чем-то непроизносимым и недоступным для запоминания людьми, но мы называем sc5
, R2
и M1
соответственно.Любые узлы o
являются коммитами, которые особо не различаются и могут фактически представлять произвольное количество коммитов.Именованные fc<number>
представляют собой некоторый набор коммитов в ветви функций, а коммиты M<number>
являются слияниями.Я переименовал первые коммиты S0
, T0
и F0
, чтобы отличать их от названий ветвей.
Некоторые слияния выполняются вручную:
$ git checkout <branch-name>
$ git merge [options] <other-branch>
... fix up conflicts if necessary, and git commit (or git merge --continue)
Другие слияниясделаны программным обеспечением и происходят только при отсутствии конфликтов.Коммиты R
запускаются:
git checkout <branch>
git revert -m 1 <hash ID of some M commit>
, где <branch>
было T1
или S
, а -m 1
потому, что вы всегда должны указывать git revert
, какого родителя использоватьпри возврате слияния, и он почти всегда является родительским # 1.
Создание коммитов перемещает имя ветки
Самый простой граф коммитов Git - это прямая линия с одним именем ветки, обычно master
:
A--B--C <-- master (HEAD)
Здесь мы должны упомянуть индекс Git .Индекс, пожалуй, лучше всего описать как место, где Git создает следующий коммит.Первоначально он содержит каждый файл, сохраненный в текущем коммите (здесь C
): вы извлекаете этот коммит, заполняя индекс и рабочее дерево файлами из коммита C
.Имя master
указывает на этот коммит, а имя HEAD
присоединяется к имени master
.
Затем вы изменяете файлы в рабочем дереве, используя git add
, чтобы скопировать их обратно виндекс, используйте git add
, чтобы скопировать новые файлы в индекс, если это необходимо, и выполните git commit
.Создание нового коммита работает путем замораживания этих индексных копий в моментальный снимок.Затем Git добавляет метаданные моментального снимка - ваше имя и адрес электронной почты, ваше сообщение журнала и т. Д. - вместе с идентификатором хэша текущего коммита, чтобы новый коммит снова указывал на существующий коммит.Результат:
A--B--C <-- master (HEAD)
\
D
с новым коммитом, с его новым уникальным идентификатором хеша, просто зависающим в воздухе, и ничего не запоминающим.Итак, последний шаг создания нового коммита заключается в записи хеш-идентификатора нового коммита в имя ветви:
A--B--C--D <-- master (HEAD)
, и теперь текущий коммит равен D
, аИндекс и текущее соответствие.Если вы git add
добавили все файлы в рабочем дереве, это также соответствует текущему коммиту и индексу.Если нет, вы можете git add
больше файлов и зафиксировать снова, заставив имя master
указать на новый коммит E
и так далее.В любом случае (единственным) родителем нового коммита будет то, что было текущим коммитом .
О слияниях
Позвольте мне описать, как на самом деле работает git merge
.В некоторых случаях и в некоторых случаях это очень просто, и давайте начнем с самого простого случая истинного слияния.Рассмотрим график, который выглядит следующим образом:
o--...--L <-- mainline (HEAD)
/
...--o--*
\
o--...--R <-- feature
Мы запустили git checkout mainline; git merge feature
, поэтому мы говорим Git объединить ветку feature
/ commit R
в ветку mainline
/ commit L
,Для этого Git сначала должен найти коммит merge base .Грубо говоря, база слияния - это «ближайший» коммит, общий для , достижимый из , обоих ветвей.В этом простом случае мы начинаем с L
и возвращаемся к более старым коммитам, начинаем с R
и идем назад, и первое место, с которым мы встречаемся, это коммит *
, так что это база слияния.
(Подробнее о достижимости см. Думай как (а) Git .)
ХаВ поисках базы слияния Git необходимо превратить снимки L
(слева / локально / --ours
) и R
(справа / удаленно / --theirs
) в наборы изменений.Эти наборы изменений сообщают Git, что мы сделали на mainline
с базы слияния *
и что они сделали на feature
с базы слияния.Все эти три коммита имеют идентификаторы хеша, которые являются реальными именами трех коммитов, поэтому Git может выполнить внутренний эквивалент:
git diff --find-renames <hash-of-*> <hash-of-L> # what we changed
git diff --find-renames <hash-of-*> <hash-of-R> # what they changed
Слияние просто объединяет два набораизменяет и применяет объединенный набор к файлам в снимке в *
.
Когда все идет хорошо, Git делает новый коммит обычным способом, за исключением того, что новый коммит имеет two родители.Это заставляет текущую ветку указывать на новый коммит слияния:
o--...--L
/ \
...--o--* M <-- mainline (HEAD)
\ /
o--...--R <-- feature
Первый родительский элемент M
равен L
, а второй - R
.Вот почему при возврате почти всегда используется родитель № 1, и поэтому git log --first-parent
только «видит» ветвь магистрали, проходя от M
до L
, полностью игнорируя ветвь R
.(Обратите внимание, что слово ответвление здесь относится к структуре графа, а не к ответвлению ИМЯ подобно feature
: на этом этапе мы можем удалить name feature
полностью. См. Также Что именно мы подразумеваем под «ветвью»? )
Когда что-то пойдет не так
Слияние будет остановлено с конфликт слияния , если два набора изменений перекрываются "плохим способом".В частности, предположим, что base-vs-L говорит об изменении строки 75 файла F
, а base-vs-R также говорит об изменении строки 75 файла F
.Если оба набора изменений говорят о том, что они делают то же самое изменение , Git согласен с этим: комбинация двух изменений состоит в том, чтобы сделать изменение один раз.Но если они говорят, чтобы сделать различные изменения, Git объявляет конфликт слияния.В этом случае Git остановится после того, как сделает все возможное, и заставит вас убрать беспорядок.
Поскольку имеется три входа, Git на этом этапе оставит все три версий файла F
в указателе.Обычно индекс содержит одну копию каждого файла для фиксации, но на этом этапе разрешения конфликтов он может содержать до трех копий.(Часть «до» заключается в том, что у вас могут быть другие виды конфликтов, в которые я не буду вдаваться здесь из-за нехватки места.) Между тем, в рабочем дереве копия файла F
,Git оставляет свое приближение для слияния с двумя или всеми тремя наборами строк в файле рабочего дерева с маркерами <<<<<<<
/ >>>>>>>
вокруг них.(Чтобы получить все три, установите merge.conflictStyle
на diff3
. Я предпочитаю этот режим для разрешения конфликтов.)
Как вы уже видели, вы можете разрешать эти конфликты любым удобным для вас способом.Git предполагает, что все, что вы делаете, - это правильный способ решения проблемы: это приводит к точному получению окончательных объединенных файлов или к отсутствию файлов в некоторых случаях.
Что бы вы ни делали,хотя окончательное слияние - при условии, что вы не прерываете его и не используете один из вариантов слияния без слияния-y - все равно дает тот же результат на графике и независимо от того, что вы добавили в индекс, путем разрешенияконфликты, является результатом слияния.Это новый снимок в коммите слияния.
Более сложные базы слияния
Когда график очень прост, как показано выше, базу слияния легко увидеть.Но графики не остаются простыми, а ваши нет.База слияния для графа, в котором есть некоторые слияния, сложнее.Рассмотрим, например, только следующий фрагмент:
...--sc4----M4---R1
\ /
...--M2---M3--------R2
Если R1
и R2
являются двумя коммитами, то какова их база слияния?Ответ M3
, а не sc4
.Причина в том, что, хотя M3
и sc4
являются обоими коммитами, которые достижимы, начиная с R1
и R2
и работая в обратном направлении, M3
"ближе" к R2
(один шаг назад).Расстояние от R1
до M3
или sc4
составляет два прыжка - перейдите к M4
, затем вернитесь еще на один шаг, но расстояние от R2
до M3
составляет один прыжок, а расстояние отR2
до sc4
- это два прыжка.Таким образом, M3
«ниже» (в терминах графа) и поэтому выигрывает соревнование.
(К счастью, в вашем графе нет случаев, когда есть ничья. Если равен ничьюПодход Git по умолчанию - объединение всех связанных коммитов, по два за один раз, для создания «виртуальной базы слияния», которая фактически является фактической, хотя и временной, коммитом, а затем использует этот временный коммит, созданный путем слияния баз слияния.Это стратегия рекурсивная , которая получила свое название от того факта, что Git рекурсивно объединяет базы слияния для получения базы слияния. Вместо этого вы можете выбрать стратегию resol * , которая просто выбираетодна из баз, на первый взгляд, случайная - какая бы база ни появлялась в начале алгоритма. Преимущества в этом нет редко: рекурсивный метод обычно либо делает то же самое, либо является улучшением по сравнению со случайным выбором победителя.)
Ключевым выводом здесь является то, что внесение изменений коммита слияния, которые фиксируют future merges, выберет какбаза слияния эйр .Это важно даже при выполнении простых слияний, поэтому я выделил их жирным шрифтом.Вот почему мы делаем коммиты слияния, в отличие от сквош-операций слияния, которые не являются слияниями.(Но слияние сквоша все еще полезно, как мы увидим чуть позже.)
Представляем проблему: что пошло не так (чтобы вы могли избежать этого в будущем)
С учетом вышесказанногоКстати, теперь мы можем взглянуть на реальную проблему.Давайте начнем с этого (немного отредактировано для использования обновленных имен коммитов и веток):
Я слил branch-T1
в branch-F
(M1
), затем branch-F
в branch-T1
(M2
).
Я предполагаю, что слияние fc2
(как тогдашний конец branch-F
) и o
(как тогдашний конец branch-T1
) прошло хорошои Git смог сделать M1
самостоятельно.Как мы видели ранее, слияние действительно основано не на ветвях , а на коммитах .Это создание нового коммита, который корректирует имена веток.Итак, это создало M1
, так что branch-F
указывало на M1
.M1
сам указывал на существующую подсказку branch-T1
- коммита, который я сейчас пометил o
- как своего второго родителя, с fc2
в качестве первого родителя.Git вычисляет правильное содержимое для этого коммита, git diff
-ing содержимое T0
, базы слияния, против o
и против fc2
:
T0-------------o <-- branch-T1
\
F0--fc1---fc2 <--- branch-F (HEAD)
Когда все идет хорошо, Git теперь делает M1
самостоятельно:
T0-------------o <-- branch-T1
\ \
F0--fc1---fc2---M1 <--- branch-F (HEAD)
Теперь вы git checkout branch-T1
и git merge --no-ff branch-F
(без --no-ff
Git просто сделает ускоренную перемотку вперед, чтоне то, что на картинке), поэтому Git находит базу слияния o
и M1
, что само по себе o
.Это объединение легко: разница от o
до o
- ничто, и ничто плюс разница от o
до M1
равно содержанию M1
.Так что M2
, как снимок, точно такой же, как M1
, и Git легко его создает:
T0-------------o----M2 <-- branch-T1 (HEAD)
\ \ /
F0--fc1---fc2---M1 <--- branch-F
Пока все хорошо, но теперь все начинает идти не так:
В ветке T1
был один файл, который имел конфликты слияния с S
... Учитывая проблемы, с которыми я сталкивался в прошлом, с разрешениями конфликтов слияния не вел себя так, как я ожидал, ядумал, что попробую что-то новое: объединить только конфликтующий файл из S
в T1
, решить конфликт слияния там, удалить все остальные файлы из объединения, а затем позволить непрерывной интеграции объединить все до S
.
Итак, что вы сделали на этом этапе:
git checkout branch-T1
git merge branch-S
which остановлен конфликтом слияния.График в этой точке такой же, как приведенный выше, но с некоторым дополнительным контекстом:
S0--sc1---sc2---sc3-----sc4 <-- branch-S
\
T0-------------o----M2 <-- branch-T1 (HEAD)
\ \ /
F0--fc1---fc2---M1 <-- branch-F
Операция слияния находит базу слияния (S0
), которая отличается от двух коммитов с наконечником (M2
и sc4
), объединяет полученные изменения и применяет их к содержимому S0
.Один конфликтующий файл теперь находится в индексе как три входные копии и в рабочем дереве как усилие Git по слиянию, но с маркерами конфликта.Между тем все не конфликтующие файлы находятся в индексе и готовы к замораживанию.
Увы, теперь вы удаляете некоторые файлы (git rm
) во время конфликтующего объединения.Это удаляет файлы из индекса и рабочего дерева.Результирующий коммит, M3
, скажет, что правильный способ объединения коммитов M2
и sc4
на основе merge-base S0
- удалить эти файлы.(Это, конечно, было ошибкой.)
Это автоматически объединено с S
(M4
).
Здесь, я предполагаю, это означает, что системаиспользуя любое предварительно запрограммированное правило, оно имело эквивалент:
git checkout branch-S
git merge --no-ff branch-T1
, который нашел базу слияния коммитов sc4
(наконечник branch-S
) и M3
, то есть M3
, так же, как база слияния o
и M1
была M1
ранее.Таким образом, новый коммит, M4
, соответствует M3
по содержанию, и в этот момент мы имеем:
S0--sc1---sc2---sc3-----sc4----M4 <-- branch-S
\ \ /
T0-------------o----M2---M3 <-- branch-T1
\ \ /
F0--fc1---fc2---M1 <-- branch-F
Я сразу заметил, что исключая эти ~ 200 файлов, похоже, стерлиполностью меняется, что равняется примерно месяцу работы между двумя командами.Я (неправильно) решил, что лучший способ действий - действовать быстро и отменить коммит слияния M4
и M3
до того, как моя ошибка попала в чьи-либо локальные репозитории.Сначала я вернул M4
(R1
), и как только это было совершено, я вернулся M3
(R2
).
На самом деле, это было прекрасно!Он получает правильный контент , что очень полезно, когда вы делаете это немедленно.Использование git checkout branch-s && git revert -m 1 branch-S
(или git revert -m 1 <hash-of-M4>
) для создания R1
из M4
в основном отменяет слияние с точки зрения содержимого, так что:
git diff <hash-of-sc4> <hash-of-R1>
вообще ничего не должно производить.Аналогично, используя git checkout branch-T1 && git revert -m 1 branch-T1
(или то же самое с хэшем) для создания R2
из M3
отмен, которые объединяются с точки зрения содержимого: сравнивая M2
и R2
, вы должны увидеть идентичное содержимое.
Отмена слияния отменяет содержимое , но не history
Теперь проблема в том, что Git считает, что все изменения в вашей ветви функций включены правильно.Любой git checkout branch-T1
или git checkout branch-S
, за которым следует git merge <any commit within branch-F>
, будет смотреть на график, следуя обратным ссылкам от коммита к коммиту, и увидит, что этот коммит в branch-F
- например, fc2
или M1
- уже объединено .
Хитрость для их получения заключается в создании нового коммита, который делает то же самое, что последовательность коммитов от F0
до M1
делает, это не уже объединено.Самый простой - хотя и самый уродливый - способ сделать это - использовать git merge --squash
.Более сложный и, возможно, лучший способ сделать это - использовать git rebase --force-rebase
для создания новой ветви функций.(Примечание: у этой опции есть три варианта написания, и самый простой для ввода - -f
, но в описании Линуса Торвальдса - --no-ff
. Я думаю, что наиболее запоминающейся является версия --force-rebase
,но я бы на самом деле использовал -f
сам.)
Давайте кратко рассмотрим оба, а затем рассмотрим, что использовать и почему.В любом случае, как только вы закончите, на этот раз вы должны будете правильно объединить новые коммиты без удаления файлов;но теперь, когда вы знаете, что на самом деле делает git merge
, сделать это будет намного проще.
Мы начнем с создания нового имени ветви.Мы можем повторно использовать branch-F
, но я думаю, что будет понятнее, если мы этого не сделаем.Если мы хотим использовать git merge --squash
, мы создаем это новое имя ветви, указывающее на фиксацию T0
(игнорируя тот факт, что после T0
есть коммиты - помните, что любое имя ветви может указывать на любой коммит):
T0 <-- revised-F (HEAD)
\
F0--fc1--fc2--M1 <-- branch-F
Если шмы хотим использовать git rebase -f
, мы создаем это новое имя, указывающее на коммит fc2
:
T0-----....
\
F0--fc1--fc2--M1 <-- branch-F, revised-F (HEAD)
Мы делаем это с:
git checkout -b revised-F <hash of T0> # for merge --squash method
или:
git checkout -b revised-f branch-F^1 # for rebase -f method
в зависимости от того, какой метод мы хотим использовать.(Суффикс ^1
или ~1
- вы можете использовать любой из них - исключает сам M1
, возвращаясь на шаг первого родителя к fc2
. Идея здесь состоит в том, чтобы исключить коммит o
и любые другие достижимые коммитыс o
. Не должно быть никаких других слияний в branch-F
вдоль этого нижнего ряда коммитов, здесь.)
Теперь, если мы хотим использовать «слияние в сквош» (которое использует механизм слияния в Gitбез слияния commit ), мы запускаем:
git merge --squash branch-F
При этом используется наш текущий коммит плюс подсказка branch-F
(commit M1
) в качестве левого и правогосторон слияния, находя их общий коммит в качестве базы слияния.Обычный коммит, конечно, просто F0
, поэтому слияние result является снимком в M1
.Тем не менее, новая сделанная фиксация имеет только одного родителя: это вообще не коммит слияния, и он выглядит так:
fc1--fc2--M1 <-- branch-F
/
F0-------------F3 <-- revised-F (HEAD)
Снимок вF3
соответствует этому в M1
, но сам коммит является новым.Он получает новое сообщение о коммите (которое вы можете редактировать), и его эффект, когда Git смотрит на F3
как коммит, заключается в том, чтобы сделать тот же набор изменений, сделанных с F0
до M1
.
Если мы выберем метод rebase, мы теперь запустим:
git rebase -f <hash-of-T0>
(Вместо этого можно использовать хеш o
, то есть branch-F^2
, то есть второй родительский элемент M1
. Вв этом случае вы можете начать с revised-F
, указывающего на M1
. Вероятно, это то, что я бы сделал, чтобы избежать необходимости вырезать и вставлять много хеш-идентификаторов с потенциальными опечатками, но не очевидно, как это работает, если вывыполнил много упражнений по манипулированию графами.)
То есть мы хотим скопировать коммиты F0
- fc2
включительно в новые коммиты с новыми идентификаторами хэшей.Это то, что будет делать git rebase
(см. Другие ответы StackOverflow и / или описание Линуса выше): мы получаем:
F0'-fc1'-fc2' <-- revised-F (HEAD)
/
T0-----....
\
F0--fc1--fc2--M1 <-- branch-F
Теперь, когда у нас есть revised-F
, указывающая либо на один коммит (F3
) или цепочку коммитов (цепочка, оканчивающаяся на fc2'
, копия fc2
), мы можем git checkout
какую-то другую ветку и git merge revised-F
.
Основываясь на комментариях, вот двапути для повторного слияния
На этом этапе я предполагаю, что у вас есть результат слияния сквоша (коммит с одним родителем, который не является слиянием, но содержит нужный снимок, который я называю F3
здесь).Нам также нужно немного пересмотреть перерисованный график, основываясь на комментариях, которые указывают, что было больше слияний в branch-F
:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2 <---- branch-T1
\ \ \ /
F0--fc1-o-fc2---M1 <--------------- branch-F
Теперь мы добавим ветку revised-F
, которая должна иметьодин коммит, который является потомком либо F0
, либо T0
.Не важно, какой.Поскольку я использовал F0
ранее, давайте рассмотрим это здесь:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2 <---- branch-T1
\ \ \ /
F0--fc1-o-fc2---M1 <--------------- branch-F
\
---------------------------------F3 <-- revised-F
Содержимое коммита F3
соответствует содержимому M1
(поэтому git diff branch-F revised-F
ничего не говорит), но родительский элемент F3
здесь F0
.(Примечание: существуют быстрые способы создания F3
с использованием git commit-tree
, но пока оно уже существует и соответствует M1
по содержанию, мы можем просто использовать его.)
Если мы сейчас это сделаем:
git checkout branch-T1
git merge revised-F
Git найдет базу слияния между коммитом R2
(вершина branch-T1) и F3
(вершина revised-F
).Если мы перейдем по всем обратным (левым) ссылкам с R2
, мы сможем добраться до T0
через M3
, затем M2
, затем до некоторого числа o
с и, наконец, T0
, или мы сможем добраться до F0
через M3
, затем M2
, затем M1
, затем fc2
и обратно до F0
.Между тем мы можем перейти от F3
прямо к F0
всего за один прыжок, поэтому база слияния, вероятно, равна F0
.
(Чтобы подтвердить это, используйте git merge-base
:
git merge-base --all branch-T1 revised-F
Это напечатает один или несколько хеш-идентификаторов, по одному для каждой базы слияния. В идеале есть только одна база слияния, которая является коммитом F0
.)
Git теперь будет запускать два git diff
с, чтобы сравнить содержимое F0
с F3
- то есть все, что мы сделали для реализации этой функции - и сравнить содержимое F0
с содержимым R2
, на кончике branch-T1
.Мы получим конфликты, когда оба различия изменяют одни и те же строки одних и тех же файлов.В другом месте Git возьмет содержимое F0
, применит объединенные изменения и оставит результат готовым к фиксации (в индексе).
Разрешение этих конфликтов и фиксация даст вам новый коммит, который приведет кв:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2-----M6 <---- branch-T1
\ \ \ / /
F0--fc1-o-fc2---M1 <-- branch-F /
\ /
---------------------------------F3 <-- revised-F
Теперь M6
, возможно, может объединяться в branch-S
.
В качестве альтернативы, мы можем объединиться непосредственно в branch-S
.Менее очевидно, какой коммит является базой слияния, но, вероятно, снова F0
.Вот снова тот же рисунок:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2 <---- branch-T1
\ \ \ /
F0--fc1-o-fc2---M1 <--------------- branch-F
\
---------------------------------F3 <-- revised-F
Начиная с коммита sc5
, мы работаем в обратном направлении до M5
до R2
, и мы сейчас находимся в той же ситуации, в которой были раньше.Таким образом, мы можем git checkout branch-S
и выполнить то же слияние, разрешить похожие конфликты - на этот раз мы сравниваем F0
с sc5
, а не с R2
, поэтому конфликты могут немного отличаться - и в конечном итоге фиксируем:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5----M6 <-- branch-S
\ \ / / /
T0-----o-------o----M2---M3--------R2 <------ / -- branch-T1
\ \ \ / /
F0--fc1-o-fc2---M1 <-- branch-F /
\ /
---------------------------------------F3 <-- revised-F
Чтобы убедиться, что F0
является базой для слияния, используйте git merge-base
как раньше:
git merge-base --all branch-S revised-F
и, чтобы увидеть, что вам нужно слить, выполните два git diff
s от базы слияния до двух советов.
(Что делать слияние - решать вам.)