Jak cofnąć wiele zatwierdzeń git?

982

Mam repozytorium git, które wygląda następująco:

A -> B -> C -> D -> HEAD

Chcę, aby głowa oddziału wskazywała na A, tzn. Chcę, aby B, C, D i HEAD zniknęły i chcę, aby głowa była synonimem A.

Wygląda na to, że mogę albo spróbować dokonać zmiany bazy (nie dotyczy, ponieważ wprowadziłem zmiany pomiędzy), albo cofnąć. Ale jak cofnąć wiele zatwierdzeń? Czy cofam pojedynczo? Czy kolejność jest ważna?

Rachunek
źródło
3
Jeśli chcesz po prostu zresetować pilota, możesz go dowolnie przerobić! Ale użyjmy czwartego zatwierdzenia temu: git push -f HEAD~4:master(zakładając, że zdalna gałąź jest masterem). Tak, możesz wcisnąć dowolne takie zatwierdzenie.
u0b34a0f6ae,
21
Jeśli ludzie wyciągnęli, musisz dokonać zatwierdzenia, które cofnie zmiany za pomocą git revert.
Jakub Narębski
1
Użyj git show HEAD ~ 4, aby upewnić się, że przesuwasz się w prawo do pilota
Mâtt Frëëman,
5
„Czy zamówienie jest ważne?” Tak, jeśli zatwierdzenia dotyczą tych samych wierszy w tych samych plikach. Następnie powinieneś zacząć cofać ostatnie zatwierdzenie i wrócić do poprzedniej wersji.
avandeursen

Odpowiedzi:

1333

Rozszerzanie tego, co napisałem w komentarzu

Ogólna zasada mówi, że nie powinieneś przepisywać (zmieniać) historii, którą opublikowałeś, ponieważ ktoś mógł na tym oprzeć swoją pracę. Jeśli przepiszesz (zmienisz) historię, będziesz miał problemy z scaleniem ich zmian i aktualizacją.

Dlatego rozwiązaniem jest utworzenie nowego zatwierdzenia, które przywróci zmiany , których chcesz się pozbyć. Możesz to zrobić za pomocą polecenia git revert .

Masz następującą sytuację:

A <- B <- C <- D <- master <- HEAD

(strzałki tutaj odnoszą się do kierunku wskaźnika: odniesienie „nadrzędne” w przypadku zatwierdzeń, górne zatwierdzenie w przypadku główki gałęzi (odniesienie do gałęzi) i nazwa gałęzi w przypadku odniesienia HEAD).

Co musisz stworzyć, to:

A <- B <- C <- D <- [(BCD) ^ - 1] <- master <- HEAD

gdzie „[(BCD) ^ - 1]” oznacza zatwierdzenie, które odwraca zmiany w zatwierdzeniach B, C, D. Matematyka mówi nam, że (BCD) ^ - 1 = D ^ -1 C ^ -1 B ^ -1, więc wymaganą sytuację można uzyskać za pomocą następujących poleceń:

$ git revert --no-commit D
$ git revert --no-commit C
$ git revert --no-commit B
$ git commit -m "the commit message"

Alternatywnym rozwiązaniem byłoby pobranie zawartości zatwierdzenia A i zatwierdzenie tego stanu:

$ git checkout -f A -- .
$ git commit -a

Wtedy miałbyś następującą sytuację:

A <- B <- C <- D <- A '<- master <- HEAD

Zatwierdzenie A 'ma taką samą treść jak zatwierdzenie A, ale jest innym zatwierdzeniem (komunikat zatwierdzenia, rodzice, data zatwierdzenia).

