Po wykonaniu rebase, zatwierdzenia Git są duplikowane w tej samej gałęzi

131

Rozumiem scenariusz przedstawiony w Pro Git o zagrożeniach związanych z ponownym użyciem . Autor w zasadzie mówi ci, jak uniknąć powielonych zatwierdzeń:

Nie zmieniaj bazy zatwierdzeń, które przesłałeś do publicznego repozytorium.

Opowiem wam o mojej szczególnej sytuacji, ponieważ uważam, że nie pasuje do scenariusza Pro Git i nadal otrzymuję zduplikowane zatwierdzenia.

Powiedzmy, że mam dwa zdalne oddziały z ich lokalnymi odpowiednikami:

origin/master    origin/dev
|                |
master           dev

Wszystkie cztery gałęzie zawierają te same commity, a programowanie zacznę w dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Po kilku zatwierdzeniach wprowadzam zmiany do origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Muszę wrócić masterdo, aby szybko naprawić:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

Wracając do devzmiany podstaw zmian, aby uwzględnić szybką poprawkę w moim rzeczywistym rozwoju:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Gdybym wyświetlić historię zatwierdzeń z GitX / gitk Zauważyłem, że origin/devteraz zawiera dwa identyczne zobowiązuje C5'i C6'które różnią się Git. Jeśli teraz wprowadzę zmiany do origin/devtego, otrzymam wynik:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Może nie do końca rozumiem wyjaśnienie w Pro Git, więc chciałbym wiedzieć dwie rzeczy:

  1. Dlaczego Git powiela te zatwierdzenia podczas zmiany bazy? Czy jest jakiś konkretny powód, aby to zrobić zamiast po prostu złożyć wniosek C5i C6później C7?
  2. Jak mogę tego uniknąć? Czy mądrze byłoby to zrobić?
elitalon
źródło

Odpowiedzi:

87

Nie powinieneś tutaj używać rebase, wystarczy zwykłe scalenie. Książka Pro Git, do której utworzyłeś łącze, wyjaśnia dokładnie tę sytuację. Wewnętrzne działanie może być nieco inne, ale oto jak to sobie wyobrażam:

  • C5i C6są tymczasowo wyciąganedev
  • C7 ma zastosowanie do dev
  • C5i C6są odtwarzane na wierzchu C7, tworząc nowe różnice, a tym samym nowe zatwierdzenia

Tak więc w twojej devbranży C5i C6skutecznie już nie istnieją: są teraz C5'i C6'. Po wciśnięciu do origin/devgit widzi C5'i C6'jak nowych zatwierdzeń i pinezki je do końca historii. Rzeczywiście, jeśli spojrzysz na różnice między C5i C5'w origin/dev, zauważysz, że chociaż zawartość jest taka sama, numery wierszy są prawdopodobnie różne - co sprawia, że ​​hash zatwierdzenia jest inny.

Powtórzę regułę Pro Git: nigdy nie zmieniaj bazy zatwierdzeń, które istniały kiedykolwiek poza Twoim lokalnym repozytorium . Zamiast tego użyj scalania.

Justin ᚅᚔᚈᚄᚒᚔ
źródło
Mam ten sam problem, jak mogę teraz naprawić historię mojej zdalnej gałęzi, czy jest jakaś inna opcja niż usunięcie gałęzi i odtworzenie jej z wybieraniem wiśni?
Wazery
1
@xdsy: Rzuć okiem na to i to .
Justin ᚅᚔᚈᚄᚒᚔ
2
Mówisz "C5 i C6 są tymczasowo wycofane z dev ... C7 jest zastosowane do dev". Jeśli tak jest, to dlaczego C5 i C6 pojawiają się przed C7 w kolejności zatwierdzeń na origin / dev?
KJ50
@ KJ50: Ponieważ C5 i C6 zostały już przesunięte do origin/dev. Po devzmianie bazy, jego historia jest modyfikowana (C5 / C6 tymczasowo usuwane i ponownie stosowane po C7). Modyfikowanie historii wypychanych repozytoriów jest generalnie naprawdę złym pomysłem ™, chyba że wiesz, co robisz. W tym prostym przypadku problem można rozwiązać, wykonując wymuszony ruch od końca devdo origin/devpo zakończeniu rebase i powiadamiając każdego, kto pracuje origin/dev, że prawdopodobnie czeka go zły dzień. Lepszą odpowiedzią jest znowu „nie rób tego ... zamiast tego użyj scalania”
Justin ᚅᚔᚈᚄᚒᚔ
3
Jedna uwaga: skróty C5 i C5 'są z pewnością różne, ale nie ze względu na różne numery wierszy, ale dla następujących dwóch faktów, z których każdy jest wystarczający dla różnicy: 1) skrót, o którym mówimy jest hashem całego drzewa źródłowego po zatwierdzeniu, a nie hashem różnicy delta, dlatego C5 'zawiera wszystko, co pochodzi z C7, podczas gdy C5 nie, oraz 2) Rodzic C5' różni się od C5 i ta informacja jest również zawarty w węźle głównym drzewa zatwierdzenia, wpływając na wynik skrótu.
Ozgur Murat
113

