Ustawienie wskaźnika nadrzędnego git na innego rodzica

220

Jeśli w przeszłości mam zatwierdzenie, które wskazuje na jednego z rodziców, ale chcę zmienić rodzica, na który wskazuje, jak mam to zrobić?

dougvk
źródło

Odpowiedzi:

404

Korzystanie git rebase. Jest to ogólne polecenie „weź zatwierdzenie (-a) i umieść je / plop na innym nadrzędnym (podstawowym)” poleceniu w Git.

Niektóre rzeczy należy jednak wiedzieć:

  1. Ponieważ zatwierdzenia SHA dotyczą ich rodziców, po zmianie elementu nadrzędnego danego zatwierdzenia zmieni się jego SHA - podobnie jak SHA wszystkich zatwierdzeń, które następują po nim (nowsze od niego) w linii rozwoju.

  2. Jeśli pracujesz z innymi ludźmi i już opublikowałeś omawiane zatwierdzenie publicznie tam, gdzie je pobrali, modyfikacja zatwierdzenia jest prawdopodobnie złym pomysłem. Wynika to z nr 1, a tym samym wynikającego z tego zamieszania w repozytoriach innych użytkowników, gdy próbuje się dowiedzieć, co się stało, ponieważ Twoje SHA nie pasują już do nich dla „tych samych” zobowiązań. (Szczegółowe informacje znajdują się w sekcji „ODZYSKIWANIE Z REBASY UPSTREAM” na połączonej stronie podręcznika).

To powiedziawszy, jeśli obecnie jesteś w oddziale z pewnymi zatwierdzeniami, które chcesz przenieść do nowego rodzica, wyglądałoby to mniej więcej tak:

git rebase --onto <new-parent> <old-parent>

To przeniesie wszystko po <old-parent> w bieżącej gałęzi, aby usiąść <new-parent>zamiast niej.

Bursztyn
źródło
Korzystam z git rebase --onto, aby zmienić rodziców, ale wygląda na to, że mam tylko jedno zatwierdzenie. Zadałem na to pytanie: stackoverflow.com/questions/19181665/…
Eduardo Mello,
7
Aha, więc do tego --ontosłuży! Dokumentacja zawsze była dla mnie całkowicie niejasna, nawet po przeczytaniu tej odpowiedzi!
Daniel C. Sobral
6
Ta linijka: git rebase --onto <new-parent> <old-parent>Powiedziała mi wszystko o tym, jak wykorzystać rebase --ontoto każde inne pytanie i dokument, które czytałem do tej pory, czego nie udało się.
jpmc26,
3
Dla mnie to polecenie powinno brzmieć: rebase this commit on that onelub git rebase --on <new-parent> <commit>. Korzystanie ze starego rodzica tutaj nie ma dla mnie sensu.
Samir Aguiar
1
@kopranb Stary rodzic jest ważny, gdy chcesz odrzucić część ścieżki z głowy do głowy zdalnej. Opisany przypadek jest obsługiwany przez git rebase <new-parent>.
clacke,
35

Jeśli okaże się, że musisz unikać ponownego wprowadzania kolejnych zatwierdzeń (np. Ponieważ zapisywanie historii byłoby niemożliwe do wykonania), możesz użyć git replace (dostępny w Git 1.6.5 i nowszych).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

Po ustaleniu powyższej zamiany wszelkie żądania dotyczące obiektu B faktycznie zwrócą obiekt C. Zawartość C jest dokładnie taka sama jak zawartość B, z wyjątkiem pierwszego rodzica (ci sami rodzice (z wyjątkiem pierwszego), to samo drzewo, ten sam komunikat zatwierdzenia).

