Git
Chapters ▾ 2nd Edition

7.8 Git инструменти - Сливане за напреднали

Сливане за напреднали

Обикновено сливането в Git е лесно. Понеже Git позволява сливането на друг клон много пъти, това означава, че можете да имате клон с много дълъг живот, да го поддържате обновен докато работите и да решавате своевременно и често малките конфликти, вместо да трябва да се оправяте с един голям конфликт в края на работата си.

Обаче, понякога възникват по-проблематични конфликти. За разлика от други системи за контрол на версиите, Git не се опитва да бъде прекалено умен що се касае до решаването на merge конфликти. Философията на системата е да е добра в установяването на това дали сливането може да се направи недвусмислено, но ако има конфликт - да не се опитва автоматично да го реши. По тази причина, ако чакате твърде дълго преди да слеете клонове, които се развиват бързо може да се сблъскате с проблеми.

В тази секция ще разгледаме какви биха могли да са някои от тези проблеми и какви инструменти имате на разположение за да се оправите с тях. Ще разгледаме и някои по-различни, нестандартни типове сливане, които можете да направите, както и ще видим как да отмените сливания.

Конфликти при сливане

Въпреки, че вече видяхме основните стъпки за разрешаване на конфликти в Конфликти при сливане, при по-заплетените такива Git осигурява инструменти, с които да установите какво точно се е случило и как по-добре да се справите с проблема.

Преди всичко, ако е възможно, уверете се, че работната ви директория е чиста, преди да опитате сливане, което може да доведе до конфликти. Ако имате текуща работа, опитайте да я къмитнете във временен клон или да я маскирате (stashing). Това ви гарантира, че ще можете да отмените всичко, което предстои да опитате. Ако имате незаписани промени в работната директория когато опитате сливане, някои от следващите действия могат да ви помогнат да съхраните тази работа.

Нека видим един прост пример. Имаме кратък 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(-)

Сега превключаме обратно към master клона и добавяме малко документация за функцията.

$ 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 и изпадаме в конфликтна ситуация заради 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 опитва да върне статуса, в който сте били преди да опитате сливането. Като казваме опитва, единствените случаи, в които не би успяла е ако имате немаскирани (unstashed) или некъмитнати промени в работната директория. Във всички останали случаи тя ще работи коректно.

Ако по някаква причина просто искате да започнете отначало, можете също така да изпълните git reset --hard HEAD и работната ви директория ще бъде върната до последния къмитнат статус. Помнете, че всяка некъмитната работа ще бъде загубена, така че се уверете, че промените наистина не ви трябват.

Игнориране на празните символи

В този конкретен случай проблемите ни възникваха от празните символи във файла. Знаем това, защото случаят е прост, но също така ситуацията е лесно откриваема и в реални случаи, когато разглеждаме причината за конфликта, понеже се вижда, че всеки ред е премахнат от едната страна и повторно добавен в другата. По подразбиране, Git вижда всички тези редове като променени и отказва сливането.

Стратегията за сливане по подразбиране също приема аргументи и някои от тях подпомагат игнорирането на празните символи.

Ако виждате, че имате много whitespace проблеми в сливането, можете просто да го откажете и да го опитате отново, този път с аргумента -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 хипотетично не може да обработи whitespace промените и трябва да направим това на ръка.

Това, което трябва действително да направим е да прекараме файла през програмата dos2unix преди да опитаме сливането. Как можем да направим това?

Първо, изпадаме в конфликтната ситуация. След това, искаме да извлечем собствената версия на файла, версията от клона, който сме опитали да слеем, както и общата версия (тази от която двата клона са стартирани). След това, искаме да поправим или нашата или другата версия и да опитаме отново сливане за само този единичен файл.

Извличането на трите версии в действителност е лесно. Git съхранява всички тях в индекса под формата на “етапи (stages)” като всеки от тях има съответен номер. Stage 1 е общия файл (common), от който произлизат другите два, stage 2 е вашата версия (ours) и stage 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

Ако искате да видите повече подробности, можете да използвате plumbing командата ls-files -u за да получите действителните SHA-1 стойности за всеки от тези файлове

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

Изразът :1:hello.rb е просто съкратен начин да потърсите blob обекта със съответния SHA-1 хеш.

След като вече имаме съдържанието на трите версии на файла в работната директория, можем ръчно да поправим whitespace проблема във файла от клона, който опитваме да слеем и след това да опитаме цялото сливане отново с малко позната команда 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 line ending символи, което смесва нещата и не изглежда красиво.

