3. týždeň

Práca s vetvami v systéme Git

Ciele

  1. Oboznámiť sa s prácou s viacerými vetvami v repozitári.
  2. Naučiť sa postup riešenia konfliktov pri nezávislých zmenách.

Postup

Krok 1: Príprava

Úloha 1.1

Vytvorte si vlastnú kópiu pripraveného repozitára.

Na adrese https://git.kpi.fei.tuke.sk/zsi-labs/git-branch máte dostupný základný repozitár, s ktorým budete pracovať na tomto cvičení.

Vytvorte si jeho kópiu do vlastného menného pristoru na serveri Gitlab pomocou tlačidla fork (rozvetvenie repozitára) vpravo hore na stránke repozitára. Ak vám následne GitLab ponúkne na výber, do ktorého menného priestru chcete vytvoriť kópiu projektu, zvoľte ten s vašim menom (alebo loginom).

Následne nastavte viditeľnosť projektu na Private v nastaveniach projektu (settings v ľavej lište).

Úloha 1.2

Naklonujte si pripravený repozitár.

Pomocou príkazu clone naklonujte repozitár, ktorý ste si pomocou funkcie "fork" sprístupnili pod vašim menným priestorom. Použite SSH prístup. Príklad príkazu na naklonovanie je nasledovný:

$ git clone git@git.kpi.fei.tuke.sk:xx000yy/git-branch.git

Poznámka

Pre overenie postupu skontrolujte, či adresa použitá v príkaze clone obsahuje vaše meno alebo jedinečný login. Ak spravíte klon priamo z originálneho repozitára, niektoré úlohy v tomto cvičení nebudete vedieť dokončiť kvôli odopretému prístupu na zápis k originálnemu repozitáru.

Prejdite do pracovného priečinka naklonovaného repozitára:

$ cd git-branch

Tento pripravený repozitár obsahuje šablónu jednoduchej HTML stránky, s ktorou budeme ďalej pracovať.

Úloha 1.3

Pripravte príkaz na zobrazovanie histórie repozitára.

Pre zobrazenie histórie používame známy príkaz log. Avšak, pre potreby tohto cvičenia pridáme niekoľko prepínačov, ktoré modifikujú jeho výstup:

  • --all — zobrazenie záznamov zo všetkých vetiev, nie len z aktuálnej,
  • --graph — "grafické" zobrazenie vetvenia,
  • --decorate — vypíše referencie na záznamy,
  • --oneline — skratka pre konfiguráciu --pretty=oneline --abbrev-commit, ktorá vypíše každý záznam len na jednom riadku so skrátenou hash kontrolnou sumou.

Výsledný príkaz na zobrazenie histórie teda bude

 $ git log --all --graph --decorate --oneline

Keďže je tento príkaz už relatívne dlhý, je možné využiť git konfiguráciu tzv. aliasu — vlastného príkazu, ktorý reprezentuje kombináciu základných git príkazov. Pomenujte nový "príkaz" napr. graphlog a vytvorte ho nasledovne:

 $ git config alias.graphlog "log --all --graph --decorate --oneline"

Podobne ako predtým, pridaním prepínača --global by ste tento príkaz nakonfigurovali pre celý systém. Príkazom uvedeným vyššie bude tento nový príkaz nakonfigurovaný len pre aktuálny repozitár.

V ďalších krokoch budeme už na výpis grafu záznamov používať tento nový príkaz graphlog.

Úloha 1.4

Pozrite si históriu naklonovaného repozitára.

Zobrazte históriu novým príkazom graphlog:

$ git graphlog

Zobrazená história bude vyzerať nasledovne:

* 2e2896c (HEAD -> master, origin/master) added basic README file
* 66904a9 modified button and heading styles
* adcc7be basic page template with bootstrap

História naklonovaného repozitára na základe vyššie uvedeného výpisu je zobrazená na nasledovnom obrázku.

História repozitára
Obr. 1: História repozitára

