Git
Chapters ▾ 2nd Edition

7.8 Orodja Git - Napredno združevanje

Napredno združevanje

Združevanje v Gitu je običajno precej enostavno. Ker Git omogoča večkratno združevanje druge veje, imate lahko zelo dolgotrajno vejo, vendar jo lahko vzdržujete tako, da pogosto rešujete majhne konflikte, namesto da vas na koncu preseneti ogromen konflikt.

Vendar včasih se pojavijo zapleteni konflikti. V primerjavi z nekaterimi drugimi sistemi za upravljanje različic, Git ne poskuša biti preveč pameten pri reševanju konfliktov združevanja. Gitova filozofija je, da je pameten pri določanju, kdaj je združevanje nedvoumno, vendar če obstaja konflikt, ga ne poskuša avtomatsko rešiti. Zato se lahko srečate s težavami, če predolgo čakate na združevanje dveh hitro razhajajočih se vej.

V tem razdelku bomo pregledali nekatere od teh težav in orodja, ki jih Git ponuja za pomoč pri reševanju teh bolj zapletenih situacij. Pokrili bomo tudi nekatere različne, nestandardne vrste združevanja ter videli, kako se lahko umaknemo iz opravljenih združevanj.

Konflikti združevanja

Medtem ko smo v razdelku Konflikti osnovnega združevanja predstavili nekaj osnov reševanja konfliktov med združevanjem, Git ponuja nekaj orodij za pomoč pri reševanju bolj zapletenih konfliktov.

Preden opravite združevanje, ki bi lahko povzročilo konflikte, poskusite najprej poskrbeti, da je delovni imenik čist. Če imate delo v teku, ga shranite v začasno vejo, ali pa ga dajte v shrambo na varno (angl. stash). Tako lahko razveljavite karkoli, kar tukaj poskušate. Če imate v delovnem imeniku neshranjene spremembe, ko poskusite združevati, vam lahko nekaj teh nasvetov pomaga pri ohranjanju tega dela.

Pojdimo skozi zelo preprost primer. Imamo zelo preprosto datoteko Ruby, ki izpiše hello world.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

V svojem repozitoriju ustvarimo novo vejo, imenovano whitespace in nadaljujemo s spreminjanjem vseh končnic vrstic Unix v končnice vrstic DOS, torej dejansko spremenimo vsako vrstico datoteke, vendar le s praznimi znaki. Nato spremenimo vrstico »hello world« v »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 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert 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 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
 1 file changed, 1 insertion(+), 1 deletion(-)

Sedaj preklopimo nazaj na našo vejo master in dodamo nekaj dokumentacije za funkcijo.

$ 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 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
 1 file changed, 1 insertion(+)

Sedaj poskusimo združiti v našo vejo whitespace in dobimo konflikte zaradi sprememb praznih znakov.

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

Prekinitev združevanja

Zdaj imamo nekaj možnosti. Najprej se pogovorimo, kako iz te situacije priti ven. Če niste pričakovali konfliktov in se z njimi ne želite spopasti, lahko preprosto razveljavite združevanje z git merge --abort.

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

$ git merge --abort

$ git status -sb
## master

Možnost git merge --abort poskuša vrniti vaše stanje na tisto pred zagonom združevanja. Edini primeri, ko morda ne bo mogla tega storiti popolnoma, so, če ste imeli ob zagonu neshranjene in nepotrjene spremembe v delovnem imeniku, sicer bi moralo delovati v redu.

Če želite iz nekega razloga preprosto začeti znova, lahko zaženete tudi git reset --hard HEAD in vaš repozitorij se bo vrnil v zadnje potrjeno stanje. Ne pozabite, da bodo izgubljene vse nepotrjene spremembe, zato preverite, da ne želite ohraniti nobenih sprememb.

Ignoriranje praznih znakov

V tem konkretnem primeru so konflikti povezani s praznimi znaki. To vemo, ker je primer preprost, a je tudi v resničnih primerih precej enostavno ugotoviti, ko gledamo konflikt, saj je na eni strani odstranjena vsaka vrstica in na drugi strani spet dodana. Privzeto Git vidi vse te vrstice kot spremembe, zato datoteke ne more združiti.

