Git
Chapters ▾ 2nd Edition

7.8 Git Tools - Mergen voor gevorderden

Mergen voor gevorderden

Mergen met Git is normaalgesproken redelijk eenvoudig. Omdat Git het je gemakkelijk maakt om meerdere malen de ene branch met de andere te mergen, betekent dit dat je branches met een lange levensduur kunt hebben maar dat je het gaandeweg up to date kunt houden, vaak kleine conflicten oplossend, in plaats van verrast te worden door een enorme conflict aan het eind van de reeks.

Echter, soms zullen lastige conflicten optreden. In tegenstelling tot andere versie beheer systemen probeert Git niet al te slim te zijn bij het oplossen van merge conflicten. De filosfie van Git is om slim te zijn over het bepalen wanneer een merge oplossing eenduidig is, maar als er een conflict is probeert het niet slim te zijn en het automatisch op te lossen. Dit is de reden dat als je te lang wacht met het meren van twee branches die snel uiteenlopen, dat je tegen een aantal situaties zult aanlopen.

In dit hoofdstuk zullen we van een aantal van die situaties laten zien wat de oorzaak kan zijn en welke instrumenten Git je geeft om je te helpen deze meer lastige situaties op te lossen. We zullen ook een aantal van de afwijkende, niet standaard type merges behandelen die je kunt uitvoeren, zowel als aangeven hoe je merges die je hebt uitgevoerd weer kunt terugdraaien.

Merge conflicten

We hebben een aantal beginselen van het oplossen van merge conflicten in Eenvoudige merge conflicten behandeld, voor meer complexe conflicten geeft Git je een aantal instrumenten om uit te vinden wat er aan de hand is en hoe beter met het conflict om te gaan.

Als eerste, als het enigszins mogelijk is, probeer je werk directory op te schonen voor je een merge uitvoert die conflicten zou kunnen bevatten. Als je onderhanden werk hebt, commit dit naar een tijdelijke branch of stash het. Dit zorgt ervoor dat je alles kunt terugdraaien wat je hier probeert. Als je niet bewaarde wijzigingen in je werk-directory hebt als je een merge probeert, kunnen een aantal tips die we geven ervoor zorgen dat je dit verliest.

Laten we een erg eenvoudig voorbeeld doorlopen. We hebben een super simpel Ruby bestand dat hello world afdrukt.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

In onze repository maken we een nieuwe branch genaamd whitespace en vervolgen we door alle Unix regel-einden te vervangen met DOS regel-einden, eigenlijk gewoon elke regel van het bestand wijzigend, maar alleen met witruimte. Dan wijzigen we de regel “hello world” in “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(-)

Nu switchen we terug naar onze master-branch en voegen wat documentatie aan de functie toe.

$ 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(+)

Nu gaan we proberen onze whitespace-branch te mergen en we zullen conflicten krijgen vanwege de witruimte wijzigingen.

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

Een merge afbreken

We kunnen nu kiezen. Als eerste, laten we bekijken hoe we uit deze situatie kunnen komen. Als je geen conflicten had verwacht en je wilt nu nog even niet met deze situatie te maken hebben, kan je eenvoudig de merge terugdraaien met git merge --abort.

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

$ git merge --abort

$ git status -sb
## master

De git merge --abort optie probeert de status terug te halen van voor je probeerde de merge te draaien. De enige gevallen waar dit misschien niet helemaal mogelijk is zou zijn als je ge-unstashde, ongecommitte wijzigingen in je werk-directory zou hebben staan toen je het aanriep, in alle andere gevallen zou het prima moeten werken.

Als je voor wat voor reden ook jezelf in een enorme bende weet te krijgen en je wilt gewoon opnieuw beginnen, kan je ook git reset --hard HEAD aanroepen of waar je ook naartoe wilt terugkeren. Onthoud: al het niet gecommitte werk gaat verloren, dus verzeker je ervan dat je hier geen onderhanden werk wilt hebben.

Witruimtes negeren

In dit specifieke geval hebben de conflicten met witruimtes te maken. We weten dit omdat dit geval eenvoudig is, maar het is ook redelijk eenvoudig te achterhalen in praktijksituaties als je naar een conflict kijkt, omdat elke regel is verwijderd aan de ene kant en weer aan de andere kant wordt toegevoegd. Standaard ziet Git al deze regels als gewijzigd en kan het dus de bestanden niet mergen.