Krótka odpowiedź

Pominąłeś fakt, że uruchomiłeś git push, pojawił się następujący błąd, a następnie przystąpiłeś do uruchamiania git pull:

To [email protected]:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Mimo że Git stara się być pomocny, jego rada „git pull” najprawdopodobniej nie jest tym, co chcesz zrobić .

Jeśli jesteś:

  • Pracując nad samą „gałęzią funkcji” lub „gałęzią dewelopera” , możesz uruchomić git push --forceaktualizację pilota za pomocą zatwierdzeń po rebase ( zgodnie z odpowiedzią użytkownika4405677 ).
  • Pracując na gałęzi z wieloma programistami w tym samym czasie, prawdopodobnie nie powinieneś używaćgit rebase w pierwszej kolejności. Aby zaktualizować devze zmianami z master, powinieneś zamiast biegać git rebase master dev, git merge mastergdy jest włączony dev( zgodnie z odpowiedzią Justina ).

Nieco dłuższe wyjaśnienie

Każdy skrót zatwierdzenia w Git jest oparty na wielu czynnikach, z których jednym jest skrót zatwierdzenia, który występuje przed nim.

Jeśli zmienisz kolejność zatwierdzeń, zmienisz skróty zatwierdzeń; rebasing (kiedy coś robi) zmieni skróty zatwierdzeń. Dzięki temu wynik uruchomienia git rebase master dev, gdzie nie devjest zsynchronizowany z master, utworzy nowe zatwierdzenia (a tym samym skróty) z taką samą zawartością jak te włączone, devale z zatwierdzeniami masterwstawionymi przed nimi.

W takiej sytuacji możesz znaleźć się na wiele sposobów. Dwa sposoby, które przychodzą mi do głowy:

  • Możesz mieć commity, na masterktórych chcesz oprzeć swoją devpracę
  • Mogłeś mieć zatwierdzenia, devktóre zostały już przesłane do pilota, a następnie przystąpisz do ich zmiany (przeredaguj komunikaty o zatwierdzeniach, zmień kolejność zatwierdzeń, zatwierdzeń squash itp.)

Lepiej zrozummy, co się stało - oto przykład:

Masz repozytorium:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Początkowy zestaw zatwierdzeń liniowych w repozytorium