Privzeta strategija združevanja lahko sprejme tudi argumente, nekaj med njimi pa se jih nanaša na ustrezno ignoriranje sprememb praznih znakov. Če ugotovite, da imate v združevanju veliko težav s praznimi znaki, ga lahko preprosto prekinete in ga ponovno zaženete, tokrat z uporabo -Xignore-all-space ali -Xignore-space-change. Prva možnost povsem ignorira prazne znake pri primerjanju vrstic, druga pa obravnava zaporedja enega ali več praznih znakov kot enakovredna.

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

Ker v tem primeru dejanske spremembe datoteke niso konfliktne, ko enkrat prezremo spremembe praznih znakov, se vse združi brez težav.

To je rešitelj življenj, če imate na svoji ekipi nekoga, ki rad občasno preoblikuje vse iz presledkov v tabulatorje ali obratno.

Ročno ponovno združevanje datotek

Čeprav se Git dobro spopada s predobdelavo praznih znakov, obstajajo drugi tipi sprememb, ki jih morda Git ne more samodejno obdelati, vendar so pa skriptni popravki. Kot primer si predstavljamo, da Git ni mogel obdelati spremembe praznih znakov in jo moramo opraviti ročno.

Tisto, kar resnično potrebujemo, je, da datoteko, ki jo želimo združiti, poženemo skozi program dos2unix, preden poskusimo dejansko združitev datoteke. Kako bi to storili?

Najprej se znajdemo v stanju konflikta združevanja. Nato želimo dobiti kopije svoje različice datoteke, njihove različice (iz veje, ki jo združujemo) in skupne različice (od koder sta se obe strani odcepili). Nato želimo popraviti bodisi njihovo stran bodisi svojo stran in ponovno poskusimo združiti samo to eno datoteko.

Dobivanje treh različic datoteke je dejansko precej enostavno. Git vse te različice shrani v indeksu pod »stopnjami«, kjer ima vsaka številko povezano z njo. Stopnja 1 je skupni prednik, stopnja 2 je vaša različica in stopnja 3 je iz MERGE_HEAD, različica, ki jo združujete (»theirs«).

Kopijo vsake od teh različic konfliktne datoteke lahko izvlečete s pomočjo ukaza git show in posebne sintakse.

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

Če želite biti malo bolj hard core, lahko uporabite ukaz ls-files -u, da dobite dejanske vrednosti SHA-1 blobov Git za vsako od teh datotek.

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

:1:hello.rb je samo bližnjica za iskanje tega bloba SHA-1.

Sedaj, ko imamo vsebine vseh treh stopenj v svojem delovnem imeniku, lahko njihovo težavo praznih znakov ročno popravimo in poskusimo ponovno združiti datoteko z manj znanim ukazom git merge-file, ki počne ravno to.

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

V tem trenutku smo lepo združili datoteko. Pravzaprav to deluje bolje kot možnost ignore-space-change, saj dejansko popravi spremembe praznih znakov pred združitvijo, namesto da jih preprosto ignorira. Pri združitvi z možnostjo ignore-space-change smo dejansko dobili nekaj vrstic s koncem vrstice DOS, kar je povzročilo zmedo.

Če želite pred dokončanjem te potrditve dobiti idejo o tem, kaj se je dejansko spremenilo med enim ali drugim delom, lahko uporabite ukaz git diff, da primerjate, kaj je v vašem delovnem direktoriju, ki ga želite potrditi kot rezultat združitve na katerokoli od teh stopenj. Pojdi skozi vse.

Da primerjate svoj rezultat s tistim, kar ste imeli v svoji veji pred združitvijo, z drugimi besedami, da vidite, kaj je združitev uvedla, lahko zaženete 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()

Tukaj lahko jasno vidimo, da se je v naši veji spremenila samo ta ena vrstica in to vnesemo v to datoteko z združevanjem.

Če želimo videti, kako se je rezultat združevanja razlikoval od tistega, kar je bilo na njihovi strani, lahko zaženemo git diff --theirs. V tem in naslednjem primeru moramo uporabiti -b, da odstranimo prazne znake, ker primerjamo s tem, kar je v Gitu in ne z našo očiščeno datoteko 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

Na koncu lahko z git diff --base vidite, kako se je datoteka spremenila iz obeh strani.

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

V tem trenutku lahko uporabimo ukaz git clean, da počistimo dodatne datoteke, ki smo jih ustvarili pri ročnem združevanju in jih ne potrebujemo več.

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

