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.
| |
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:
| |
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:
| |
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:
| |
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:
| Flaga | Working tree | Index | Historia (HEAD) |
|---|---|---|---|
--soft | bez zmian | bez zmian | przesunięty |
--mixed (domyślna) | bez zmian | zresetowany do celu | przesunięty |
--hard | zresetowany do celu | zresetowany do celu | przesunię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:
| |
--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ł:
| |
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:
| |
Wystaw najpierw więcej zmian do staging area i trafią do tego samego commitu, na tym samym rodzicu:
| |
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.
| |
Git otwiera edytor na opis commitu revert, a potem go zapisuje:
| |
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:
| |
--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:
| |
| |
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 commit | git 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ólna | git revert <commit> |
Zniknęła po reset --hard, którego żałujesz | git 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.