Seriál Online kurz Git – Mergovanie s Konfliktom, Tagy a Skrytie Zmien – 5. diel

Práca s branchami je v Gite veľká a dôležitá téma. Preto po poslednom diely, kde sme si ukázali niektoré jednoduché scenáre, sa budeme v tomto venovať hlavne zložitejším postupom, ktoré s ňou súvisia. Pozrieme sa na to, čo je to konflikt pri mergi a ako ho vyriešiť, ale tiež na niektoré neštandardné techniky branchovania a spätného mergu. Okrem toho si ešte ukážeme prácu s tagmi a stashom.

Rýchle preopakovanie alebo kde sme to skončili

Naposledy sme si vysvetlili, čo to branchovanie je a načo je to dobré. Branche sú kópie kódu, ktoré dokážu žiť svojím vlastným nezávislým životom. Vytváranie branchov má mnoho účelov a podľa toho potom vyzerá scenár, ako sa s Gitom pracuje. V zásade sa ale vždy branch vytvorí a po čase sa z neho alebo do neho mergujú zmeny. Pre Git je branch len séria commitov a referencia, ktorá sa odkazuje na jeho posledný stav. Teraz si povieme niečo o tom, keď to s mergovaním nedopadne dobre.

Git branch merge

Konflikty alebo keď to ani Git nezvládne

V 1. diely seriálu (Začíname s Gitom) som napísal, že Git je optimista, ktorý dovolí viacerým vývojárom meniť tie isté súbory a potom sa všetky zmeny pokúsi zosúladiť. No, niekedy sa to podarí a niekedy nie. Niekedy sú tie zmeny také, že Git ich jednoducho nevie spojiť automaticky a vtedy vzniká takzvaný konflikt pri mergovaní.

Konflikt často vzniká vtedy, ak sa v dvoch branchoch menili tie isté riadky kódu, alebo v jednom sa súbor presunul/zmazal a v tom druhom sa menil. V takom prípade Git nedokáže zmeny spojiť, označí ich ako najlepšie vie a zahlási, že došlo ku konfliktu. Je potom na vývojárovi, aby zosúladil zmeny ručne.

Git merge

Poďme si teda taký konflikt vyrobiť. Najprv si vytvoríme branch (nazveme ho „third“), v ktorom následne (v súbore index.html) zmením ten istý riadok ako v branchi master. Potom sa pokúsime third zamergovať do mastera.

Najprv zmena v masteri:

> git branch third

(na koniec riadku v súbore index.html doplníme znak „!“ – to bude zmena v masteri)

> git add subdir\index.html

> git commit -m „Zmena v masteri“

[master ac154da] Zmena v masteri

1 file changed, 1 insertion(+), 1 deletion(-)

Teraz zmena v branchi third:

> git checkout third

Switched to branch ‚third‘

(na koniec riadku v súbore index.html doplníme znak „?“ – to bude zmena v third)

> git add subdir\index.html

> git commit -m „Zmena v third“

[third 40516b1] Zmena v third

1 file changed, 1 insertion(+), 1 deletion(-)

A teraz sa prepneme naspäť na master a pokúsime sa o merge:

> git checkout master

Switched to branch ‚master‘

> git merge third

Auto-merging subdir/index.html

CONFLICT (content): Merge conflict in subdir/index.html

Automatic merge failed; fix conflicts and then commit the result.

Áno, správa od Gitu je jasná. Merge sa nepodaril a výsledok je konflikt, ktorý treba vyriešiť. Je niekoľko spôsobov, ako sa dá pozrieť aktuálny stav. Jeden je pomocou príkazu git log:

> git log –pretty=oneline –merge –left-right -p

> 40516b1af1e39e3e4e62b23efe777dd616f95eb6 Zmena v third

diff –git a/subdir/index.html b/subdir/index.html

index 8e8a9fc..09abf69 100644

— a/subdir/index.html

+++ b/subdir/index.html

