Git Reset vs Revert vs Rebase

V tomto textu se podíváme na různé postupy, jak efektivně pracovat s commity v systému Git.

Jako vývojáři se nejednou ocitnete v situaci, kdy budete chtít obnovit stav kódu do jednoho z dřívějších uložení, ale nebudete si úplně jistí, jak na to. I když znáte základní příkazy Gitu jako reset, revert a rebase, může být nejasné, jaký je mezi nimi rozdíl. Pojďme se tedy společně podívat na to, co přesně tyto příkazy dělají.

Git Reset

Příkaz git reset je nástroj pro manipulaci s historií změn, který slouží k návratu k předchozím stavům kódu.

Zjednodušeně řečeno, git reset slouží jako funkce „zpět“. Umožňuje přecházet mezi různými commity. Příkaz má tři hlavní režimy: --soft, --mixed a --hard. Implicitně se používá režim --mixed. Pro pochopení, jak git reset funguje, je důležité porozumět třem klíčovým oblastem správy v Gitu: HEAD, pracovní oblast (index) a pracovní adresář.

Pracovní adresář je místo, kde máte fyzicky uložené soubory. Změny v souborech, které zde provedete, se neprojeví v Gitu, dokud je nepřidáte do pracovní oblasti. Pomocí příkazu git status můžete zjistit, jaké soubory a složky se v pracovním adresáři nacházejí.

Pracovní oblast (index) je místo, kde Git sleduje změny v souborech před jejich uložením do historie. Změny přidané do této oblasti se uloží v adresáři .git. K přidání souboru do pracovní oblasti slouží příkaz git add "název_souboru". Opět pomocí příkazu git status můžete zjistit, které soubory jsou v pracovní oblasti.

HEAD je ukazatel na aktuální větev a poslední commit v této větvi. Lze ho chápat jako pohyblivý ukazatel, který se přesouvá do jiné větve, pokud provedete přepnutí.

Nyní si objasníme, jak git reset pracuje v režimech hard, soft a mixed. Režim hard (tvrdý) přesune HEAD na specifikovaný commit, přepíše obsah pracovního adresáře soubory z tohoto commitu a resetuje pracovní oblast. Režim soft (měkký) pouze přesune HEAD na daný commit, ale pracovní adresář a pracovní oblast zůstanou beze změn. Režim mixed (smíšený), který je implicitní, resetuje HEAD i pracovní oblast.

Git Hard Reset

Účelem tvrdého resetu je přesunout ukazatel HEAD na konkrétní commit a smazat veškeré commity, které se objevily po tomto commitu. Tento příkaz mění historii commitů a je nevratný.

V následujícím příkladu nejprve přidáme tři nové soubory, uložíme je do Gitu a poté provedeme tvrdý reset.

Jak je vidět z výstupu příkazu níže, v tuto chvíli není co commitovat.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.

(use "git push" to publish your local commits)

nothing to commit, working tree clean

Nyní vytvoříme 3 soubory a přidáme do nich nějaký obsah.

$ vi file1.txt
$ vi file2.txt
$ vi file3.txt

Přidáme tyto soubory do repozitáře.

$ git add file*

Když znovu spustíme příkaz status, zobrazí se nové soubory, které jsme vytvořili.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.

(use "git push" to publish your local commits)

Changes to be committed:

(use "git restore --staged <file>..." to unstage)

new file:
file1.txt

new file:
file2.txt

new file:
file3.txt

Před commitem si ukážeme, že v Gitu máme v současnosti v historii 3 commity.

$ git log --oneline
0db602e (HEAD -> master) one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Nyní provedeme commit do repozitáře.

$ git commit -m 'added 3 files'
[master d69950b] added 3 files
3 files changed, 3 insertions(+)
create mode 100644 file1.txt
create mode 100644 file2.txt
create mode 100644 file3.txt

Po provedení příkazu ls-files zjistíme, že nové soubory byly přidány.

$ git ls-files
demo
dummyfile
newfile
file1.txt
file2.txt
file3.txt

Při spuštění příkazu log v Gitu máme v historii 4 commity a HEAD ukazuje na nejnovější commit.

$ git log --oneline
d69950b (HEAD -> master) added 3 files
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Pokud ručně odstraníme soubor file1.txt a použijeme git status, zjistíme, že změny nejsou připravené ke commitu.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

Changes not staged for commit:

(use "git add/rm <file>..." to update what will be committed)

(use "git restore <file>..." to discard changes in working directory)

deleted:
file1.txt

no changes added to commit (use "git add" and/or "git commit -a")

Nyní spustíme příkaz pro tvrdý reset.

$ git reset --hard
HEAD is now at d69950b added 3 files

Když znovu zkontrolujeme stav, zjistíme, že není co commitovat a soubor, který jsme smazali, se vrátil do repozitáře. K rollbacku došlo, protože po smazání souboru jsme neprovedli commit, a tak se po tvrdém resetu vrátil do předchozího stavu.

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