Ако преди да завършите къмита искате да получите представа за това какво действително е променено между едната страна или другата, можете да поискате от 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. Това ще извлече файла отново и ще замени merge conflict маркерите. Това може да е полезно, ако искате да нулирате маркерите и да опитате да разрешите конфликта отново.

Можете да подадете на --conflict или diff3 или merge (което е по подразбиране). Ако подадете diff3, Git ще използва малко по-различна версия на маркерите за конфликти и ще ви покаже не само “ours” и “theirs” версиите, но също и “base” версията вътре във файла, за да имате повече информация.

$ 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 конфликти задавайки стойност diff3 за настройката merge.conflictstyle.

$ git config --global merge.conflictstyle diff3

Командата git checkout също може да приема --ours и --theirs параметри, което може да е наистина бърз начин за избор на едната или другата страна без въобще да сливаме.

Това може да е особено полезно за конфликти при двоични файлове, където можете просто да изберете едната страна или когато искате да слеете само определени файлове от друг клон - можете да направите сливането и след това да изберете дадени файлове от едната или другата страна преди къмитването.

Дневник на сливанията

Друг полезен инструмент при разрешаване на merge конфликти е git log. Командата може да ви помогне да получите представа за обстоятелствата, при които вие самите бихте могли да сте допринесли за конфликта. Разглеждането на малко история за да си припомните защо два паралелни работни процеса модифицират едно и също място в кода може да е много полезно понякога.

За да получим списък на всички уникални къмити интегрирани в клоновете, които участват в сливането, можем да използваме “triple dot” синтаксиса, за който научихме в Три точки.

$ 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 в такова положение, командата ви дава информация в специфичен 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” и ви дава две колони с информация в началото на всеки ред. Първата колона показва дали редът е различен (добавен или изтрит) между “ours” клона и файла в работната ви директория, а втората колона показва същото но сравнявайки “theirs” клона и копието в работната директория.

Така в този пример можем да видим, че редовете <<<<<<< и >>>>>>> са в работното копие, но не са били в нито едната от страните участващи в сливането. Това има смисъл, защото сливащият механизъм ги е оставил там за нашия контекст, но ние сме очаквали да ги премахне.

Ако разрешим конфликта и изпълним 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 за merge къмит или ако добавите аргумента --cc към git log -p (която по подразбиране показва само пачове за non-merge къмити).

$ 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 е, че системата няма проблем с вашите грешки, понеже е възможно (и в много случаи лесно) да ги поправите.

Сливащите къмити не правят изключение. Да кажем, че сте започнали работа по topic клон, по невнимание сте го сляли в master клона и сега вашата история изглежда така:

Инцидентен сливащ къмит.
Figure 138. Инцидентен сливащ къмит

Има два подхода за справяне с проблема, в зависимост от желания резултат.

Корекция на референциите

Ако нежеланият къмит съществува само в локалното ви хранилище, най-лесното и добро решение е да преместите клоновете така, че да сочат където искате. В повечето случаи, ако след погрешната git merge изпълните git reset --hard HEAD~, това ще коригира указателите на клоновете, така че да изглеждат по следния начин:

Историята след изпълнение на `git reset --hard HEAD~`.
Figure 139. Историята след изпълнение на 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).

Историята с revert къмита сега изглежда така:

Историята след `git revert -m 1`.
Figure 140. Историята след git revert -m 1

Новият къмит ^M има същото съдържание като C6, така че оттук нататък нещата са такива сякаш сливането въобще не се е случвало с изключение на факта, че сега не-слетите къмити още са в историята на HEAD. Git ще изпадне в затруднение, ако сега отново се опитате да слеете topic в master:

$ git merge topic
Already up-to-date.

В topic сега няма нищо, което да не е вече достъпно през master. Което е по-лошо, ако сега направите промени в topic и слеете отново, Git ще вземе промените направени след reverted сливането.

Историята след лошо сливане.
Figure 141. Историята след лошо сливане

Най-добрият начин да заобиколите това е да откажете revert-a на оригиналното сливане, понеже сега искате да върнете отменените промени и след това да създадете нов сливащ къмит:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
История след повторно сливане на reverted merge.
Figure 142. История след повторно сливане на reverted merge

В този пример, M и ^M са отказани. ^^M ефективно слива промените от C3 и`C4`, а C8 слива промените от C7, така че сега topic е напълно слят.

