Setzen des git-Elternzeigers auf einen anderen Elternteil

220

Wenn ich in der Vergangenheit ein Commit habe, das auf ein Elternteil verweist, aber das Elternteil ändern möchte, auf das es verweist, wie würde ich das tun?

Dougvk
quelle

Antworten:

404

Verwenden von git rebase . Es ist der generische Befehl "take commit (s) und plopiere es auf ein anderes übergeordnetes Element (Basis)" in Git.

Einige Dinge zu wissen:

  1. Da Commit-SHAs ihre Eltern einbeziehen, ändert sich beim Ändern des übergeordneten Elements eines bestimmten Commits dessen SHA - ebenso wie die SHAs aller Commits, die danach (neuer als sie) in der Entwicklungslinie folgen .

  2. Wenn Sie mit anderen Personen zusammenarbeiten und das betreffende Commit bereits öffentlich an die Stelle verschoben haben, an der sie es abgerufen haben, ist das Ändern des Commits wahrscheinlich eine schlechte Idee. Dies liegt an Nummer 1 und damit an der daraus resultierenden Verwirrung, auf die die Repositorys der anderen Benutzer stoßen, wenn sie herausfinden möchten, was passiert ist, weil Ihre SHAs nicht mehr mit ihren für die "gleichen" Commits übereinstimmen. (Weitere Informationen finden Sie im Abschnitt "WIEDERHERSTELLUNG VON UPSTREAM REBASE" auf der verlinkten Manpage.)

Das heißt, wenn Sie sich derzeit in einem Zweig mit einigen Commits befinden, die Sie zu einem neuen Elternteil verschieben möchten, sieht dies ungefähr so ​​aus:

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

Dadurch wird alles nach <old-parent> dem aktuellen Zweig verschoben , um <new-parent>stattdessen darauf zu sitzen .

Bernstein
quelle
Ich verwende git rebase --onto, um die Eltern zu wechseln, aber es scheint, dass ich nur ein einziges Commit habe. Ich habe eine Frage gestellt: stackoverflow.com/questions/19181665/…
Eduardo Mello
7
Aha, das ist es, wofür --ontoes verwendet wird! Die Dokumentation war für mich auch nach dem Lesen dieser Antwort immer völlig dunkel !
Daniel C. Sobral
6
Diese Zeile: git rebase --onto <new-parent> <old-parent>Sagte mir alles darüber, wie man das benutzt rebase --onto, was jede andere Frage und jedes andere Dokument, das ich bisher gelesen habe, nicht konnte.
jpmc26
3
Für mich sollte dieser Befehl lauten : rebase this commit on that one, oder git rebase --on <new-parent> <commit>. Die Verwendung des alten Elternteils hier macht für mich keinen Sinn.
Samir Aguiar
1
@kopranb Der alte Elternteil ist wichtig, wenn Sie einen Teil des Pfades von Ihrem Kopf zum entfernten Kopf verwerfen möchten. Der von Ihnen beschriebene Fall wird von behandelt git rebase <new-parent>.
Clacke
35

Wenn sich herausstellt, dass Sie vermeiden müssen, die nachfolgenden Commits neu zu gründen (z. B. weil ein Umschreiben des Verlaufs unhaltbar wäre), können Sie den Git-Ersatz verwenden (verfügbar in Git 1.6.5 und höher).

# …---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.

Wenn die obige Ersetzung festgelegt ist, geben alle Anforderungen für das Objekt B tatsächlich das Objekt C zurück. Der Inhalt von C ist genau der gleiche wie der Inhalt von B, mit Ausnahme des ersten Elternteils (gleiche Eltern (außer dem ersten), des gleichen Baums). gleiche Festschreibungsnachricht).