nothing to commit, working tree clean

Pokud se podíváme na log, bude vypadat následovně.

$ git log
commit d69950b7ea406a97499e07f9b28082db9db0b387 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 19:53:31 2020 +0530

added 3 files

commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

test

Účelem tvrdého resetu je posunout HEAD na zadaný commit a aktualizovat pracovní adresář a pracovní oblast. Podívejme se na další příklad. Vizualizace mých commitů nyní vypadá takto:

Zde spustíme příkaz s HEAD^, což znamená, že se chceme vrátit k předchozímu commitu (o jeden commit zpět).

$ git reset --hard HEAD^
HEAD is now at 0db602e one more commit

Je vidět, že ukazatel HEAD se nyní změnil z d69950b na 0db602e.

$ git log --oneline
0db602e (HEAD -> master) one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Po kontrole logu vidíme, že commit d69950b zmizel a HEAD nyní ukazuje na SHA 0db602e.

$ git log
commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

Test

Po provedení ls-files uvidíme, že soubory file1.txt, file2.txt a file3.txt již v repozitáři nejsou, protože tento commit a jeho soubory byly tvrdým resetem smazány.

$ git ls-files
demo
dummyfile
newfile

Git Soft Reset

Nyní si ukážeme příklad měkkého resetu. Opět přidáme 3 soubory, jak bylo popsáno výše, a provedeme commit. Git log zobrazí historii, jak je znázorněno níže. Můžeme vidět, že soft reset je náš poslední commit a HEAD na něj také ukazuje.

$ git log --oneline
aa40085 (HEAD -> master) soft reset
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Podrobnosti o commitu v logu lze zobrazit pomocí níže uvedeného příkazu.

$ git log
commit aa400858aab3927e79116941c715749780a59fc9 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 21:01:36 2020 +0530

soft reset

commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

test

Nyní se pomocí měkkého resetu chceme vrátit k jednomu ze starších commitů s SHA 0db602e085a4d59cfa9393abac41ff5fd7afcb14

Provedeme to spuštěním níže uvedeného příkazu. Stačí zadat prvních 6 znaků SHA, není nutné zadávat celé SHA.

$ git reset --soft 0db602e085a4

Když nyní spustíme git log, vidíme, že HEAD byl resetován na zadaný commit.

$ git log
commit 0db602e085a4d59cfa9393abac41ff5fd7afcb14 (HEAD -> master)
Author: mrgeek <[email protected]>
Date:
Mon May 17 01:04:13 2020 +0530

one more commit

commit 59c86c96a82589bad5ecba7668ad38aa684ab323
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:54:53 2020 +0530

new commit

commit e2f44fca2f8afad8e4d73df6b72111f2f2fd71ad (origin/master, origin/HEAD)
Author: mrgeek <[email protected]>
Date:
Mon May 17 00:16:33 2020 +0530

test

Rozdíl je však v tom, že soubory z commitu (aa400858aab3927e79116941c715749780a59fc9), kam byly přidány 3 soubory, jsou stále v našem pracovním adresáři. Nebyly smazány. To je důvod, proč byste měli používat spíše měkký než tvrdý reset. V měkkém režimu nehrozí ztráta souborů.

$ git ls-files
demo
dummyfile
file1.txt
file2.txt
file3.txt
newfile

Git Revert

V Gitu se příkaz revert používá k provedení operace vrácení, tj. k odvolání některých změn. Je to podobné příkazu reset, ale jediný rozdíl je v tom, že vytvoříte nový commit, abyste se vrátili ke konkrétnímu commitu. Stručně řečeno, je správné říci, že příkaz git revert je commit.

Příkaz git revert při provádění operace vrácení nesmaže žádná data.

Řekněme, že přidáme 3 soubory a provedeme operaci git commit pro příklad návratu.

$ git commit -m 'add 3 files again'
[master 812335d] add 3 files again
3 files changed, 3 insertions(+)
create mode 100644 file1.txt
create mode 100644 file2.txt
create mode 100644 file3.txt

Log zobrazí nový commit.

$ git log --oneline
812335d (HEAD -> master) add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Nyní se chceme vrátit k jednomu z dřívějších commitů, dejme tomu „59c86c9 new commit„. Spustíme následující příkaz.

$ git revert 59c86c9

Tím se otevře soubor, kde najdeme podrobnosti o commitu, ke kterému se pokoušíme vrátit, zde můžeme pojmenovat náš nový commit a následně soubor uložit a zavřít.

Revert "new commit"

This reverts commit 59c86c96a82589bad5ecba7668ad38aa684ab323.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is ahead of 'origin/master' by 4 commits.
# (use "git push" to publish your local commits)
#
# Changes to be committed:
# modified: dummyfile

Po uložení a zavření souboru získáme tento výstup.

$ git revert 59c86c9
[master af72b7a] Revert "new commit"
1 file changed, 1 insertion(+), 1 deletion(-)