De standaard merge strategie kan echter argumenten meekrijgen, en een aantal van deze gaan over het op een nette manier negeren van witruimte wijzigingen. Als je ziet dat je een groot aantal witruimte issues hebt in een merge, kan je deze eenvoudigweg afbreken en nogmaals uitvoeren, deze keer met -Xignore-all-space of -Xignore-space-change. De eerste optie negeert alle witruimte volledig bij het vergelijken van de regels, en de tweede beschouwt volgorderlijke witruimte karakters als gelijk.

$ 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(-)

Omdat in dit geval de eigenlijke bestandswijzigingen niet conflicteren, zal het negeren van de witruimte wijzigingen leiden tot een geslaagde merge.

Dit bespaart je veel werk als je iemand in je team hebt die regelmatig graag alles herformateert van spaties naar tabs of omgekeerd.

Handmatig bestanden opnieuw mergen

Hoewel Git het voorbewerken van witruimtes redelijk goed doet, zijn er andere soorten van wijzigingen waar Git misschien niet automatisch mee om kan gaan, maar zijn er oplossingen die met scripts gevonden kunnen worden. Als voorbeeld, stel dat Git de witruimte wijziging niet had kunnen verwerken en we hadden dit met de hand moeten doen.

Wat we echt zouden moeten doen is het bestand dat we proberen te mergen met een dos2unix programma bewerken voordat we de echte bestandsmerge uitvoeren. Dus, hoe zouden we dat doen?

Allereerst moeten we in de situatie van de merge conflict geraken. Dan willen we kopieën maken van mijn versie van het bestand, hun versie van het bestand (van de branch die we proberen te mergen) en de gezamelijke versie (van waar beide branches zijn afgesplitst). Daarna willen we een van de twee kanten verbeteren (die van hun of van ons) en dan de merge opnieuw proberen maar dan voor alleen deze ene bestand.

Het krijgen van de drie bestandversies is eigenlijk vrij makkelijk. Git bewaart al deze versies in de index onder “stages” die elk met getallen zijn geassocieerd. Stage 1 is de gezamelijke voorouder, stage 2 is jouw versie en stage 3 is van de MERGE_HEAD, de versie die je probeert te mergen (“theirs”).

Je kunt een kopie van elk van deze versie van het conflicterende bestand met het git show commando gecombineerd met een speciale syntax verkrijgen.

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

Als je iets dichter op het ijzer wilt werken, kan je ook het ls-files -u binnenwerk commando gebruiken om de echte SHA-1 nummers op te zoeken van de Git blobs voor elk van deze bestanden.

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

Het :1:hello.rb is gewoon een verkorte manier om de SHA-1 van de blob op te zoeken.

Nu we de inhoud van de drie stages in onze werk directory hebben, kunnen we handmatig die van hun opknappen om het witruimte probleem op te lossen en de bestanden opnieuw te mergen met het vrij onbekende git merge-file commando die precies dat doet.

$ 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()

We hebben op dit moment het bestand fijn gemerged. Sterker nog, dit werkt eigenlijk beter dan de ignore-space-change optie omdat dit echt de witruimte wijzigingen verbetert voor we mergen inplaats van ze gewoon te negeren. In de ignore-space-change merge, eindigen we uiteindelijk met een paar regels met DOS regel-einden, waardoor we dingen vermengen.

Als je voordat de commit wordt beeindigd een indruk wilt krijgen van wat daadwerkelijk gewijzigd is tussen de ene en de andere kant, kan je git diff vragen om wat in je werk directory zit te vergelijken met wat je van plan bent te committen als resultaat van de merge met elk van deze stages. Laten we ze eens allemaal bekijken.

Om het resultaat te vergelijken met wat je in je branch had voor de merge, met andere woorden om te zien wat de merge geïntroduceerd heeft, kan je git diff --ours aanroepen

$ 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()

Dus hier kunnen we makkelijk zien wat er in onze branch gebeurd is, wat we daadwerkelijk in dit bestand introduceren met deze merge is de wijziging in die ene regel.

Als we willen zien hoe het resultaat van de merge verschilt met wat er op hun kant stond, kan je git diff --theirs aanroepen. In deze en het volgende voorbeeld, moeten we -b gebruiken om de witruimtes te verwijderen omdat we het vergelijken met wat er in Git staat, niet onze opgeschoonde hello.their.rb bestand.

$ 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

Als laatste kan je zien hoe het bestand is veranderd ten opzichte van beide kanten met 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()