Други типове сливания

Досега прегледахме нормално сливане между два клона, което обикновено се осъществява с т. нар. “recursive” сливаща стратегия. Обаче, има и други начини за сливане на клонове. Нека видим някои от тях набързо.

Our или Theirs преференция

Преди всичко има още едно полезно нещо, което можем да правим с нормалния “recursive” режим на сливане. Вече видяхме ignore-all-space и ignore-space-change опциите подавани с -X, но можем също така да кажем на Git да дава предимство на едната или другата страна при сливането, когато установи конфликт.

По подразбиране, когато Git види конфликт между два сливащи се клона, ще добави merge conflict маркери в кода и ще маркира файла като конфликтен, очаквайки да го коригирате. Ако предпочитате Git просто да избере една от двете опции и да игнорира другата, вместо да остави на вас решаването на проблема, можете да подадете на merge параметрите -Xours или -Xtheirs.

В такъв случай маркери няма да бъдат добавяни. Всички промени, които могат да бъдат слети чисто, ще бъдат слети. При всички промени, които предизвикват конфликт, 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” merge стратегия. Това е различно от “ours” recursive merge опцията.

В действителност това ще направи лъжливо сливане. Процесът ще запише нов сливащ къмит с двата клона като родители, но практически дори и няма да погледне в клона, който сливате. Резултатът ще е сливане, което просто записва съдържанието на текущия клон.

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

Може да видите, че няма разлика между клона, в който сме били и резултата от сливането.

Често подобен подход може да е полезен, за да прилъжете Git да приема, че клонът е вече слят, когато искате да правите сливане по-късно. Да кажем, че сте създали клон release и сте свършили някаква работа в него, която по-късно ще искате да слеете в master. Междувременно се оказва, че в master е направена някаква спешна корекция на грешка и тази промяна трябва да се интегрира в release.

Можете да слеете bugfix клона в release и също да направите merge -s ours за същия клон в master клона (въпреки, че поправката е вече вътре). Така, когато по-късно слеете release клона отново, няма да има конфликти от поправката на грешката.

Subtree сливане

Идеята на subtree сливането е че имате два проекта и единия от тях съществува в поддиректория на другия. Когато укажете subtree сливане, 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 read-tree. Ще научим повече за тази команда и придружаващите я други в Git на ниско ниво, засега просто приемете, че тя прочита главното дърво на един клон в текущия индекс и работна директория. Току що превключихме към master клона и издърпваме rack_branch клона в поддиректорията rack на нашия master клон за основния ни проект:

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

Когато къмитнем, изглежда имаме всички Rack файлове в тази поддиректория — също както ако бяхме копирали директно вътре от архив. Интересното в случая е, че можем сравнително лесно да сливаме промени от единия клон в другия. Така, ако Rack проектът бъде обновен, можем да издърпаме upstream промените като превключим към този клон и стартираме издърпването:

$ git checkout rack_branch
$ git pull

След това, можем да слеем новите промени обратно в нашия master клон. За да изтеглим промените и да попълним предварително къмит съобщението, използваме --squash опцията, както и -Xsubtree параметъра на recursive merge стратегията. (Рекурсивната стратегия се подразбира тук, но я указваме за яснота.)

$ 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 клона по-късно за да ги изпратите на собствениците на проекта.

Това ни дава възможност да следваме работен процес донякъде подобен на submodule без да използваме подмодули (което ще разгледаме в Подмодули). В подобен маниер можем да пазим клонове с други свързани проекти в нашето хранилище и да ги интегрираме при необходимост в нашия собствен проект посредством subtree сливане. В някои аспекти това е хубаво, защото целият код се къмитва на едно място. От друга страна, един такъв подход си има и недостатъци, защото е малко по-сложен, по-лесно е да се правят грешки при повторна интеграция на промени и също така може по невнимание да се публикува погрешен клон в неподходящо хранилище.

Друго неудобство е, че за да получите diff между съдържанията на rack поддиректорията и кода в rack_branch клона (така че да разберете дали се налага да ги сливате) — не можете да използвате нормалната diff команда. Вместо това, трябва да използвате git diff-tree с клона, който искате да сравнявате:

$ git diff-tree -p rack_branch

Или, за да сравните съдържанието на rack поддиректорията със съдържанието на master клона на сървъра последния път когато сте го издърпали, може да изпълните

$ git diff-tree -p rack_remote/master