Ersetzungen sind standardmäßig aktiv, können jedoch durch Verwenden der --no-replace-objectsOption zum Git (vor dem Befehlsnamen) oder durch Festlegen der GIT_NO_REPLACE_OBJECTSUmgebungsvariablen deaktiviert werden. Ersetzungen können durch Drücken refs/replace/*(zusätzlich zum Normalen) geteilt werdenrefs/heads/* ) geteilt werden.

Wenn Ihnen das Commit-Munging (wie oben mit sed ausgeführt ) nicht gefällt , können Sie Ihr Ersatz-Commit mit Befehlen höherer Ebene erstellen:

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

Der große Unterschied besteht darin, dass diese Sequenz die zusätzlichen Eltern nicht weitergibt, wenn B ein Merge-Commit ist.

Chris Johnsen
quelle
Und wie würden Sie dann die Änderungen in ein vorhandenes Repository übertragen? Es scheint lokal zu funktionieren, aber Git Push drückt danach nichts mehr
Baptiste Wicht
2
@BaptisteWicht: Die Ersetzungen werden in einem separaten Satz von Referenzen gespeichert. Sie müssen festlegen, dass die Referenzhierarchie verschoben (und abgerufen) wird refs/replace/. Wenn Sie dies nur einmal tun müssen, können Sie dies git push yourremote 'refs/replace/*'im Quell-Repository und git fetch yourremote 'refs/replace/*:refs/replace/*'in den Ziel-Repositorys tun . Wenn Sie dies mehrmals tun müssen, können Sie diese Referenzspezifikationen stattdessen einer remote.yourremote.pushKonfigurationsvariablen (im Quellrepository ) und einer remote.yourremote.fetchKonfigurationsvariablen (in den Zielrepositorys) hinzufügen .
Chris Johnsen
3
Eine einfachere Möglichkeit, dies zu tun, ist die Verwendung git replace --grafts. Ich bin mir nicht sicher, wann dies hinzugefügt wurde, aber sein ganzer Zweck besteht darin, einen Ersatz zu erstellen, der mit Commit B identisch ist, jedoch mit den angegebenen Eltern.
Paul Wagland
32

Beachten Sie, dass zum Ändern eines Commits in Git alle darauf folgenden Commits geändert werden müssen. Dies wird nicht empfohlen, wenn Sie diesen Teil der Geschichte veröffentlicht haben und jemand seine Arbeit möglicherweise auf der Geschichte aufgebaut hat, die es vor der Änderung war.

Eine alternative Lösung, git rebasedie in Bernsteins Antwort erwähnt wird, besteht darin, den Transplantationsmechanismus (siehe Definition von Git-Transplantaten im Git-Glossar und Dokumentation der .git/info/graftsDatei in der Dokumentation zum Git-Repository-Layout ) zu verwenden, um das übergeordnete Element eines Commits zu ändern und zu überprüfen, ob es mit einem Verlaufs-Viewer ( gitk, git log --graphusw.) und verwenden Sie dann git filter-branch(wie im Abschnitt "Beispiele" der Manpage beschrieben), um es dauerhaft zu machen (und entfernen Sie dann das Transplantat und entfernen Sie optional die Original-Refs, die durch gesichert wurdengit filter-branch das Repository , oder klonen Sie das Repository neu):

Echo "$ commit-id $ transplant-id" >> .git / info / transplants
Git Filter-Zweig $ Graft-ID..HEAD

HINWEIS !!! Diese Lösung unterscheidet sich von der Rebase-Lösung darin, dass Änderungen durchgit rebase Rebase / Transplantation wiederhergestellt werden , während die auf Transplantaten basierende Lösung die Commits einfach so wie sie ist reparieren würde , ohne die Unterschiede zwischen alten und neuen Eltern zu berücksichtigen!

Jakub Narębski
quelle
3
Soweit ich weiß (von git.wiki.kernel.org/index.php/GraftPoint ), wurden git replaceGit-Transplantate ersetzt (vorausgesetzt, Sie haben Git 1.6.5 oder höher).
Alexander Bird
4
@ Thr4wn: Im Großen und Ganzen wahr, aber wenn Sie den Verlauf neu schreiben möchten, ist Grafts + Git-Filter-Branch (oder interaktive Rebase oder Fast-Export + zB Reposurgeon) der richtige Weg. Wenn Sie die Geschichte bewahren wollen / müssen , ist Git-Replace den Transplantaten weit überlegen.
Jakub Narębski
@ JakubNarębski, könnten Sie erklären , was Sie bedeuten Rewrite vs bewahren Geschichte? Warum kann ich git-replace+ nicht verwenden git-filter-branch? In meinem Test filter-branchschien der Ersatz zu respektieren und den gesamten Baum neu zu gründen.
cdunn2001
1
@ cdunn2001: git filter-branchschreibt die Geschichte neu, respektiert Transplantate und Ersetzungen (aber in der umgeschriebenen Geschichte werden Ersetzungen strittig sein); Push führt zu einer nicht fasf-forward Änderung. git replaceschafft vor Ort übertragbare Ersatzprodukte; Push wird schnell vorgespult, aber Sie müssten Push ausführen refs/replace, um Ersatz zu übertragen, um den Verlauf zu korrigieren. HTH
Jakub Narębski
23

Um die obigen Antworten zu klären und mein eigenes Skript schamlos einzustecken:

Es hängt davon ab, ob Sie "neu gründen" oder "reparieren" möchten. Eine Rebase , wie von Amber vorgeschlagen , bewegt sich um Unterschiede . Ein Reparent bewegt sich , wie von Jakub und Chris vorgeschlagen , um Schnappschüsse des gesamten Baumes. Wenn Sie reparieren möchten, empfehle ich, git reparentdie Arbeit nicht manuell , sondern zu verwenden.

Vergleich

Angenommen, Sie haben das Bild links und möchten, dass es wie das Bild rechts aussieht:

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

Sowohl das Rebasieren als auch das Reparentieren ergeben das gleiche Bild, aber die Definition von C'unterscheidet sich. With git rebase --onto A B, C'enthält keine Änderungen, die von eingeführt wurden B. Mit git reparent -p A, C'wird identisch sein C(außer , dass Bnicht in der Geschichte sein wird).

Mark Lodato
quelle
1
+1 Ich habe mit dem reparentSkript experimentiert . es funktioniert wunderbar; sehr zu empfehlen . Ich würde auch empfehlen, vor dem Aufruf ein neues branchLabel und checkoutdiesen Zweig zu erstellen (da sich das Zweigetikett verschiebt).
Rhubbarb
Es ist auch erwähnenswert, dass: (1) der ursprüngliche Knoten intakt bleibt und (2) der neue Knoten die untergeordneten Knoten des ursprünglichen Knotens nicht erbt.
Rhubbarb
0

Auf jeden Fall hat mir die Antwort von @ Jakub vor einigen Stunden geholfen, als ich genau das Gleiche wie das OP versuchte.
Allerdings git replace --graftist jetzt die einfachere Lösung in Bezug auf Transplantationen. Ein Hauptproblem bei dieser Lösung war auch, dass ich durch jeden Filterzweig jeden Zweig verlor, der nicht mit dem HEAD-Zweig zusammengeführt wurde. Dann git filter-repohat der Job perfekt und fehlerfrei gemacht.

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

Weitere Informationen: Lesen Sie den Abschnitt "Re-Grafting-Verlauf" in den Dokumenten

Teodoro
quelle