Cofanie zmian w Git – restore, reset, revert i reflog

by Patryk

Podczas codziennej pracy z gitem często zdarza się, że zmienimy niewłaściwy plik, przypadkowo wykonamy git add lub zapiszemy commit, którego nie chcieliśmy. Dobra wiadomość jest taka, że git pozwala cofnąć każdą z tych operacji, ale do każdej z nich używamy innego polecenia.

W poprzednich lekcjach nauczyliśmy się tworzyć commity, pracować na gałęziach i rozwiązywać konflikty podczas merge. Teraz wrócimy do naszego repozytorium devops-lab i sprawdzimy, jak cofać zmiany na różnych etapach pracy. Przećwiczymy git restore, git reset i git revert. Zajrzymy też do git reflog, który może pomóc, gdy przez pomyłkę cofniemy niewłaściwy commit.

Cofanie zmian w Git. Od czego zacząć?


Zanim użyjesz polecenia do cofania zmian, sprawdź, na jakim etapie pracy znajduje się plik lub commit.

Przypomnijmy sobie trzy miejsca, które poznaliśmy w pierwszej lekcji Gita:

  • Working Directory – pliki, które edytujemy w katalogu projektu.
  • Staging Area – zmiany przygotowane do zapisania po wydaniu polecenia git add.
  • Repozytorium lokalne – commity, które utworzyliśmy za pomocą git commit.

Do tego dochodzi repozytorium zdalne na GitHubie. Jeżeli wykonaliśmy już git push, nasz commit może być dostępny także dla innych osób.

Od tego zależy wybór polecenia. Każde z nich przyda się w codziennej pracy, ważne, żeby wiedzieć, co każde z nich zmieni w repozytorium

SytuacjaPolecenie
Zmieniłeś śledzony plik i chcesz odrzucić niezapisane zmianygit restore
Wykonałeś git add, ale chcesz wycofać plik ze staging areagit restore --staged
Chcesz cofnąć lokalny commit i zachować jego zmianygit reset --soft lub git reset --mixed
Chcesz odwrócić zmiany zapisane w commicie, zachowując historięgit revert
Chcesz sprawdzić, gdzie wcześniej znajdował się HEADgit reflog

Przygotowanie laboratorium


Wracamy na naszą maszynę Debian, do katalogu z projektem:

cd ~/devops-lab

W poprzedniej lekcji pracowaliśmy na plikach configs/web.conf i configs/service.conf. Wywoływaliśmy konflikty między gałęziami, a na koniec anulowaliśmy jeden merge przez git merge --abort.

Sprawdźmy stan repozytorium, powinniśmy znajdować się na gałęzi master.

git status
git branch --show-current

Osobna gałąź do ćwiczeń

Nie będziemy cofać commitów bezpośrednio na master. Utworzymy gałąź przeznaczoną do eksperymentów:

git switch -c lab/undo

Sprawdź:

git branch --show-current
git status

Aktualną gałęzią powinno być lab/undo. Wszystkie kolejne ćwiczenia wykonujemy właśnie tutaj. Nie będziemy wysyłać tej gałęzi na GitHuba.

Git restore – cofanie zmian w pliku


Zacznijmy od najprostszej sytuacji, w której pracujemy nad dokumentacją projektu i przypadkowo dopisujemy do README.md linię, której nie chcemy zachować. Nie wykonaliśmy jeszcze git add ani git commit.

Dodajmy taką zmianę:

echo "THIS CHANGE IS A MISTAKE" >> README.md

Sprawdź stan repozytorium:

git status

Git powinien pokazać README.md jako zmodyfikowany plik, którego zmian nie dodaliśmy do staging area. Zobaczmy, co się zmieniło:

git diff

W wyniku znajdziemy między innymi:

+THIS CHANGE IS A MISTAKE

Ta linia znajduje się tylko w naszym katalogu roboczym.

