Git Reset vs Revert vs Rebase

Photo of author

By maciekx

W tym artykule zgłębimy różnorodne metody manipulowania historią zmian w systemie Git.

Jako programista, z pewnością nieraz staniesz przed sytuacją, w której konieczne będzie cofnięcie się do wcześniejszej wersji projektu, lecz nie będziesz pewien, jak tego dokonać. Nawet jeśli znasz podstawowe komendy Gita, takie jak `reset`, `revert` czy `rebase`, możesz nie być w pełni świadomy różnic między nimi. Rozpocznijmy zatem od wyjaśnienia, czym charakteryzują się te trzy polecenia.

Zastosowanie `git reset`

Polecenie `git reset` to zaawansowane narzędzie służące do wycofywania zmian w projekcie.

Można je traktować jako funkcję umożliwiającą cofanie zmian. Za jego pomocą można swobodnie przemieszczać się pomiędzy różnymi etapami zatwierdzeń (commitami). `Git reset` posiada trzy tryby działania: `–soft`, `–mixed` i `–hard`. Domyślnie używany jest tryb mieszany (`–mixed`). W procesie resetowania zmian w Git wykorzystywane są trzy elementy: `HEAD`, obszar przygotowania zmian (indeks) oraz katalog roboczy.

Katalog roboczy to przestrzeń, w której aktualnie pracujesz, zawierająca Twoje pliki. Za pomocą komendy `git status` można sprawdzić zawartość tego katalogu.

Obszar przygotowania zmian (indeks) to miejsce, w którym Git przechowuje informacje o wprowadzonych modyfikacjach w plikach. Zapisane zmiany są odzwierciedlane w katalogu `.git`. Użyj `git add „nazwa pliku”`, aby umieścić plik w obszarze przygotowania. Ponownie, używając `git status`, możesz sprawdzić, jakie pliki znajdują się w tym obszarze.

Bieżąca gałąź w Git jest oznaczana jako `HEAD`. Wskazuje ona na ostatni commit w obecnej gałęzi. Jest traktowana jako wskaźnik dla każdego odniesienia. Gdy przełączysz się na inną gałąź, `HEAD` również zostanie przeniesiony do nowej gałęzi.

Przejdźmy do wyjaśnienia, jak działa `git reset` w trzech trybach: `hard`, `soft` i `mixed`. Tryb `hard` służy do powrotu do konkretnego zatwierdzenia. Katalog roboczy jest aktualizowany do stanu tego zatwierdzenia, a obszar przygotowania zostaje zresetowany. W przypadku resetu `soft`, zmieniany jest tylko wskaźnik `HEAD` na wskazany commit, a pliki wszystkich commitów przed resetem pozostają w katalogu roboczym i obszarze przygotowania. Tryb mieszany (domyślny) resetuje zarówno wskaźnik, jak i obszar przygotowania.

`Git reset` w trybie twardym (hard)

Celem twardego resetu jest przesunięcie wskaźnika `HEAD` do wskazanego zatwierdzenia. Powoduje on usunięcie wszystkich commitów dokonanych po danym commitcie. Ta komenda zmienia historię zatwierdzeń, kierując wskaźnik na konkretny commit.

W poniższym przykładzie dodam trzy nowe pliki, zatwierdzę je, a następnie wykonam twardy reset.

Jak widać, poniższa komenda wskazuje, że aktualnie nie ma żadnych zmian do zatwierdzenia.

$ 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

Teraz utworzę trzy pliki i dodam do nich treść.

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

Dodaję te pliki do istniejącego repozytorium.

$ git add file*

Po ponownym uruchomieniu komendy `status` zobaczymy nowe pliki, które zostały dodane.

$ 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

Zanim zatwierdzę zmiany, pokażę, że aktualnie w Git mam historię trzech commitów.

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

Teraz zatwierdzam zmiany w repozytorium.

$ 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

Uruchamiając `ls-files`, można zauważyć, że nowe pliki zostały dodane.

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