Zastąpienia są domyślnie aktywne, ale można je wyłączyć, używając --no-replace-objectsopcji git (przed nazwą polecenia) lub ustawiając GIT_NO_REPLACE_OBJECTSzmienną środowiskową. Zamienniki można udostępniać, naciskając refs/replace/*(oprócz zwykłego refs/heads/*).

Jeśli nie podoba ci się zatwierdzanie zmian (wykonane przy pomocy sed powyżej), możesz utworzyć zamianę zamienną za pomocą poleceń wyższego poziomu:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

Duża różnica polega na tym, że ta sekwencja nie propaguje dodatkowych rodziców, jeśli B jest zatwierdzeniem scalania.

Chris Johnsen
źródło
A w jaki sposób wprowadziłbyś zmiany do istniejącego repozytorium? Wygląda na to, że działa lokalnie, ale git push niczego już nie wypycha
Baptiste Wicht 11.11.14
2
@BaptisteWicht: Zamienniki są przechowywane w osobnym zestawie referencji. Konieczne będzie zorganizowanie wypychania (i pobierania) refs/replace/hierarchii odwołań. Jeśli musisz to zrobić tylko raz, możesz to zrobić git push yourremote 'refs/replace/*'w repozytorium źródłowym i git fetch yourremote 'refs/replace/*:refs/replace/*'repozytoriach docelowych. Jeśli musisz to zrobić wiele razy, możesz zamiast tego dodać te refspecs do remote.yourremote.pushzmiennej config (w źródłowym repozytorium) i remote.yourremote.fetchzmiennej config (w docelowych repozytoriach).
Chris Johnsen
3
Prostszym sposobem na to jest użycie git replace --grafts. Nie jestem pewien, kiedy to zostało dodane, ale jego jedynym celem jest utworzenie zamiennika, który jest taki sam jak zatwierdzenie B, ale z określonymi rodzicami.
Paul Wagland
32

Zauważ, że zmiana zatwierdzenia w Git wymaga zmiany wszystkich zatwierdzeń następujących po nim. Odradza to, jeśli opublikowałeś tę część historii, a ktoś mógł oprzeć swoją pracę na historii sprzed zmiany.

Alternatywnym rozwiązaniem git rebasewymienionym w odpowiedzi Amber jest użycie mechanizmu przeszczepów (patrz definicja przeszczepów Git w Słowniku Git i dokumentacja .git/info/graftspliku w dokumentacji układu repozytorium Git ) w celu zmiany elementu nadrzędnego zatwierdzenia, sprawdź, czy działało poprawnie w niektórych przeglądarkach historii ( gitk, git log --graph, itp.), a następnie użyj git filter-branch(zgodnie z opisem w sekcji „Przykłady” strony podręcznika), aby uczynić go trwałym (a następnie usuń przeszczep i opcjonalnie usuń oryginalne git filter-branchreferencje, których kopie zapasowe utworzono w kopii zapasowej , lub ponownie skonfiguruj repozytorium):

echo "$ commit-id $ graft-id" >> .git / info / grafts
git filter-branch $ graft-id..HEAD

UWAGA !!! Rozwiązanie to różni się od rozwiązania rebase że git rebasebyłoby rebase / przeszczepie zmiany , natomiast rozwiązanie oparte na przeszczepy po prostu reparent commity jak jest , nie biorąc pod uwagę różnice między starym i nowym rodzica rodzica!

Jakub Narębski
źródło
3
Z tego, co rozumiem (z git.wiki.kernel.org/index.php/GraftPoint ), git replacezastąpił przeszczepy git (zakładając, że masz git 1.6.5 lub nowszy).
Alexander Bird
4
@ Thr4wn: W dużej mierze prawda, ale jeśli chcesz przepisać historię, możesz to zrobić za pomocą przeszczepów + git-filter-branch (lub interaktywnego rebase lub szybkiego eksportu + np. Reposurgeon). Jeśli chcesz / musisz zachować historię , git-replace jest znacznie lepszy niż przeszczepy.
Jakub Narębski
@ JakubNarębski, czy mógłbyś wyjaśnić, co masz na myśli, przepisując i zachowując historię? Dlaczego nie mogę używać git-replace+ git-filter-branch? W moim teście filter-branchzdawałem się szanować zastępstwo, wypuszczając całe drzewo.
cdunn2001
1
@ cdunn2001: git filter-branchprzepisuje historię, szanuje przeszczepy i zamienniki (ale w przerobionej historii zastąpienia będą dyskusyjne); push spowoduje zmianę non-faf-forward. git replacetworzy w miejscu zbywalne zamienniki; push będzie przewijać do przodu, ale będziesz musiał pchnąć, refs/replaceaby przenieść zamienniki, aby poprawić historię. HTH
Jakub Narębski
23

Aby wyjaśnić powyższe odpowiedzi i bezwstydnie podłączyć własny skrypt:

Zależy to od tego, czy chcesz „rebase” czy „reparent”. Rebase , jak sugeruje Amber , porusza dyferencjału . Reparent , jak sugeruje Jakuba i Chris , porusza migawek całego drzewa. Jeśli chcesz się zregenerować, sugeruję używanie git reparentzamiast wykonywania pracy ręcznie.

Porównanie

Załóżmy, że masz zdjęcie po lewej stronie i chcesz, aby wyglądało jak zdjęcie po prawej:

                   C'
                  /
A---B---C        A---B---C

Zarówno ponowne bazowanie, jak i ponowne rodzenie utworzy ten sam obraz, ale definicja C'różni się. Dzięki git rebase --onto A B, C'nie będzie zawierać żadnych zmian wprowadzonych B. Dzięki git reparent -p A, C'będzie identyczny C(poza tym, że Bnie będzie w historii).

Mark Lodato
źródło
1
+1 Eksperymentowałem ze reparentskryptem; działa pięknie; wysoce zalecane . Poleciłbym także utworzenie nowej branchetykiety i do checkouttej gałęzi przed wywołaniem (ponieważ etykieta gałęzi się przeniesie).
Rhubbarb
Warto również zauważyć, że: (1) oryginalny węzeł pozostaje nienaruszony, oraz (2) nowy węzeł nie dziedziczy potomków pierwotnego węzła.
Rhubbarb
0

Zdecydowanie odpowiedź @ Jakuba pomogła mi kilka godzin temu, kiedy próbowałem dokładnie tego samego co OP.
Jest to jednak git replace --graftteraz łatwiejsze rozwiązanie dotyczące przeszczepów. Poważnym problemem związanym z tym rozwiązaniem było to, że gałąź filtrów spowodowała, że ​​straciłem każdą gałąź, która nie została scalona z gałęzią HEAD. Następnie git filter-repowykonał pracę idealnie i bezbłędnie.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

Fore więcej informacji: zapoznaj się z sekcją „Przeszczepianie historii” w dokumentacji

Teodoro
źródło