Preverjanje konfliktov

Morda iz nekega razloga nismo zadovoljni z rešitvijo v tem trenutku, ali pa je morda ročno urejanje ene ali obeh strani še vedno slabo delovalo in potrebujemo več konteksta.

Naj malo spremenimo primer. V tem primeru imamo dve dolgotrajni veji, kjer ima vsaka od njiju nekaj potrditev, vendar ob združevanju ustvarita legitimni konflikt vsebine.

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

Sedaj imamo tri unikatne potrditve, ki se nahajajo samo v veji master ter tri ostale, ki se nahajajo v veji mundo. Če poskusimo združiti vejo mundo, dobimo konflikt.

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

Videti bi želeli, za kateri konflikt združevanja gre. Če odpremo datoteko, bomo videli nekaj takega:

#! /usr/bin/env ruby

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

hello()

Obe strani združevanja sta dodali vsebino v to datoteko, vendar nekaj potrditev je spremenilo datoteko na istem mestu, kar je povzročilo ta konflikt.

Raziščimo nekaj orodij, ki so vam na voljo, da ugotovite, kako je prišlo do tega konflikta. Morda ni očitno, kako točno bi ta konflikt morali rešiti. Potrebujete več konteksta.

Eno izmed uporabnih orodij je git checkout z možnostjo --conflict. To bo ponovno izvleklo datoteko in zamenjalo oznake konfliktov med združevanjem. To je lahko koristno, če želite ponastaviti oznake in poskusiti znova rešiti konflikte.

Možnost --conflict lahko podate diff3 ali merge (kar je privzeto). Če jo podate diff3, bo Git uporabil nekoliko drugačno različico oznak konfliktov, ki vam ne bodo dale samo »naših« in »njihovih« različic, ampak tudi »osnovno« različico, ki vam bo dala več konteksta.

$ git checkout --conflict=diff3 hello.rb

Ko enkrat to poženemo, bo datoteka videti takole:

#! /usr/bin/env ruby

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

hello()

Če vam je ta oblika ustrezna, jo lahko nastavite kot privzeto za prihodnje konflikte združevanja z nastavitvijo merge.conflictstyle pri diff3.

$ git config --global merge.conflictstyle diff3

Ukaz git checkout lahko uporabimo tudi z možnostima --ours in --theirs, kar je zelo hiter način izbire samo ene strani brez združevanja.

To je lahko posebej uporabno za konflikte binarnih datotek, kjer lahko preprosto izberete eno stran, ali pa za združevanje določenih datotek iz druge veje — izvedete lahko združevanje in nato preprosto izvlečete določene datoteke z ene strani ali druge, preden izvedete potrditev.

Dnevnik združevanja

Drugo uporabno orodje pri reševanju konfliktov združevanja je git log. To vam lahko pomaga dobiti kontekst o tem, kaj je lahko prispevalo h konfliktom. Včasih lahko zelo pomaga pregledati nekaj zgodovine, da se spomnite, zakaj sta se dve vrstici razvoja dotaknili istega dela kode.

Če želimo dobiti popoln seznam vseh edinstvenih potrditev, ki so bile vključene v katerokoli vejo, ki sodeluje pri tem združevanju, lahko uporabimo sintakso »trojne pike«, ki smo se je naučili v Trojna pika.

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'

To je dober seznam šestih vključenih skupnih potrditev, kot tudi na kateri vrsti razvoja je bila vsaka od teh potrditev.

To lahko še bolj poenostavimo, da dobimo natančnejši kontekst. Če dodamo ukazu git log možnost --merge, bo prikazal samo tiste potrditve na vsaki strani združevanja, ki se dotikajo datoteke, ki je trenutno v konfliktu.

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

Če to zaženete z možnostjo -p, dobite samo razlike v datoteki, ki so povzročile konflikt. To lahko zelo pomaga, saj vam hitro zagotovi kontekst, ki ga potrebujete, da razumete, zakaj je nekaj v konfliktu in kako ga bolj inteligentno rešiti.

Kombinirana oblika razlike

Ker Git osnuje vsako uspešno združitev, ko se izvaja ukaz git diff v stanju konflikta združevanja, se prikaže samo tisto, kar je trenutno še vedno v konfliktu. To lahko pomaga pri ogledu tega, kar morate še rešiti.

