Čtvrtý důvod proč zvolit Git : Úpravy a opravy historie

V minulém článku jsme rozebrali důsledky, které má pro naši práci s repositáři decentralizovaná povaha Gitu. Jedním z nich je možnost upravovat historii — tedy alespoň historii našeho projektu. Pojďme se tedy podívat na to, proč a jak to pro nás může být výhodné.

Seriál: Pět důvodů, proč zvolit Git (5 dílů)

  1. První důvod, proč zvolit Git: Neříká vám, jak máte pracovat 21.12.2009
  2. Druhý důvod proč zvolit Git: Snadné vytváření a slučování větví 28.12.2009
  3. Třetí důvod proč zvolit Git : Decentralizace 4.1.2010
  4. Čtvrtý důvod proč zvolit Git : Úpravy a opravy historie 11.1.2010
  5. Pátý důvod, proč zvolit Git : Zkušenosti uživatelů Gitu 18.1.2010

Jak jsme viděli v minulém článku, v distribuovaných verzovacích systémech probíhá všechna práce lokálně. Jedním z příjemných důsledků je, že můžeme plnohodnotně pracovat offline. Stejně důležité je ale to, že všechna práce je „soukromá“ — dokud ji nezpřístupníme ostatním.

Tím pádem se nám otevírá zajímavá možnost: upravit to, co chceme zveřejnit. Než v šoku vykřiknete „ale historie přece musí zůstat neměnná!“, uvažte, o jakou „historii“ se zde jedná. Je to lokální historie, moje práce, kterou jsem ještě nepublikoval a nesdílel s ostatními. Takovou práci bych přece mohl udělat i jinak, pokud bych měl jen o trochu jiný nápad.

Důležitá je přitom otázka, proč vlastně historii měnit. Odpověď je jednoduchá: protože nám to přináší úplně jiné možnosti práce, jiné využití verzovacího nástroje.

Nejjednodušším nástrojem pro úpravu historie je v Gitu přepínač --amend pro příkaz git commit : dovolí nám upravit poslední commit, který jsme provedli. To můžeme chtít udělat třeba proto, abychom opravili překlep v popisku commitu, nebo abychom přidali (git add ) zapomenutý soubor; vadný commit tak v repositáři nezůstane na věky.

Otevírá nám ale cestu k zajímavému způsobu práce: commitovat průběžně práci na jednom tématu do „pracovního“ commitu (WIP, work in progress). Vždy, když máme hotovou určitou část funkcionality, s kterou jsme spokojeni, uložíme ji přes git commit --amend do „pracovního“ commitu, který tak stále upravujeme. Při práci na rozsáhlejší změně napříč více soubory tak máme stále v repositáři bezpečně uloženu cennou část práce, a rozpracovaný kód můžeme kdykoliv pomocígit reset nebogit checkout zahodit. Teprve jakmile máme úkol hotový, opět pomocí --amend napíšeme smysluplný popisek commitu — doteď jsme se jím nemuseli zabývat — a commit již necháme být a pracujeme na dalším úkolu.

Díky --amend tedy získáváme „hyperaktivní undo“, které funguje napříč projektem. Navíc nám umožňuje průběžně doplňovat popisek commitu, jedná-li se o rozsáhlejší změnu: klasickým příkladem znehodnocení historie v repositáři leckdy bývá např. commit typu „honem honem“, který obsahuje popisek oprava formulare a kromě opravy formuláře třeba zahrnuje ještě úpravu metody modelu, aby oprava fungovala (ale na to jsme v době psaní popisku již dávno zapomněli).

Chcete-li se naučit efektivně využívat všechny možnosti Gitu, objednejte si od autora článku školení, nebo si rezervujte místo v kurzech na www.git-fu.cz.

Určitě vás ihned napadne, že by bylo skvělé, kdyby šlo v případě rozsáhlých změn podobných „pracovních“ commitů dělat více a pak je sloučit do jednoho. A právě pro takovou operaci v Gitu využijeme příkazgit rebase s přepínačem --interactive. Uvažujme tedy tuto situaci, kdy jsme postupně do repositáře uložili sekvenci „pracovních“ commitů:

$ git log --oneline
66123b8 WIP > doplneny testy pro XML, refactor, dokumentace, etc
83dc11b WIP > FIX chybne parsovani prazdneho krestniho jmena
0bb359a WIP > pridany fce pro import z XML
05b23f1 WIP > fce pro import z CSV, testy
b700909 Infrastruktura pro import
... 

Nyní je chceme sloučit do jednoho commitu, který bude kombinací všech „pracovních“ commitů se smysluplným popiskem. Učiníme tak příkazem:

$ git rebase -i b700909 