Następnie przystępujesz do zmiany zatwierdzeń.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(Tutaj musisz uwierzyć na moje słowo: istnieje wiele sposobów zmiany zatwierdzeń w Git. W tym przykładzie zmieniłem czas C3, ale ty wstawiasz nowe zatwierdzenia, zmieniasz komunikaty zmian, zmieniam kolejność zatwierdzeń, zgniatanie zatwierdzeń, itp.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

To samo dotyczy nowych haszów

W tym miejscu ważne jest, aby zauważyć, że skróty zatwierdzania są różne. Jest to oczekiwane zachowanie, ponieważ zmieniłeś coś (cokolwiek) w nich. To jest w porządku, ALE:

Dziennik wykresów pokazujący, że urządzenie główne nie jest zsynchronizowane z pilotem

Próba pchnięcia spowoduje wyświetlenie błędu (i podpowiedź, że powinieneś uruchomić git pull).

$ git push origin master
To [email protected]:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Jeśli biegniemy git pull, widzimy ten dziennik:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Lub pokazane w inny sposób:

Dziennik wykresów przedstawiający zatwierdzenie scalania

A teraz mamy lokalnie zduplikowane zatwierdzenia. Gdybyśmy mieli uruchomić git push, wysłalibyśmy je na serwer.

Aby uniknąć dotarcia do tego etapu, mogliśmy biec git push --force(gdzie zamiast tego biegliśmy git pull). To bez problemu wysłałoby nasze zatwierdzenia z nowymi hashami na serwer. Aby rozwiązać problem na tym etapie, możemy cofnąć się do stanu sprzed uruchomienia git pull:

Spójrz na reflog ( git reflog), aby zobaczyć, jaki był skrót zatwierdzenia przed uruchomieniem git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Powyżej widzimy, że ba7688abyło to zobowiązanie, na którym byliśmy przed uruchomieniem git pull. Z tym hashem commita w ręku możemy zresetować z powrotem do tego ( git reset --hard ba7688a), a następnie uruchomić git push --force.

Gotowe.

Ale czekaj, kontynuowałem pracę na podstawie zduplikowanych zatwierdzeń

Jeśli w jakiś sposób nie zauważyłeś, że zatwierdzenia zostały zduplikowane i kontynuowałeś pracę nad zduplikowanymi zatwierdzeniami, naprawdę narobiłeś sobie bałaganu. Rozmiar bałaganu jest proporcjonalny do liczby zatwierdzeń, które masz na szczycie duplikatów.

Jak to wygląda:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Dziennik Git pokazujący liniowe zatwierdzenia na zduplikowanych zatwierdzeniach

Lub pokazane w inny sposób:

Wykres dziennika pokazujący liniowe zatwierdzenia na podstawie zduplikowanych zatwierdzeń

W tym scenariuszu chcemy usunąć zduplikowane zatwierdzenia, ale zachować zmiany, które oparliśmy na nich - chcemy zachować C6 do C10. Podobnie jak w przypadku większości rzeczy, można to zrobić na kilka sposobów:

Zarówno:

  • Utwórz nową gałąź przy ostatnim zduplikowanym zatwierdzeniu 1 , cherry-pickkażdym zatwierdzeniu (włącznie z C6 do C10) na tę nową gałąź i traktuj tę nową gałąź jako kanoniczną.
  • Uruchom git rebase --interactive $commit, gdzie $commitjest zatwierdzeniem przed obydwoma zduplikowanymi zatwierdzeniami 2 . Tutaj możemy wprost usunąć linie dla duplikatów.

1 To nie ma znaczenia, który z nich wybrać, albo ba7688aczy 2a2e220działają poprawnie.

2 W tym przykładzie byłoby to 85f59ab.

TL; DR

Ustaw advice.pushNonFastForwardna false:

git config --global advice.pushNonFastForward false
Whymarrh
źródło
1
Można postępować zgodnie z radą „git pull…”, o ile zdajemy sobie sprawę, że wielokropek ukrywa opcję „--rebase” (aka „-r”). ;-)
G. Sylvie Davies
4
Polecam używanie git pushich w --force-with-leasedzisiejszych czasach, ponieważ jest to lepsze ustawienie domyślne
Whymarrh
4
To albo ta odpowiedź, albo wehikuł czasu. Dzięki!
ZeMoon
Bardzo zgrabne wyjaśnienie ... Natknąłem się na podobny problem, który powodował powielanie mojego kodu 5-6 razy po kilkukrotnej próbie rebase ... tylko po to, aby mieć pewność, że kod jest aktualny z masterem ... ale za każdym razem pchał nowe zatwierdzenia do mojej gałęzi, powielające również mój kod. Czy możesz mi powiedzieć, czy wymuszenie wypychania (z opcją dzierżawy) jest bezpieczne, jeśli jestem jedynym programistą pracującym w moim oddziale? A może lepszym sposobem jest połączenie mistrza z moim zamiast zmiany bazy?
Dhruv Singhal
12

Myślę, że opisując swoje kroki, pominąłeś ważny szczegół. Mówiąc dokładniej, twój ostatni krok, git pushna dev, faktycznie spowodowałby błąd, ponieważ normalnie nie możesz przesuwać zmian, które nie są szybkie do przodu.

Zrobiłeś to git pullprzed ostatnim wypchnięciem, co spowodowało zatwierdzenie scalenia z C6 i C6 jako rodzicami, dlatego oba pozostaną wymienione w dzienniku. Ładniejszy format dziennika mógł uczynić bardziej oczywistym, że są to połączone gałęzie zduplikowanych zatwierdzeń.