Rozwiązanie Jeff Ferland, zmodyfikowany przez Charles Bailey opiera się na tej samej idei, ale korzysta git resetu :

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit
Jakub Narębski
źródło
40
Jeśli dodałeś pliki w B, C lub D. Nie zostaną git checkout -f A -- .one usunięte, będziesz musiał to zrobić ręcznie. Zastosowałem tę strategię teraz, dzięki Jakubowi
oma
18
Te rozwiązania nie są równoważne. Pierwszy nie usuwa nowo utworzonych plików.
m33lky
10
@Jerry: git checkout foomoże oznaczać gałąź kasy foo(przejście do gałęzi) lub plik kasy foo (z indeksu). --służy do jednoznaczności, np. git checkout -- foozawsze dotyczy pliku.
Jakub Narębski
87
Oprócz świetnej odpowiedzi. Ten skrót działa dla mniegit revert --no-commit D C B
welldan97
9
@ welldan97: Dzięki za komentarz. Podczas pisania tej odpowiedzi git revertnie akceptowałem wielu zatwierdzeń; to całkiem nowy dodatek.
Jakub Narębski
247

Czysty sposób, który uważam za użyteczny

git revert --no-commit HEAD~3..

To polecenie przywraca ostatnie 3 zatwierdzenia tylko jednym zatwierdzeniem.

Nie przepisuje też historii.

głębokie nurkowanie
źródło
16
Oto prosta i najlepsza odpowiedź
Julien Deniau,
3
@JohnLittle zmienia etapy. git commitstamtąd faktycznie wykona zatwierdzenie.
x1a4
14
To nie zadziała, jeśli niektóre zatwierdzenia są zatwierdzeniami scalania.
MegaManX,
5
Co robią dwie kropki na końcu?
kardamon
5
@cardamom Określają zakres. HEAD~3..jest taki sam jakHEAD~3..HEAD
Toine H,
238

W tym celu wystarczy użyć polecenia revert , określając zakres zatwierdzeń, które chcesz przywrócić.

Biorąc pod uwagę twój przykład, musisz to zrobić (zakładając, że jesteś w gałęzi „master”):

git revert master~3..master

Spowoduje to utworzenie nowego zatwierdzenia w Twoim lokalnym z odwrotnym zatwierdzeniem B, C i D (co oznacza, że ​​cofnie zmiany wprowadzone przez te zatwierdzenia):

A <- B <- C <- D <- BCD' <- HEAD
Zwycięzca
źródło
129
git revert --no-commit HEAD~2..jest nieco bardziej idiomatycznym sposobem na zrobienie tego. Jeśli jesteś w gałęzi master, nie musisz ponownie określać master. Ta --no-commitopcja pozwala git spróbować cofnąć wszystkie zatwierdzenia naraz, zamiast zaśmiecać historię wieloma revert commit ...wiadomościami (zakładając, że tego właśnie chcesz).
kubi
6
@Victor Naprawiłem twój zakres zatwierdzeń. Początek zakresu jest wyłączny, co oznacza, że ​​nie jest uwzględniony. Więc jeśli chcesz cofnąć ostatnie 3 zatwierdzenia, musisz rozpocząć zakres od rodzica trzeciego zatwierdzenia, tj master~3.
2
@kubi nie ma możliwości włączenia SHA do komunikatu zatwierdzenia za pomocą pojedynczego zatwierdzenia (twoja metoda, ale bez konieczności ręcznego wprowadzania zatwierdzeń, które zostały cofnięte)?
Chris S
@ChrisS Moją pierwszą myślą byłoby nieużywanie --no-commit(więc dostajesz osobne zatwierdzenie dla każdego przywrócenia), a następnie zmiażdżyłeś je wszystkie razem w interaktywnej bazie. Połączony komunikat zatwierdzenia będzie zawierał wszystkie SHA i możesz je ustawić w dowolny sposób, używając ulubionego edytora komunikatów zatwierdzania.
Radon Rosborough
71