V rámci minulého cvičenia sme už spomínali značku HEAD, ktorá ukazuje na záznam v histórii, na ktorom sa aktuálne nachádzame. Posúvala sa dopredu pri vytvorení nového záznamu príkazom commit a späť v prípade použitia príkazu reset na návrat do stavu zaznamenaného v minulosti.

Ako je zobrazené na obrázku, vetva predstavuje podobnú značku ako značka HEAD, a ukazuje na určité miesto v zaznamenanej histórií projektu. Ďalej uvidíte, že pridaním ďalších značiek vetiev môžeme vytvárať viaceré verzie projektu, medzi ktorými sa môžeme jednoduchým spôsobom pohybovať a pracovať na nich nezávisle.

Poznámka

V ďalších zobrazeniach histórie projektu už nebudeme uvádzať snímky stavu pre ich zjednodušenie. Každý záznam vždy ale ukazuje na konkrétny snímok.

Krok 2: Práca s vetvami

V kontexte systémov pre správu verzií (SSV), vetvenie znamená odklonenie sa od určitej vývojovej línie (vetvy, z angl. branch) a pokračovanie v práci bez zásahu do tejto línie.

Pri úlohách z minulého cvičenia sme pracovali len v rámci jednej vetvy projektu — vetvy master , ktorá je automaticky vytvorená systémom Git pri použití príkazu init. V tomto kroku si ukážeme, ako pracovať s viacerými vetvami v repozitári.

Práca na rozšírení funkcionality projektu sa často vykonáva v samostatnej vetve, čo má niekoľko výhod:

  • nedôjde k porušeniu existujúceho funkčného stavu, pokiaľ sa pracuje na novej funkcionalite,
  • v prípade potreby kritickej opravy existujúce verzie je možné presunúť sa späť do hlavnej vetvy a vykonať opravu,
  • samostatne vykonané zmeny v novej vetve je možné začleniť späť do hlavnej vetvy po ukončení a otestovaní novej funkcionality.

Úloha 2.1

Vytvorte novú vetvu.

Povedzme, že aktuálny stav repozitára predstavuje nejakú konkrétnu verziu webovej stránky, ktorú je ale potrebné rozšíriť. Rozšírenie vykonáme v rámci samostatnej vetvy.

S vetvami sa pracuje príkazom branch. Novú vetvu s názvom extension vytvoríme príkazom

 $ git branch extension

Po výpise histórie príkazom graphlog uvidíte, že bola pridaná nová značka vetvy. To je možné zobraziť obrázkom nasledovne:

Nová značka vetvy
Obr. 2: Nová značka vetvy

Pribudla teda značka vetvy, ale projekt ostáva na vetve master, keďže tam ukazuje HEAD.

Úloha 2.2

Presuňte sa do novej vetvy.

Do novej vetvy sa presuniete pomocou príkazu checkout.

$ git checkout extension

Teraz sa značka HEAD presunula na značku tejto novej vetvy. V tomto prípade sme ale ostali na tom istom zázname, keďže obe vetvy ukazujú na ten istý. V inom prípade (ak by táto vetva ukazovala na iný záznam), obsah pracovného priečinka by bol aktualizovaný na stav snímky projektu z daného záznamu.

Repozitár je nastavený do novej vetvy
Obr. 3: Repozitár je nastavený do novej vetvy

Poznámka

Existuje aj varianta príkazu checkout s prepínačom -b, ktorá najskôr vytvorí novú vetvu a prepne sa do nej:

$ git checkout -b extension

Takýmto spôsobom je možné v jednom kroku vykonať to, čo bolo vyššie popísané cez dva samostatné príkazy.

Úloha 2.3

Vykonajte zmenu v súboroch repozitára, no zatiaľ nevytvárajte nový záznam.

Požadovanou zmenou je pridanie troch ďalších nadpisov s krátkym popisom a tlačidlom na zobrazenie stránky s detailom. Túto zmenu vykonáte tak, že v súbore index.html skopírujete celý element div s CSS triedou row (až po ukončovaciu znáčku </div>) a vložíte ho ešte raz za ten skopírovaný. Oba elementy <div class="row">…</div> tak budú potomkami elementu div s triedou container. Dodatočne upravte nadpisy tak, aby ich číslovanie pokračovalo a neopakovalo sa.