Op dit moment kunnen we het git clean commando gebruiken om de extra bestanden te verwijderen die we hebben gemaakt om de handmatige merge uit te voeren maar die we niet langer meer nodig hebben.

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

Conflicten beter bekijken

Misschien zijn we om de een of andere reden niet tevreden met de huidige oplossing, of misschien werkt het handmatig wijzigen van een of beide kanten nog steeds niet goed en moeten we meer van de omstandigheden te weten komen.

Laten we het voorbeeld een beetje veranderen. In dit voorbeeld hebben we twee langer doorlopende branches die elk een aantal commits hebben maar die, wanneer ze worden gemerged, een echt conflict op inhoud opleveren.

$ 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

We hebben nu drie unieke commits die alleen in de master-branch aanwezig zijn en drie andere die op de mundo-branch zitten. Als we de mundo-branch willen mergen krijgen we een conflict.

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

We zouden willen zien wat de merge conflict is. Als we het bestand openen, zie we iets als dit:

#! /usr/bin/env ruby

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

hello()

Beide kanten van de merge hebben inhoud aan dit bestand toegevoegd, maar een aantal van de commits hebben het bestand op dezelfde plaats gewijzigd waardoor dit conflict optreedt.

Laten we een aantal instrumenten verkennen die je tot je beschikking hebt om te bepalen hoe dit conflict tot stand is gekomen. Misschien is het niet duidelijk hoe je dit conflict precies moet oplossen. Je hebt meer kennis van de context nodig.

Een handig instrument is git checkout met de --conflict optie. Dit zal het bestand opnieuw uitchecken en de merge conflict markeringen vervangen. Dit kan nuttig zijn als je de markeringen wilt verwijderen en de conflicten opnieuw wilt oplossen.

Je kunt aan --conflict of diff3 of merge doorgeven (de laatste is de standaard). Als je het diff3 doorgeeft zal Git iets andere soorten conflict markeringen gebruiken, waarbij je niet alleen de “ours” en “theirs” versies krijgt, maar ook de “base” versie inline waardoor je meer context krijgt.

$ git checkout --conflict=diff3 hello.rb

Als we dat nu aanroepen, zal het bestand er nu zo uit zien:

#! /usr/bin/env ruby

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

hello()

Als dit formaat je bevalt, kan je dit als standaard instellen voor toekomstige merge conflicten door de merge.conflictstyle instelling op diff3 te zetten.

$ git config --global merge.conflictstyle diff3

Het git checkout commando kan ook de opties --ours en --theirs verwerken, wat een ontzettend snelle manier kan zijn om gewoon een van de twee kanten te kiezen waarbij er gewoon niet gemerged zal worden.

Dit is in het bijzonder handig voor conflicten tussen binaire bestanden waar je gewoon een kant kiest, of waar je alleen bepaalde bestanden wilt mergen van een andere branch - je kunt de merge uitvoeren en dan bepaalde bestanden bekijken dan een of de andere kant voordat je commit.

Merge log

Een ander nuttig instrument bij het oplossen van merge conflicten is git log. Dit kan je helpen bij het verkrijgen van inzicht in wat kan hebben bijgedragen tot het conflict. Een stukje historie nakijken om boven water te krijgen waarom twee ontwikkelingen dezelfde gebieden raakten kan soms erg behulpzaam zijn.

Om een complete lijst te krijgen van alle unieke commits die in elk van beide branches zitten en die betrokken zijn bij deze merge, kunnen we de “drievoudige punt” syntax gebruiken die we geleerd hebben in Drievoudige punt.

$ 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

Dat is een mooie lijst van de in totaal zes betrokken commits, zowel als bij welke ontwikkellijn elke commit gedaan is.

We kunnen dit echter verder vereenvoudigen om ons een meer specifieke context te geven. Als we de --merge optie gebruiken bij git log, zal het alleen de commits tonen van beide kanten van de merge die een bestand raken dat op dit moment in een conflict betrokken is.

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

Als je het daarentegen met de -p optie aanroept, krijg je alleen de diffs met het bestand dat in een conflict betrokken is geraakt. Dit kan heel handig zijn bij het snel verkrijgen vn de context die je nodig hebt om te begrijpen waarom iets conflicteerd en hoe het beter overwogen op te lossen.

Gecombineerde diff formaat

Omdat Git alle merge resultaten die succesvol zijn staget, zal het aanroepen van git diff terwijl je in een conflicterende merge status zit, je alleen laten zien wat op dit moment zich nog steeds in een conflicterende status bevindt. Dit kan heel handig zijn om te zien wat je nog steeds moet oplossen.