@@ -1 +1 @@

-Hello world!!

\ No newline at end of file

+Hello world!!?

\ No newline at end of file

<ac154daa391caa2d27b4a1640fb58bee02335ce1 Zmena v masteri

diff –git a/subdir/index.html b/subdir/index.html

index 8e8a9fc..19b67fb 100644

— a/subdir/index.html

+++ b/subdir/index.html

@@ -1 +1 @@

-Hello world!!

\ No newline at end of file

+Hello world!!!

\ No newline at end of file

Príkaz ukáže, ktoré commity v jednotlivých branchoch konflikt spôsobili a čo sa v rámci nich menilo. Iný pohľad sa naskytne, ak sa pozrieme priamo na obsah súboru:

<<<<<<< HEAD

Hello world!!!

=======

Hello world!!?

>>>>>>> third

Git do súboru vložil riadky z oboch branchov, pričom jasne označil, ktorý odkiaľ pochádza (o referencii HEAD sme si povedali minule – je to symbolická referencia, ktorá odkazuje na aktuálne nastavený branch – teda na to, čo reálne máme vo working copy).

Čo ďalej? V zásade je potrebné dať súbor do poriadku – teda stanoviť jeho finálny obsah po mergi. Povedzme, že v našom prípade chcem, aby výsledný súbor obsahoval nové znaky. V tomto prípade je najjednoduchšie v textovom editore odstrániť špeciálne Git značky, duplicitný riadok a ten, ktorý ostane, upraviť. Výsledný obsah teda môže vyzerať takto:

Hello world!!!?

To je jeden so spôsobov, ako sa s konfliktom vysporiadať. Pre zložitejšie konflikty je dobré použiť grafický nástroj, v ktorom sa postupne poskladajú časti výsledného súboru. Takýchto nástrojov je mnoho a okrem tých samostatných má dnes každé dobré IDE nejaký doplnok (ak to nie je rovno jeho súčasťou), ktorý to umožňuje robiť priamo v ňom.

To, že dáme do poriadku obsah súboru ešte neznamená, že pre Git sa všetko skončilo. Je potrebné mu povedať, že problém je vyriešený a to pomocou príkazu git add:

> git add subdir\index.html

Rýchly test stavu ukáže, že súbor je Gitom vnímaní už len ako štandardne zmenený súbor:

> git status –short

M  subdir/index.html

Následne je potrebné zmeny commitnuť:

> git commit

Git commit bez parametrov ti otvorí východzí editor pre písanie commit správy. Novinkou je, že časť správy už bude predvyplnená, keďže Git vie, že ide vytvárať merge commit. A takto vyzerá výsledný graf:

> git log –oneline –graph

*   07cdff8 Merge branch ‚third‘

|\

| * 40516b1 Zmena v third

* | ac154da Zmena v masteri

|/

Scenáre a stratégie mergovania

Mergovanie je v Gite dôležitá operácia. Bez toho, aby fungovalo podľa možností čo najlepšie, by bolo celé branchovanie nanič. A bez branchovania by samotný Git stratil veľa zo svojej pridanej hodnoty. Aby mergovanie fungovalo v čo najväčšom počte prípadov dobre, existujú v Gite dve špeciálne scenáre, ktoré Git zvolí, ak sú pri mergi na to vhodné podmienky:

  • already up-to-date – tento scenár nastáva, ak sa merguje branch, z ktorého už ale všetky zmeny boli zamergované. Teda netreba prenášať žiadne nové zmeny. Git sa ich nepokúsi mergovať druhýkrát, ani nevyhlási konflikt.
  • fast-forward – scenár, ktorý Git automaticky zvolí, ak merguje do branchu, v ktorom ale nie sú žiadne nové commity. Je to v podstate opačný scenár ako already up-to-date. V takom prípade nie je potrebné vytvárať merge commit, ale preniesť zmeny do cieľového branchu. To Git aj vykoná. Vezme commity z branchu, z ktorého sa merguje a jednoducho ich pridá do cieľového branchu a posunie referenciu, aby ukazovala na nový vrchol branchu.

