Як детально працює 'git merge'?


93

Я хочу знати точний алгоритм (або майже такий), який стоїть за 'git merge'. Відповіді принаймні на ці підзапитання будуть корисними:

  • Як git виявляє контекст певної неконфліктної зміни?
  • Як git дізнається, що в цих точних рядках існує конфлікт?
  • Які речі git автоматично об’єднує?
  • Як працює git, коли немає спільної бази для злиття гілок?
  • Як працює git, коли існує кілька загальних баз для об’єднання гілок?
  • Що трапляється, коли я об'єдную відразу кілька гілок?
  • У чому різниця між стратегіями злиття?

Але опис цілого алгоритму буде набагато кращим.


8
Думаю, ти міг би наповнити цією книгою ці відповіді ...
Даніель Хілгарт,

2
Або ви можете просто зайти і прочитати код, що займе приблизно стільки часу, скільки "описує весь алгоритм"
Невік Ренель,

3
@DanielHilgarth Буду радий дізнатися, чи десь уже є така книга. Посилання вітаються.
безодня. 7

5
@NevikRehnel Так, я можу. Але це може стати набагато простіше, якщо хтось уже знає теорію цього коду.
безодня. 7

1. Що таке "контекст певної неконфліктної зміни"? Пункти 2. та 3. однакові, але заперечені, об’єднаємо ці два питання?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Відповіді:


65

Можливо, вам найкраще шукати опис алгоритму 3-стороннього злиття. Опис високого рівня може виглядати приблизно так:

  1. Знайдіть відповідну базу злиття B- версію файлу, яка є родоначальником обох нових версій ( Xі Y), і, як правило, останню таку базу (хоча бувають випадки, коли їй доведеться повернутися далі, що є одним із особливості gits recursiveзлиття за замовчуванням )
  2. Виконайте різниці Xз Bі Yз B.
  3. Пройдіться по блоках змін, позначених у двох відмінностях. Якщо обидві сторони вносять однакову зміну в одному місці, прийміть одну з них; якщо один вносить зміни, а інший залишає цю область сам, внесіть зміни у фінал; якщо обидва вносять зміни в місце, але вони не збігаються, позначте конфлікт, який потрібно вирішити вручну.