Wiemy, że jej nie potrzebujemy, więc możemy przywrócić wcześniejszą zawartość pliku:

git restore README.md

Sprawdźmy rezultat:

cat README.md
git status

Dopisanej linii już nie ma. Git przywrócił plik do wersji zapisanej w staging area. Ponieważ w tym ćwiczeniu nie mieliśmy żadnych zmian przygotowanych przez git add, otrzymaliśmy zawartość z ostatniego commita.

Na co uważać przy git restore?

Polecenie to może bezpowrotnie usunąć niezapisane zmiany w śledzonym pliku.

Jeżeli przez godzinę poprawiałeś skrypt i wykonasz:

git restore scripts/monitoring.sh

Git zastąpi jego niezapisane zmiany wersją ze staging area. Nie poprosi wcześniej o potwierdzenie. Dlatego przed użyciem git restore warto sprawdzić:

git diff

Jeżeli zmiana jest potrzebna, pozostaw ją w pliku albo zapisz w odpowiedni sposób. Jeżeli chcesz ją odrzucić, dopiero wtedy wykonaj git restore.

Git restore –staged – cofanie git add


Teraz wykonamy podobne ćwiczenie, ale tym razem dodamy zmianę do staging area.

Dopisz nową linię do README.md:

echo "Temporary test line" >> README.md

Następnie:

git add README.md

Sprawdź status:

git status

Plik powinien znajdować się w sekcji zmian przygotowanych do commita.

Możemy zobaczyć ich zawartość:

git diff --staged

Wśród różnic pojawi się:

+Temporary test line

Załóżmy, że wykonaliśmy git add zbyt wcześnie. Chcemy usunąć zmianę ze staging area, ale nadal nad nią pracować. Do tego służy:

git restore --staged README.md

Teraz ponownie sprawdź:

git status
git diff

Co się zmieniło?

README.md nadal zawiera linię Temporary test line, ale nie jest już przygotowany do commita.

Właśnie cofnęliśmy wcześniejsze git add.

To ważna różnica:

  • **git restore --staged README.md** usuwa zmianę ze staging area i pozostawia ją w pliku.
  • **git restore README.md** odrzuca niezapisane zmiany w katalogu roboczym, przywracając wersję ze staging area.

Nie potrzebujemy już testowej linii w README. Skoro usunęliśmy ją ze staging area, możemy teraz odrzucić samą zmianę:

git restore README.md

Sprawdź:

git status

Repozytorium powinno być czyste. W ten sposób przećwiczyliśmy dwa różne przypadki: wycofanie pliku ze staging area i usunięcie niezapisanej zmiany.

Git revert – jak cofnąć błędny commit?


Do tej pory cofaliśmy zmiany, których jeszcze nie zapisaliśmy w historii co było bardzo prostą sytuacją. Teraz utworzymy commit, którego nie chcemy zachować w aktualnej konfiguracji.

W poprzedniej lekcji pracowaliśmy z katalogiem configs. Dodajmy do niego nowy plik:

echo "debug=true" > configs/debug.conf

Następnie zapiszmy zmianę:

git add configs/debug.conf
git commit -m "Enable debug mode"

Sprawdź ostatnie commity:

git log --oneline -3

Na początku historii zobaczysz:

a1b2c3d Enable debug mode

Załóżmy, że tryb debugowania został włączony przez pomyłkę. Nie chcemy, aby plik configs/debug.conf znajdował się w projekcie. Mamy już jednak commit zapisany w historii.

Cofanie commita przez git revert

Aby cofnąć utworzony commit należy wykonać poniższe polecenie:

git revert --no-edit HEAD

HEAD wskazuje aktualny commit, czyli w naszym przypadku Enable debug mode.

Opcja --no-edit pozwala Gitowi użyć automatycznie przygotowanej wiadomości nowego commita, bez otwierania edytora. Możesz też spotkać krótszą wersję git revert HEAD, która przy uruchomieniu z terminala zwykle otwiera edytor wiadomości.

