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
| Sytuacja | Polecenie |
|---|---|
| Zmieniłeś śledzony plik i chcesz odrzucić niezapisane zmiany | git restore |
Wykonałeś git add, ale chcesz wycofać plik ze staging area | git restore --staged |
| Chcesz cofnąć lokalny commit i zachować jego zmiany | git 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ę HEAD | git 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
| Polecenie | Zastosowanie |
|---|---|
git restore PLIK | Odrzucenie niezapisanych zmian w śledzonym pliku |
git restore --staged PLIK | Usunięcie zmian pliku ze staging area |
git revert HASH | Utworzenie commita odwracającego zmiany wskazanego commita |
git reset --soft HEAD~1 | Cofnięcie commita z zachowaniem stanu staging area |
git reset --mixed HEAD~1 | Cofnięcie commita i usunięcie jego zmian ze staging area |
git reset --hard HEAD~1 | Cofnięcie commita wraz z przywróceniem plików do wskazanego stanu |
git reflog | Sprawdzenie lokalnej historii zmian położenia referencji |
git diff | Sprawdzenie niezapisanych zmian w katalogu roboczym |
git diff --staged | Sprawdzenie zmian przygotowanych do commita |
git status | Kontrola 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