Git
Chapters ▾ 2nd Edition

7.8 Інструменти Git - Складне злиття

Складне злиття

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

Утім, іноді трапляються й хитромудрі конфлікти. На відміну від інших систем контролю версій, Git не намагається бути надто розумним щодо розв’язання конфліктів. Філософія Git — бути розумним, коли злиття можна зробити однозначно, проте, якщо є конфлікт, Git не намагається бути розумним та автоматично його вирішити. Отже, якщо ви надто зволікаєте зі злиттям двох гілок, що розходяться швидко, у вас можуть виникнути проблеми.

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

Конфлікти злиття

Хоч ми й розглянули деякі основи розв’язання конфліктів зливання в Основи конфліктів зливання, для складніших конфліктів Git пропонує декілька утиліт, що допоможуть вам збагнути що коїться та як краще мати справу з конфліктом.

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

Розгляньмо дуже простий приклад. У нас є надпростий файл Ruby, що друкує hello world.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

У нашому репозиторії, ми створюємо нову гілку whitespace та замінюємо всі Unix символи нового рядка на варіант DOS, тобто змінюємо кожен рядок файлу, проте виключно пробільні символи. Потім ми змінюємо рядок “hello world” на “hello mundo”.

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'converted hello.rb to DOS'
[whitespace 3270f76] converted hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 1 file changed, 1 insertion(+), 1 deletion(-)

Тепер перейдемо назад до гілки та додамо якийсь опис цієї функції.

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'document the function'
[master bec6336] document the function
 1 file changed, 1 insertion(+)

Тепер ми спробуємо злити до гілки whitespace та отримаємо конфлікти через зміни пробільних символів.

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Припинення злиття

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

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

Опція git merge --abort намагається повернутися до стану, в якому ви були до початку зливання. Єдиний випадок, коли це може не вийти бездоганно — якщо у вас були несховані незбережені зміни в робочій директорії, коли ви почали злиття, інакше все пройде без проблем.

Якщо з будь-якої причини ви просто бажаєте почати все з початку, ви можете виконати git reset --hard HEAD, і ваше сховище повернеться до стану останнього коміту. Памʼятайте, що будь-які не збережені в коміті зміни будуть втрачені, отже переконайтеся, що всі локальні зміни вам не потрібні.

Ігнорування пробільних символів

У цьому окремому випадку, конфлікти пов’язані з пробільними символами. Ми знаємо про це, адже випадок простий, проте це доволі легко побачити в реальних ситуаціях, подивившись на конфлікт: кожен рядок видалено з одного боку та знову додано з іншого. Без додаткових опцій, Git розглядає всі рядки як змінені, отже не може злити файли.

Втім, типова стратегія зливання може приймати опції, а декілька з них про правильне ігнорування зміни пробільних символів. Якщо ви бачите, що у вас купа проблем з пробільними символами при злитті, ви можете просто припинити його та спробувати ще раз, цього разу з -Xignore-all-space або -Xignore-space-change. Перша опція цілковито ігнорує пробільні символи при порівнянні рядків, а друга сприймає послідовності одного чи більше пробільних символів як рівнозначні.

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

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

Повторне злиття файла вручну

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

Що нам насправді треба зробити — це прогнати файл, який ми намагаємося злити, через програму dos2unix до того, як починати власне злиття файла. То як нам це зробити?

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