Po wykonaniu polecenia Git utworzy nowy commit, który odwróci zmiany z poprzedniego. Sprawdź historię:

git log --oneline -4

Powinieneś zobaczyć coś podobnego:

de78a89 Revert "Enable debug mode"
59173aa Enable debug mode

Oba commity pozostały w historii. Pierwszy dodał plik configs/debug.conf, a drugi usunął zmianę wprowadzoną przez pierwszy.

Sprawdź katalog:

ls configs/
git status

Pliku debug.conf nie powinno już być.

Dlaczego revert nie usuwa starego commita?

Commit C nadal istnieje. Git dopisał commit D, którego zmiany odwracają efekt C.

Dzięki temu możemy sprawdzić, kiedy wprowadziliśmy błędną konfigurację i kiedy ją cofnęliśmy.

To szczególnie przydatne w repozytorium współdzielonym z innymi osobami. Jeżeli commit został już wysłany na GitHuba, zwykle lepiej dodać commit odwracający jego zmiany niż przepisywać historię, którą ktoś mógł pobrać.

W takim przypadku możemy wskazać konkretny commit:

git revert HASH_COMMITA

A po jego odwróceniu wysłać nowy commit do repozytorium zdalnego:

git push

W naszym ćwiczeniu pozostajemy na lokalnej gałęzi lab/undo, więc niczego nie wysyłamy.

Git reset – cofanie lokalnych commitów


git revert dopisuje nowy commit. git reset działa inaczej. Może przesunąć bieżącą gałąź do wcześniejszego miejsca w historii.

Załóżmy, że mamy trzy kolejne commity:

A---B---C
        ↑
       HEAD

Jeśli cofniemy gałąź o jeden commit, zacznie ona wskazywać B:

A---B     C
    ↑
   HEAD

Commit C nie należy już do bieżącej historii tej gałęzi. To jednak tylko część działania git reset. Musimy jeszcze zdecydować, co stanie się ze zmianami zapisanymi w cofanym commicie.

Git udostępnia trzy podstawowe tryby:

  • --soft – zachowuje zmiany w staging area,
  • --mixed – zachowuje zmiany w katalogu roboczym, ale usuwa je ze staging area,
  • --hard – przywraca staging area i katalog roboczy do wskazanego commita.

Przećwiczymy je po kolei, na tym samym pliku.

Przygotowanie testowej historii

Będąc na gałęzi lab/undo utwórz plik:

echo "one" > reset-test.txt

Zapisz pierwszy commit:

git add reset-test.txt
git commit -m "Add reset test file"

Dodaj drugi wiersz:

echo "two" >> reset-test.txt
git add reset-test.txt
git commit -m "Add second reset test line"

I trzeci:

echo "three" >> reset-test.txt
git add reset-test.txt
git commit -m "Add third reset test line"

Sprawdź zawartość pliku:

cat reset-test.txt

Wynik:

one
two
three

Wyświetl historię:

git log --oneline -5

Na początku powinny znaleźć się trzy nowe commity:

265b329 Add third reset test line
5a54030 Add second reset test line
44c6962 Add reset test file

Teraz możemy sprawdzić różne sposoby cofania ostatniego commita.

Git reset –soft – cofnięcie commita z zachowaniem zmian w staging area


Załóżmy, że ostatni commit utworzyliśmy zbyt wcześnie. Chcemy go cofnąć, ale zachować wszystkie jego zmiany przygotowane do ponownego zapisania. Służy do tego polecenie:

git reset --soft HEAD~1

HEAD~1 oznacza pierwszy commit wstecz od aktualnego HEAD, czyli w naszym przypadku Add second reset test line.

Sprawdź:

git log --oneline -4
git status
git diff --staged

Najnowszym commitem będzie teraz Add second reset test line.