Kiedy uruchamiam komendę `log` w Git, mam 4 commity, a `HEAD` wskazuje na ostatni z nich.

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

Jeśli ręcznie usunę plik `file1.txt` i wykonam `git status`, zobaczę informację o tym, że zmiana nie jest przygotowana do zatwierdzenia.

$ 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")

Teraz uruchomię twardy reset.

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

Po ponownym sprawdzeniu statusu, okazuje się, że nie ma zmian do zatwierdzenia, a usunięty plik został przywrócony do repozytorium. Cofnięcie nastąpiło, ponieważ po usunięciu pliku nie dokonałem zatwierdzenia, więc twardy reset przywrócił poprzedni stan.

$ 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

Tak prezentuje się log Git po wykonaniu resetu.

$ 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

Celem twardego resetu jest wskazanie konkretnego zatwierdzenia i aktualizacja katalogu roboczego oraz obszaru przygotowania. Pokażę jeszcze jeden przykład. Aktualnie moje commity wizualnie wyglądają następująco:

Teraz uruchomię komendę z `HEAD^`, co oznacza, że chcę zresetować do poprzedniego commita (jeden commit wstecz).

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

Widać, że wskaźnik `HEAD` zmienił się teraz na `0db602e` z `d69950b`.

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

Jeśli sprawdzisz log, commit `d69950b` zniknął, a `HEAD` wskazuje teraz na `0db602e` SHA.

$ 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

Uruchamiając `ls-files`, zobaczymy, że pliki `file1.txt`, `file2.txt` oraz `file3.txt` nie znajdują się już w repozytorium, ponieważ ten commit oraz jego pliki zostały usunięte po wykonaniu twardego resetu.

$ git ls-files
demo
dummyfile
newfile

`Git reset` w trybie miękkim (soft)

Teraz pokażę przykład miękkiego resetu. Załóżmy, że ponownie dodałem 3 pliki, jak opisano wcześniej i zatwierdziłem zmiany. Log Git prezentuje się jak poniżej. Ostatni commit to „soft reset”, a `HEAD` na niego wskazuje.

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

Szczegóły commita w logu można zobaczyć za pomocą poniższej komendy.

$ 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

Teraz, za pomocą miękkiego resetu, chcę powrócić do starszego commita o SHA `0db602e085a4d59cfa9393abac41ff5fd7afcb14`.

W tym celu uruchamiam poniższą komendę. Wystarczy podać więcej niż 6 początkowych znaków SHA; nie jest wymagane pełne SHA.

$ git reset --soft 0db602e085a4

Uruchamiając `git log`, widzę, że `HEAD` został zresetowany do wskazanego commita.

$ 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

Różnica polega jednak na tym, że pliki z commita `aa400858aab3927e79116941c715749780a59fc9`, w którym dodałem 3 pliki, nadal są obecne w katalogu roboczym. Nie zostały usunięte. Dlatego w sytuacji, gdy nie chcesz utracić danych, warto zastosować miękki reset zamiast twardego. W trybie miękkim nie ma ryzyka utraty plików.

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

`Git revert` – Przywracanie zmian

W systemie Git polecenie `revert` służy do wykonywania operacji przywracania, czyli cofania wprowadzonych zmian. Działa podobnie do polecenia `reset`, z tą różnicą, że tworzy nowy commit, aby cofnąć zmiany do konkretnego stanu. Krótko mówiąc, można powiedzieć, że `git revert` jest rodzajem commitu.

Polecenie `git revert` nie usuwa żadnych danych podczas operacji przywracania.

Dodajmy dla przykładu 3 pliki i zatwierdźmy je za pomocą `git commit`.

$ 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 pokaże nowy commit.

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

Chcąc powrócić do wcześniejszego commita, na przykład – `59c86c9 new commit`, uruchamiam poniższą komendę.

$ git revert 59c86c9

Spowoduje to otwarcie pliku, gdzie zobaczysz szczegóły commita, do którego próbujesz powrócić. Możesz nadać nowemu commitowi nazwę, a następnie zapisać i zamknąć plik.

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 zapisaniu i zamknięciu pliku, otrzymamy następujący rezultat.

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