Als je git diff direct aanroept na een merge conflict, zal het je informatie geven in een nogal unieke diff uitvoer formaat.

$ 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()

Dit formaat heet “Gecombineerde Diff” (Combined Diff) en geeft je twee kolommen met gegevens naast elke regel. De eerste kolom laat je zien dat die regel anders is (toegevoegd of verwijderd) tuseen de “ours” branch en het bestand in je werk directory en de tweede kolom doet hetzelfde tussen de “theirs” branch en de kopie in je werk directory.

Dus in het voorbeeld kan je zien dat de <<<<<<< en >>>>>>> regels in de werk kopie zitten maar in geen van beide kanten van de merge. Dit is logisch omdat de merge tool ze daar in heeft gezet voor onze context, maar het wordt van ons verwacht dat we ze weghalen.

Als we het conflict oplossen en git diff nogmaals aanroepen, zien we hetzelfde, maar het is iets bruikbaarder.

$ 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()

Het laat ons zien dat “hola world” aan onze kant zat maar niet in de werk kopie, dat “hello mundo” aan hun kant stond maar niet in de werk kopie en uiteindelijk dat “hola mundo” in geen van beide kanten zat maar nu in de werk kopie staat. Dit kan nuttig zijn om na te kijken voordat de oplossing wordt gecommit.

Je kunt dit ook krijgen van de git log voor elke merge nadat deze is gedaan om achteraf te zien hoe iets was opgelost. Git zal deze uitvoer-vorm kiezen als je git show aanroept op een merge commit, of als je de --cc optie gebruikt bij een git log -p (die standaard alleen patces voor non-merge commits laat zien).

$ 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()

Merges ongedaan maken

Nu je weet hoe merge commits te maken, zal je waarschijnlijk er een aantal per ongeluk maken. Een van de mooie dingen van het werken met Git is dat het niet erg is om vergissingen te begaan, omdat het mogelijk is (en in veel gevallen makkelijk) om ze te herstellen.

Merge commits zijn niet anders. Stel dat je bent begonnen met werken op een topic branch, deze abusiefelijk in master heb gemerged, en nu zie je commit historie er zo uit:

Abusievelijke merge commit.
Figuur 138. Abusievelijke merge commit

Er zijn twee manieren om dit probleem te benaeren, afhankelijk van wat de gewenste uitkomst is.

De referenties herstellen

Als de ongewilde merge commit alleen bestaat in je lokale repository, is de eenvoudigste en beste oplossing om de branches dusdanig te verplaatsten dat ze wijzen naar waar je wilt hebben. In de meeste gevallen, als je de foutieve git merge opvolgt met git reset --hard HEAD~, zal dit de branch verwijzingen herstellen zodat ze er zo uit zien:

Historie na `git reset --hard HEAD~`.
Figuur 139. Historie na git reset --hard HEAD~

We hebben reset in Reset ontrafeld behandeld, dus het zou niet al te moeilijk uit te vinden wat hier gebeurt. Hier is een snelle opfrisser: reset --hard volgt normaalgesproken de volgende drie stappen:

  1. Verplaats de branch waar HEAD naar wijst In dit geval willen we master verplaatsen naar waar het was voor de merge commit (C6).

  2. Laat de index eruit zien als HEAD.

  3. Laat de werk directory eruit zien als de index.

Het nadeel van deze aanpak is dat het de historie herschrijft, wat problematisch kan zijn met een gedeelde repository. Bestudeer De gevaren van rebasen> om te zien wat er dan gebeuren kan; in het kort houdt het in dat als andere mensen de commits hebben die jij aan het herschrijven bent, je reset eigenlijk wilt vermijden. Deze aanpak zal ook niet werken als er andere commits gemaakt zijn sinds de merge; het verplaatsen van de referenties doet deze wijzigingen ook teniet.

De commit terugdraaien

Als het verplaatsen van de branch verwijzingen niet gaat werken voor je, geeft Git je de optie van het maken van een nieuwe commit die alle wijzigingen van een bestaande terugdraait. Git noemt deze operatie een “revert”, en in dit specifieke scenario zou je het als volgt aanroepen:

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

De -m 1 vlag geeft aan welke ouder de “hoofdlijn” (mainline) is en behouden moet blijven. Als je een merge naar HEAD begint (git merge topic), heeft de nieuwe commit twee ouders: de eerste is HEAD (C6), en de tweede is de punt van de branch die erin wordt gemerged (C4). In dit geval, willen we alle wijzigingen die zijn geïntroduceerd door het mergen van ouder #2 (C4) terugdraaien, terwijl we de alle inhoud van ouder #1 (C6) behouden.