Výsledok zmien vo vetve extension
Obr. 4: Výsledok zmien vo vetve extension

Úloha 2.4

Vykonajte zmenu vo vetve master.

Teraz si predstavte, že počas práce na zmene príšla náhla požiadavka pridať pätu stránky, ale v tom stave, ako je vo vetve master. Rozpracované zmeny teda musíme odložiť a presunúť sa do vetvy master.

Presunutie sa do vetvy master by sa malo dať vykonať pomocou príkazu checkout. Avšak, keďže pracovný priečinok momentálne obsahuje nezaznamenané zmeny a obe vetvy ukazujú na ten istý záznam histórie, po presunutí sa do vetvy master by sme presunuli aj modifikácie v súbore index.html, čo nie je našim cieľom. A v prípade, ak by sme sa po prepnutí vetvy mali presunúť na iný záznam, zmeny v pracovnom priečinku by boli stratené. V takom prípade by Git vypísal upozornenie a checkout by neumožnil, pokiaľ by ste buď zmeny manuálne nezrušili, nezaznamenali ich, alebo nepoužili odkladací priestor (z angl. stash). Ukážeme si práve použitie odkladacieho priestoru, keďže sme ešte nevykonali všetky zmeny, ktoré by mali byť súčasťou záznamu vo vetve extension.

Odkladací priestor (stash) slúži ako zásobník, do ktorého je možné "odkladať" zmeny v sledovaných súboroch pracovného priečinka a súboroch v prípravnej oblasti (staging area). Aktuálne zmeny pracovného priečinka je teda možné presunúť do odkladacieho priestoru príkazom

 $ git stash

Ak si následne pozriete stav pracovného priečinka príkazom status, nebudú v ňom žiadne zmeny oproti poslednému záznamu. Zároveň, príkazom git stash list je možné vypísať všetky "odložené" pracovné priečinky.

Teraz môžeme vykonať prechod do vetvy master:

$ git checkout master

Výsledkom bude opäť stav repozitára, ako je uvedený na obrázku 2: HEAD ukazuje na poslený záznam vo vetve master (vo vetve extension sme ešte žiaden záznam nevykonali).

Požadovanú zmenu budeme ale tiež vytvárať v rámci novej vetvy, do ktorej sa prepneme už spomenutým príkazom checkout -b:

 $ git checkout -b footer

Repozitár tak bude vyzerať nasledovne:

Nová vetva footer
Obr. 5: Nová vetva footer

Pridajte teraz požadovanú pätku stránky na koniec elementu body v súbore index.html:

 <footer>
  <div class="container">
    <hr />
    KPI TUKE
  </div>
</footer>

Zaznamenajte zmenu

$ git commit -am "added footer"

Nový stav histórie repozitára je nasledovný:

Vetva footer po zázname
Obr. 6: Vetva footer po zázname

Úloha 2.5

Zlúčte vetvu footer do vetvy master.

Zlúčenie vetvy (z angl. branch merging) zlúči zmeny vykonané v rámci jednej vetvy do druhej. V tomto prípade je úlohou zlúčiť zmeny z vetvy footer do master. Proces zlúčenia vo všeobecnosti prebieha tak, že sa presunieme do tej vetvy, do ktorej chceme zmeny zlúčiť (v uvedenom prípade do vetvy master) a v príkaze merge špecifikujeme tú vetvu, ktorú chceme zlúčiť (nateraz footer).

$ git checkout master

Značka HEAD presunutá na vetvu master
Obr. 7: Značka HEAD presunutá na vetvu master

$ git merge footer

Updating 2e2896c..a28be26
Fast-forward
 index.html | 7 +++++++
 1 file changed, 7 insertions(+)