Albo zamiast tego utworzyłeś git pull --rebase(lub bez jawnego, --rebasejeśli jest to sugerowane przez twoją konfigurację), co ściągnęło oryginalne C5 i C6 z powrotem do twojego lokalnego dewelopera (i ponownie przestawiłeś następujące na nowe skróty, C7 'C5' 'C6' ').

Jednym ze sposobów wyjścia z tego mogło być git push -fwymuszenie pchnięcia, gdy wystąpił błąd i wyczyszczenie C5 C6 z początku, ale jeśli ktoś inny również je wyciągnął, zanim je wyczyściłeś, będziesz miał o wiele więcej kłopotów. w zasadzie każdy, kto ma C5 C6, musiałby wykonać specjalne kroki, aby się ich pozbyć. Właśnie dlatego mówią, że nigdy nie należy zmieniać bazy niczego, co zostało już opublikowane. Nadal jest to wykonalne, jeśli powiedzenie „publikowanie” odbywa się w małym zespole.

user4405677
źródło
1
Pominięcie git pulljest kluczowe. Twoja rekomendacja git push -f, choć niebezpieczna, jest prawdopodobnie tym, czego szukają czytelnicy.
Whymarrh
W rzeczy samej. Kiedy pisałem pytanie, które faktycznie zrobiłem git push --force, tylko po to, aby zobaczyć, co zrobi Git. Od tamtej pory wiele się nauczyłem o Git i obecnie rebasejest to część mojego normalnego przepływu pracy. Jednak staram git push --force-with-leasesię unikać nadpisywania cudzej pracy.
elitalon
Używanie --force-with-leasejest dobrym ustawieniem domyślnym, zostawię komentarz również pod moją odpowiedzią
Whymarrh
2

Dowiedziałem się, że w moim przypadku ten problem jest konsekwencją problemu z konfiguracją Gita. (Obejmuje ciągnięcie i scalanie)

Opis problemu:

Objawy: Zatwierdzenia zduplikowane w gałęzi podrzędnej po rebase, co oznacza liczne scalenia w trakcie i po ponownym bazowaniu.

Przepływ pracy: Oto kroki przepływu pracy, które wykonywałem:

  • Pracuj nad „gałęzią funkcji” (elementem podrzędnym „gałęzi deweloperskiej”)
  • Zatwierdzanie i wypychanie zmian w „gałęzi Funkcje”
  • Sprawdź „gałąź deweloperska” (gałąź główna funkcji) i pracuj z nią.
  • Zatwierdzanie i wypychanie zmian w „gałęzi deweloperskiej”
  • Sprawdź „gałąź funkcji” i pobierz zmiany z repozytorium (na wypadek, gdyby ktoś inny zlecił pracę)
  • Przebuduj „gałąź funkcji” na „gałąź deweloperska”
  • Wymuszenie zmian w „gałęzi funkcji”

Konsekwencją tego przepływu pracy jest powielenie wszystkich zatwierdzeń „Feature-branch” od czasu poprzedniej rebase ... :-(

Problem wynikał z konieczności wprowadzenia zmian w gałęzi potomnej przed rebase. Domyślna konfiguracja ściągania Git to „scalanie”. To zmienia indeksy zatwierdzeń wykonywanych na gałęzi potomnej.

Rozwiązanie: w pliku konfiguracyjnym Git skonfiguruj pull do pracy w trybie rebase:

...
[pull]
    rebase = preserve
...

Mam nadzieję, że to pomoże JN Grx

JN Gerbaux
źródło
1

Być może ściągnąłeś ze zdalnej gałęzi innej niż Twoja obecna. Na przykład mogłeś wyciągnąć z Mastera, gdy twoja gałąź rozwija śledzenie rozwoju. Git sumiennie pobierze zduplikowane zatwierdzenia, jeśli zostanie pobrany z nieśledzonej gałęzi.

Jeśli tak się stanie, możesz wykonać następujące czynności:

git reset --hard HEAD~n

gdzie n == <number of duplicate commits that shouldn't be there.>

Następnie upewnij się, że wyciągasz z właściwej gałęzi, a następnie uruchom:

git pull upstream <correct remote branch> --rebase

Ciągnięcie za pomocą --rebasezapewni, że nie dodasz zbędnych zatwierdzeń, które mogłyby zagmatwać historię zmian.

Oto trochę trzymania ręki dla rebase git.

ScottyBlades
źródło