Отримати три версії файла насправді доволі просто. Git зберігає кожну з цих версій в індексі під “станами” (stages), які мають пов’язані з ними номери. Стан 1 — це спільний предок, стан 2 — це ваша версія, а стан 3 — з `MERGE_HEAD, версія, яку ви зливаєте до себе (“їхня”, theirs)

Ви можете витягнути копію кожної з цих версій конфліктного файлу за допомогою команди git show зі спеціальним синтактом.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

Якщо ви бажаєте чогось суворішого, скористайтесь кухонною командою ls-files -u, щоб отримати власне SHA-1 суми Git блобів кожного з цих файлів

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

:1:hello.rb — це просто скорочення для пошуку SHA-1 цього блобу.

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

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

Тепер ми гарно злили файл. Насправді, це працює навіть краще, ніж опція ignore-space-change, адже зміни пробільних символів виправлено до злиття, а не просто проігноровано. У злитті з ignore-space-change, ми в результаті отримали декілька рядків з DOS символами нового рядка, що призвело до змішання.

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

Щоб порівняти результат з тим, що було до зливання, іншими словами, щоб побачити, що саме з’явилось від злиття, можна виконати git diff --ours

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Отже тут ми легко можемо побачити, що саме сталося в нашій гілці, що ми насправді змінюємо у файлі цим злиттям: змінюємо один рядок.

Якщо ми бажаємо побачити, чим результат зливання відрізняється від того, що було на їхній стороні, можемо виконати git diff --theirs. У цьому й наступному прикладі, ми маємо використати -b, щоб прибрати пробільні символи, бо порівняння проходить зі збереженим у Git, а не з нашим очищеним файлом hello.theirs.rb.

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

Нарешті, ви можете побачити, як файл змінився з обох сторін за допомогою git diff --base.

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Тепер ми можемо використати команду git clean, щоб прибрати зайві файли, що ми їх створили для зливання вручну, бо вони більше не потрібні.

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

Отримання при конфліктах

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

Змінімо трохи наш приклад. У цьому прикладі, у нас є дві більш довготривалі гілки, у кожній по декілька комітів, проте при зливанні створюють конфлікт саме за змістом.

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed text to hello mundo
|/
* b7dcc89 initial hello world code

Тепер у нас три унікальних коміти, які є лише в гілці master, та три інших коміти, які є в гілці mundo. Якщо ми спробуємо злити mundo, то отримаємо конфлікт.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Ми хотіли б побачити, у чому власне конфлікт. Якщо відкрити файл, то побачимо щось таке:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

Обидві сторони зливання додали зміст до цього файлу, проте деякі з комітів змінили файл в одному місці, що й спричинило цей конфлікт.

Дослідімо декілька утиліт у вашому розпорядженні, щоб визначити, як виник цей конфлікт. Можливо, як саме варто розв’язати конфлікт не є очевидним. Вам потрібно більше контексту.

Одна з корисних команд — git checkout з опцією --conflict. Ця команда ще раз отримає файл, та замінить позначки конфлікту (conflict markers). Це може бути корисним, якщо ви бажаєте відновити позначки та спробувати розв’язати їх знову.

Ви можете встановити --conflict значення diff3, або merge (що є типовим значенням). Якщо передати diff3, то Git використає трохи іншу версію позначок конфлікту: не тільки надасть вам “вашу” та “їхню” версії, а ще й “базову” версію до файлу, щоб надати вам більше контексту.

$ git checkout --conflict=diff3 hello.rb

Після виконання, файл стане виглядати так:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

Якщо вам подобається такий формат, ви можете зробити його типовим для майбутніх конфліктів злиття, якщо встановите налаштування merge.conflictstyle у значення diff3.

$ git config --global merge.conflictstyle diff3

Команда git checkout також приймає опції --ours та --theirs, які є дійсно швидким засобом вибору лише однієї сторони, взагалі без зливання іншої.

Це особливо корисно для конфліктів двійкових файлів, адже ви можете просто вибрати одну сторону, або коли ви хочете злити окремі файли з іншої гілки - ви можете злити, а потім отримати (checkout) окремі файли з однієї сторони до створення коміту.

Журнал зливання

Ще одна корисна утиліта при розв’язанні конфліктів злиття — git log. Вона може допомогти вам отримати інформацію про те, що могло сприяти конфлікту. Переглянути трохи історії, щоб запам’ятати, чому два рядки розробки зачепили одну область коду, може бути дуже корисним подеколи.

Щоб отримати повний список усіх унікальних комітів, що є у будь-якій з конфліктуючих гілок, можна використати синтаксис “потрійної крапки”, про який ми дізналися в Потрійна крапка.

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo

Це гарний список усіх шести комітів, а також з якої лінії розробки надійшов кожен коміт.

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

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo

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

Поєднаний формат різниці (diff)

Оскільки Git індексує будь-які успішні результати злиття, коли ви виконуєте git diff у стані конфлікту злиття, ви отримуєте лише конфліктуючі зміни. Це може бути корисним, щоб побачити, що вам ще треба розв’язати.

Якщо виконати git diff одразу після конфлікту злиття, то вона видасть інформацію в доволі унікальному форматі різниці.

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

Формат називається “Поєднана різниця” (combined diff) та видає дві колонки даних навпроти кожного рядка. Перша колонка показує вам, якщо рядок змінився (доданий чи вилучений) між “вашою” гілкою та файлом у робочій директорії, а друга колонка робить те саме між “їхньою” гілкою та робочою директорією.

Отже в цьому прикладі можна побачити, що рядки <<<<<<< та >>>>>>> є в робочій копії, проте їх нема в жодній зі сторін зливання. Це має сенс, адже утиліта злиття додала їх для нас, але від нас очікується їх вилучення.

Якщо розв’язати конфлікт та виконати git diff знову, ми побачимо схожий результат, проте трохи корисніший.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Це показує нам, що “hola world” був на нашій стороні, проте не в робочій копії; “hello mundo” був на їхній стороні, проте не в робочій копії; та нарешті, “hola mundo” не був у жодній зі сторін, проте є в робочій копії. Це може бути корисним, щоб переглянути розв’язання конфлікту до створення коміту.

Ви також можете отримати таке від git log для будь-якого вже зробленого зливання, щоб побачити як щось було розв’язано. Git зробить видрук у цьому форматі, якщо виконати git show для коміту злиття, або якщо додати опцію --cc до git log -p (яка типово показує лише латки для комітів не злиття).

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Скасування зливань

Тепер, коли ви знаєте, як створити коміт злиття, ви напевно зробите якусь помилку. Одна з чудових речей роботи з Git — що це нормально робити помилки, адже їх можливо (та зазвичай легко) виправити.

Коміти зливання тут не відрізняються. Скажімо, ви почали працювати над тематичною гілкою, та випадково злили її до master, і тепер історія комітів виглядає так:

Випадковий коміт злиття.
Figure 137. Випадковий коміт злиття

Є два підходи до цієї проблеми, у залежності від бажаного результату.

Виправити посилання

Якщо небажаний коміт зливання існує лише у вашому локальному репозиторії, найлегше та найкраще рішення — пересунути гілки так, щоб вони вказували на що ви забажаєте. У більшості випадків, якщо одразу після помилки git merge, виконати git reset --hard HEAD~, то вказівники гілок будуть скинуті і будуть виглядати так:

Історія після `git reset --hard HEAD~`.
Figure 138. Історія після git reset --hard HEAD~

Ми розглядали reset у Усвідомлення скидання (reset), отже, вам має бути неважко зрозуміти, що тут сталося. Ось швидке повторення: reset --hard зазвичай робить наступні кроки:

  1. Пересунути гілку, на яку вказує HEAD. У даному випадку, ми бажаємо пересунути master до того, де вона була до коміту злиття (C6).

  2. Скопіювати HEAD до індекс.

  3. Скопіювати індекс до робочої директорії.

Недолік цього підходу є те, що це переписування історії, що може стати проблемою в спільному репозиторії. Прогляньте Небезпеки перебазовування для докладного опису того, що може статися; коротка версія така: якщо інші люди мають коміти, які ви переписуєте, вам варто уникати reset. Цей підхід також не спрацює, якщо інші коміти були створені після злиття; переміщення посилань у результаті втратить ці зміни.

Вивертання коміту

Якщо переміщення вказівників гілок не спрацює для вас, Git дає ще один варіант: створити новий коміт, що скасовує всі зміни з існуючого коміту. Git називає цю операцію “вивертання” (revert), і в цьому конкретному випадку, треба викликати його так:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

Опція -m 1 вказує, який батько є “стрижневим” (mainline) та має бути збереженим. Якщо виконати злиття до HEAD (git merge topic), новий коміт матиме двох батьків: перший це HEAD (C6), а другий — останній коміт гілки, яку ви зливаєте (C4). У даному випадку, ми бажаємо скасувати всі зміни, спричинені у батьку #2 (C4), проте зберігти весь зміст з батька #1 (C6).

Історія з виверненим комітом виглядає так:

Історія після `git revert -m 1`.
Figure 139. Історія після git revert -m 1

Новий коміт ^M має точно такий зміст, як C6, отже починаючи звідти, усе нібито злиття ніколи не було, окрім того, що тепер не злиті коміти досі присутні в історії HEAD. Git заплутається, якщо ви спробуєте злити topic до master знову:

$ git merge topic
Already up-to-date.

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

Історія з поганим злиттям.
Figure 140. Історія з поганим злиттям

Найкращій вихід — це вивернути виворіт першого злиття, адже тепер ви бажаєте повернути зміни, які ви були вивернули, потім створити новий коміт злиття:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
Історія після повторного злиття вивернутого злиття.
Figure 141. Історія після повторного злиття вивернутого злиття

У цьому прикладі, M та ^M взаємно знищились. ^^M в результаті зливає зміни C3 та C4, а C8 зливає зміну C7, отже тепер topic повністю злито.

Інші типи зливань

Досі ми розглядали нормальні злиття двох гілок, що зазвичай обробляються тим, що називається стратегією злиття “recursive”. Одначе, існують й інші методи зливати гілки разом. Швидко розгляньмо декілька з них.

Перевага нашого чи їхнього

Почнімо з додаткових можливостей звичайного режиму зливання “recursive”. Ми вже зустрічали опції ignore-all-space та ignore-space-change, які передаються до -X, проте ми також можемо сказати Git надавати перевагу одній чи іншій стороні, коли він бачить конфлікт.

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

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

Якщо ми повернемося до попереднього прикладу “hello world”, ми можемо бачити, що зливання нашої гілки призводить до конфліктів.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Проте якщо виконати з -Xours або -Xtheirs, конфлікту не буде.

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

У даному випадку, замість отримання позначок конфлікту у файлі з “hello mundo” з одного боку та “hola world” з іншого, Git просто обере “hola world”. Однак, усі інші неконфліктуючі зміни на цій гілці успішно злиті.

Цю опцію також можна передати команді git merge-file, яку ми бачили раніше, наприклад виконавши git merge-file --ours для окремих зливань файлів.

Якщо вам потрібно щось схоже, проте щоб Git навіть не намагався зливати зміни з іншої сторони, є більш драконівська опція, а саме стратегія злиття “ours”. Це не те саме, що опція рекурсивного (recursive) злиття “ours”.

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

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

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

Це може бути корисним для введення Git в оману, бо він буде вважати, що гілка вже злита при подальших зливаннях. Наприклад, якщо ви відгалузили гілку release, та зробили якусь роботу в ній, яку ви потім забажаєте злити до гілки master. У той же час, якесь виправлення з master має бути додано до вашої гілки release. Ви можете злити виправлення до гілки release і також merge -s ours ту ж гілку до гілки master (хоча виправлення вже там), тоді при подальшому зливанні гілки release, ніяких конфліктів від цього виправлення не виникне.

Зливання піддерев

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

Ми розглянемо приклад додавання окремого проекту до вже існуючого, та зливання коду з другого до піддиректорії першого.

Спочатку, ми додамо застосунок Rack до вашого проекту. Ми додамо проект Rack у якості віддаленого посилання до нашого проекту, а потім отримаємо його в окрему гілку:

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

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

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

Це дещо дивна концепція. Не всі гілки вашого репозиторія насправді мають бути гілками одного проекту. Це не поширено, адже зрідка корисно, проте мати гілки, що містять цілковито різні історії, доволі легко.

У даному випадку, ми хочемо втягнути проект Rack до нашого проекту master в якості піддиректорії. Ми можемо зробити це в Git за допомогою git read-tree. Ви дізнаєтесь більше про read-tree, та йому подібних, у Git зсередини, проте, покищо знайте, що вона зчитує корінь дерева однієї гілки до вашого поточного індексу та робочої директорії. Ми щойно переключились назад до вашої гілки master, та втягуємо гілку rack_branch до піддиректорії нашої гілки master нашого головного проекту:

$ git read-tree --prefix=rack/ -u rack_branch

Коли ми збережемо коміт, здається, ніби всі файли Rack є в тій піддиректорії – нібито ми скопіювали їх з архіву. Цікаво те, що ми легко можемо зливати зміни з однієї гілки в іншу. Отже, якщо проект Rack буде оновлено, ми зможемо отримати останні зміни, коли переключимося на ту гілку та виконаємо pull:

$ git checkout rack_branch
$ git pull

Потім, ми можемо злити ці зміни назад до нашої гілки master. Щоб взяти зміни та отримати заповненим повідомлення коміту, використайте опцію --squash, а також опцію -Xsubstree стратегії зливання recursive. (Рекурсивна стратегія зливання є типовою, проте ми включили її для ясності.)

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

Усі зміни з проекту Rack злиті та готові до збереження в локальному коміті. Ви можете робити й навпаки – зберігати зміни у піддиректорії rack вашої гілки master та потім зливати їх до гілки rack_branch, щоб пізніше відправити їх до супроводжувачів або надсилання до джерела проекту.

Це надає спосіб мати процес роботи схожий на процес роботи з підмодулями без використання підмодулів (які ми розглянемо в Підмодулі). Ми можемо зберігати гілки інших повʼязаних проектів у нашому репозиторії та іноді робити зливання піддерев з ними. Це мило з якогось боку, наприклад, весь код збережено в одному місці. Проте, є інші недоліки: це трохи складніше, та легше наробити помилок при реінтеграції змін або випадково надіслати гілку до геть не того репозиторія.

Ще одна трохи дивна річ: щоб отримати різницю між вашою піддиректорією rack та кодом у вашій гілці rack_branch – щоб побачити, чи треба їх зливати – ви не можете використовувати звичайну команду diff. Замість цього, ви маєте виконати git diff-tree з гілкою, яку ви бажаєте порівняти зі своєю:

$ git diff-tree -p rack_branch

Або, щоб порівняти вашу піддиректорію rack зі змістом гілки master, що був на сервері, коли ви востаннє отримували звідти зміни, ви можете виконати:

$ git diff-tree -p rack_remote/master