Rebase vs. Merge: Integration von Änderungen in Git

Avatar of Tobias Günther
Tobias Günther am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Dieser Artikel ist Teil unserer Serie „Advanced Git“. Folgen Sie uns unbedingt auf Twitter oder melden Sie sich für unseren Newsletter an, um über die nächsten Artikel informiert zu werden!

Die meisten Entwickler verstehen, dass es wichtig ist, Branches in Git zu verwenden. Tatsächlich habe ich bereits einen ganzen Artikel über Branching-Strategien in Git geschrieben, in dem das mächtige Branching-Modell von Git, die verschiedenen Arten von Branches und zwei der gängigsten Branching-Workflows erklärt werden. Zusammenfassend lässt sich sagen: Separate Container, also Branches, für die eigene Arbeit zu haben, ist unglaublich hilfreich und einer der Hauptgründe für die Verwendung eines Versionskontrollsystems.

In diesem Artikel werden wir uns mit der Integration von Branches beschäftigen. Wie kann man neuen Code wieder in eine bestehende Entwicklungslinie zurückbringen? Es gibt verschiedene Möglichkeiten, dies zu erreichen. Die fünfte Folge unserer Serie „Advanced Git“ behandelt die Integration von Änderungen in Git, nämlich das Merging und Rebasing.

Bevor wir ins Detail gehen, ist es wichtig zu verstehen, dass beide Befehle – `git merge` und `git rebase` – dasselbe Problem lösen. Sie integrieren Änderungen von einem Git-Branch in einen anderen Branch; sie tun dies nur auf unterschiedliche Weise. Beginnen wir mit Merging und wie es tatsächlich funktioniert.

Verständnis von Merges

Um einen Branch in einen anderen zu mergen, können Sie den Befehl `git merge` verwenden. Nehmen wir an, Sie haben einige neue Commits auf einem Ihrer Branches, branch-B, und möchten diesen Branch nun in einen anderen, branch-A, mergen. Dazu können Sie Folgendes eingeben:

$ git checkout branch-A
$ git merge branch-B

Als Ergebnis erstellt Git einen neuen Merge-Commit in Ihrem aktuellen Arbeitsbranch (im Beispiel branch-A), der die Historien beider Branches verbindet. Um dies zu bewerkstelligen, sucht Git nach drei Commits

  • Der erste ist der „gemeinsame Vorfahren-Commit“. Wenn man die Historie zweier Branches in einem Projekt verfolgt, haben sie immer mindestens einen gemeinsamen Commit. An diesem Punkt haben beide Branches den gleichen Inhalt. Danach haben sie sich unterschiedlich entwickelt.
  • Die beiden anderen interessanten Commits sind die Endpunkte jedes Branches, d.h. ihre aktuellen Zustände. Denken Sie daran, dass das Ziel einer Integration darin besteht, die aktuellen Zustände zweier Branches zu kombinieren. Ihre neuesten Revisionen sind also natürlich wichtig.

Die Kombination dieser drei Commits führt zur gewünschten Integration.

Zugegebenermaßen ist dies ein vereinfachtes Szenario – einer der beiden Branches (branch-A) hat seit seiner Erstellung keine neuen Commits mehr erhalten, was in den meisten Softwareprojekten sehr unwahrscheinlich ist. Sein letzter Commit in diesem Beispiel ist daher auch der gemeinsame Vorfahre.

In diesem Fall ist die Integration denkbar einfach: Git kann einfach alle neuen Commits von branch-B oben auf den gemeinsamen Vorfahren-Commit legen. In Git wird diese einfachste Form der Integration als „Fast-Forward“-Merge bezeichnet. Beide Branches teilen dann exakt dieselbe Historie (und es ist kein zusätzlicher „Merge-Commit“ erforderlich).

In den meisten Fällen würden sich jedoch beide Branches mit unterschiedlichen Commits weiterentwickeln. Nehmen wir also ein realistischeres Beispiel

Um eine Integration durchzuführen, muss Git einen neuen Commit erstellen, der alle Änderungen enthält, und sich um die Unterschiede zwischen den Branches kümmern – das nennen wir einen Merge-Commit.

Menschliche Commits und Merge-Commits

Normalerweise wird ein Commit sorgfältig von einem Menschen erstellt. Er ist eine sinnvolle Einheit, die nur zusammenhängende Änderungen enthält, plus eine aussagekräftige Commit-Nachricht, die Kontext und Anmerkungen liefert.

Ein Merge-Commit ist nun etwas anders: Er wird nicht von einem Entwickler erstellt, sondern automatisch von Git. Außerdem enthält ein Merge-Commit nicht unbedingt eine „semantische Sammlung zusammenhängender Änderungen“. Stattdessen ist sein Zweck lediglich, zwei (oder mehr) Branches zu verbinden und den Knoten zu knüpfen.

Wenn man eine solche automatische Merge-Operation verstehen möchte, muss man sich die Historie aller Branches und ihre jeweiligen Commit-Historien ansehen.

Integration mit Rebases

