Přejít k navigační liště

Zdroják » Různé » Čtvrtý důvod proč zvolit Git : Úpravy a opravy historie

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

Články Různé

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é.

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.)

Komentáře

Subscribe
Upozornit na
guest
15 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
mkyral

Super článek. Díky.

olin

Souhlas. Že se dá rebase použít tímhle způsobem, jsem opravdu netušil :-)

Jirka P

Tak on se dá použít i jinak. Např. když chcete vybrat nějaké commity z jiné větvě, a nechce se vám psát git cherry-pick… git cherry-pick …, tak se dá udělat rebase a do toho todo skriptu napsat

p sha-prvniho-commitu
p sha-druheho-commitu

um_7

jsem celkem spokojený uživatel svn, ale tohle se mi hodně líbí :)

Dundee5

Opravdu dobrý článek. Předpokládám, že řady pochybovačů značně prořídly :)

O těchto možnostech úpravy historie jsem vůbec nevěděl a opravdu moc se mi to líbí.

Umí TortoiseGit pracovat s git rebase? Přecejen ručně přepisovat „pick“ na „e“ je trochu otravné.

ondra.novacisko.cz

Spíš se nikomu nechce zakládat další flame. Rozhodně menit repozitáře a celý systém práce nehodlám. Článek jsem četl a nenašel jsem v tom nic, co by mi zaplatilo práci na změně (byť by vlastní převod měl být automatizovaný).

Jo, mohl by SVN umět přenášet změny mezi repozitáři, pak by bylo zajímavý umět branchovat v lokálním SVN a do centrálního následně mergovat. Tedy, možná už to uměj, nezkoumal jsem.

Úpravy historie neshledávam zajímavým. Historie se nesmí měnit, i kdyby tam bylo tisce překlepů. Věřte mi, že je kolikrát lepší vidět i několik vzájemně se opravujících commitů, než bordel v historii.

KarelI

> Historie se nesmí měnit
Pokud uvazujeme podminky z clanku (lokalni, nezverejnene commity), proc se nesmi menit historie?

Kde konkretne pri uklidu historie vznikne bordel? Myslite, ze by to lidi delali kdyby vysledkem byl bordel?

Ja jsem zrovna dneska delal cistku, prekopaval jsem nekolik commitu. Zacinal jsem s par na muj vkus vetsimi zmenami, vysledkem je prehledna serie, kde je v kazdem commitu jedna izolovana zmena. Kdyz budu v budoucnu treba chtit zkoumat, kde se stala chyba, tak to budu mit rozhodne snazsi.

Riny

> Spíš se nikomu nechce zakládat další flame.
Heh, hezký názor :). Rozhodně osobně jsem z flamů dost unavený, jak už se jeden objeví, mizím.

Každopádně osobně si myslím, že na tom něco je. Na obojím. Souhlasím s tím, že historie by se neměla měnit, že by měla být něco pevného, o co se dá opřít. Ale zase na druhou stranu uznávám, že jsou důvody, proč ji upravit. Hodně častým důvodem např. je to, že se náhodou podaří odeslat soubor s heslama (i když uznávám, že pak už je pozdě bycha honit vzhledem k tomu, že už si je mohl někdo stáhnout – i když to lze ověřit v logech). Mě osobně se jeden důvod naskytl poměrně nedávno; dřív jsem nepoužíval žádný verzovací systém (přeci jen jsem spíš začátečník) a spíš jsem si dělal „zálohy“ s postupně číslovanými adresáři. Nedávno jsem si usmyslel, že potřebuji nový a skončil jsem u gitu. Tak jsem do něj začal cpát jednotlivé zálohy. No a po poměrně dlouhé práci jsem zjistil, že když checkoutuji starší verzi – nefunguje. Velice brzy jsem našel příčinu – systém (jedná se o PHP stránky) potřebuje k práci složky, jejichž existenci předpokládá, ale které jsou prázdné – no a git prázdné složky neukládá a tedy nevytváří. A tak, i když ve skutečné historii byly, do historie gitu se nedostaly a bylo potřeba historii upravit a nacpat do nich něco, aby ty složky zůstaly (např. .gitignore nebo .htaccess jsou velmi dobře použitelné).

Proto se mi třeba líbí, že git při změně historie změní hashe všech dalších commitů, takže třeba změna veřejného repositáře nemůže zůstat bez povšimnutí (a u kterého se obecně značně nedoporučuje historii měnit).

Osobně to vlastně vnímám, že je to o tom, co člověk může a co člověk musí. Je to jako s programovacími jazyky – taková Java člověku přikazuje, jak má struktura kódu vypadat (jedna třída na stejnojmenný soubor, systém balíčků), ale C++ dává mnohem více volnosti, se kterou přichází zodpovědnost a nutnost určité disciplíny. Ne že bych tím chtěl Javu nebo SVN hanit (i když Javu fakt nemusím, respektive pomalost programů v ní), na druhou stranu když někdo umí dobře a rychle dělat v Javě, proč se učit něco dalšího :).

gilhad

Myslím, že se mýlíte v pohledu na danou věc. Rozhodně se nesmí měnit historie, kerou už někdo jiný viděl, nebo dokonce použil. Stejně jako se tu nedají (snadno) měnit už odeslané příspěvky. Ale opravit chyby v příspěvku než ho odešlu na server bych naopak považoval za slušnost. A to je právě ta „povolená“ změna historie v gitu.
Myslím, že zdejší diskuze bude rozhodně přehlednější s tímto příspěvkem v tomto tvaru, než kdybych tu odeslal první verzi a hned za ní 21 menších, které by opravovaly překlepy a 6, které by vsouvaly do toho prvního zapomenutá slova a upravovaly věty. (čísla průběžně upravována podle toho, jak jsem tento příspěvek psal)

Pokud budu chtít k tomuto příspěvku něco dodat, tak samozřejmě použiju další (protože tento už BYL) zveřejněn – a zase si ho před odesláním přečtu, opravím a případně rozdělím na víc logických celků.

Představte si centrální server jako root.cz a lokální repozitář jako rozepsané okénko v prohlížeči – dokud neklepnete na „Odeslat“ je lepší nalezené chyby opravit a špatně seřazené odstavečky přerovnat do logického pořadí. Jakmile odklepnete „Odeslat“, tak už je historie daná a nesmí se měnit.

Yenya

Necekal jsem ze se v clanku dozvim neco noveho, ale delat rebase vetve dovnitr sebe samotne me fakt jeste nenapadlo. Zajimave, diky za tip.

-Yenya

pasky

V clanku s takovymhle titulkem jsem trochu cekal i zminku o git filter-branch, ktery funguje v trochu jinem modu nez git rebase -i, je o neco lowlevelovejsi a urcen na batch processing vetsich kusu historie. Napr. jim lze snadno hromadne opravovat informace o autorstvi commitu, smazat z cele historie nejake nezadouci soubory, nebo naopak „vykousnout“ z historie jen tu, ktera se tyka urciteho podadresare.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.