Vo výpise si je možné všimúť spojenie fast-forward. To predstavuje určitý typ zlúčenia vetvy, kedy sa je možné do aktuálneho záznamu vetvy, ktorú zlučujeme, dostať priamo z aktuálneho záznamu vetvy, do ktorej zlučujeme, čo je presne tento prípad: vetva footer je len o jeden záznam posunutá oproti vetve master. Pri takomto zlúčení nedochádza ku konfliktom a Git jednoducho posunie značku vetvy do ktorej zlučujeme na pozíciu zlučovanej vetvy. Stav po zlúčení zobrazuje aj nasledujúci obrázok.

Stav po zlúčení vetvy footer do vetvy master
Obr. 8: Stav po zlúčení vetvy footer do vetvy master

Úloha 2.6

Odstránte ďalej nepotrebnú vetvu footer.

Keďže ďalej vetvu footer nepotrebujeme, môžeme ju (jej značku) zmazať príkazom branch -d:

 $ git branch -d footer

Zmazaním jej vetvy je odstránená značka footer
Obr. 9: Zmazaním jej vetvy je odstránená značka footer

Úloha 2.7

Dokončite zmeny vo vetve extension.

Najskôr sa nastavte späť do vetvy extension:

$ git checkout extension

Tu si môžete všimnúť, že súbor index.html teraz neobsahuje pätku, ktorú ste pridali vo vetve footer a zlúčili do vetvy master; vetva extension je odvodená zo záznamu, kde tieto zmeny neexistujú.

HEAD opäť na vetve extension
Obr. 10: HEAD opäť na vetve extension

Ďalej potrebujeme obnoviť odložené zmeny do pracovného priečinka. To je možné vykonať viacerými spôsobmi pomocou príkazu stash. Môžeme použiť stash apply na aplikovanie posledne odložených zmien na pracovný priečinok, pričom tieto naďalej ostanú aj v odkladacom priestore. Použijeme však príkaz stash pop, ktorý okrem toho, že aplikuje posledné odložené zmeny, následne ich aj odstráni zo zásobníka odkladacieho priestoru.

 $ git stash pop

Výpis príkazu by mal potvrdiť, že odložené zmeny v súbore index.html boli opäť aplikované na súbor v pracovnom priečinku.

Zostávajúcou zmenou je zmena tlačidiel s textom "View Detail" na zelené. To docielite zmenením ich CSS triedy btn-primary na btn-success vo všetkých šiestich prípadoch ich výskytu.

 <a class="btn btn-success" href="#" role="button">View details &raquo;</a>

Ukončené zmeny zaznamenajte v novej verzii.

$ git commit -am "extended page"

To spôsobí vytvorenie nového záznamu pre vetvu extension, kam sa presunú značka vetvy a HEAD.

Záznam po zmenách v extension vetve
Obr. 11: Záznam po zmenách v extension vetve

Úloha 2.8

Zlúčte vetvu extension do vetvy master.

Pre požiadavku zlúčenia vetvy extension späť do vetve master je postup podobný ako pri minulom zlučovaní. Najskôr potrebujeme presunúť značku HEAD do vetvy, kam ideme zlúčiť zmeny, teda do vetvy master.

 $ git checkout master

Presun HEAD opäť na vetvu master
Obr. 12: Presun HEAD opäť na vetvu master

Sme pripravení vykonať príkaz merge.

 $ git merge extension

Prvú odlišnosť oproti minulému zlúčeniu si všimnete, keď sa otvorí editor s predpripravenou správou pre záznam o zlúčení vetvy. V čom teda spočíva rozdiel?

Rozdiel je v tom, že tentoraz nie je možné presunúť značku na zlučovanú vetvu, keďže v histórii zlučovaných vetiev existuje bod, kde sa ich zmeny rozišli. Git preto potrebuje spraviť trojcestné porovnanie (medzi dvoma záznamami zlučovaných vetiev a záznamom, ktorý je ich spoločným predkom — v našom prípade to je ten, kam ukazuje značka vzdialenej vetvy origin/master) a zlúčenie zmien jednotlivých súborov. Výsledkom bude nový záznam vo vetve, do ktorej sme inú vetvu zlučovali — teda vo vetve master. Tento záznam sa nazýva aj záznam zlúčenia (z angl. merge commit) a je špeciálny tým, že má viac ako jedného predchodcu.

