Seriál Online kurz Git – Clean, Reset, Rebase, Revert nástroje do každého počasia – 7. diel

Som pravdepodobne jeden z mála ľudí, ktorý si užíva nákup v záhradníctve a železiarstve. Ako hobby-záhradník (doplň si kľudne iné remeslá) sa prechádzam medzi regálmi a sledujem to množstvo rôznych nástrojov, čo sa dá dnes kúpiť. Stalo sa mi niekoľkokrát, že som objavil nástroj presne určený na riešenie problému, s ktorým som predtým zápasil. Aj pri vývoji softvéru (rád hovorím, že je to remeslo ako každé iné) je to tak, že vhodný nástroj je niekedy na nezaplatenie. Práve preto si dnes povieme niečo o ďalších nástrojoch (príkazoch) Gitu. Jedného dňa sa ti môže taký nástroj znenazdania hodiť.

Upratujeme nesledované súbory pomocou príkazu Git clean

Neviem ako ty, ale ja nemám rád neporiadok. Väčšinou ma oberá o čas a životnú energiu (existuje niekoľko dobrých kníh o tom, že to tak naozaj je). Aj preto sa raz za čas hodí príkaz, ktorým je možné upratať working copy. Na nesledované (ale aj na ignorované) súbory je tu príkaz git clean.

Príkaz pri základnom spustení vymaže všetky nesledované súbory. Tak to poďme skúsiť:

> git clean

fatal: clean.requireForce defaults to true and neither -i, -n, nor -f given; refusing to clean

Príkaz nič nezmazal a skončil s chybou. Git sa nás snaží chrániť. Príkaz clean totiž odstraňuje nesledované súbory, takže ak ich dám raz zmazať, už ich nie je možné dostať späť. Prvá možnosť je zmeniť nastavenie clean.requireForce na false. Tou druhou je pustiť príkaz s prepínačom -f:

> git clean -f

Príkaz clean má ešte jeden zaujímavý prepínač, a to -X (musí to byť veľké X, malé x má iný význam). Spustenie v takom režime vymaže ignorované súbory. Na čo je to dobré? No ak chcete robiť full rebuild a mať pri tom istotu, že žiadne generované súbory vám neovplyvnia proces.

Upratujeme sledované súbory pomocou príkazu Git reset

Ako na upratovanie nesledovaných a ignorovaných súborov už vieme. Teraz si ukážeme niečo so sledovanými. S nimi to už nie je také jednoduché. Pri nich existujú tri veci, na ktoré treba myslieť: symbolická referencia HEAD, index (alebo staged changes) a samotná working copy.

Git reset

Na upratanie nám tentokrát bude slúžiť príkaz git reset, a to v troch režimoch:

  1. git reset –soft <commit> – resetuje len HEAD
  2. git reset –mixes <commit> – (východzia možnosť) resetuje HEAD a index
  3. git reset –hard <commit> – resetuje HEAD, index aj working copy

Hneď prvá možnosť nie je tak úplne upratovanie. Je to v podstate presun HEAD na nejaký iný commit. Ak sa napríklad chceš vrátiť o jeden commit späť, stačí zadať:

> git reset –soft HEAD^

Znak „^“ v zápise HEAD^ znamená „zober HEAD, posuň sa o jeden commit späť a použi tento commit“. V praxi to znamená, že som posunul HEAD o jeden commit späť a práve tento druhý v poradí bude rodičovský commit zmien, ktoré sa chystám commitnúť.

git reset –mixes robí to isté, ale okrem toho tiež nastaví index do stavu podľa commitu, ktorý som zadal. Jediné zmeny, ktoré tak tento reset prežijú sú tie vo working copy. Ak sa chceš dostať do stavu, ako si bol presne po vytvorení určitého commitu (teda aj index aj working copy), tak musíš použiť príkaz v tvare git reset –hard. Takto vlastne dostanem všetky sledované súbory a všetko čo s nimi súvisí (HEAD aj index) do stavu, ako boli vo vybranom commite.

A kde je to spomínané upratovanie? No ak mám vo working copy zmeny, a tiež som už pridal niečo do indexu, tak:

  1. git reset –hard HEAD ma dostane do stavu ako po poslednom commite
  2. git reset –mixed HEAD mi vyčistí index, takže ho môžem znova začať plniť

Upratujeme históriu pomocou príkazu Git rebase

V prvom diely tohto seriálu sme si povedali, že na Gite je dobré, že jeho história je nemenná. Väčšinu času je to pravda, ale existujú tu výnimky. Takou je aj príkaz git rebase, ktorý je schopný presúvať commity z jedného branchu do druhého.