W przeciwieństwie do resetu, revert utworzył nowy commit. Sprawdzenie logu pokaże nowy commit będący wynikiem operacji przywracania.

$ 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

Log Git będzie zawierał pełną historię commitów. Jeśli chcesz usunąć commity z historii, `revert` nie będzie dobrym wyborem. Jeśli jednak zależy Ci na zachowaniu zmian w historii, `revert` jest lepszym rozwiązaniem niż `reset`.

`Git rebase`

W Git `rebase` to metoda przenoszenia lub scalania commitów jednej gałęzi nad inną. W praktyce programiści rzadko tworzą funkcje w gałęzi głównej. Zamiast tego pracują w osobnej gałęzi („gałąź funkcji”), a po dodaniu nowych funkcjonalności, przenoszą je do gałęzi głównej.

Rebase bywa mylący, ponieważ jest bardzo podobny do scalania (`merge`). Celem zarówno `merge`, jak i `rebase` jest przeniesienie commitów z jednej gałęzi do innej. Załóżmy, że mamy wykres commitów, który wygląda następująco:

Wyobraźmy sobie, że pracujesz w zespole z innymi programistami. W takiej sytuacji śledzenie zmian może stać się bardzo skomplikowane, gdy wiele osób pracuje nad różnymi gałęziami i integruje zmiany. Staje się to niejasne.

W takim przypadku z pomocą przychodzi rebase. Zamiast używać `git merge`, wykorzystamy `rebase`, aby przenieść dwa commity z gałęzi funkcji do gałęzi głównej. `Rebase` przenosi wszystkie commity z gałęzi funkcji na szczyt gałęzi głównej. W efekcie Git duplikuje commity z gałęzi funkcji w gałęzi głównej.

Dzięki takiemu podejściu otrzymujemy czysty, liniowy wykres commitów.

Ułatwia to śledzenie zmian. Wszystkie commity znajdują się w jednej linii, co ułatwia orientację w historii zmian, nawet gdy nad projektem pracuje wielu programistów.

Pokażę to w praktyce.

Tak aktualnie wygląda moja gałąź główna. Ma 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

Uruchomię poniższą komendę, aby utworzyć i przejść do nowej gałęzi o nazwie „feature”. Gałąź zostanie utworzona na podstawie drugiego commita `59c86c9`.

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

Sprawdzając log w gałęzi funkcji, widzę tylko 2 commity pochodzące z gałęzi głównej (master).

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

Utworzę funkcję 1 i zatwierdzę ją w gałęzi funkcji.

(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

Dodam jeszcze jedną funkcję, tj. funkcję 2, w gałęzi funkcji i zatwierdzę ją.

(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

Log gałęzi funkcji pokazuje, że ma dwa nowe commity, które właśnie wykonałem.

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

Teraz chcę dodać te dwie nowe funkcje do gałęzi master. W tym celu użyję komendy rebase. Z gałęzi funkcji wykonam rebase w stosunku do gałęzi głównej. Spowoduje to ponowne zakotwiczenie mojej gałęzi funkcji w stosunku do najnowszych zmian.

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

Teraz przejdę i sprawdzę gałąź główną.

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

Na koniec wykonam rebase gałęzi master na moją gałąź funkcji. Spowoduje to pobranie dwóch nowych commitów z gałęzi funkcji i odtworzenie ich na szczycie mojej gałęzi głównej.

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

Sprawdzając log w gałęzi głównej, widzimy, że dwa commity z gałęzi funkcji zostały pomyślnie dodane do mojej gałęzi głównej.

(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 wszystko, jeśli chodzi o polecenia `reset`, `revert` i `rebase` w systemie Git.

Podsumowanie

W niniejszym artykule omówiliśmy polecenia `reset`, `revert` i `rebase` w Git. Mam nadzieję, że ten przewodnik krok po kroku był pomocny. Teraz wiesz, jak modyfikować historię zatwierdzeń, wykorzystując omówione polecenia.


newsblog.pl