Git учитывает время, чтобы избежать "гонок"? или имеет значение порядок слияния? - PullRequest
0 голосов
/ 29 октября 2018

Предположим, в полдень на ветке А вы создаете файл:

A

в 1 вы создаете ветку B из A и изменяете файл на:

A
B    
C

в 2, кто-то еще создает C off A и изменяет файл на:

A
B
C
D

в 3, они объединяют C в A, поэтому A теперь:

A
B
C
D

в 4 кто-то создает ответвление D от A и изменяет файл на:

A

в 5, исходный человек, создающий ветвь, сливает B в A, означая, что на A файл:

A
B
C

наконец, в 6, ветвь D объединяется с A, и в результате получается конечный результат (?):

A

Мой вопрос: знает ли Git, как это рассматривать как конфликт слияния? Это смотрит на время? Или просто 3-сторонний diff с общим предком, и если он «идет» лексически, он идет?

В этом примере ветвь D имеет удар B, хотя она была создана из A после B (в B, но до слияния обратно в A). Если это так, вы получите другой результат, если последние два коммита были не B-> A, а D-> A, но наоборот (сначала D-> A, затем B-> A).

1 Ответ

0 голосов
/ 29 октября 2018

Для Git время не важно.Только life граф важен. (Конечно, сам граф строится со временем, поэтому можно утверждать, что время действительно имеет значение, но продолжайте читать.)

Предположим, что в полдень на ветке A вы создаете файл [содержащий одну строку с чтением A]

Ветви тоже не имеют значения, не в том смысле, в котором вы могли бы подумать.Значение ветвей зависит от того, что вы подразумеваете под ответвлением (см. Что именно мы подразумеваем под «ответвлением»? ).Помните, что каждый коммит, идентифицируемый уникальным хеш-идентификатором, имеет:

  • полный снимок всех исходных файлов;
  • имя автора, адрес электронной почты и отметку времени;
  • имя коммиттера, электронная почта и отметка времени;
  • идентификатор хеша родительского коммита (ов)
  • сообщение журнала.

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

Давайте дадим каждому коммиту уникальный C<number> ID (я обычно использую заглавные буквы, но вы назвали здесь ветви A, B, C и D, поэтому давайте перейдем с C1 до C7 для коммитов).

Между тем ветвь имя - см. Что именно мы подразумеваем под "ветвью"? снова - это просто указатель на один конкретныйcommit с особым свойством: когда мы git checkout делаем эту ветвь и делаем new commit, Git обновляет имя, чтобы указать на новый коммит, который мы только что сделали.Между тем, новый коммит, который мы только что сделали, хранит, как его родитель, хэш-идентификатор предыдущего коммита.Итак, допустим, у нас есть ветка с именем A, указывающая на некоторый существующий коммит C1, у которого есть родительский элемент, в который мы не собираемся рисовать:

...  <-C1   <-- A

Имя ветки Aсодержит идентификатор хэша для C1.Вы git checkout A, так что C1 становится текущим коммитом, а A становится текущей ветвью:

...--C1   <-- A (HEAD)

(рисование соединителей из коммитов для их родителей, так как стрелки могут стать проблемой, поэтомуЯ переключаюсь на -- вместо этого здесь).Затем вы создаете новый файл git add и делаете новый коммит C2.Это заставляет имя A идентифицировать коммит C2, чей родитель C1:

...--C1--C2   <-- A (HEAD)

в 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, а в тех немногих случаях, когда результаты отличаются, они имеют тенденцию к улучшению .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...