De historie met de terugdraaiende commit ziet er zo uit:

Historie na `git revert -m 1`.
Figuur 140. Historie na git revert -m 1

De nieuwe commit ^M heeft precies dezelfde inhoud als C6, dus beginnende vanaf hier is het alsof de merge nooit heeft plaatsgevonden, behalve dat de commits die zijn ge-unmerged nog steeds in de geschiedenis van HEAD aanwezig zijn. Git zal in de war raken als je probeert topic weer in master te mergen:

$ git merge topic
Already up-to-date.

Er is niets in topic wat niet al bereikbaar is vanaf master. Wat erger is, als je werk toevoegt aan topic en weer merged, zal Git alleen de wijzigingen meenemen sinds de teruggedraaide merge:

Historie met een slechte merge.
Figuur 141. Historie met een slechte merge

De beste manier om hiermee om te gaan is om de originele merge te ont-terugdraaien, omdat je nu de wijzigingen wilt doorvoeren die eruit waren verwijderd door ze terug te draaien, en dan een nieuwe merge commit te maken:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
Historie na het opnieuw mergen van een teruggedraaide merge.
Figuur 142. Historie na het opnieuw mergen van een teruggedraaide merge

In dit voorbeeld, neutraliseren M en ^M elkaar. ^^M merget effectief de wijzigingen van C3 en C4, en C8 merged de wijzigingen van C7, dus nu is topic volledig gemerged.

Andere soorten merges

Tot zover hebben we de reguliere merge van twee branches behandeld, normaalgesproken afgehandeld met wat de “recursive” merge strategie wordt genoemd. Er zijn echter andere manieren om branches samen te voegen. Laten we een aantal van deze snel bespreken.

Voorkeur voor de onze of de hunne

Allereerst, is er een ander nuttig iets wat we met de normale “recursive” merge wijze kunnen doen. We hebben de ignore-all-space en ignore-space-change opties gezien die met een -X worden doorgegeven, maar we kunnen Git ook vertellen om de ene of de andere kant de voorkeur te geven als het een conflict bespeurt.

Standaard als Git een conflict ziet tussen twee branche die worden gemerged, zal het merge conflict markeringen in je code toevoegen en het bestand als conflicted bestempelen en je het laten oplossen. Als je de voorkeur hebt dat Git eenvoudigweg een bepaalde kant kiest en de andere kant negeert in plaats van je handmatig het conflict te laten oplossen kan je het merge commando een -Xours of -Xtheirs doorgeven.

Als Git dit ziet, zal het geen conflict markeringen invoegen. Alle verschillen die merge-baar zijn zal het mergen. Bij alle verschillen die conflicteren zal het simpelweg in het geheel de kant kiezen doe je opgeeft, ook bij binaire bestanden.

Als we terugkijken naar het “hello world” voorbeeld die we hiervoor gaven, kunnen we zien dat mergen in onze branch conflicten gaf.

$ 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.

Echter als we het gebruiken met -Xours of -Xtheirs gebeurt dit niet.

$ 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

In dat geval, in plaats van conflict markeringen te krijgen in het bestand met “hello mundo” aan de ene en “hola world” aan de andere kant, zal het simpelweg “hola world” kiezen. Echter alle andere niet conflicterende wijzigingen in die branch zijn succesvol samengevoegd.

Deze optie kan ook aan het git merge-file commando die we eerder zagen worden doorgegeven door iets als git merge-file --ours aan te roepen voor individuele file merges.

Als je iets als die wilt doen maar Git niet eens wilt laten proberen wijzigingen van de andere kant te laten samenvoegen, is er een meer draconische optie, en dat is de “ours” merge strategie. Dit verschilt met de “ours” recursieve merge optie.

Dit zal feitelijk een nep merge uitvoeren. Het zal een nieuwe merge commit vastleggen met beide branches als ouders, maar het zal niet eens kijken naar de branch die je merget. Het zal eenvoudigweg de exacte code in je huidige branch vastleggen als het resultaat van de merge.

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

Je kunt zien dat er geen verschil is tussen de branch waar we op zaten en het resultaat van de merge.