Ko zaženete git diff neposredno po konfliktnem združevanju, vam bo dala informacije v dokaj edinstveni obliki izpisa 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()

Format imenovan »Combined Diff« vam da dva stolpca podatkov poleg vsake vrstice. Prvi stolpec vam pokaže, ali je ta vrstica drugačna (dodana ali odstranjena) med vejo »ours« in datoteko v vašem delovnem imeniku, drugi stolpec pa naredi enako med vejo »theirs« in kopijo vašega delovnega imenika.

Tako lahko v tem primeru vidite, da sta vrstici <<<<<<< in >>>>>>> v delovni kopiji, vendar nista bili na nobeni strani združitve. To ima smisel, saj jih je orodje za združevanje tam postavilo za naš kontekst, pričakuje pa se, da jih bomo odstranili.

Če rešimo konflikt in znova zaženemo git diff, bomo videli isto stvar, vendar je to nekoliko bolj uporabno.

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

To bi nam pokazalo, da je »hola world« obstajal v naši veji vendar ne v delovni kopiji, »hello mundo« je obstajal v njihovi veji vendar ne v delovni kopiji in »hola mundo« ni obstajal v nobeni veji, vendar je sedaj v delovni kopiji. To lahko pomaga pri pregledu pred potrditvijo rešitve.

To lahko dobite tudi iz git log za vsako združitev, da vidite, kako je bila neka težava v resnici rešena. Git bo izpisal to obliko, če na potrditvi združitve zaženete git show, ali pa če dodate možnost --cc h git log -p (ki privzeto prikazuje popravke samo za potrditve nezdružitev).

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

Razveljavitev združitev

Ko sedaj znate ustvariti potrditev združitve, jih boste verjetno naredili nekaj po pomoti. Ena izmed dobrih stvari pri delu z Gitom je, da je v redu narediti napake, saj jih je mogoče (in v mnogih primerih enostavno) popraviti.

Potrditve združitev niso nič drugačne. Recimo, da ste začeli delati na tematski veji, jo po nesreči združili v master in zdaj je vaša zgodovina potrditev videti takole:

Nenamerna potrditev združitve
Slika 155. Nenamerna potrditev združitve

Na voljo sta dva načina za pristop k temu problemu, odvisno od tega, kaj je vaš željeni izid.

Popravek referenc

Če je neželena potrditev združitve prisotna samo v vašem lokalnem repozitoriju, je najlažja in najboljša rešitev premik vej tako, da kažejo, kamor jih želite. V večini primerov bo sledenje nepravilnemu ukazu git merge z git reset --hard HEAD~ ponastavilo kazalnike vej, tako da bodo videti takole:

Zgodovina po `git reset --hard HEAD~`
Slika 156. Zgodovina po git reset --hard HEAD~

reset smo pokrili v razdelku Demistifikacija ponastavitve, zato ne bi smelo biti pretežko razumeti, kaj se tu dogaja. Tukaj je hitra osvežitev: reset --hard običajno opravi tri korake:

  1. Premakne kazalec HEAD veje. V tem primeru želimo premakniti master tja, kjer je bila pred potrditvijo združitve (C6).

  2. Naredi, da je indeks videti kot HEAD.

  3. Naredi, da je delovni imenik videti kot indeks.

Slaba stran tega pristopa je, da se prepisuje zgodovina, kar lahko predstavlja težave z deljenim repozitorijem. Preverite Nevarnosti ponovnega baziranja za več o tem, kaj se lahko zgodi; na kratko, če imajo druge osebe potrditve, ki jih prepisujete, se je treba izogibati uporabi reset. Ta pristop prav tako ne bo deloval, če so bile ustvarjene druge potrditve od časa združitve; premikanje referenc bi dejansko izgubilo te spremembe.

Preklicane potrditve

Če vam premikanje kazalcev vej ne bo delovalo, vam Git omogoča možnost ustvarjanja nove potrditve, ki razveljavi vse spremembe obstoječe. Git to operacijo imenuje »revert« in v tem posebnem scenariju bi to sprožili takole:

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

Zastavica -m 1 kaže na to, da je nadrejeni »mainline« in bi se moral obdržati. Ko kličete združitev v HEAD (git merge topic), ima nova potrditev dve nadrejeni: prva je HEAD (C6), druga pa vrh veje, ki se združuje (C4). V tem primeru želimo razveljaviti vse spremembe, ki jih je uvedla združitev druge nadrejene (C4), hkrati pa ohraniti vsebino nadrejene #1 (C6).