Číslo b700909 je identifikátorem commitu, který chceme ponechat nedotčený, od něhož dále tedy chceme upravovat historii. Otevře se nám výchozí textový editor s tímto obsahem:

pick 05b23f1 WIP > fce pro import z CSV, testy
pick 0bb359a WIP > pridany fce pro import z XML
pick 83dc11b WIP > FIX chybne parsovani prazdneho krestniho jmena
pick 66123b8 WIP > doplneny testy pro XML, refactor konstruktoru, dokumentace, etc

# Rebase b700909..28bf867 onto b700909
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit 

Vidíme, že revize jsou vypsány v opačném pořadí než v git log: časově poslední jsou nahoře, novější dole. Jak Git napovídá, máme na výběr tři možnosti: každý z commitů buď použít nedotčený, upravit jej, nebo sloučit s předchozím  — a to je to co nás zajímá. (Pokud některý řádek vymažeme, commit zcela odstraníme z historie.) My proto zvolíme u posledních třech commitů variantu squash nebo zkráceně  s:

pick 05b23f1 WIP > fce pro import z CSV, testy
s 0bb359a WIP > pridany fce pro import z XML
s 83dc11b WIP > FIX chybne parsovani prazdneho krestniho jmena
s 66123b8 WIP > doplneny testy pro XML, refactor konstruktoru, dokumentace, etc 

Po uložení souboru nám Git v editoru umožní editovat popisek sdruženého commmitu a po jeho uložení ohlásí, co provedl:

[detached HEAD 766e7d0] Pridana komponenta pro import (CSV, XML) do databaze + testy
 3 files changed, 140 insertions(+), 29 deletions(-)
 rewrite importer.py (74%)
 create mode 100644 fixtures/people.csv
Successfully rebased and updated refs/heads/import. 

Výpisem historie a detailu commitu si ověříme, že všechno proběhlo tak, jak mělo:

$ git log --oneline
766e7d0 Pridana komponenta pro import (CSV, XML) do databaze + testy
b700909 Infrastruktura pro import
...

$ git show 766e7d0
commit 766e7d05b9230f9c8ad73e42c5abb2c20fcd3751
Author: John Gitster <gister@example.com>
... 

A vskutku, naše čtyři pracovní commity jsou sloučené do jednoho. Zaujalo nás ale, že můžeme s commity pomocí rebase -i provádět mnoho dalších operací. Kromě sloučení, které jsme si předvedli, je to například:

  • Přeskládat commity do jiné posloupnosti, tím, že změníme pořadí jednotlivých řádků
  • Jednoduše upravit popisek commitu, zvolíme-li edit a pak provedeme git commit --amendgit rebase --continue
  • Úplně změnit commit tím, že zvolíme edit, provedeme změny, dáme git add (příp. git rm) a postupujeme jako v předchozím případě
  • Rozdělit nesprávný commit, který spojuje nesouvisející změny, do dvou či více logičtějších

„Přepisování dějin“ prakticky

Pojďme si projít nejčastější příklad, tedy úpravu konkrétního commitu hlouběji v historii. Uvažujme, že jsme pokračovali v práci a naše historie nyní vypadá takto:

$ git log --oneline
f5fc893 GUI pro importni komponentu
766e7d0 Pridana komponenta pro import (CSV, XML) do databaze + testy
b700909 Infrastruktura pro import 

Přišli jsme však na malou, ale ošklivou chybu přímo v importní komponentě, a nechceme samozřejmě opravu přidávat do commitu obsahujícího GUI, ani přidávat další, „opravný“ commit. Pomůže nám opět interaktivní rebase:

$ git rebase -i b700909
pick f5fc893 GUI pro importni komponentu
edit 766e7d0 Pridana komponenta pro import (CSV, XML) do databaze + testy 

Po uložení v editoru nám Git zobrazí nápovědu:

Stopped at 766e7d0... Pridana komponenta pro import (CSV, XML) do databaze + testy
You can amend the commit now, with
    git commit --amend
Once you are satisfied with your changes, run
    git rebase --continue 

Nyní můžeme provést potřebné opravy v kódu, a po připravení souborů ke commitu ( git add) jej uložíme a dáme příkaz pro pokračování rebase:

$ git commit --amend
[detached HEAD 71e78c8] Pridana komponenta pro import (CSV, XML) do databaze + testy
 1 files changed, 24 insertions(+), 35 deletions(-)

$ git rebase --continue
Successfully rebased and updated refs/heads/import. 

Prohlédneme si nyní, jak naše operace dopadly:

$ git log --oneline
37db3b7 GUI pro importni komponentu
71e78c8 Pridana komponenta pro import (CSV, XML) do databaze + testy
b700909 Infrastruktura pro import 

