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ť. Git je nepostrádateľný nástroj pre vývojárov, ktorý poskytuje širokú škálu príkazov a nástrojov na správu kódu. Medzi najdôležitejšie a často používané patria príkazy clean
, reset
, rebase
a revert
. V tomto článku sa pozrieme na to, ako tieto nástroje fungujú a kedy ich používať.
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.
Príkaz git clean
sa používa na odstránenie neotvorených súborov a adresárov z pracovného stromu. Je užitočný, keď potrebujete vyčistiť svoj projekt od nepotrebných súborov.
Použitie:
git clean -n
– zobrazí, ktoré súbory by boli odstránené bez ich skutočného odstránenia.git clean -f
– skutočne odstráni neotvorené súbory.git clean -fd
– odstráni neotvorené súbory a adresáre.
git clean -n # Zobrazenie súborov na odstránenie
git clean -f # Odstránenie neotvorených súborov
git clean -fd # Odstránenie neotvorených súborov a adresárov
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. Príkaz git reset
sa používa na návrat do predchádzajúceho stavu, pričom môže meniť alebo zachovať históriu commitov. Je vhodný na odstránenie alebo presunutie zmien.
Na upratanie nám tentokrát bude slúžiť príkaz git reset, a to v troch režimoch:
- git reset –soft <commit> – resetuje len HEAD
- git reset –mixes <commit> – (východzia možnosť) resetuje HEAD a index
- git reset –hard <commit> – resetuje HEAD, index aj working copy
Možnosti použitia:
git reset --soft <commit>
– presunie HEAD na daný commit, ale zachová zmeny v staging area.git reset --mixed <commit>
– presunie HEAD na daný commit a zmeny sa vrátia do pracovného stromu (predvolená možnosť).git reset --hard <commit>
– presunie HEAD na daný commit a odstráni všetky zmeny.
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:
- git reset –hard HEAD ma dostane do stavu ako po poslednom commite
- git reset –mixed HEAD mi vyčistí index, takže ho môžem znova začať plniť
git reset –soft HEAD~1 # Presun na predchádzajúci commit, zmeny zostávajú v staging area
git reset –mixed HEAD~1 # Presun na predchádzajúci commit, zmeny v pracovnom strome
git reset –hard HEAD~1 # Presun na predchádzajúci commit, odstránenie všetkých zmien
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. Príkaz git rebase
umožňuje presunúť alebo kombinovať série commitov. Je užitočný na udržanie čistej a lineárnej histórie projektu.
Nasledujúce obrázky zobrazujú ako bude taký presun vyzerať:
Feature branch obsahuje commity X, Y a Z. Tie idem teraz presunúť do mastera.
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
Použitie:
git rebase <base>
– presunie aktuálnu vetvu na základný commit.git rebase -i <base>
– interaktívny rebase, umožňuje upravovať, meniť poradie alebo kombinovať commity.
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ť.
git rebase master # Presun vetvy na master
git rebase -i HEAD~3 # Interaktívny rebase posledných 3 commitov
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. Príkaz git revert
sa používa na vytvorenie nového commit-u, ktorý zruší zmeny z predchádzajúceho commit-u. Tento príkaz je užitočný, keď chcete zachovať históriu projektu a zároveň odstrániť neželané zmeny. Jeho použitie je jednoduché:
Použitie:
git revert <commit>
– vytvorí nový commit, ktorý zruší zmeny daného commit-u.
> 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.
git revert HEAD~1 # Vytvorí nový commit, ktorý zruší zmeny predchádzajúceho commit-u
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.
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:
- vykonáš merge, ktorý je problémový takže si v stave, že v súboroch ti Git označí konflikty
- spustíš git rerere – Git si poznačí konfliktný stav
- vyriešiť konflikty
- 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
Príkazy clean
, reset
, rebase
a revert
sú základné nástroje, ktoré vám umožnia efektívne spravovať váš kód a históriu projektu v Gite. Správne použitie týchto nástrojov vám pomôže udržiavať čistý a organizovaný projekt, minimalizovať chyby a zjednodušiť tímovú prácu. 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.
Odporúčania na prácu s GITom
Pri práci s týmito príkazmi vždy dbajte na to, aby ste mali zálohu svojho projektu, najmä pri používaní príkazov, ktoré môžu meniť históriu commitov, ako sú reset
a rebase
. Pri správnom použití sa Git stáva neoceniteľným pomocníkom v každodennej práci vývojára.
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