Повний алгоритм розглядає це набагато детальніше і навіть має певну документацію ( https://github.com/git/git/blob/master/Documentation/technical/trivial-merge.txt для одного, а також git help XXXсторінки , де XXX є одним з merge-base, merge-file, merge, merge-one-fileі , можливо, деяких інших). Якщо це недостатньо глибоко, завжди є вихідний код ...


11

Як працює git, коли існує кілька загальних баз для об’єднання гілок?

Ця стаття була дуже корисною: http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html (ось частина 2 ).

Рекурсивно використовує diff3 рекурсивно для створення віртуальної гілки, яка буде використовуватися як родоначальник.

Наприклад:

(A)----(B)----(C)-----(F)
        |      |       |
        |      |   +---+
        |      |   |
        |      +-------+
        |          |   |
        |      +---+   |
        |      |       |
        +-----(D)-----(E)

Тоді:

git checkout E
git merge F

Є 2 найкращих спільних предка (спільні предки, які не є предками жодного іншого), Cі D. Git об'єднує їх у нову віртуальну гілку V, а потім використовує Vяк основу.

(A)----(B)----(C)--------(F)
        |      |          |
        |      |      +---+
        |      |      |
        |      +----------+
        |      |      |   |
        |      +--(V) |   |
        |          |  |   |
        |      +---+  |   |
        |      |      |   |
        |      +------+   |
        |      |          |
        +-----(D)--------(E)

Я припускаю, що Git просто продовжував би, якби було більше найкращих спільних предків, зливаючись Vіз наступним.

У статті сказано, що якщо виникає конфлікт злиття при генерації віртуальної гілки, Git просто залишає маркери конфлікту там, де вони є, і продовжується.

Що трапляється, коли я об'єдную відразу кілька гілок?

Як пояснив @Nevik Rehnel, це залежить від стратегії, це добре пояснено в man git-merge MERGE STRATEGIESрозділі.

Тільки octopusта ours/ theirsпідтримує злиття декількох гілок одночасно, recursiveнаприклад, ні.

octopusвідмовляється об'єднуватися, якщо виникають конфлікти, і oursє тривіальним об'єднанням, тому конфліктів бути не може.

Ці команди для створення нового коміту матимуть більше 2 батьків.

Я зробив один merge -X octopusна Git 1.8.5 без конфліктів, щоб побачити, як це буде.

Початковий стан:

   +--B
   |
A--+--C
   |
   +--D

Дія:

git checkout B
git merge -Xoctopus C D

Новий штат:

   +--B--+
   |     |
A--+--C--+--E
   |     |
   +--D--+

Як очікувалося, Eмає 3 батьків.

ЗАВДАННЯ: як саме восьминіг працює з одним файлом модифікацій. Рекурсивне двобічне з’єднання в три сторони?

Як працює git, коли немає спільної бази для злиття гілок?

@Torek зазначає, що з 2.9 злиття неможливе без --allow-unrelated-histories.

Я спробував це емпірично на Git 1.8.5:

git init
printf 'a\nc\n' > a
git add .
git commit -m a

git checkout --orphan b
printf 'a\nb\nc\n' > a
git add .
git commit -m b
git merge master

a містить:

a
<<<<<<< ours
b
=======
>>>>>>> theirs
c

Тоді:

git checkout --conflict=diff3 -- .

a містить:

<<<<<<< ours
a
b
c
||||||| base
=======
a
c
>>>>>>> theirs

Інтерпретація:

  • база порожня
  • коли база порожня, неможливо вирішити будь-які зміни в одному файлі; можна вирішити лише такі речі, як додавання нових файлів. Вищезазначений конфлікт можна було б вирішити шляхом 3-стороннього злиття з базою a\nc\nяк додавання в один рядок
  • Я думаю, що 3-стороннє злиття без базового файлу називається двостороннім злиттям, що є просто різницею

1
На це питання є нове посилання SO, тому я проглянув цю відповідь (що цілком непогано) і помітив, що нещодавня зміна Git трохи застаріла в останньому розділі. Починаючи з версії Git 2.9 (коміт e379fdf34fee96cd205be83ff4e71699bdc32b18), Git тепер відмовляється об'єднуватись, якщо немає бази злиття, якщо ви не додасте --allow-unrelated-histories.
torek

1
Ось наступна стаття з опублікованого @Ciro
adam0101

Якщо поведінка не змінилася з моменту останньої спроби: її --allow-unrelated-historiesможна опустити, якщо між гілками, які ви об’єднуєте, немає загальних шляхів до файлів.
Jeremy List

Невелике виправлення: oursстратегія злиття існує, але theirsстратегія злиття відсутня . recursive+ theirsстратегія може вирішити лише дві гілки. git-scm.com/docs/git-merge#_merge_strategies
nekketsuuu

9

Мені теж цікаво. Я не знаю відповіді, але ...

Незмінно виявляється, що складна система, яка працює, еволюціонувала із простої системи, яка працювала

Я думаю, що злиття git є надзвичайно складним, і його буде дуже важко зрозуміти, але один із способів досягти цього - його попередники та зосередитись на суті вашого питання. Тобто, враховуючи два файли, які не мають спільного предка, як git merge працює, як їх об’єднати, і де конфлікти?

Спробуємо знайти кілька попередників. Від git help merge-file:

git merge-file is designed to be a minimal clone of RCS merge; that is,
       it implements all of RCS merge's functionality which is needed by
       git(1).

З wikipedia: http://en.wikipedia.org/wiki/Git_%28software%29 -> http://en.wikipedia.org/wiki/Three-way_merge#Three-way_merge -> http: //en.wikipedia .org / wiki / Diff3 -> http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf

Це останнє посилання є PDF-документом із diff3детальним описом алгоритму. Ось версія google pdf-viewer . Він займає всього 12 сторінок, а алгоритм - лише пару сторінок - але повноцінна математична обробка. Це може здатися дещо надто офіційним, але якщо ви хочете зрозуміти злиття git, вам потрібно спочатку зрозуміти простішу версію. Я ще не перевіряв, але з таким іменем diff3, напевно, вам також потрібно буде зрозуміти diff (який використовує найдовший загальний алгоритм послідовності ). Однак, можливо, є більш інтуїтивне пояснення diff3, якщо у вас є Google ...


Тепер я просто зробив експеримент, порівнюючи diff3і git merge-file. Вони приймають одні і ті ж три вхідних файлу version1 oldversion Version2 і оцінка конфліктів Те , як же, з <<<<<<< version1, =======, >>>>>>> version2( diff3також ||||||| oldversion), показуючи їх спільну спадщину.

Я використав порожній файл для старої версії та майже ідентичні файли для версії1 та версії2, додавши лише один зайвий рядок до версії2 .

Результат: git merge-fileідентифікований єдиний змінений рядок як конфлікт; але diff3розглянув ці два файли як конфлікт. Таким чином, складний, як і diff3, злиття git є ще більш складним, навіть для цього найпростішого випадку.

Ось фактичні результати (я використав для тексту відповідь @ twalberg). Зверніть увагу на необхідні параметри (див. Відповідні сторінки).

$ git merge-file -p fun1.txt fun0.txt fun2.txt

You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.
<<<<<<< fun1.txt
=======
THIS IS A BIT DIFFERENT
>>>>>>> fun2.txt

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...

$ diff3 -m fun1.txt fun0.txt fun2.txt

<<<<<<< fun1.txt
You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...
||||||| fun0.txt
=======
You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.
THIS IS A BIT DIFFERENT

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...
>>>>>>> fun2.txt

Якщо ви справді зацікавлені в цьому, це трохи кроляча нора. Для мене це здається таким глибоким, як регулярні вирази, найдовший загальний алгоритм підпослідовності різниці, без контексту граматики або реляційної алгебри. Якщо ви хочете докластися до його суті, я думаю, що можете, але для цього знадобиться певне рішуче дослідження.


2

Ось оригінальна реалізація

http://git.kaarsemaker.net/git/blob/857f26d2f41e16170e48076758d974820af685ff/git-merge-recursive.py

В основному ви створюєте список загальних предків для двох комітів, а потім рекурсивно зливаєте їх, або швидко пересилаючи їх вперед, або створюючи віртуальні коміти, які використовуються для основи тристороннього злиття на файлах.


0

Як git виявляє контекст певної неконфліктної зміни?
Як git дізнається, що в цих точних рядках існує конфлікт?

Якщо однаковий рядок змінився з обох сторін злиття, це конфлікт; якщо вони цього не зробили, зміна з одного боку (якщо така існує) приймається.

Які речі git автоматично об’єднує?

Зміни, які не суперечать (див. Вище)

Як працює git, коли існує кілька загальних баз для об’єднання гілок?

За визначенням бази злиття Git , існує лише одна (останній загальний предок).

Що трапляється, коли я об'єдную відразу кілька гілок?

Це залежить від стратегії об’єднання (лише стратегії octopusта ours/ та / theirsпідтримують об’єднання більше двох гілок).

У чому різниця між стратегіями злиття?

Це пояснюється в git mergeсторінках керівництва .


2
Що означає "однаковий рядок"? Якщо я вставляю новий непустий рядок між двома іншими та зливаю - які рядки однакові? Якщо я видалю кілька рядків в одній гілці, які з них однакові в іншій гілці?
безодня. 7

1
Відповісти текстово трохи хитро. Git використовує [diffs] (en.wikipedia.org/wiki/Diff) для вираження різниці між двома файлами (або двома версіями файлу). Він може виявити, чи були додані чи видалені рядки, порівнюючи контекст (за замовчуванням три рядки). "Той самий рядок" тоді означає контекст, маючи на увазі доповнення та видалення.
Невік Ренель

1
Ви припускаєте, що зміна "тієї самої лінії" означатиме конфлікт. Чи двигун automerge справді базується на лінійці? Або це на основі кусків? Чи є коли-небудь один спільний предок? Якщо так, то чому git-merge-recursiveіснує?
Едвард Томсон,

1
@EdwardThomson: Так, роздільна здатність базується на лініях (шматочки можна розбити на менші шматки, поки не залишиться лише один рядок). За замовчуванням стратегія злиття використовує останнього загального предка як посилання, але є й інші, якщо ви хочете використовувати щось інше. І я не знаю, що git-merge-recursiveповинно бути (немає сторінки користувача, а Google нічого не дає). Більше інформації про це можна знайти на сторінках git mergeта git merge-baseman.
Nevik Rehnel

1
Сторінка git-mergeлюдини та git-merge-baseсторінки, на які ви вказуєте, обговорюють кілька спільних предків та рекурсивне злиття. Я вважаю, що ваша відповідь є неповною без обговорення таких питань.
Едвард Томсон,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.