Ветви тоже не имеют значения, не в том смысле, в котором вы могли бы подумать.Значение ветвей зависит от того, что вы подразумеваете под ответвлением (см. Что именно мы подразумеваем под «ответвлением»? ).Помните, что каждый коммит, идентифицируемый уникальным хеш-идентификатором, имеет:
Здесь важен выделенный жирным шрифтом элемент - хэш-идентификатор родителя коммита - здесь формируется график.Две метки времени, автор и коммиттер, по существу произвольны: вы можете переопределить одну или обе в момент выполнения коммита.(Как только коммит сделан, он становится частью данных коммита и не может быть изменен. Никакая часть коммита не может быть изменена: самое большее, вы можете сделать копию этого коммита, с некоторымиэлемент (ы) изменился и получил новый коммит с новым уникальным хеш-идентификатором и убедил всех начать использовать новый коммит вместо старого.)
(рисование соединителей из коммитов для их родителей, так как стрелки могут стать проблемой, поэтомуЯ переключаюсь на --
вместо этого здесь).Затем вы создаете новый файл git add
и делаете новый коммит C2
.Это заставляет имя A
идентифицировать коммит C2
, чей родитель C1
:
в 1, вы создаете ветку B из A ...
* 1080.*
(через, например: git checkout -b B A
.)
Теперь у вас есть:
...--C1--C2 <-- A, B (HEAD)
... и измените файл на [иметь три строки для чтенияA
B
и C
]
Если вы запустили git add
и git commit
, у вас теперь есть новый коммит, и B
идентифицирует его;родитель нового коммита C2
:
...--C1--C2 <-- A
\
C3 <-- B (HEAD)
в 2, кто-то еще создает C с A ...
(например, git checkout -b C A
).Итак, давайте нарисуем это:
...--C1--C2 <-- A, C (HEAD)
\
C3 <-- B
и изменим файл на [четыре строки, A
B
C
и D
, в этом порядке]
Предполагая обычное добавление и принятие, мы получаем коммит C4
, на который C
указывает:
C4 <-- C (HEAD)
/
...--C1--C2 <-- A
\
C3 <-- B
на 3, они объединяют C в A
На этом этапе становится важным выяснить , какие команды (ы) они используют для этого, потому что commit C4
строго опережает commit C2
.Это означает, что:
git checkout A
git merge C
выполнит слияние fast-forward , в результате чего:
C4 <-- A (HEAD), C
/
...--C1--C2
\
C3 <-- B
Если они используют git merge --no-ff C
, однако, Git будетвыполнить истинное слияние (которое, тем не менее, тривиально), в результате чего:
C4 <-- C
/ \
...--C1--C2----C5 <-- A (HEAD)
\
C3 <-- B
whПрежде чем содержимое файла, нового в C2
, хранящегося в коммите C5
, соответствует содержимому файла с таким же именем в C4
. Тривиальное слияние просто берет содержимое коммита «график-позже», через очевидное упрощение общего правила, которое мы увидим через мгновение.
В любом случае имя A
указывает на коммит, в котором файл имеет трехстрочную форму, но в одном случае A
указывает на другой коммит, чем C
.
в 4, кто-то создает ветку D от A
То есть git checkout -b D A
. Чтобы нарисовать это, нам нужно знать, с какого из двух графиков начать снова. Хотя какое-то время это не будет иметь значения, поэтому сейчас я выберу тот, который объединен с ускоренной перемоткой вперед:
C4 <-- A, C, D (HEAD)
/
...--C1--C2
\
C3 <-- B
и изменяет файл на [только одну строку A
снова]
$ git checkout -b D A
Switched to a new branch 'D'
$ echo A > file
$ git add file
$ git commit -m c7
[D 5724954] c7
1 file changed, 3 deletions(-)
Я позвонил этому C7
, чтобы покинуть какую-то комнату, пропустив несколько идентификаторов коммитов, потому что мы собираемся на некоторое время прекратить его рисовать; но теперь у нас есть:
C7 <-- D (HEAD)
/
C4 <-- A, C
/
...--C1--C2
\
C3 <-- B
в 5, первоначальный человек, создающий ветку, сливает B в A
На этот раз слияние в ускоренном режиме невозможно, независимо от того, какая структура графа фиксации является входной. Мы получаем настоящее слияние, которое не является тривиальным слиянием.
Давайте теперь рассмотрим алгоритм слияния
На этом этапе я немного перемотаю свой демо-репозиторий и перенесу ускоренное слияние, чтобы вернуться к первой ситуации:
$ git checkout A
Switched to branch 'A'
$ git reset --hard 6626cd2
HEAD is now at 6626cd2 c2
$ git merge C
Updating 6626cd2..7af3a02
Fast-forward
file | 3 +++
1 file changed, 3 insertions(+)
$ git log --all --decorate --oneline --graph
* 5724954 (D) c7
* 7af3a02 (HEAD -> A, C) c4
| * 5915b1d (B) c3
|/
* 6626cd2 c2
* 80e22c8 (master) initial
или в моем предпочтительном формате:
C7 <-- D
/
C4 <-- A (HEAD), C
/
...--C1--C2
\
C3 <-- B
У нас есть коммит C4
в индексе и рабочем дереве, а наш HEAD
присоединен к имени A
. Теперь мы запускаем git merge B
или git merge <hash of C3>
- не имеет значения, какой именно. Git просматривает этот график, чтобы найти узел самого низкого общего предка, который в этом случае, очевидно, является коммитом C2
.
Алгоритм объединения теперь, по сути, запускает две git diff
команды:
git diff --find-renames <hash-of-C2> <hash-of-C4> # what we changed
git diff --find-renames <hash-of-C2> <hash-of-C3> # what they changed
Предполагая, что «мы» (C2
-vs- C4
) изменили только один файл, найденное здесь изменение будет следующим: добавить три строки, B
C
D
, в конец файла. Аналогично, найденное изменение для "их" работы: , добавьте две строки, B
и C
, в конце файла.
Задача Git - объединить эти два изменения. Но они конфликтуют: невозможно добавить только две строки и одновременно добавить три строки. Так что Git останавливается с конфликтом слияния:
$ git checkout A
Already on 'A'
$ git merge B
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
Следовательно, ваше следующее утверждение является проблемой:
означает, что на A файл:
A
B
C
потому что, как мы видим, пока мы не исправим конфликт слияния, это не то, что находится в файле:
$ cat file
A
<<<<<<< HEAD
B
C
D
||||||| merged common ancestors
=======
B
C
>>>>>>> B
(у меня merge.conflictStyle
установлено на diff3
, здесь создается раздел ||||||| merged common ancestors
, хотя в этом случае он в любом случае пуст).
Конечно, человек, осуществляющий слияние, может установить содержимое так, как вы предлагаете, или использовать -X ours
или -X theirs
, чтобы выбрать одно из двух изменений, имеющих приоритет. Но по умолчанию это конфликт слияния, и в этом случае вы, человек, должны решить его правильно, для любого определения правильного в вашем случае. Давайте выберем A
B
C
здесь:
$ git checkout --theirs file
$ cat file
A
B
C
$ git add file
$ git commit -m c5
[A eec968d] c5
График теперь выглядит так:
C4 <-- C
/ \
...--C1--C2 C5 <-- A (HEAD)
\ /
C3 <-- B
и содержание file
в коммите C5
- это те, которые мы выбрали.
Что, если мы форсируем тривиальное слияние? Почему тривиальное слияние так тривиально?
Мы можем сбросить наши условия снова и вернуться, прежде чем мы позволим A
перемотать вперед:
$ git reset --hard HEAD~2
HEAD is now at 6626cd2 c2
$ git log --all --decorate --oneline --graph
* 5724954 (D) c7
* 7af3a02 (C) c4
| * 5915b1d (B) c3
|/
* 6626cd2 (HEAD -> A) c2
* 80e22c8 (master) initial
Перерисовывая этот график по-моему, мы вернулись к:
C7 <-- D
/
C4 <-- C
/
...--C1--C2 <-- A (HEAD)
\
C3 <-- B
Давайте на этот раз используем git merge --no-ff C
и рассмотрим, что делает обычный алгоритм. Мы находим два коммитных наконечника, C2
и C4
, и находим их базу слияния - наименее общего предка, который является C2
. Затем мы делаем две разницы:
git diff --find-renames C2 C2 # what we changed (nothing!)
git diff --find-renames C2 C4 # what they changed
Затем мы объединяем «ничто» с тем, что они изменили Результатом этого объединения, конечно же, являются их изменения; они применяются к C2
, и в результате получается коммит, содержимое которого соответствует содержимому C4
:
$ git merge --no-ff C -m c5
Merge made by the 'recursive' strategy.
file | 3 +++
1 file changed, 3 insertions(+)
$ git log --all --decorate --oneline --graph
* b47cf02 (HEAD -> A) c5
|\
| | * 5724954 (D) c7
| |/
| * 7af3a02 (C) c4
|/
| * 5915b1d (B) c3
|/
* 6626cd2 c2
* 80e22c8 (master) initial
Обратите внимание, что содержимое C4
и C5
соответствует:
$ git diff 7af3a02 b47cf02
$
и график теперь соответствует прогнозу (хотя рисунок Git трудно читать):
C7 <-- D
/
C4 <-- C
/ \
...--C1--C2----C5 <-- A (HEAD)
\
C3 <-- B
Если мы сейчас запустим git merge B
, мы обязательно призовем к настоящему слиянию - невозможно переместить имя A
«вперед» до C3
- но снова получим конфликт:
$ git merge B
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
$ cat file
$ cat file
A
<<<<<<< HEAD
B
C
D
||||||| merged common ancestors
=======
B
C
>>>>>>> B
Конфликт слияния идентичен предыдущему конфликту, так как три входных содержимого также были идентичны.
Давайте еще раз решим эту проблему так, как вы предлагали, используя трехстрочную --theirs
версию (и я собираюсь немного обмануть с помощью ярлыка, который устраняет необходимость git add
это):
$ git checkout MERGE_HEAD -- file
$ git commit -m c6
[A 2e66e76] c6
$ cat file
A
B
C
(«чит» состоит в том, что git checkout MERGE_HEAD
извлекает файл из коммита C3
, на который указывает B
, а не извлекает его из индексного слота 3. Это стирает три конфликтующие записи в индексном слоте, заменяя их с разрешенной записью в слот-ноль, так что мы готовы зафиксировать результат.)
Теперь у нас есть этот график:
C7 <-- D
/
C4 <-- C
/ \
...--C1--C2----C5--C6 <-- A (HEAD)
\ /
\ /
\ /
C3 <-- B
Вернуться к используемым командам
наконец, в 6, ветвь D сливается с A ...
Для этого у нас должно быть HEAD
, прикрепленное к A
- оно уже есть, и мы запускаем git merge D
или git merge <hash of C7>
. Давайте предскажем, что произойдет, найдя базу слияния коммитов C6
и C7
, следуя графическим соединениям назад к лучшему («низшему») общему предку. На этот раз это коммит C4
. Что снова в file
в C4
? Давайте посмотрим на это, используя тот факт, что имя C
указывает на это:
$ git show C:file
A
B
C
D
$
Таким образом, Git будет сравнивать содержимое <hash-of-C4>:file
с содержимым <hash-of-C6>:file
, чтобы увидеть, что мы изменили - мы не будем беспокоиться об обнаружении переименования или других файлах, так как нет переименований для обнаружения или изменений в другие файлы:
$ git diff C:file A:file
diff --git a/file b/file
index 8422d40..b1e6722 100644
--- a/file
+++ b/file
@@ -1,4 +1,3 @@
A
B
C
-D
Итак, что мы изменили, так это удалить окончательный D
.
Отдельно Git будет сравнивать содержимое <hash-of-C4>:file
с содержимым <hash-of-C7>:file
, чтобы увидеть, что они изменили, поэтому:
$ git diff C:file D:file
$ git diff C:file D:file
diff --git a/file b/file
index 8422d40..f70f10e 100644
--- a/file
+++ b/file
@@ -1,4 +1 @@
A
-B
-C
-D
Они удалили три строки. Эти изменения должны конфликтовать. Посмотрим, правы ли мы:
$ git merge D
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
$ cat file
A
<<<<<<< HEAD
B
C
||||||| merged common ancestors
B
C
D
=======
>>>>>>> D
Мы действительно правы: общий предок (сохраненный в рабочем дереве, потому что у меня есть diff3
в качестве моей настройки стиля конфликта) имеет три строки, в то время как версия HEAD
сохраняет две из них, но их (D
) версия удаляет все три.
(Мы можем выполнить то же упражнение с немного более простым графиком, возникающим из ускоренной перемотки вперед, но эффект в итоге будет таким же: мы получаем конфликт слияния на одном и том же наборе линий. Ключ в том, чтобы найти объединение базы и двух наконечников фиксирует и сравнивает базу с каждым ответвлением ветви. В этом случае вход для окончательного объединения имеет следующий график:
C7 <-- D
/
C4 <-- C
/ \
...--C1--C2 C5 <-- A (HEAD)
\ /
C3 <-- B
, где C5
имеет то же содержимое, созданное вручную, что и C6
на более сложном графике, а C4
и C7
идентичны, а база слияния все еще C4
.)
Рекурсивное слияние
В комментарии к ответу iBug вы спрашиваете о рекурсивных слияниях. Это происходит, когда существует более одного Низшего Общего Предка. В простой древовидной структуре данных существует только один LCA, но в ориентированных графах их может быть больше одного. См. Майкл Бендер, Мартин Фарах-Колтон, Гиридар Пеммасани, Стивен Шиена и Павел Сумазин. Самые низкие общие предки на деревьях и направленные ациклические графы. Журнал Алгоритмов, 57 (2): 75–94, 2005. для двух формальных определений; но в целом это происходит в системах управления версиями с графическим управлением (таких как Git и Mercurial)
когда вы делаете "крест-накрест" сливается. Например, предположим, что мы начинаем с этого графика:
o--A <-- branch1
/
...--o--*
\
o--B <-- branch2
Мы сейчас git checkout branch1 && git merge branch2
, а затем, когда это удастся, мы имеем:
o--A---M1 <-- branch1
/ /
...--o--* /
\ /
o--B <-- branch2
Мызапустите git checkout branch2 && git merge branch1~1
(или эквивалент, например, git merge <hash of commit A>
), чтобы получить:
o--A---M1 <-- branch1
/ \ /
...--o--* X
\ / \
o--B---M2 <-- branch2
Если мы теперь сделаем еще несколько коммитов на двух ветвях, у нас может получиться, например ::
o--A---M1--C <-- branch1
/ \ /
...--o--* X
\ / \
o--B---M2--D <-- branch2
Теперь мы спрашиваем: какой коммит (ы) является или является наименьшим общим предком кончиков двух ветвей, коммитов C
и D
? Начиная с C
и работая в обратном направлении, мы находим коммит B
до M1
, который доступен с D
до M2
, так что это LCA. Но мы также находим коммит A
на прямом пути, и он доступен с D
через другого родителя M2
.
Используя любое из их определений, или более простое, которое мне нравится, включает подсчет прыжков (но это не работает так же хорошо, как метод индуцированного подграфа, когда есть много входных данных), мы находим, что фиксирует A
и B
оба LCA для коммитов C
и D
. Вот где стратегии Git -s resolve
и -s recursive
отличаются.
При -s resolve
Git просто выбирает одного предка с (очевидно) случайным образом и использует его в качестве базы слияния для двух различий. В -s recursive
Git находит все LCA и использует все из них в качестве базы слияния. Для этого Git объединяет все LCA, как если бы вы запустили git merge <lca1> <lca2>
, затем - если есть еще LCA - git merge <resulting commit> <lca3>
и так далее. Каждый коммит выполняется даже при наличии конфликтов: Git просто берет конфликтующее слияние с маркерами конфликта и делает из него коммит.
(Для слияния любой пары LCA может потребоваться слияние нескольких LCA. Если это так, Git рекурсивно объединяет их: отсюда и название.)
Последний коммит, который является временным, так как не имеет имени для его сохранения, используется в качестве базы слияния, с которой сравниваются подсказки ветвления. Когда у внутренних слияний возникают конфликты, это приводит к удивительно запутанным результатам и / или конфликтам слияний. По большей части, однако, это работает довольно хорошо. Он дает результаты, которые в большинстве случаев аналогичны -s resolve
, а в тех немногих случаях, когда результаты отличаются, они имеют тенденцию к улучшению .