Git je veľmi flexibilný nástroj pre správu verzií, ktorý ponúka rôzne spôsoby, ako spravovať a optimalizovať váš kód. V tomto článku sa pozrieme na tri dôležité témy: ako riešiť konflikty pri mergovaní, ako používať tagy na označenie dôležitých verzií a ako skryť (stashovať) zmeny. 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.
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 je veľmi flexibilný nástroj pre správu verzií, ktorý ponúka rôzne spôsoby, ako spravovať a optimalizovať váš kód. V tomto článku sa pozrieme na tri dôležité témy: ako riešiť konflikty pri mergovaní, ako používať tagy na označenie dôležitých verzií a ako skryť (stashovať) zmeny.
Kroky pri mergovaní s konfliktom:
Zlučovanie vetiev:
git checkout master
git merge feature-vetva
Identifikácia konfliktov:
- Git vás upozorní na konflikty a označí ich v súboroch, ktoré spôsobujú problém
- Konfliktné časti budú vyznačené značkami
<<<<<<
,======
a>>>>>>
Riešenie konfliktov:
- Otvorte konfliktný súbor v textovom editore
- Manuálne upravte konfliktné časti podľa potreby
- Odstráňte značky konfliktov
Potvrdenie riešenia konfliktov:
git commit -m „Riešenie konfliktu pri zlúčení“
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.
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. Tagy sú spôsob, ako označiť dôležité body v histórii projektu, ako sú verzie vydaní, míľniky a ďalšie významné udalosti. V Gite existujú dva typy tagov: ľahké (lightweight) a anotované (annotated).
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.
Vytváranie tagov:
- Ľahké tagy:
- Ľahký tag je jednoduchý ukazovateľ na konkrétny commit.
- git tag v1.0
- Anotované tagy:
- Anotovaný tag obsahuje dodatočné informácie, ako je meno autora, dátum a komentár.
- git tag -a v1.0 -m „Prvá stabilná verzia“
- Zobrazenie a push tagov:
- Zobrazenie tagov: git tag
- Push tagov do vzdialeného repozitára: git push origin v1.0
- Push všetkých tagov: git push origin –tags
- Odstránenie tagov:
- Lokálne odstránenie tagu: git tag -d v1.0
- Odstránenie tagu z vzdialeného repozitára: git push origin –delete tag v1.0
Tagy sa teda 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. Git stash umožňuje dočasne uložiť necommitované zmeny, aby ste mohli prepnúť na inú vetvu alebo vykonať inú činnosť bez straty rozpracovaných zmien.
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.
Použitie stash:
- Uloženie zmien do stash: git stash
- Zobrazenie uložených zmien: git stash list
- Obnovenie zmien zo stash:
- Aplikovanie posledného stash: git stash apply
- Aplikovanie konkrétneho stash: git stash apply stash@{0}
- Odstránenie zmien zo stash po ich aplikovaní: git stash drop
- Obnovenie zmien a odstránenie zo stash naraz: git stash pop
- Vymazanie všetkých stash: git stash clear
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. Git poskytuje robustné nástroje na správu a optimalizáciu kódu, ako sú riešenie konfliktov pri mergovaní, používanie tagov na označenie dôležitých verzií a stashovanie zmien pre flexibilnejšiu prácu s kódom. Správne využitie týchto nástrojov vám pomôže udržať váš projekt organizovaný a efektívny. 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.
Odporúčania na prácu s Gitom
Pri práci s Gitom dbajte na pravidelnú synchronizáciu s hlavným repozitárom, používajte tagy na označenie významných verzií a využívajte stash na dočasné ukladanie zmien. Tieto postupy vám pomôžu predísť zbytočným problémom a zefektívniť váš vývojový proces.
- Používajte nástroje na vizuálne riešenie konfliktov, ako sú VSCode, P4Merge alebo KDiff3, ktoré vám uľahčia prácu s konfliktami.
- Pravidelne synchronizujte vetvy s hlavným repozitárom, aby ste minimalizovali konflikty.
Objavte online kurzy na Git a GitHub
Prehľad publikovaných článkov
- Seriál Online kurz Git – Začíname s Gitom – 1. diel
- Seriál Online kurz Git – Lokálna Práca so Súbormi – 2. diel
- Seriál Online kurz Git – V Hlbinách Súborového Systému – 3. diel
- Seriál Online kurz Git – Paralelné svety a Git branch – 4. diel
- Seriál Online kurz Git – Mergovanie s Konfliktom, Tagy a Skrytie Zmien – 5. diel
- Seriál Online kurz Git – Vzdialené repozitáre, GitHub, Bitbucket – 6. diel
- Seriál Online kurz Git – Clean, Reset, Rebase, Revert nástroje do každého počasia – 7. diel
- Seriál Online kurz Git – Najčastejšie problémy, faily a fuckupy – 8. diel