Možno sa pýtaš, na čo je to dobré? Prečo sa vôbec obťažovať históriou? Ten dôvod je jednoduchý: história verzionovacieho systému je vlastne súčasťou projektovej dokumentácie. Pohľad do histórie môže objasniť niektoré veci. A o histórii platí, že sa raz píše a stokrát (tisickrát…) číta. Preto trochu snahy, aby nevyzerala ako tanier plný špagiet, stojí za to. Takže čo presne robí príkaz rebase?

git rebase presúva commity z jedného branchu do druhého. Aby som bol presnejší, on ich nepresunie, ale vytvorí ich kópiu. Preto majú napríklad iný hash. Môžeš ho použiť, ak si mal feature branche, v ktorom máš vývoj hotový a chceš zmeny dostať do mastera. Samozrejme vieš urobiť aj merge, ale v tom prípade tvoj dočasný branch ostáva súčasťou histórie. Ak je takých branchov veľa, môže to byť neprehľadné. Preto namiesto toho použiješ git rebase a branch de facto vložíš do master brancha, takže to bude vyzerať, že bol v ňom vyvíjaný celý čas.

Nasledujúce obrázky zobrazujú ako bude taký presun vyzerať:

Pred Git rebase

Feature branch obsahuje commity X, Y a Z. Tie idem teraz presunúť do mastera.

Po Git rebase

Po presune pôvodný branch zaniká. Namiesto jeho commitov vznikli nové commity X’, Y’ a Z’. Na prvý pohľad jednoduchý proces sa môže občas skomplikovať. Čo sa stane, ak pri presune dôjde napríklad ku konfliktu? Git v takom prípade zastaví presun a je na tebe, aby si rozhodol, čo s tým:

  • git rebase –continue – po vyriešení konfliktov spustíš presun ďalej
  • git rebase –skip – konfliktný commit preskočíš
  • git rebase –abort – zrušíš celý presun

V prípade, že chceš mať nad rebasom väčšiu kontrolu, je možné použiť prepínač git rebase –interactive. Tento príkaz berie ako parameter commit. Následne otvorí východzí editor, do ktorého umiestni riadok pre všetky commity z aktuálneho branchu, ktoré nasledujú po zvolenom commite. Poďme si to ukázať. Najprv spustíme príkaz:

> git rebase –interactive ce3944e86fa3cdf3458620634f7ae74328ccea5e

Po jeho vyhodnotení sa zobrazí editor s nasledujúcim textom:

pick e83a556 Pridany testovaci subor

pick e9cce4c Zmena v master

pick 532f7b4 Zmena v second

pick ac154da Zmena v masteri

pick 40516b1 Zmena v third

# Rebase ce3944e..07cdff8 onto ce3944e (5 command(s))

Každý riadok má na začiatku slovo pick, čo je vlastne príkaz pre Git, aby ten riadok pri rebase použil. Je to východzí príkaz pre všetky riadky. Dá sa ale zmeniť napríklad na reword, kedy sa commit použije, ale je možné zmeniť správu commitu. Alebo sa vymení za squash, čo znamená, že sa zmeny použijú, ale vložia sa do predchádzajúceho commitu a ďalšie iné príkazy. Alebo riadok jednoducho zmažeš, v takom prípade nebude presúvaný.

Takto si vieš v súbore všetko pripraviť a následne po uložení sa rebase spustí a vykoná naprogramované zmeny. Zaujímavé, že? Povedali sme si, že mať peknú históriu je dôležité, ale prečo dávať ľuďom do ruky príkaz ako je rebase? Odpoveď je: nemusíš sa kvalitou minulosti zaoberať v momente, keď ju píšeš.

Aby som ten koncept trochu vysvetlil, odskočím si do sveta TDD, teda Test-Driven Development. V ňom vieš postupovať tak, že najprv napíšeš test, ktorý nefunguje, potom implementuješ (jednoduché, škaredé) riešenie, ktoré funguje (a teda ten skončí úspechom) a následne (už pod kontrolou testu, ktorý vieš stále spúšťať) riešenie refaktoruješ. Ide hlavne o to, že implementáciu si rozdelíš na dve fázy: implementovanie funkčnosti a zlepšenie kvality návrhu.

Niečo podobné sa dá robiť s Gitom pri commitoch. Keď commituješ, tak vlastne tvoríš minulosť projektu a tá (ako som už napísal vyššie) je súčasťou jeho dokumentácie. Problém je, že keď sa sústredíš na kód, tak tvorba kvalitnej histórie je niečo, čo ťa bude spomaľovať a komplikovať ti život. Hlavne niekde naboku vo feature branchy, ktorý je len dočasný, je najjednoduchšie commitovať ako sa to hodí a nie tak, aby bolo ľahko čitateľné.