Okrem týchto scenároch Git obsahuje aj takzvané stratégie mergovania. Vo väčšine prípadov pri mergovaní stratégiu nebudeš musieť vyberať – existuje nejaká, ktorá sa použije ako východzia. Pre niektoré špeciálne prípady ju ale je možné meniť. Poďme sa teda pozrieť na ich zoznam:

  • recursive – zamergovanie zmien z jedného brancha do druhého. Toto je východzia stratégia.
  • octopus – toto je východzia stratégia, ak sa pokúsiš zamergovať naraz viac ako jeden branch (áno, aj to sa dá, príkazom „git merge branch1 branche2 branch3“)
  • ours – mergovanie prebehne len naoko. V skutočnosti žiadne zmeny nie sú prenesené. Jediný účel, ktorý táto stratégia má je, že branch, z ktorého nechceš preniesť žiadne zmeny (povedzme, že to bol nejaký skúšobný branch) chceš nejako rozumne v histórii ukončiť. Tým, že sa do histórie zaznačí jeho mergovanie je jasné, že už v ňom neostalo nič otvorené.
  • subtree – podobné ako recursive, ale vie si poradiť s tým, ak nejaký podpriečinok je v stromovej hierarchii o čosi nižšie alebo vyššie. Vie takúto zmenu detekovať a merge súborov vykonať aj tak.

Zmena merge stratégie nie je niečo, čo budeš robiť každý deň. Sú to skôr špeciálne prípady, ale uviedol som ich tu preto, aby bolo jasné, že aj mergovanie je niečo, čo sa môže diať rôznymi spôsobmi v niektorých prípadoch.

Git zlučovacie stratégie

Mergujeme selektívne

V určitých prípadoch je potrebné urobiť merge len určitého jedného commitu, nie celého branchu. Na také prípady slúži príkaz cherry-pick. Jeho účel presne vystihuje jeho názov – vyberáš čerešničku z torty.

Pre ukážku som si vyrobil v branchi third ďalší commit (presný postup už verím zvládneš sám). Teraz sa ho pokúsim pridať do mastera:

>git cherry-pick c008bc5

[master c41d559] Dalsia zmena v third

Date: Sun May 8 21:43:30 2016 +0200

1 file changed, 2 insertions(+), 1 deletion(-)

To, čo sa udialo je, že commit sa v podstate prekopíroval do master brancha, kde má ale iný hash. Je to preto, lebo všetky údaje o commite (autor, dátum) sú rovnaké, ale tento nový má iný commit-predchodcu a jeho hash je vo výpočte zahrnutý tiež.

Môžeš si tiež všimnúť, že som neuviedol branch odkiaľ commit beriem. Je to preto, lebo v tomto prípade ma zaujíma len commit a nie branch, a tiež preto, lebo commity majú unikátny hash v celom repozitári. Takže Git si ho vie dohľadať, nech je v hociktorom branchi.

S cherry-pick som sa stretol najčastejšie pri backportoch. To je prípad, kedy sa urobí nejaká zmena v hlavnej vetve kódu (napr. oprava chyby) a je potrebné ju preniesť do stabilnej vetvy (teda tej, čo je o čosi pozadu). Vtedy je cieľ mergovať naozaj len jeden commit.

Cherry-pick ale v takých prípadoch nemusí pomôcť stále. Treba si uvedomiť, že commity sú naviazané jeden na druhý rovnako ako zmeny v kóde nasledujú jedna za druhou. Ak z tej série vyberieš jeden commit a pokúsiš sa ho preniesť, tak môže byť problém, že neprenášaš to, čo mu predchádzalo. Inak povedané, že zmena je v branchi, do ktorého ju prenášaš, aplikovaná na iný stav súborov ako v zdrojovom branchi. A to môže byť problém alebo nemusí. Treba len myslieť na to, že niekedy cherry-pick jednoducho nemusí fungovať.