Vše vypadá v pořádku a commit jsme úspěšně opravili. Povšimněte si však malé drobnosti: identifikátor obou commitů, které jsme rebasovali, se změnil. Historii lze tedy v Gitu radikálně měnit a přepisovat, ale nikdy to neprojde bez povšimnutí.

V každém návodu na úpravy historie Gitu proto najdete velmi výrazné upozornění, že nemáte nebo nesmíte měnit veřejnou historii.

Není to taková anarchie, jak by se zdálo

Historie, která (ve většině případů) nebo musí zůstat neměnná, je historie veřejná, publikovaná, sdílená s ostatními: tedy commity, které už jsem „poslal dál“. Taková historie už není „moje“, už mi nepatří. Ostatní na ní založili svoji práci, jiné branche, atd. Veřejná historie je — jak jsme právě viděli — v Gitu chráněna proti nepozorované změně: SHA-1 hash revize, základního prvku historie, je spočítán z data, autora, popisku revize i „stavu“ repositáře, na který revize ukazuje. Změní-li se tedy jakákoliv z těchto součástí, změní se i hash. Důležité je přitom si uvědomit, že „veřejná“ je pro Git i historie lokální větve, která byla začleněna do jiné. Pokud bychom historii takové větve změnili, začlenili ji do jiné, provedli další commity a pak ji začlenili znova, se zlou se potážeme. Důsledky takové změny sice pocítíte jenom vy, ale stejně bolestivě a může vás to mást. A konečně, při silně decentralizovaném vývoji je „veřejná“ i historie větve, z níž si ode mně změny stahují ostatní (typicky master).

Dodejme, že veřejnou historii samozřejmě můžeme změnit — víme, jak je Git je flexibilní. Ale všichni, s nimiž historii nějakým způsobem sdílíte, se to dozvědí. A dozvědí se to zpravidla v podobě více či méně špatných zpráv: merge commitem či jejich sekvencí, nutností rebasovat celou historii oproti vaší, a často výmluvnou zprávou „Automatic merge failed; fix conflicts and then commit the result.“

Git se přitom v základním nastavení brání tomu, aby někdo veřejnou historii měnil: v případě, že se snažíte provést git push změněné historie, dostanete chybovou hlášku … non-fast-forward updates were rejected … Díky flexibilitě Gitu je ale jakákoliv změna veřejné historie je jen tak daleko jako přepínač --force  — pokud je repositář příslušně nastaven . Musíte proto dobře zvážit, jaké důsledky interaktivní rebase bude mít. Pokud jej provádíme rozumně — v topic branch před mergováním a pak jej zakonzervujeme nebo smažeme, nebo pokud jej provádíme v části historie, kterou jsme ještě neučinili veřejnou — nic pokazit nemůžeme; kromě větší či menší šance, že dojde ke konfliktům. (V takovém případě můžeme celou interaktivní rebase opustit příkazem  git rebase --abort.)

V Gitu tedy nanejvýš platí „commitujte brzy a commitujte často“. Klasickým a častým příkladem znehodnocení historie je kupříkladu „commit na konci dne“, kdy se zkrátka do repositáře natlačí kód tak, jak odpadl „za pět minut pět“ od ruky. V Gitu můžeme rozdělanou práci uložit, ale před její publikací historii upravit do logičtější a srozumitelnější podoby. (Nezapomínejme přitom, že „zveřejnit“ neznamená jen git push do sdíleného repositáře, ale také například merge z privátní větve do větve sdílené.)

A pokud nám na srozumitelné historii záleží — používáme přece verzovací nástroj —, je to dost dobrý důvod, proč zvolit Git. V příští, již poslední části našeho seriálu se zeptáme několika gitsterů na to, jaké výhody jim Git přinesl a proč si jej pro správu verzování vybrali.

(Autor děkuje Jiřímu Kubíčkovi za cenné připomínky a podněty k článku. Rovněž děkuje za upozornění na nesprávně uvedené pořadí revizí ve výpise interaktivní rebase pozornému čtenáři Štefanu Ľuptákovi – pořadí bylo opraveno.)

Karel Minařík navrhuje a programuje webové stránky a aplikace, poskytuje konzultace a školení v oblasti vývoje pro web a žije v Praze se svojí ženou a dvěma dcerami.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 15

Přehled komentářů

mkyral Díky
olin Re: Díky
Jirka P Re: Díky
um_7 Dost dobrý
Karel Minařík Re: Dost dobrý
Dundee5 Good
ondra.novacisko.cz Re: Good
ijacek Re: Good
Laethnes Re: Good
Karel Minařík Re: Good
gilhad Re: Good
Karel Minařík Re: Good
Yenya Rebase vetve samu na sebe
pasky filter-branch
Karel Minařík Re: filter-branch
Zdroj: https://www.zdrojak.cz/?p=3148