Podobnie jak odpowiedź Jakuba, pozwala to łatwo wybrać kolejne zatwierdzenia do cofnięcia.

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'
konyak
źródło
9
Twoje rozwiązanie działało dla mnie dobrze, ale z niewielką modyfikacją. Jeśli mamy ten przypadek Z -> A -> B -> C -> D -> HEAD i jeśli chciałbym powrócić do stanu A, to dziwnie musiałbym wykonać polecenie git revert - no-commit Z .. GŁOWA
Bogdan
3
Zgadzam się z @Bogdan, zakres przywracania jest taki: SHA_TO_REVERT_TO..HEAD
Vadym Tyemirov
11
Zakres jest nieprawidłowy. Powinno być B^..HEAD, w przeciwnym razie B jest wykluczone.
tessus
3
Zgadzam się z @tessus, więc właściwą rzeczą byłoby: git revert --no-commit B^..HEADlubgit revert --no-commit A..HEAD
Yoho
64
git reset --hard a
git reset --mixed d
git commit

To zadziała dla wszystkich jednocześnie. Daj dobry komunikat o zatwierdzeniu.

Jeff Ferland
źródło
4
Jeśli chce HEADwyglądać, Ato prawdopodobnie chce, aby indeks się zgadzał, więc git reset --soft Djest to prawdopodobnie bardziej odpowiednie.
CB Bailey,
2
- miękkie resetowanie nie przenosi indeksu, więc kiedy się zatwierdzi, będzie wyglądać, jakby zatwierdzenie pochodziło bezpośrednio z zamiast z D. To spowodowałoby podział gałęzi. --mixed pozostawia zmiany, ale przesuwa wskaźnik indeksu, więc D stanie się nadrzędnym zatwierdzeniem.
Jeff Ferland
5
Tak, myślę, że git reset - utrzymanie jest dokładnie tym, co mam powyżej. Pojawiło się w wersji 1.7.1, wydanej w kwietniu 2010 roku, więc odpowiedzi nie było w tym czasie.
Jeff Ferland
git checkout ANastępnie git commitpowyżej nie działa dla mnie, ale ta odpowiedź nie.
SimplGy,
Dlaczego jest to git reset --mixed Dwymagane? W szczególności dlaczego reset? Czy dlatego, że bez resetowania do D, HEAD wskazywałby na A, powodując, że B, C i D były „wiszące” i zbierane w śmieciach - czego nie chce? Ale dlaczego --mixed? Już odpowiedziałeś: „ --softzresetowanie nie przenosi indeksu ...” Zatem przenoszenie indeksu oznacza, że ​​indeks będzie zawierał zmiany D, podczas gdy katalog roboczy będzie zawierał zmiany A - w ten sposób a git statuslub git diff(który porównuje Indeks [D] z Katalog roboczy [A]) pokaże substancję; ten użytkownik wraca z D do A?
The Red Pea,
39

Najpierw upewnij się, że twoja kopia robocza nie jest modyfikowana. Następnie:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

a następnie po prostu popełnij. Nie zapomnij udokumentować przyczyny wycofania.

mateusz.fiolka
źródło
1
Pracował dla mnie! Wystąpił problem ze zmianami zaciemnienia gałęzi funkcji wprowadzonymi w gałęzi programistycznej (niektóre poprawki błędów), więc gałąź funkcji musiała zastąpić wszystkie zmiany dokonane podczas programowania (w tym usunięcie niektórych plików).
silentser
1
Nie będzie działać z plikami binarnymi:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux
2
Jest to o wiele bardziej elastyczne rozwiązanie niż zaakceptowana odpowiedź. Dzięki!
Brian Kung,
2
re: pliki binarne używają - opcja git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
binarna
2
Działa to nawet, jeśli chcesz cofnąć zakres zatwierdzeń, który zawiera zatwierdzenia scalania. Podczas korzystania git revert A..Zdostanieszerror: commit X is a merge but no -m option was given.
Juliusz Gonera
35