Zgodovina s preklicano potrditvijo združitve je videti tako:

Zgodovina po `git revert -m 1`
Slika 157. Zgodovina po git revert -m 1

Nova potrditev ^M ima enake vsebine kot C6, zato je od tu dalje, kot da se združitev ni nikoli zgodila, razen, da so zdaj nezdružene potrditve še vedno v zgodovini HEAD. Git bo zmeden, če boste poskusili znova združiti topic v master:

$ git merge topic
Already up-to-date.

V topic ni ničesar, kar ne bi bilo že dosegljivo iz master. Kar je še huje, če dodate delo v topic in znova združite, bo Git prinesel samo spremembe od razveljavitve združitve:

Zgodovina s slabo združitvijo
Slika 158. Zgodovina s slabo združitvijo

Najboljši način za rešitev tega problema je razveljavitev prvotne združitve, saj želite zdaj uvoziti spremembe, ki so bile razveljavljene, in nato ustvariti novo potrditev združitve:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
Zgodovina po ponovni združitvi preklicane združitve
Slika 159. Zgodovina po ponovni združitvi preklicane združitve

V tem primeru sta ^M in ^M preklicana. ^^M se učinkovito združi v spremembe iz C3 in C4 ter C8 se združi v spremembe iz C7, tako da je sedaj veja topic polno združena.

Druge vrste združitev

Do sedaj smo pogledali običajno združitev dveh vej, kar se ponavadi izvede s t. i. »rekurzivno« strategijo združevanja. Vendar pa obstajajo še drugi načini združevanja vej skupaj. Poglejmo na hitro nekaj od njih.

Naša ali njihova želja

Najprej imamo na voljo še eno uporabno funkcionalnost normalnega »rekurzivnega« načina združevanja vej. Videli smo že možnosti ignore-all-space in ignore-space-change, ki se podajata s parametrom -X, lahko pa Gitu tudi povemo, naj v primeru konflikta raje izbere eno ali drugo stran.

Privzeto Git, ko naleti na konflikt med dvema vejama, ki ju poskuša združiti, v kodo doda oznake konflikta ter datoteko označi kot konfliktno, da lahko uporabnik konflikt reši ročno. Če bi raje, da Git izbere določeno stran in ignorira drugo stran, namesto da bi ročno reševali konflikt, lahko ukazu merge podate -Xours ali -Xtheirs.

Če Git to zazna, ne bo dodal oznak konflikta. Vse razlike, ki so združljive, jih bo združil. Vse razlike, ki se konfliktno prekrivajo, bo preprosto izbral celotno stran, ki ste jo določili, vključno z binarnimi datotekami.

Če se vrnemo na primer »hello world«, ki smo ga uporabljali prej, lahko vidimo, da združitev naše veje povzroči konflikt.

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

Vendar, če ga zaženemo z -Xours ali -Xtheirs, se to ne zgodi.

$ 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

V tem primeru namesto tega, da bi dobili oznake za konflikt v datoteki s »hello mundo« na eni strani in »hola world« na drugi, bo preprosto izbral »hola world«. Vendar pa so vse druge nekonfliktne spremembe na tisti veji uspešno združene.

Ta možnost se lahko poda tudi ukazu git merge-file, ki smo ga videli prej, tako da za posamezne združitve datotek zaženete nekaj podobnega kot git merge-file --ours.

Če želite narediti nekaj takega, vendar ne želite, da Git poskuša združiti sprememb iz druge strani, obstaja še bolj stroga možnost, kar je združitvena strategija »ours«. To je drugačno od možnosti rekurzivnega združevanja »ours«.

To bo v bistvu naredilo ponarejeno združevanje. Posnelo bo novo potrditev združitve z obema vejama kot nadrejeno, vendar ne bo niti pogledalo veje, ki jo združujete. Za rezultat združitve bo preprosto posnelo natančno kodo na vaši trenutni veji.

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

Vidite, da ni razlike med vejo, na kateri smo bili, in rezultatom združevanja.