Historické udalosti alebo Tagy

Raz za čas sa v histórii repozitára objaví priam pamätihodná udalosť. Niečo, čo si chceš označiť, aby (ak by to bolo treba) si sa k tomu vedel jednoducho vrátiť. Keďže história je v repozitári tvorená commitmi, tak čo si chceš označiť je vlastne commit a nástroj, ktorý na to Git ponúka, sa volá Tag.

Tag je v skutočnosti špeciálny typ referencie, ktorá sa nedá presúvať. Je to teda pomenovaný odkaz na niektorý commit a keď je už raz vytvorený, tak by sa nemal meniť. V skutočnosti Git umožňuje zmenu tagov, ale len za použitia špeciálnych prepínačov príkazov a vo všeobecnosti je to považované za bad practice. Ak si sa pomýlil a chceš tag presunúť, tak radšej vytvor nový s jasným popisom, že ide o opravu existujúceho.

Často sa tagy používajú na označenie verzie zdrojového kódu, napríklad takej, ktorá bola nasadená u zákazníka. Alebo sa dajú použiť na označenie pomerne stabilného stavu zdrojových kódov pred ďalšími zmenami. Pomocou takého tagu je možné neskôr získať stabilnú verziu pre novú inštaláciu, aj keď medzitým už prebehol nejaký vývoj a kód nie je taký stabilný, ako bol v čase vytvárania tagu.

Tagy sa vytvárajú príkazom git tag. Poďme si teda otagovať commit, ktorý sme zamergovali v predchádzajúcom prípade:

git tag -m „Tag verzie 1.0“ V1.0 c41d559

Práve sme otagovali commit s hashom c41d559. To, čo sa podarilo vytvoriť, je takzvaný anotovaný tag. Ten vzniká, ak sa použije niektorý z prepínačov: -a, -m -s alebo -u. Anotovaný tag má definované, kto a kedy ho vytvoril. Je to teda naozaj značka, ktorá označuje špeciálny commit. Naproti tomu neanotovaný tag slúži na označenie nejakého objektu: blob, tree alebo commitu. Je skôr len na osobné použitie ako pomôcka na rýchle nájdenie daného objektu.

V prípade, že tagy používate alebo o ich použití uvažujete, spomeniem ešte príkaz git describe. Jeho úlohou je vygenerovať popis aktuálnej verzie zdrojových kódov. Robí to aj pomocou tagov, resp. jedného, ktorý nájde ako prvý trasovaním od commitu smerom späť v histórii. Okrem toho do výsledku pridáva ďalšie informácie. Cieľom je vygenerovať niečo ako kombinovaný názov verzie vhodný napríklad pri automatických zostaveniach.

Dočasné skrytie zmien

Predstav si modelovú situáciu. Pracuješ na nejakej úlohe, celé to máš rozpracované vo working copy a zrazu sa zastaví šéf ohľadom chyby, ktorú objavil zákazník. Ako to býva dobrým zvykom je ASAP všetkých ASAPov, a teda to treba čím skôr opraviť. Je jasné, že s rozpracovanými zmenami, čo máš práve vo working copy, to bude problém. Jedným z riešení je vytvoriť branch a presunúť zmeny do neho. O čosi jednoduchším je použitie git stash.

Stash je príkaz, ktorým je možné si odložiť zmeny „bokom“. V praxi to znamená, že ich odložíš na miesto, odkiaľ ich vieš jednoducho opäť vyzdvihnúť. Dokonca sa dá odkladať viacero nezávislých zmien (pod pojmom „zmena“ teraz myslím kolekciu všetkých zmien v rôznych súboroch, ktoré vo working copy práve máš). Vtedy stash funguje ako zásobník – teda, čo ide do neho posledné, musí ísť aj prvé von. Git sa tak snaží udržať kontinuitu zmien ako postupne prichádzali do working copy.