Bevor wir über Rebasing sprechen, möchte ich eines klarstellen: Ein Rebase ist nicht besser oder schlechter als ein Merge, er ist einfach anders. Man kann ein glückliches (Git-)Leben führen, indem man nur Branches mergt und nie über Rebasing nachdenkt. Es hilft jedoch, zu verstehen, was ein Rebase tut, und die damit verbundenen Vor- und Nachteile zu lernen. Vielleicht erreichen Sie in einem Projekt einen Punkt, an dem ein Rebase hilfreich sein könnte…

Also, los geht's! Erinnern Sie sich, dass wir gerade über automatische Merge-Commits gesprochen haben? Manche Leute sind von diesen nicht so begeistert und ziehen es vor, ohne sie auszukommen. Außerdem gibt es Entwickler, die möchten, dass die Projekt-Historie wie eine gerade Linie aussieht – ohne jeglichen Hinweis darauf, dass sie zu einem bestimmten Zeitpunkt in mehrere Branches aufgeteilt war, selbst nachdem die Branches integriert wurden. Dies ist im Grunde das, was während eines Git-Rebase geschieht.

Rebasing: Schritt für Schritt

Lassen Sie uns eine Rebase-Operation Schritt für Schritt durchgehen. Das Szenario ist dasselbe wie in den vorherigen Beispielen, und so sieht der Ausgangspunkt aus

Wir möchten die Änderungen von branch-B in branch-A integrieren – aber per Rebase, nicht per Merge. Der eigentliche Git-Befehl dafür ist sehr einfach:

$ git checkout branch-A
$ git rebase branch-B

Ähnlich wie beim Befehl git merge geben Sie Git an, welchen Branch Sie integrieren möchten. Werfen wir einen Blick hinter die Kulissen…

In diesem ersten Schritt wird Git alle Commits auf branch-A „entfernen“, die nach dem gemeinsamen Vorfahren-Commit stattgefunden haben. Keine Sorge, sie werden nicht verworfen: Sie können sich diese Commits so vorstellen, als wären sie „geparkt“ oder vorübergehend an einem sicheren Ort gespeichert.

Im zweiten Schritt wendet Git die neuen Commits von branch-B an. Zu diesem Zeitpunkt sehen beide Branches vorübergehend exakt gleich aus.

Schließlich werden die „geparkten“ Commits (die neuen Commits von branch-A) hinzugefügt. Da sie oben auf den integrierten Commits von branch-B platziert werden, werden sie rebased.

Als Ergebnis sieht die Projekt-Historie so aus, als hätte die Entwicklung in einer geraden Linie stattgefunden. Es gibt keinen Merge-Commit, der alle kombinierten Änderungen enthält, und die ursprüngliche Commit-Struktur bleibt erhalten.

Mögliche Fallstricke beim Rebasing

Noch etwas – und das ist wichtig, um Git-Rebase zu verstehen – ist, dass es die Commit-Historie umschreibt. Werfen Sie noch einmal einen Blick auf unser letztes Diagramm. Commit C3* hat ein Sternchen. Obwohl C3* denselben Inhalt wie C3 hat, ist es effektiv ein anderer Commit. Warum? Weil es nach dem Rebase einen neuen Eltern-Commit hat. Vor dem Rebase war C1 der Eltern-Commit. Nach dem Rebase ist der Eltern-Commit C4 – in den es rebased wurde.

Ein Commit hat nur eine Handvoll wichtiger Eigenschaften, wie Autor, Datum, Änderungen und seinen Eltern-Commit. Die Änderung irgendeiner dieser Informationen erstellt einen komplett neuen Commit mit einer neuen SHA-1-Hash-ID.

Das Umschreiben der Historie ist kein Problem für Commits, die noch nicht veröffentlicht wurden. Aber Sie könnten in Schwierigkeiten geraten, wenn Sie Commits umschreiben, die Sie bereits in ein Remote-Repository gepusht haben. Vielleicht hat jemand anderes seine Arbeit auf dem ursprünglichen C3-Commit aufgebaut, und nun existiert er plötzlich nicht mehr…

Damit Sie nicht in Schwierigkeiten geraten, hier eine einfache Regel für die Verwendung von Rebase: Verwenden Sie niemals Rebase auf öffentlichen Branches, d.h. auf Commits, die bereits in ein Remote-Repository gepusht wurden! Verwenden Sie stattdessen `git rebase`, um Ihre lokale Commit-Historie zu bereinigen, bevor Sie sie in einen gemeinsamen Team-Branch integrieren.

Integration ist alles!

Letztendlich sind Merging und Rebasing beides nützliche Git-Strategien, je nachdem, was Sie erreichen möchten. Merging ist eher zerstörungsfrei, da ein Merge die bestehende Historie nicht verändert. Rebasing hingegen kann helfen, Ihre Projekt-Historie zu bereinigen, indem unnötige Merge-Commits vermieden werden. Denken Sie nur daran, dies nicht in einem öffentlichen Branch zu tun, um Ihre Mitentwickler nicht zu verärgern.

Wenn Sie tiefer in fortgeschrittene Git-Tools eintauchen möchten, können Sie sich gerne meinen (kostenlosen!) „Advanced Git Kit“ ansehen: Es ist eine Sammlung kurzer Videos zu Themen wie Branching-Strategien, Interaktives Rebase, Reflog, Submodules und vieles mehr.

Viel Spaß beim Mergen und Rebasing – und bis bald zum nächsten Teil unserer „Advanced Git“-Serie!