Provedením nezbytných změn, na rozdíl od resetu, revert provedl ještě jeden nový commit. Když se znovu podíváme do logu, najdeme nový commit kvůli operaci vrácení.

$ git log --oneline
af72b7a (HEAD -> master) Revert "new commit"
812335d add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Git log bude obsahovat celou historii commitů. Pokud chceme odstranit commity z historie, revert není dobrá volba, ale pokud chceme ponechat změny commitu v historii, je vhodné použít příkaz revert namísto reset.

Git Rebase

V Gitu je rebase způsob, jak přesunout nebo zkombinovat commity z jedné větve do jiné. Jako vývojář bychom neměli vytvářet nové funkce na hlavní větvi. Pracovali bychom ve vlastní větvi (feature branch) a jakmile bychom měli v naší feature větvi několik commitů s novou funkcí, chtěli bychom ji přesunout do hlavní větve.

Příkaz rebase může být někdy těžší pochopit, protože se velmi podobá sloučení. Cílem sloučení i rebase je vzít commity z feature větve a umístit je do hlavní větve nebo jiné větve. Uvažujme graf, který vypadá takto:

Představte si, že pracujete v týmu s dalšími vývojáři. V takovém případě by to mohlo být velmi složité, pokud by mnoho vývojářů pracovalo na různých feature větvích a slučovali by se. Sledování by se stalo nepřehledným.

Zde pomůže rebase. Tentokrát místo slučování provedeme rebase, kde chceme vzít naše dvě revize funkcí a přesunout je do hlavní větve. Rebase vezme všechny commity z feature větve a přesune je na vrchní commit hlavní větve. V podstatě Git duplikuje commity z feature větve do hlavní větve.

Tímto přístupem získáte přehledný lineární graf se všemi commity v řadě.

Usnadňuje sledování, kam byly commity uloženy. Pokud pracujete v týmu s mnoha vývojáři, všechny commity jsou stále v řadě. Proto je velmi snadné sledovat historii, i když na stejném projektu pracuje současně mnoho lidí.

Podívejme se na to prakticky.

Aktuální stav naší hlavní větve je takový. Má 4 commity.

$ git log --oneline
812335d (HEAD -> master) add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Spustíme níže uvedený příkaz pro vytvoření nové větve s názvem feature a přepnutí na ni, tato větev bude vycházet z 2. commitu, tedy 59c86c9

(master)
$ git checkout -b feature 59c86c9
Switched to a new branch 'feature'

Pokud zkontrolujeme log ve větvi feature, má pouze 2 commity z hlavní větve.

(feature)
$ git log --oneline
59c86c9 (HEAD -> feature) new commit
e2f44fc (origin/master, origin/HEAD) test

Vytvoříme feature1 a uložíme ji do feature větve.

(feature)
$ vi feature1.txt

(feature)
$ git add .
The file will have its original line endings in your working directory

(feature)
$ git commit -m 'feature 1'
[feature c639e1b] feature 1
1 file changed, 1 insertion(+)
create mode 100644 feature1.txt

Vytvoříme ještě jednu funkci, tj. feature2, ve větvi feature a commitneme ji.

(feature)
$ vi feature2.txt

(feature)
$ git add .
The file will have its original line endings in your working directory

(feature)
$ git commit -m 'feature 2'
[feature 0f4db49] feature 2
1 file changed, 1 insertion(+)
create mode 100644 feature2.txt

Nyní, když zkontrolujeme log feature větve, má dva nové commity, které jsme provedli výše.

(feature)
$ git log --oneline
0f4db49 (HEAD -> feature) feature 2
c639e1b feature 1
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

Nyní chceme přidat tyto dvě nové funkce do hlavní větve. K tomu použijeme příkaz rebase. Z feature větve provedeme rebase oproti hlavní větvi. Tím znovu ukotvíme feature větev k nejnovějším změnám v hlavní větvi.

(feature)
$ git rebase master
Successfully rebased and updated refs/heads/feature.

Nyní přejdeme a zkontrolujeme hlavní větev.

(feature)
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 3 commits.

(use "git push" to publish your local commits)

A nakonec provedeme rebase hlavní větve oproti naší hlavní větvi. Tím se tyto dva nové commity z feature větve přesunou do hlavní větve.

(master)
$ git rebase feature
Successfully rebased and updated refs/heads/master.

Když nyní zkontrolujeme log v hlavní větvi, uvidíme, že dva commity z naší feature větve byly úspěšně přidány do naší hlavní větve.

(master)
$ git log --oneline
766c996 (HEAD -> master, feature) feature 2
c036a11 feature 1
812335d add 3 files again
0db602e one more commit
59c86c9 new commit
e2f44fc (origin/master, origin/HEAD) test

To je vše o příkazech reset, revert a rebase v Gitu.

Závěr

Toto bylo vše o příkazech reset, revert a rebase v Gitu. Doufám, že vám tento průvodce krok za krokem pomohl. Nyní víte, jak efektivně pracovat s vašimi commity pomocí příkazů uvedených v tomto článku.