Poďme si ukázať praktický príklad. Najprv vyrobíme zmenu v súbore test (opäť stačí pridať len jeden znak do súboru – podstatné ale je, že máme nejakú zmenu):

> git status –short

M test

Zmenu máme vytvorenú a teraz si ju dočasne skryjeme:

> git stash save

Saved working directory and index state WIP on master: c41d559 Dalsia zmena v third

HEAD is now at c41d559 Dalsia zmena v third

A kontrola stavu:

> git status –short

Žiadnu zmenu teda vo working copy momentálne nemáme a môžeme sa venovať novej úlohe. Zoznam odložených zmien sa dá pozrieť takto:

> git stash list

stash@{0}: WIP on master: c41d559 Dalsia zmena v third

Ten text „WIP on master: c41d559 Dalsia zmena v third“ je generovaný Gitom. Pri ukladaní zmien do stashu je možné definovať svoj vlastný názov pomocou prepínača -m. Vybrať zmenu zo stashu je možné príkazom git stash pop:

> git stash pop

On branch master

Changes not staged for commit:

(use „git add <file>…“ to update what will be committed)

(use „git checkout — <file>…“ to discard changes in working directory)

modified:   test

no changes added to commit (use „git add“ and/or „git commit -a“)

Dropped refs/stash@{0} (b33a22c4b6b336642c43e5bd33f2bc6689c394f2)

Git vrátil stav working copy do situácie pred odkladaním zmien a ešte spustil aj git status, aby sme hneď videli, v akom stave working copy je. Okrem príkazu pop sa dá použiť aj príkaz git stash apply. Ten funguje podobne len s tým rozdielom, že zmenu neodstráni zo stashu (teda ju aplikuje, ale zároveň ostane zachovaná v stashi).

Záver a sumarizácia

V predchádzajúcom diely seriálu sme si ukázali jednoduchý príklad branchu a mergu. Život je ale omnoho komplikovanejší a situácie, ktoré prinesie, si vyžadujú, aby si mal za opaskom širšiu sadu nástrojov. Aj preto sme sa dnes venovali ďalej branchovaniu a ukázali jeho rôzne aspekty. V ďalšom diely už budeme cestovať na veľké vzdialenosti. Konkrétne sa pozrieme ako sa pracuje so vzdialeným repozitárom.

Tip: Mrknite na náš online kurz Git.

Prehľad publikovaných článkov

  1. Seriál Online kurz Git – Začíname s Gitom – 1. diel
  2. Seriál Online kurz Git – Lokálna Práca so Súbormi – 2. diel
  3. Seriál Online kurz Git – V Hlbinách Súborového Systému – 3. diel
  4. Seriál Online kurz Git – Paralelné svety a Git branch – 4. diel
  5. Seriál Online kurz Git – Mergovanie s Konfliktom, Tagy a Skrytie Zmien – 5. diel
  6. Seriál Online kurz Git – Vzdialené repozitáre, GitHub, Bitbucket – 6. diel
  7. Seriál Online kurz Git – Clean, Reset, Rebase, Revert nástroje do každého počasia – 7. diel
  8. Seriál Online kurz Git – Najčastejšie problémy, faily a fuckupy – 8. diel

Autor

Miroslav Reiter

Programátor a marketér, ktorý mudruje vo vlastnej vzdelávacej spoločnosti IT Academy. Workoholik so 114 certifikáciami a 12 titulmi. Vytvoril som vzdelávaciu platformu vita.sk, pretože milujem vzdelávanie a všetko čo k nemu patrí. Pomáham firmám ale aj jednotlivcom zlepšovať ich podnikanie a IT. Certifikácie: Microsoft certifikovaný tréner, Google certifikovaný tréner, ITIL, PRINCE2 tréner. Referencie: IBM, Panasonic, Ministerstvo obrany SR, ČSOB, Generali, Tatra banka, Európska komisia, SPP, Pixel Federation, ESET.