Jestem tak sfrustrowany, że na to pytanie nie można po prostu odpowiedzieć. Każde inne pytanie dotyczy tego, jak prawidłowo przywrócić i zachować historię. To pytanie brzmi: „Chcę, aby szef oddziału wskazywał na A, tzn. Chcę, aby B, C, D i HEAD zniknęły, i chcę, aby głowa była synonimem A.”

git checkout <branch_name>
git reset --hard <commit Hash for A>
git push -f

Nauczyłem się dużo czytając post Jakuba, ale jakiś facet w firmie (z dostępem do wypychania do naszego oddziału „testującego” bez Pull-Request) naciskał jak 5 złych popełnień próbując naprawić i naprawić błąd, który popełnił 5 lat temu. Nie tylko to, ale jedno lub dwa żądania ściągnięcia zostały zaakceptowane, które były teraz złe. Więc zapomnij o tym, znalazłem ostatnie dobre zatwierdzenie (abc1234) i po prostu uruchomiłem podstawowy skrypt:

git checkout testing
git reset --hard abc1234
git push -f

Powiedziałem pozostałym 5 facetom pracującym w tym repozytorium, że lepiej zanotują swoje zmiany w ciągu ostatnich kilku godzin i Wipe / Re-Branch z ostatnich testów. Koniec historii.

Suamere
źródło
2
Nie opublikowałem zatwierdzeń, więc takiej odpowiedzi potrzebowałem. Dzięki, @Suamere.
Tom Barron,
Lepszym sposobem na to jest git push --force-with-leasezapisanie historii tylko wtedy, gdy nikt inny nie zaangażował się w oddział po ani w zakresie zatwierdzeń do vapourize. Jeśli inni ludzie korzystali z gałęzi, jego historia nigdy nie powinna być przepisywana, a zatwierdzenie powinno być po prostu widoczne.
frandroid
1
@frandroid „historia nigdy nie powinna być przepisywana na nowo”, tylko transakcje Sithów w absolutach. Pytanie tego wątku i sedno mojej odpowiedzi jest takie, że w przypadku konkretnego scenariusza cała historia powinna zostać wyczyszczona.
Suamere
@ Suamere Pewnie, to jest pytanie. Ale gdy w twojej odpowiedzi wspomniano o tym, co musiałeś powiedzieć innym chłopakom, mogą pojawić się problemy. Z własnego doświadczenia, push -f może zepsuć bazę kodu, jeśli inne osoby popełniły to, co próbujesz usunąć. --force-with-lease osiąga ten sam wynik, z wyjątkiem tego, że oszczędza twoją dupę, jeśli masz zamiar zepsuć repo. Po co ryzykować? Jeśli --force-with-leasing nie powiedzie się, możesz sprawdzić, które zatwierdzenie przeszkadza, odpowiednio ocenić, skorygować i spróbować ponownie.
frandroid
1
@ Suamere Dziękujemy! Zgadzam się, że pytanie jasno stwierdza, że ​​chce przepisać historię. Jestem w takiej samej sytuacji jak ty i zgaduję, że OP w tym, że ktoś dokonał dziesiątek brzydkich powrotów i dziwnych zobowiązań i powrotów powrotów przez przypadek (kiedy byłem na wakacjach) i państwo musi zostać przywrócone. W każdym razie wraz z dobrym zdrowym ostrzeżeniem powinna to być zaakceptowana odpowiedź.
Lee Richardson,
9

Jest to rozwinięcie jednego z rozwiązań zawartych w odpowiedzi Jakuba

Miałem do czynienia z sytuacją, w której zmiany, które musiałem wycofać, były nieco skomplikowane, przy czym kilka z nich to scalanie zmian i musiałem unikać przepisywania historii. Nie byłem w stanie użyć serii git revertpoleceń, ponieważ w końcu wpadłem na konflikty między dodawanymi zmianami wersji. Skończyło się na następujących krokach.

Najpierw sprawdź zawartość zatwierdzenia celu, pozostawiając HEAD na końcu gałęzi:

$ git checkout -f <target-commit> -- .

