Zrobisz git reset --hard na commicie, który zespół już pobrał, i twoja historia zaczyna się różnić od jego historii. Zrobisz git revert na literówce, której jeszcze nie wypchnąłeś, i dodajesz do logu bezsensowny commit. Git ma cztery polecenia do cofania zmian: restore, reset, commit --amend i revert. Nie są wymienne. Wybierz niewłaściwe i albo nic się nie stanie, albo stanie się dużo więcej, niż chciałeś.

O wyborze decyduje to, gdzie zmiana aktualnie się znajduje: w working tree, w staging area, w commicie, który masz tylko ty, czy w commicie, który już wypchnąłeś.

Gdzie w Git żyje zmiana: working tree, index, historia

Każde polecenie do cofania zmian działa na jednej lub kilku z trzech warstw:

  • Working tree: pliki na dysku, takie, jakie właśnie edytujesz.
  • Index (staging area): snapshot, który budujesz za pomocą git add, czekający, aż stanie się kolejnym commitem.
  • Historia: commity już zapisane, dostępne z HEAD.

Zmiana zaczyna się w working tree, po git add przechodzi do index, a po git commit trafia do historii. Cofnięcie jej oznacza przejście z powrotem przez te same warstwy. Im dalej zaszła, tym bardziej zdecydowane musi być polecenie.

1
2
working tree  --add-->  index  --commit-->  history
   restore              restore --staged     reset / revert / amend

Jak usunąć niezacommitowane zmiany za pomocą git restore

Edytowałeś config.py, zepsułeś go i chcesz z powrotem wersję, którą pamięta Git:

1
git restore config.py

To nadpisuje plik w working tree wersją z index, która jest taka sama jak HEAD, jeśli niczego nie wystawiłeś do staging area. Edycja przepada. Nie trafia do stasha i żadne polecenie Git jej nie przywróci. Żeby odrzucić wszystkie niezacommitowane zmiany na raz:

1
git restore .

git restore zastępuje starsze git checkout -- <file>. Oba wciąż działają, ale restore istnieje właśnie po to, żeby „oddaj mi ten plik” nie dzieliło polecenia z „przełącz branch”.

Jak wycofać plik ze staging area bez utraty zmian

Zrobiłeś git add na trzech plikach i zauważyłeś, że jeden z nich nie powinien trafić do tego commitu. Sama edycja jest w porządku:

1
git restore --staged migration.sql

migration.sql opuszcza index i wraca do stanu „zmodyfikowany, niewystawiony”. Plik na dysku zostaje nietknięty. To jest to samo, co starsze git reset HEAD -- migration.sql, i to jedyne miejsce, gdzie reset i restore się pokrywają: reset z pathspecem dotyka tylko index, a to jest wszystko, co robi restore --staged.

Jak cofnąć commit za pomocą git reset: soft, mixed i hard

git restore nie dotyka commitów. Do tego potrzebujesz git reset, a flaga decyduje, jak dużo cofa:

FlagaWorking treeIndexHistoria (HEAD)
--softbez zmianbez zmianprzesunięty
--mixed (domyślna)bez zmianzresetowany do celuprzesunięty
--hardzresetowany do celuzresetowany do celuprzesunięty

Wszystkie trzy przesuwają wskaźnik brancha. Różnica jest w tym, ile twojej pracy idzie z nim do tyłu.

--soft przewija commity, ale zostawia ich zmiany wystawione w staging area. To najszybszy sposób, żeby zrobić squash z trzech ostatnich commitów:

1
2
git reset --soft HEAD~3
git commit -m "Add payment retry logic"

--mixed to efekt zwykłego git reset HEAD~1. Przewija commit i zrzuca jego zmiany z powrotem do working tree jako niewystawione edycje, więc możesz je podzielić na mniejsze commity albo część z nich odrzucić.

--hard odrzuca cały diff. Working tree wygląda tak, jakby commit nigdy nie istniał:

1
git reset --hard HEAD~1

Nie ma żadnego potwierdzenia. Commity, przez które przeszedłeś, wciąż można znaleźć przez git reflog (patrz niżej), ale wszystkie niezacommitowane zmiany, które były w working tree w tym momencie, przepadają na zawsze. Sprawdź najpierw git status. I nigdy nie rób tego na commitach, które ktoś inny już pobrał: przesunięcie brancha do tyłu na wspólnym branchu sprawia, że twoja historia i jego zaczynają się różnić co do tego, co się wydarzyło — ten sam problem, o którym mówi złota zasada rebase.