Problém je, že keď už si hotový a pozrieš sa na tú sériu commitov, tak vidíš minimálne 2 prípady, kedy si po sebe upratoval veci a jeden kedy si nenapísal dobrú commit správu.  A práve vtedy je ten moment, kedy vezmeš do ruky nástroj rebase a svoju rozdrobenú a trochu chaotickú minulosť zmien dáš dokopy.

Ak máš stále pocit, že upratovanie minulosti je predsa len niečo, čo nestojí za námahu, tak mi nedá nespomenúť, že k open-source Git repozitárom pristupujú väčšinou ľudia v dvoch rolách. Ako vývojár (ten, čo generuje zmeny) a správca (ten, čo zmeny spravuje). Skús sa teraz na chvíľu vžiť do role správu repozitára, ktorý musí rozhodnúť, či do hlavnej vetvy priberie zmeny od vývojára, ktoré sú ťažko čitateľné a rozdrobené, alebo množinu commitov, ktoré sú vnútorne koherentné (zmeny v jednom commite spolu súvisia) s dobrými commit správami. Ktorá situácia by bola pre teba lepšia? A o tom množstve ľudí tam vonku, čo bude tvoje commity čítať ani nehovoriac.

Git rebase ti umožní pracovať na zmenách kódu bez ohľadu na to, ako vyzerá história tvojich prác. A už keď si hotový s riešením, tak vezmeš tento nástroj a ukuješ si históriu, za ktorú sa nemusíš hanbiť.

Oprava histórie pomocou príkazu Git revert

Skôr alebo neskôr dôjdeš do situácie, že budeš potrebovať zrušiť jeden commit v histórii. Za určitých okolností (napríklad, ak si ešte neurobil push) sa dá použiť príkaz git reset. V ostatných prípadoch ti vie byť nápomocný git revert. Jeho použitie je jednoduché:

> git revert ce3944e86fa3cdf3458620634f7ae74328ccea5e

git revert vytvorí nový commit, ktorý bude obsahovať presne opačné zmeny ako pôvodný. Okrem jedného commitu vie zobrať aj množinu commitov. V takom prípade opäť vzniká len jeden revertovací commit, v ktorom sú všetky zmeny všetkých revertovaných commitov.

Hľadanie problému

Predstav si modelovú situáciu: Ráno otvoríš bug tracking systém a v ňom záznam o tom, že niečo nefunguje. Niečo, čo si sám pred týždňom skúšal a vieš, že to fungovalo. Pozeráš do kódu a nie je ti jasné, prečo to nejde, keď všetko vyzerá byť OK. Niekde medzi tým stavom pred týždňom a teraz sa to muselo pokaziť. Niekde v tej kope 50-tich commitov. Len tak vedieť, ktorý tú zmenu priniesol…

Ono to nie je tak úplne modelová situácia. Vlastne sa mi to stalo niekoľkokrát. Našťastie Git má jeden nástroj, ktorý je na takýto typ problémov ako stvorený. Je to príkaz git bisect a jeho základom je binárny algoritmus hľadania.

Binárny algoritmus hľadania predpokladá zoznam usporiadaných hodnôt, v ktorom má nájsť pozíciu jednej, vybranej hodnoty. Robí to tak, že rozdelí celý zoznam na 2 časti a porovná hľadanú hodnotu s tými, čo sú naľavo a napravo od rozdelenia. Podľa toho určí, v ktorej polovici pôvodného zoznamu hľadaná hodnota bude a znovu aplikuje postup na túto vybranú polovicu.

Binárne hľadanie binary search

Takže na začiatok hľadania v Gite potrebujeme usporiadanú množinu. Našťastie ju máme. Väzby medzi commitmi tvoria usporiadaný zoznam commitov tak, ako boli vytvárané za sebou. Teraz už len potrebujeme nájsť commit, ktorý spôsobil problém. Aj keď by sa to dalo robiť ručne pozeraním do logu a git reset príkazom, git bisect to výrazne uľahčuje. Postup je nasledovný:

> git reset –hard HEAD – na začiatok je dobre upratať working copy a index

> git bisect start – týmto príkazom sa git inicializuje

> git bisect bad – označím aktuálny stav za zlý (automaticky berie HEAD ale viem zadať hash commitu)

> git bisect good <commit> – commit o ktorej viem, že to fungovalo označím ako dobrý