Wciąż jednak zachowane zostaną zmiany w staging area. Po otworzeniu pliku reset-test.txt nadal zobaczysz:

one
two
three

Linia three nie zniknęła. Git pozostawił ją również w staging area. Dlatego git status pokaże zmianę przygotowaną do commita.

W naszym ćwiczeniu **git reset --soft** cofnął ostatni commit, zachowując jego zawartość w staging area.

Możemy od razu zapisać ją ponownie:

git commit -m "Add third reset test line"

Po wydaniu polecenia git log zobaczysz, że mamy znów trzy commity testowe.

Warto wiedzieć, że nowy commit otrzyma inny hash niż ten, który cofnęliśmy. Nawet jeśli zawartość pliku pozostanie taka sama, tworzymy nowy zapis w historii.

Git reset –mixed – cofnięcie commita bez usuwania zmian z pliku


Teraz sprawdzimy drugi tryb:

git reset --mixed HEAD~1

Polecenie ponownie cofnie nas do commita Add second reset test line.

Tym razem Git pozostawi linię three w pliku, ale usunie ją ze staging area.

Sprawdź:

git status
git diff
git diff --staged

Powinieneś zauważyć, że git diff pokazuje dopisaną linię:

+three

Natomiast git diff --staged nie pokaże żadnych zmian. To oznacza, że mamy zwykłą, niezapisaną zmianę w katalogu roboczym.

**--mixed** przydaje się wtedy, gdy chcesz cofnąć lokalny commit i jeszcze popracować nad jego zawartością. Pliki zachowują zmiany, ale musisz ponownie wybrać to, co trafi do kolejnego commita.

Możesz również spotkać zapis:

git reset HEAD~1

Działa tak samo jak git reset --mixed HEAD~1, ponieważ --mixed jest domyślnym trybem tego polecenia.

Przywróćmy testowy commit

Przed kolejnym ćwiczeniem zapiszmy trzecią linię ponownie:

git add reset-test.txt
git commit -m "Add third reset test line"

Sprawdź:

git status
cat reset-test.txt

Repozytorium powinno być czyste, a plik powinien zawierać trzy wiersze.

Git reset –hard – cofnięcie commita i odrzucenie zmian


Został nam trzeci tryb, przy którym trzeba zachować szczególną ostrożność.

Wykonaj:

git reset --hard HEAD~1

Używamy go wyłącznie na naszej testowej gałęzi lab/undo, z czystym drzewem roboczym.

Sprawdź zawartość pliku:

cat reset-test.txt

Tym razem zobaczysz, że linia three zniknęła z pliku:

one
two

Sprawdź również:

git status
git log --oneline -5

Gałąź wskazuje teraz commit Add second reset test line. Git dopasował do niego także zawartość staging area i katalogu roboczego.

Dlatego git reset --hard jest niebezpieczny. Jeżeli masz niezapisane zmiany w śledzonych plikach, możesz je w ten sposób utracić. W pewnych sytuacjach Git może też usunąć nieśledzone pliki, jeśli przeszkadzają w odtworzeniu wskazanego stanu.

Nie używaj --hard jako uniwersalnego sposobu na posprzątanie repozytorium. Najpierw sprawdź git status i upewnij się, które zmiany zostaną odrzucone.

Reset soft, mixed i hard – porównanie


Po trzech ćwiczeniach różnica powinna być już widoczna.

Tabela opisuje sytuację z naszego ćwiczenia, w której przed resetem repozytorium było czyste. Przy istniejących wcześniej niezapisanych zmianach trzeba dodatkowo uwzględnić ich stan.

Najprościej zapamiętać to na przykładzie ostatniego commita:

  • Soft: cofasz commit, ale jego zmiany są gotowe do ponownego commita.
  • Mixed: cofasz commit, a zmiany pozostają w plikach do dalszej pracy.
  • Hard: cofasz commit i przywracasz pliki do wcześniejszego stanu.