Jak poprawić ostatni commit za pomocą git commit –amend

Jeśli błąd jest w najnowszym commicie, reset to przesada. --amend zamienia commit na szczycie, zamiast przewijać się przed niego:

1
git commit --amend -m "Fix payment retry logic"

Wystaw najpierw więcej zmian do staging area i trafią do tego samego commitu, na tym samym rodzicu:

1
2
git add retry.py
git commit --amend --no-edit

Poprawiony commit to nowy obiekt z nowym hashem, tak jak przy rebase. Dlatego zasada jest taka sama jak dla rebase: poprawiaj tylko to, czego jeszcze nie wypchnąłeś. Zrób amend commitu, który już jest na wspólnym branchu, i każdy, kto go pobrał, musi godzić dwa commity, które rzekomo są tą samą zmianą.

Jak cofnąć wypchnięty commit za pomocą git revert

Gdy commit jest już na wspólnym branchu, reset i --amend wypadają z gry. Przepisywanie historii, którą inni już pobrali, to przepis na rozjechane branche i zduplikowane commity. git revert idzie inną drogą: zostawia zły commit na miejscu i dodaje nowy, który robi coś odwrotnego.

1
git revert a1b2c3d

Git otwiera edytor na opis commitu revert, a potem go zapisuje:

1
2
3
Revert "Add aggressive cache invalidation"

This reverts commit a1b2c3d4e5f6789a1b2c3d4e5f6789a1b2c3d4.

Historia trzyma teraz oryginalny commit i jego odwrotność. Nic nie zostało przepisane, więc zwykły git pull na każdym innym klonie pobiera tę poprawkę. Żeby cofnąć cały zakres jako jeden commit:

1
2
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert the last three deploy changes"

--no-commit (-n) nakłada odwrotność każdego commitu na working tree i index bez commitowania, więc trzy reverty trafiają do jednego commitu, a nie trzech. Jeśli revert konfliktuje ze zmianami zrobionymi od tamtej pory, Git zatrzymuje się tak samo jak w środku merge: rozwiąż znaczniki konfliktu, zrób git add na pliku, a potem git revert --continue.

Czyli „cofnięcie pusha” znaczy: zrób revert commitu, a potem wypchnij ten revert. Nie wymuszaj pusha przepisanej historii na branch, z którego inni pobierają.

Jak odzyskać dane po nieudanym reset za pomocą git reflog

Po reset --hard commity znikają z git log. Wciąż istnieją. git reflog zapisuje każdą pozycję, na którą HEAD wskazywał na twojej maszynie, łącznie z tą tuż przed resetem:

1
git reflog
1
2
f3a9c21 HEAD@{0}: reset: moving to HEAD~1
7d4e1b8 HEAD@{1}: commit: Add payment retry logic

git reset --hard HEAD@{1} wraca cię na 7d4e1b8. Jak długo żyją wpisy reflog i czemu reflog działa tylko lokalnie, opisuje sekcja o odzyskiwaniu w Git Rebase vs Merge. Działa tak samo, niezależnie od tego, które polecenie przesunęło HEAD.

Reflog wraca cię do miejsca, w którym byłeś. Nie powie ci, który starszy commit był ostatnim dobrym. Do tego git bisect zwęża setki commitów do tego jednego, który wprowadził buga.

Git reset vs revert vs restore: co i kiedy wybrać

Zmiana jest…Polecenie
Edytowana, niewystawiona do staging area, i chcesz ją usunąćgit restore <file>
Wystawiona do staging area, i chcesz ją z niej zdjąć (edycja zostaje)git restore --staged <file>
Zacommitowana, niewypchnięta, i to jest ostatni commitgit commit --amend
Zacommitowana, niewypchnięta, i chcesz przewinąć się dalej w tyłgit reset --soft / --mixed / --hard HEAD~n
Zacommitowana i już wypchnięta albo wspólnagit revert <commit>
Zniknęła po reset --hard, którego żałujeszgit reflog + git reset --hard HEAD@{n}

Dla wszystkiego, co już jest zacommitowane, jedno pytanie wybiera wiersz: czy ktoś inny widział już ten commit? Jeśli nikt inny tego nie widział, bierzesz reset albo amend i idziesz dalej. Jeśli widział, bierzesz revert. Kosztuje jeden dodatkowy commit w logu, i nikt z zespołu nie musi rozplątywać historii, która już nie zgadza się z jego.

Powiązane artykuły