V tomto momente Git nájde commit, ktorý je v polovici postupnosti commitov medzi tými, čo som označil ako good a bad a working copy dostane do stavu, ktorý zodpovedá tomuto commitu. V takom stave ja viem otestovať či problém pretrváva (napr. skompilujem aplikáciu a urobím jednoduché manuálne testovanie). Ak je problém stále tam, zadám príkaz:

> git bisect bad

ktorým mu poviem, že aj tento commit je zlý. Ak problém nie je, dostali sme sa do časti commitov, kde ten problém ešte nebol. V takom prípade zadám príkaz:

> git bisect good

Ak som zadal bad, Git vezme staršie commity a túto množinu opäť rozdelí na polovicu a working copy pripraví do stavu, ktorý ten commit v strede reprezentuje. Ak som zadal good, tak urobí to isté len nie so staršími ale s novšími commitmi. A ja mu po teste opäť poviem, či sme na tom good alebo bad. A takto spolu s Gitom postupujem, až kým nie je čo deliť a ostane jeden commit, pred ktorým to bolo good, ale po ktorom to bolo bad. A to bude ten, čo som hľadal.

Aby toho nebolo málo, tak git bisect so sebou prináša ešte niekoľko podpríkazov:

git bisect reset – ukončí proces hľadania a working copy vráti do stavu pred jeho spustením

git bisect visualize – zobrazí zoznam zostávajúcich netestovaných commitov

git bisect log – zobrazí celý proces ako postupovalo hľadanie

git bisect replay <súbor> – prehrá proces rozhodovania podľa súboru. Ten získaš tak, že si uložíš výsledok podpríkazu log. A môžeš ho napríklad zmodifikovať a potom spustiť cez replay.

Čerešnička na torte je git bisect run <script>. Ten vždy po nastavení working copy zavolá skript, ktorý otestuje aktuálny stav a povie Gitu, či je to good alebo bad. Tak vie celý proces bežať automaticky až do momentu, keď sa nenájde ten prvý zlý commit. Ako hovorí jeden môj známy: „Konečne sú tie počítače na niečo užitočné.“

Automaticky merge konfliktov

A máme tu ďalšiu modelovú situáciu (aj táto je celkom reálna). Máš svoj feature branch, v ktorom pracuješ na zadaní. Robíš na ňom niekoľko dní a vieš, že byť tak dlho odstrihnutý od master brancha nie je žiadna sranda, tak raz denne urobíš merge z mastera do tvojho brancha, pozrieš sa, či je všetko OK a potom tie mergované zmeny (z mastera) zahodíš. Nepríjemné je, že v masteri sú commity, ktoré spôsobujú, že musíš pri každom takom testovacom mergi riešiť tie isté konflikty. Čo keby si to Gitu vysvetlil raz a nech to už vie vykonať sám. Máme tu ďalší nástroj: git rerere.

Iste ťa napadne: čo je to za názov? rerere je skratka od Reuse Recorded Resolution. Robí to presne to, čo by si podľa názvu tipoval. Najprv si nahrá, ako si vyriešil konflikty a potom toto riešenie vie znova prehrať. Postup je takýto:

  1. vykonáš merge, ktorý je problémový takže si v stave, že v súboroch ti Git označí konflikty
  2. spustíš git rerere – Git si poznačí konfliktný stav
  3. vyriešiť konflikty
  4. znova spustíš git rerere – Git si poznačí riešenie konfliktov

Ak budeš niekedy v budúcnosti mergovať to isté a dostaneš sa do rovnakého konfliktného stavu, tak stačí spustiť git rerere a Git konflikty vyrieši. Treba ešte pamätať na to, že príkaz rerere funguje len ak je premenná rerere.enabled v konfigurácii zapnutá. A tiež na to, že Git si takúto nahrávku konfliktu pamätá presne 60 dní a potom ju zahodí.

Záver a sumarizácia

V tomto seriáli sme si, okrem základných vecí, ako je napríklad commitovanie, pozreli aj tie zaujímavejšie, ako je mergovanie, riešenie konfliktov alebo si Git interne ukladá informácie. Každopádne si treba uvedomiť, že Git je veľmi plastický nástroj, ktorý sa vďaka tomu dokáže prispôsobiť rôznorodým požiadavkám projektu (čo je dobré). Za tú plastickosť ale Git zaplatil občasnou komplikovanosťou (čo je zle). Preto je vždy dobré ovládať teóriu na pozadí a vyvarovať sa tak nečakaným prekvapeniam. Okrem nej je tiež samozrejme dobre poznať aj to množstvo možností (nástrojov), ktoré Git ponúka. Aj preto sme si teraz, v poslednom diely urobili prehľad niektorých zaujímavých príkazov.

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.