(- upewnia się, że <target-commit>jest interpretowane raczej jako zatwierdzenie niż plik;. Odnosi się do bieżącego katalogu.)

Następnie określ, które pliki zostały dodane do wycofanych zatwierdzeń, a zatem należy je usunąć:

$ git diff --name-status --cached <target-commit>

Dodane pliki powinny pokazywać „A” na początku wiersza i nie powinny występować inne różnice. Teraz, jeśli jakieś pliki wymagają usunięcia, przygotuj te pliki do usunięcia:

$ git rm <filespec>[ <filespec> ...]

Na koniec dokonaj zmiany wersji:

$ git commit -m 'revert to <target-commit>'

W razie potrzeby upewnij się, że wróciliśmy do pożądanego stanu:

$git diff <target-commit> <current-commit>

Nie powinno być różnic.

Warren Dew
źródło
Czy na pewno możesz gitHEAD tylko wierzchołkiem gałęzi?
Suamere,
2
To było dla mnie znacznie lepsze rozwiązanie, ponieważ w moim miałem fuzje scalania.
sovemp,
3

Prostym sposobem na przywrócenie grupy zatwierdzeń w repozytorium współdzielonym (używanym przez ludzi i chcącym zachować historię) jest użycie git revertw połączeniu z gitrev-list . Ten ostatni dostarczy ci listę zmian, pierwszy dokona samego wycofania.

Można to zrobić na dwa sposoby. Jeśli chcesz przywrócić wiele zatwierdzeń w jednym zatwierdzeniu, użyj:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert -n $i; done

spowoduje to cofnięcie potrzebnej grupy zatwierdzeń, ale pozostaw wszystkie zmiany w działającym drzewie, powinieneś je wszystkie zatwierdzić jak zwykle.

Inną opcją jest posiadanie jednego zatwierdzenia na cofniętą zmianę:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-edit -s $i; done

Na przykład, jeśli masz drzewo zatwierdzania, takie jak

 o---o---o---o---o---o--->    
fff eee ddd ccc bbb aaa

aby przywrócić zmiany z eee na bbb , uruchom

for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done
Ruslan Kabalin
źródło
właśnie tego użyłem. Dzięki!
Ran Biron,
2

Żadne z nich nie działało dla mnie, więc miałem trzy zatwierdzenia do wycofania (ostatnie trzy zatwierdzenia), więc zrobiłem:

git revert HEAD
git revert HEAD~2
git revert HEAD~4
git rebase -i HEAD~3 # pick, squash, squash

Działa jak urok :)

dorycki
źródło
2
Jest to opłacalna opcja tylko wtedy, gdy zmiany nie zostały jeszcze wprowadzone.
kboom
0

Moim zdaniem bardzo łatwym i czystym sposobem może być:

wróć do A.

git checkout -f A

skieruj głowę mistrza na bieżący stan

git symbolic-ref HEAD refs/heads/master

zapisać

git commit
nulll
źródło
1
Czy możesz wyjaśnić powód oddania głosu?
nulll
To działa dobrze. Po co głosować za tą przydatną odpowiedzią, czy ktoś może wyjaśnić, jaka jest najlepsza praktyka?
Levent Divilioglu
Czy to to samo co git checkout master; git reset --hard A? A jeśli nie, czy mógłbyś wyjaśnić nieco więcej na temat tego, co to robi?
MM
po prostu działa, ale symboliczne polecenie HEAD wydaje się nie być „bezpiecznym” poleceniem
Sérgio
err Nie chcę naprawić mistrza, chcę naprawić gałąź
Sérgio
-6

Jeśli chcesz tymczasowo cofnąć zatwierdzenia funkcji, możesz użyć serii następujących poleceń.

Oto jak to działa

git log --pretty = oneline | grep „nazwa_funkcji” | cut -d '' -f1 | xargs -n1 git revert --no-edit

Ajit Singh
źródło