Nový záznam vo vetve master po zlúčení s vetvou extension
Obr. 13: Nový záznam vo vetve master po zlúčení s vetvou extension

Aj výstup príkazu je odlišný od predchádzajúceho zlúčenia:

Auto-merging index.html
Merge made by the 'recursive' strategy.
 index.html | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

Ak skontrolujete obsah súboru index.html, uvidíte, že zmeny z oboch vetiev (nové nadpisy a zmen farba tlačidiel z vetvy extension a pätka stránky z vetvy master) boli korektne spojené a obe sú prítomné v novom súbore.

Výsledok zlúčenia zmien
Obr. 14: Výsledok zlúčenia zmien

Krok 3: Riešenie konfliktných zmien pri zlučovaní vetiev

Doteraz sme si ukázali dva prípady zlučovania vetiev. Pri oboch zlúčenie prebehlo plne automaticky, keďže žiadna zmena v zlučovaných vetvách (ich súboroch) nebola súčasne na rovnakých riadkoch pre obe vetvy. V praxi ale môže nastať situácia, že či už vy sami v inej vetve, alebo niekto, s kým spolupracujete, vykoná zmenu, ktorá sa pri zlučovaní dostane do konfliktu s inou zmenou na tom istom riadku určitého súboru. Takúto situáciu už nie je možné vyhodnotiť automaticky a musí do nej zasiahnuť človek. Pomoc, ktorú tu systém Git poskytne, je vyznačenie konfliktných oblastí v súbore po zlyhanom automatickom zlučovaní.

V nasledujúcich úlohách nasimulujeme situáciu, kedy sa zmena, ktorá nám lokálne spôsobí konflikt, najskôr stane súčasťou vzdialeného repozitára. V reálnej situácii by ju tam mohol odoslať niektorý zo spolupracovníkov na projekte.

Úloha 3.1

Upravte súbor index.html pomocou webového rozhrania servera Gitlab.

Vo webovom rozhraní vášho projektu získaného cez funkciu rozvetvenia repozitára (fork) otvorte súbor index.html. Na stránke, kde vidíte jeho obsah, by ste mali mať vpravo hore dostupné tlačidlo Edit.

Vojdite do editačného režimu a upravte popisky na tlačidlách "View details" tak, aby obsahovali text "More". Keďže sme lokálne menili triedu tlačidiel, ktorá je zapísaná na tom istom riadku, docielime tak zmenu, ktorá bude v konflikte.

Po upravení textu tlačidiel vyplňte formulárové políčko Commit message (napr. "changed text on buttons") a zaznamenajte túto zmenu tlačidlom Commit Changes.

Pre kontrolu, v sekcii Commits skontrolujte existenciu nového záznamu.

Úloha 3.2

Pokúste sa odoslať vašu prácu z lokálneho repozitára do vzdialeného.

Na minulom cvičení sme na túto úlohu použili príkaz push.

$ git push -u origin master

Ak ho ale použijeme teraz, výsledkom bude:

To git@git.kpi.fei.tuke.sk:janko.hrasko/git-branch.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@git.kpi.fei.tuke.sk:janko.hrasko/git-branch.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Podstatnou informáciou z tohto textu je to, že príkaz bol zamietnutý, kedže vzdialený repozitár obsahuje záznamy, ktoré nemáme lokálne. Najskôr je potrebné tieto záznamy dostať do lokálnej kópie repozitára, vyriešiť prípadné konflikty a až tak odoslať späť do vzdialeného repozitára.

Úloha 3.3

Stiahnite si informácie o vzdialenom repozitári.

Aktualizáciu informácií (napr. histórie) o vzdialenom repozitári je možné vykonať príkazom fetch:

$ git fetch

Ak si pozriete históriu repozitára našim príkazom graphlog, zistíte, že značka origin/master sa presunula a nastalo rozvetvenie podobné tomu, ktoré sme mali v prípade vetiev master a extension lokálne. Zobrazuje to aj nasledujúci obrázok.

Rozvetvenie origin/master a master
Obr. 15: Rozvetvenie origin/master a master

Potrebujeme teraz zlúčiť tieto dve vetvy.

Úloha 3.4

Zlúčte vetvu origin/master do vetvy master.

Keďže sa nachádzame vo vetve master, stačí vykonať príkaz merge.

 $ git merge origin/master

Vo výstupe dostaneme očakávaný konflikt:

Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Pri výpise stavu repozitára pomocou príkazu status uvidíte aj informáciu o tom, ktoré súbory sú v nezlúčenom stave - budú označené ako "both modified".

Ak si pozriete obsah súboru index.html, nájdete v ňom značkami vyznačené konfliktné časti kódu, napríklad:

<<<<<<< HEAD
          <p><a class="btn btn-success" href="#" role="button">View details &raquo;</a></p>
=======
          <p><a class="btn btn-primary" href="#" role="button">More &raquo;</a></p>
>>>>>>> origin/master

Časť medzi <<<<<<< HEAD a ======= je kód, ktorý bol lokálne, časť medzi ======= a >>>>>>> origin/master zas ten, ktorý sa stiahol zo vzdialeného repozitára a bol na tých istých riadkoch. Vašou úlohou je tieto zmeny zjednotiť. Upravte teda tlačidlá tak, aby boli zachované všetky zmeny - trieda tlačidiel ostane btn-success a ich popis "More". Upravte popisy aj troch nových tlačidiel, ktoré vo verzii vzdialeného repozitára neboli, aby bol výsledok konzistentný.

 <p><a class="btn btn-success" href="#" role="button">More &raquo;</a></p>

Poznámka

Opäť, aj pre tento postup existuje v systéme Git skratka. Je ňou príkaz pull, ktorý najskôr vykoná fetch a potom merge FETCH_HEAD, kde značka FETCH_HEAD bude ukazovať na záznam vo vetve, ktorá bola aktualizovaná príkazom fetch. Prípadný konflikt je ale stále potrebné vyriešiť manuálne.

Po odstránení všetkých značiek konfliktov môžeme dokončiť zlúčenie vetiev.

Úloha 3.5

Dokončite zlúčenie vetiev origin/master a master.

Konflikty v súbore označíme ako vyriešené tým, že súbor presunieme do prípravnej oblasti

 $ git add index.html

Výpis príkazu status teraz informuje, že

...
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)
...

Posledným krokom je teda nový záznam, ktorý už bude mať predvyplnenú záznamovú správu (ktorú ale môžete modifikovať, ak chcete).

$ git commit

Po jeho ukončení by mal Git vypísať správu o ukončení zluočvania vetiev. Nový stav repozitára bude nasledovný:

Zlúčené vetvy master a origin/master
Obr. 16: Zlúčené vetvy master a origin/master

Úloha 3.6

Odošlite lokálne zmeny na vzdialený server.

V tomto stave už môžeme použiť príkaz push

$ git push -u origin master

Po jeho vykonaní sa značka origin/master presunie na pozíciu značky master (a HEAD) a vetva master bude zosynchronizovaná zo vzdialenou vetvou.

Finálny stav
Obr. 17: Finálny stav

Poznámka

Vetva extension naďalej ostáva existovať iba lokálne. Ak by ste chceli aj ju odoslať do vzdialeného repozitára, použite príkaz

$ git push origin extension

Doplňujúce úlohy

Úloha A.2

Zistite, ako je možné pridať vlastné značky (angl. tag) k významným záznamom, napríklad na označenie verzií vyvíjaného systému.

Doplňujúce zdroje

  1. Git e-book
  2. Learn Version Control with Git