To je lahko pogosto uporabno, da preprosto prevarate Git, da misli, da je veja že združena, ko pozneje izvajate združevanje. Na primer, recimo, da ste razvejali vejo release in na njej opravili nekaj dela, ki ga boste nekega dne želeli združiti nazaj v svojo vejo master. V tem času je iz veje master potrebno prenesti nazaj popravek napake na vejo release. Lahko združite vejo z odpravo napake v vejo release in enako vejo tudi združite z merge -s ours v vašo vejo master (čeprav je popravek že tam), tako da pozneje, ko spet združite vejo release, ni konfliktov zaradi odprave napake.

Združevanje poddreves (angl. subtree merge)

Ideja združevanja poddreves je, da imate dva projekta in eden izmed projektov je preslikan v poddirektorij drugega. Ko določite združitev poddreves, je Git pogostokrat dovolj pameten, da ugotovi, da je eno poddrevo drugega in ju ustrezno združi.

Šli bomo skozi primer dodajanja ločenega projekta v obstoječi projekt in nato združili kodo drugega v poddirektorij prvega.

Najprej bomo v svoj projekt dodali aplikacijo Rack. Dodali bomo projekt Rack kot oddaljeno referenco v svojem lastnem projektu in ga nato izvlekli v svojo lastno vejo:

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

Sedaj imamo vrh projekta Rack v naši veji rack_branch in naš lastni projekt v veji master. Če izpišete enega in nato drugega, lahko vidite, da imata različna vrha projektov:

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

To je nekako čudna zasnova. Ni potrebno, da so vse veje v vašem repozitoriju dejansko veje istega projekta. Ni pogosto, saj je redkokdaj koristno, vendar je precej enostavno imeti veje, ki vsebujejo popolnoma različne zgodovine.

V tem primeru želimo povleči projekt Rack v naš projekt master kot poddirektorij. To lahko naredimo v Gitu z git read-tree. Več o read-tree in njegovih prijateljih se boste naučili v Notranjost Gita, vendar za sedaj vedite, da prebere vrh drevesa ene veje v vaše trenutno področje priprave in delovni direktorij. Smo ravno preklopili nazaj na vašo vejo master in povlečemo vejo rack_branch v poddirektorij rack naše veje master našega glavnega projekta:

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

Ko naredimo potrditev, je videti, kot da imamo vse datoteke Rack pod tem poddirektorijem — kakor da bi jih kopirali iz stisnjega arhiva tar (angl. tarball). Kar postane zanimivo, je, da lahko precej enostavno združimo spremembe iz ene veje v drugo. Torej, če se projekt Rack posodobi, lahko povlečemo zgornje spremembe s preklopom na tisto vejo in povlekom:

$ git checkout rack_branch
$ git pull

Nato lahko združimo te spremembe nazaj v našo vejo master. Da povlečemo spremembe in vnaprej napolnimo sporočilo potrditve, uporabimo možnost --squash kot tudi rekurzivno združevanje strategije možnosti -Xsubtree. Rekurzivna strategija je tu privzeta, vendar jo dodajamo zaradi večje jasnosti.

$ 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

Vse spremembe iz projekta Rack so združene in pripravljene za lokalno potrditev. Lahko naredite tudi nasprotno — naredite spremembe v poddirektoriju rack vaše veje master in jih nato kasneje združite v vašo vejo rack_branch, da jih pošljete vzdrževalcem ali potisnete navzgor.

To nam da način, da imamo potek dela, ki je nekako podoben tistemu s podmoduli, le brez uporabe podmudolov (kar bomo pokrili v Podmoduli). Obdržimo lahko veje z ostalimi povezanimi projekti v svojem repozitoriju in jih občasno poddrevesno združimo v svoj projekt. Na neki način je dobro, na primer, da je vsa koda potrjena na enem mestu. Vendar ima ostale slabosti v tem, da je malo bolj kompleksno in hitro se naredi napake pri ponovnem integriranju sprememb, ali pa se po nesreči potisne veja v nepovezani repozitorij.

Druga nekoliko čudna stvar je, da za dobiti razliko med tem, kar imate v vašem poddirektoriju rack in kodi v vaši veji rack_branch — da vidite, če jih morate združiti — ne morete uporabiti običajnega ukaza diff. Namesto tega morate pognati git diff-tree z vejo, ki jo želite primerjati:

$ git diff-tree -p rack_branch

Da primerjate, kaj je v vašem poddirektoriju rack s tem, kakšna je bila veja master na strežniku nazadnje, ko ste prenašali, lahko poženete:

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