Dit kan vaak handig zijn als je Git wilt laten denken dat een branch al ingemerget is wanneer je later een merge aan het doen bent. Bijvoorbeeld, stel dat je een “release” gemaakt hebt en daar wat werk aan gedaan hebt dat je later zeker naar je master-branche wilt mergen. In de tussentijd moet er een of andere bugfix op master moet teruggebracht (backported) worden naar je release-branch. Je kunt de bugfix branch in de release-branch mergen en dezelfde branch ook met merge -s ours in je master-branch mergen (zelfs als de fix daar al aanwezig is) zodat later als je de release-branch weer merget, er geen conflicten met de bugfix zijn.

Het mergen van subtrees

Het idee achter de subtree merge is dat je twee projecten hebt, en een van de projecten verwijst naar een subdirectory van de ander en vice versa. Als je een subtree merge specificeert, is Git vaak slim genoeg om uit te vinden dat de ene een subtree van de ander en zal daarvoor passend mergen.

We zullen een voorbeeld doornemen van het toevoegen van een separaat project in een bestaand project en dan de code mergen van de tweede naar een subdirectory van de eerste.

Eerst zullen we de Rack applicatie aan ons project toevoegen. We gaan het Rack project als een remote referentie in ons project toevoegen en deze dan uitchecken in zijn eigen branch:

$ 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"

Nu hebben we de root van het Rack project in onze rack_branch-branch en ons eigen project in de master-branch. Als je eerst de ene en dan de andere uitcheckt, kan je zien dat ze verschillende project roots hebben:

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

Dit is een beetje een raar concept. Het is niet verplicht dat alle branches in je repository branches van hetzelfde project zijn. Het is niet iets wat vaak voorkomt, omdat het zelden behulpzaam is, maar het is relatief eenvoudig om branches te hebben die volledig verschillende histories hebben.

In dit geval willen we het Rack project in onze master-project binnenhalen (pull) als een subdirectory. We kunnen dat in Git doen met git read-tree. Je zult meer over read-tree en zijn vriendjes leren in Git Binnenwerk, maar neem voor nu aan dat het de root tree van een branch naar je huidige staging area en werk dirctory inleest. We zijn zojuist teruggeschakeld naar de master-branch, en we pullen de rack_branch-branch in de rack subdirectory van de master-branch van ons hoofdproject:

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

Als we committen, zal het lijken alsof we alle Rack bestanden onder die subdirectory hebben - alsof we ze vanuit een tarball gekopieerd hebben. Wat dit interessant maakt, is dat we relatief eenvoudig wijzigingen van de ene branch naar de andere kunnen mergen. Dus, als het Rack project wijzigt, kunnen we upstream wijzigingen binnenhalen door naar die branch over te schakelen en te pullen:

$ git checkout rack_branch
$ git pull

Daarna kunnen we die wijzigingen in onze master-branch mergen. Om de wijzigingen binnen te halen en de commit message alvast in te vullen, gebruik je de --squash optie zowel als de -Xsubtree optie van de recursieve merge strategy. (De recursieve strategie is hier de standaard, maar we voegen het voor de duidelijkheid toe).

$ 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

All de wijzigingen van het Rack project zijn gemerged en klaar om lokaal te worden gecommit. Je kunt ook het tegenovergestelde doen - de wijzigingen in de rack subdirectory van je master branch maken en ze dan later naar je rack_branch-branch mergen om ze dan in te dienen bij de beheerders of ze stroomopwaarts te pushen.

Dit is een manier om een workflow te krijgen die lijkt op de submodule workflow zonder submodules te gebruiken (wat we in Submodules zullen behandelen). We kunnen in onze repository branches aanmaken met andere gerelateerde projecten en ze bij tijd en wijle subtree mergen in ons project. Dit is in sommige opzichten handig, bijvoorbeeld omdat alle code op een enkele plaats wordt gecommit. Het heeft echter ook nadelen in de zin dat het iets complexer is en gevoeliger voor fouten in het herintegreren van wijzigingen of abusievelijk een branch te pushen naar een niet gerelateerde repository.

Een ander gek iets is dat om een diff te krijgen tussen wat je in je rack subdirectory hebt en de code in je rack_branch-branch - om te zien of je ze moet mergen - kan je niet het normale diff commando gebruiken. In plaats daarvan moet je git diff-tree aanroepen met de branch waar het je mee wilt vergelijken:

$ git diff-tree -p rack_branch

Of, om wat in je rack subdirectory zit te vergelijken met wat de master-branch op de server was de laatste keer dat je gefetcht hebt kan je dit aanroepen

$ git diff-tree -p rack_remote/master
scroll-to-top