Dlaczego reset po git push może sprawić problemy?

Załóżmy, że na GitHubie mamy historię:

A---B---C
        ↑
    origin/master

Ktoś z zespołu pobrał już commit C. Tymczasem my cofamy lokalną gałąź:

git reset --hard HEAD~1

Lokalnie otrzymujemy:

A---B
    ↑
  master

Gdy spróbujemy wykonać zwykły git push, Git zazwyczaj odrzuci taką operację. Próbowalibyśmy przesunąć zdalną gałąź wstecz, usuwając z jej historii commit C. Taką zmianę można wymusić, ale przepisanie historii współdzielonej gałęzi może utrudnić pracę osobom, które pobrały już wcześniejsze commity.

Dlatego podczas pracy zespołowej nie wykonujemy git push --force na master bez wyraźnego powodu i ustalonych zasad. W przypadku błędnego commita na współdzielonej gałęzi możemy zamiast tego użyć:

git revert HASH_COMMITA

A następnie:

git push

Dzięki temu historia zostanie rozszerzona o nowy commit, zamiast zastępować wcześniejsze zapisy.

Git reflog – jak znaleźć commit po przypadkowym reset?


W poprzednim ćwiczeniu wykonaliśmy:

git reset --hard HEAD~1

Ostatni commit przestał być widoczny na początku historii naszej gałęzi. Nie oznacza to jednak, że Git natychmiast usunął wszystkie informacje o tym commicie. Możemy to sprawdzić poniższym poleceniem:

git reflog -10

Wynik może wyglądać podobnie do tego:

abc1234 HEAD@{0}: reset: moving to HEAD~1
def5678 HEAD@{1}: commit: Add third reset test line
abc1234 HEAD@{2}: reset: moving to HEAD~1

Widzimy zapis operacji reset, a pod nim wcześniejsze położenie HEAD. git reflog przechowuje lokalną historię zmian położenia referencji, między innymi podczas tworzenia commitów, przełączania gałęzi czy wykonywania reset.

Możemy dzięki temu odnaleźć hash commita, który przestał być dostępny przez aktualną nazwę gałęzi.

Jeżeli chcesz zobaczyć zawartość takiego commita, wykonaj:

git show 5a54030

Zastąp 5a54030 hashem z własnego wyniku git reflog.

Git wyświetli informacje o wskazanym commicie i wprowadzone przez niego zmiany. To przydatne, gdy przez pomyłkę przesuniesz gałąź zbyt daleko i chcesz ustalić, gdzie znajdowała się wcześniej.

Pamiętaj jednak, że reflog nie jest kopią zapasową. Jego wpisy są lokalne i mogą z czasem wygasnąć. Nie przywróci też automatycznie każdej niezapisanej zmiany, którą usunęliśmy z katalogu roboczego.

Git log a git reflog

Oba polecenia pokazują informacje związane z historią, ale odpowiadają na inne pytania.

git log pozwala przejrzeć commity w historii projektu:

git log --oneline -5

git reflog pokazuje lokalny zapis tego, jak przemieszczał się HEAD i inne referencje:

git reflog -5

Po wykonaniu git reset commit może zniknąć ze zwykłego git log aktualnej gałęzi, ale nadal być widoczny w git reflog.

Dlatego kiedy przypadkowo cofniesz niewłaściwy commit, warto sprawdzić reflog, zanim wykonasz kolejne operacje.

Jak dobrać polecenie do sytuacji?


Po tych ćwiczeniach możemy wrócić do problemów, które pojawiają się podczas normalnej pracy.

Zmodyfikowałeś README.md, ale nie wykonałeś git add i chcesz wyrzucić zmianę?

git restore README.md

Przypadkowo dodałeś plik do staging area, ale chcesz zachować jego zawartość?

git restore --staged README.md

Utworzyłeś lokalny commit i chcesz poprawić go bez ponownego wybierania zmian?

git reset --soft HEAD~1

Chcesz cofnąć lokalny commit i wrócić do edytowania jego zmian?

git reset --mixed HEAD~1

Błędny commit został już opublikowany we współdzielonej historii?

git revert HASH_COMMITA

A jeśli wykonałeś reset i nie pamiętasz hasha poprzedniego commita:

git reflog

Zanim skopiujesz którąkolwiek z tych komend, sprawdź git status. Przy cofaniu zmian ważne jest, żeby wiedzieć, co masz zapisane, a co istnieje tylko w katalogu roboczym.

Zadanie praktyczne – konfiguracja bazy danych


Na zakończenie wykonamy krótkie ćwiczenie z plikiem konfiguracyjnym. Będąc na lab/undo Utwórz plik configs/database.conf:

cat > configs/database.conf <<'EOF'
host=localhost
port=5432
pool=10
EOF

Dodaj go do repozytorium:

git add configs/database.conf
git commit -m "Add database configuration"

Teraz spróbuj wykonać kolejne kroki samodzielnie.

1. Cofanie zmiany ze staging area

Zmień w pliku:

pool=10

na:

pool=1000

Dodaj plik przez git add, ale nie twórz commita.

Następnie usuń zmianę ze staging area tak, aby pozostała w pliku. Sprawdź to poleceniem git diff.

Na końcu odrzuć tę zmianę i przywróć wartość pool=10.

2. Cofanie lokalnego commita

Dopisz do konfiguracji:

timeout=30

Utwórz commit:

git add configs/database.conf
git commit -m "Add database timeout"

Teraz cofnij ostatni commit tak, aby linia timeout=30 nadal znajdowała się w staging area.

Sprawdź stan repozytorium, a następnie ponownie zapisz zmianę w commicie.

3. Odwrócenie błędnego commita

Dodaj do pliku jeszcze jeden parametr:

debug=true

Zapisz go w osobnym commicie. Następnie odwróć ten commit za pomocą git revert.

Na koniec wyświetl historię:

git log --oneline --decorate --graph --all -15

Zwróć uwagę na różnicę między cofnięciem commita przez reset a odwróceniem go przez revert.

Po zakończeniu sprawdź:

git status
git reflog -10
cat configs/database.conf

Plik powinien zawierać konfigurację z pool=10 i timeout=30, bez testowego debug=true.

Najważniejsze polecenia do cofania zmian w Git

PolecenieZastosowanie
git restore PLIKOdrzucenie niezapisanych zmian w śledzonym pliku
git restore --staged PLIKUsunięcie zmian pliku ze staging area
git revert HASHUtworzenie commita odwracającego zmiany wskazanego commita
git reset --soft HEAD~1Cofnięcie commita z zachowaniem stanu staging area
git reset --mixed HEAD~1Cofnięcie commita i usunięcie jego zmian ze staging area
git reset --hard HEAD~1Cofnięcie commita wraz z przywróceniem plików do wskazanego stanu
git reflogSprawdzenie lokalnej historii zmian położenia referencji
git diffSprawdzenie niezapisanych zmian w katalogu roboczym
git diff --stagedSprawdzenie zmian przygotowanych do commita
git statusKontrola aktualnego stanu repozytorium

Następny krok: Potrafisz już przygotowywać commity, łączyć gałęzie, rozwiązywać konflikty i cofać błędne zmiany. W kolejnej lekcji przejdziemy przez pracę zespołową na GitHubie. Utworzymy osobną gałąź, wyślemy ją do repozytorium zdalnego i przygotujemy Pull Request. Sprawdzimy też, jak wygląda przegląd zmian przed włączeniem ich do głównej gałęzi.

Kolejna lekcja: Praca zespołowa i code review.

← Poprzednia lekcja: Konflikty w Git – rozwiązywanie merge conflict · Wróć do spisu kursu

Może Cię również zainteresować

Dodaj komentarz