Git im Team

Git Merge oder Rebase: welche Historie euer Team in zwei Jahren noch lesen kann

Die Frage klingt nach Geschmack und ist keine. Sie entscheidet, ob ihr einen Fehler in zwanzig Minuten eingrenzt und ob ein erzwungener Push fremde Arbeit kostet.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Die Diskussion dreht sich um das Bild im Werkzeug, nicht um die Arbeit danach

Der Streit läuft in fast jedem Team gleich ab. Die eine Seite zeigt den Verlauf in der grafischen Oberfläche, sieht ein Geflecht aus Linien und nennt es unlesbar. Die andere Seite hält dagegen, dass ein Rebase die Geschichte fälscht. Beide reden über ein Bild. Was am Ende zählt, ist etwas anderes: ob jemand in einem halben Jahr herausfindet, warum eine Zeile so aussieht, wie sie aussieht, und ob sich ein Beitrag ohne Nebenwirkungen zurücknehmen lässt.

In die Rebase-Richtung falsch entschieden kostet Arbeit anderer Leute. Wer einen Branch umschreibt, den ein Kollege bereits geholt hat, erzeugt zwei Historien mit demselben Namen. Der nächste Push geht nur noch mit Gewalt durch, und wenn dazwischen jemand anders etwas hochgeladen hat, ist genau das weg. Das passiert nicht aus Böswilligkeit, sondern weil git push --force die naheliegende Reaktion auf eine Fehlermeldung ist, die man nicht versteht.

In die Merge-Richtung falsch entschieden kostet stiller. Wenn jeder Zwischenstand aus jedem Branch ungefiltert in main landet, wächst eine Historie, in der git log nichts mehr erzählt und git bisect auf Commits stößt, die gar nicht bauen. Die Fehlersuche dauert dann nicht Minuten, sondern einen Vormittag, und niemand rechnet diesen Vormittag der Entscheidung von damals zu.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der direkte Vergleich

Die Kriterien, an denen die Wahl tatsächlich hängt

Verglichen werden drei Wege, einen fertigen Beitrag in den Hauptstand zu bringen, und zwar an dem, was danach passiert: Fehlersuche, Rücknahme, Konfliktarbeit und die Frage, wie viel jemand wissen muss, um nichts kaputt zu machen.

Merge

git merge erzeugt einen Commit mit zwei Vorgängern, beide Linien bleiben unverändert erhalten

Rebase

git rebase setzt deine Commits neu auf einen anderen Ausgangspunkt auf, sie bekommen dabei neue IDs

Squash

der gesamte Beitrag wird zu einem einzigen neuen Commit auf dem Hauptstand zusammengefasst

Wie leicht erkennt jemand später, in welchem Zusammenhang eine Änderung entstanden ist?

Merge

Der Merge-Commit hält fest, welcher Beitrag wann in den Hauptstand gelangt ist. Mit git log --first-parent bekommst du eine Liste der übernommenen Beiträge statt aller Einzelschritte, und wer tiefer schauen will, geht in den Branch hinein.

Rebase

Die Commits stehen in einer geraden Reihe, die Zugehörigkeit zu einem Beitrag ergibt sich nur noch aus den Nachrichten. Wer sie braucht, muss sie hineinschreiben, etwa als Vorgangsnummer, sonst ist sie nach dem Übernehmen verloren.

Squash

Ein Beitrag wird ein Commit. Die Zwischenschritte, an denen ein größerer Umbau nachvollziehbar wird, sind weg, und die zusammengesetzte Nachricht ist selten so gut wie die, die jemand bewusst geschrieben hätte.

Wie riskant ist das Vorgehen auf einem Branch, an dem mehrere gleichzeitig arbeiten?

Merge

Es wird nichts umgeschrieben. Jeder kann jederzeit holen und schieben, und ein Stand, den jemand bereits hat, bleibt gültig.

Rebase

Die Commits bekommen neue IDs, der alte Stand verschwindet aus dem Branch. Wer ihn schon geholt hat, hat danach eine abweichende Historie, und der nötige Push mit Gewalt kann Arbeit überschreiben, die dazwischen hochgeladen wurde.

Squash

Auf dem Zielbranch ist es harmlos, auf dem Quellbranch nicht. Nach dem Zusammenfassen kennt der Hauptstand die ursprünglichen Commits nicht, deshalb gehört der Branch danach gelöscht und nicht weiterbenutzt.

Wie viel Konfliktarbeit fällt an?

Merge

Es entsteht ein Ergebnis, also wird ein Konflikt einmal aufgelöst und einmal geprüft. Bei lange getrennten Linien ist diese eine Auflösung dafür groß.

Rebase

Jeder Commit wird einzeln neu aufgesetzt, deshalb kann derselbe Konflikt mehrfach hintereinander auflaufen. git rerere merkt sich einmal getroffene Auflösungen und wendet sie wieder an, das nimmt einen Teil der Wiederholung ab.

Squash

Es entsteht ein einziges Ergebnis und damit höchstens eine Auflösung, unabhängig davon, wie viele Zwischenstände der Beitrag hatte.

Wie gut lässt sich ein Fehler mit git bisect eingrenzen?

Merge

git bisect läuft über alle Commits, auch über die Zwischenstände aus den Branches, und trifft dort auf Stände, die gar nicht bauen. Mit git bisect skip oder einer Suche entlang der ersten Elternlinie bleibt es beherrschbar, es kostet aber Handgriffe.

Rebase

Eine gerade Reihe von Commits, die alle auf einem aktuellen Stand aufsetzen. Wenn jeder einzelne für sich lauffähig war, führt die Halbierungssuche ohne Umwege zum verursachenden Schritt.

Squash

Wenige große Commits, die Suche ist in kurzer Zeit fertig. Das Ergebnis zeigt danach allerdings auf eine Änderung über viele Dateien, und die eigentliche Zeile musst du darin selbst finden.

Wie leicht nimmst du einen Beitrag vollständig zurück?

Merge

git revert -m 1 auf den Merge-Commit nimmt den gesamten Beitrag zurück. Eine Falle gehört dazu: Ein späteres erneutes Zusammenführen desselben Branches bringt die Änderungen nicht von allein wieder, dafür muss zuerst die Rücknahme zurückgenommen werden.

Rebase

Es gibt keine Klammer um den Beitrag. Du musst die zugehörigen Commits selbst heraussuchen und einzeln zurücknehmen, und ob du alle erwischt hast, hängt an den Nachrichten.

Squash

Ein Commit, ein git revert, fertig. Das ist der Grund, warum viele Teams bei kleinen Beiträgen genau dabei bleiben.

Wie viel muss jemand wissen, um nichts kaputt zu machen?

Merge

git merge, git pull, im Konfliktfall git merge --abort. Wer neu dazukommt, kann mit diesen drei Befehlen nichts anrichten, das nicht rückgängig zu machen wäre.

Rebase

Interaktiver Umbau, abgebrochene Läufe, das Zurückholen eines verlorenen Stands über git reflog und der Push mit --force-with-lease sind mehrere Handgriffe, die sitzen müssen. Sitzen sie nicht, entstehen genau die Vorfälle, wegen derer Rebase seinen Ruf hat.

Squash

Auf der Plattform ist es ein Knopf im Merge Request. Zu erklären bleibt, dass der Quellbranch danach nicht weiterverwendet wird, weil er sonst beim nächsten Mal alles noch einmal mitbringt.

Was passt wann

Wenn mehrere gleichzeitig auf demselben Branch arbeiten oder eine Pipeline darauf läuft
führ zusammen und schreib nichts um, das schon jemand anders geholt hat.
Wenn du deinen eigenen Branch vor dem Review auf den aktuellen Hauptstand heben willst
nimm git rebase, solange dieser Branch außer dir niemand hat, und schieb ihn mit --force-with-lease.
Wenn ein Beitrag aus vielen Zwischenständen besteht, von denen keiner für sich lauffähig ist
fass ihn beim Übernehmen zusammen und steck die gesparte Zeit in eine Commit-Nachricht, die den Grund nennt.

Fünf Fragen, und das Vorgehen steht fest

  1. 01 Hat außer dir schon jemand diese Commits geholt? Dann wird nichts umgeschrieben.
  2. 02 Geht es um deinen eigenen Branch vor dem Review? Dann ist Rebase der saubere Weg.
  3. 03 Geht es in den Hauptstand hinein? Dann führ zusammen und behalte die Klammer.
  4. 04 Steht jeder Zwischenschritt für sich? Wenn nicht, fass den Beitrag zusammen.
  5. 05 Braucht ihr einen Force-Push? Dann nur mit --force-with-lease.
Was du mitnimmst

Was ihr nach dieser Seite in einer Sitzung festlegt

Es sind sechs Festlegungen, mehr braucht eine Vereinbarung zu Git nicht. Sie passen auf eine halbe Seite im Wiki, und der wichtigste Teil ist nicht die Wahl zwischen Merge und Rebase, sondern die Grenze, ab der ein Branch als geteilt gilt.

Die Grenze zwischen privat und geteilt ziehen

Solange ein Branch nur bei dir liegt, darfst du damit alles machen. Sobald er auf dem Server steht und jemand anders daran arbeitet oder eine Pipeline darauf läuft, ist er geteilt und wird nicht mehr umgeschrieben. Diese eine Linie beseitigt den größten Teil der Vorfälle.

Den eigenen Branch vor dem Review aufräumen

Ein git rebase auf den aktuellen Hauptstand kurz vor dem Review sorgt dafür, dass der Beitrag gegen das geprüft wird, was gerade gilt. Mit git rebase -i lassen sich dabei Tippfehler-Commits zusammenfassen, sodass im Review nur noch Schritte stehen, die jemand nacheinander lesen kann.

In den Hauptstand hinein immer zusammenführen

Der Beitrag geht per Merge in main, nicht per Rebase des Hauptstands. Damit bleibt eine Klammer um den Beitrag, an der ihr später sehen könnt, was zusammengehört, und an der ein git revert -m 1 ansetzen kann.

Force-Push nur mit Sicherung

Wenn ein erzwungener Push nötig ist, dann als git push --force-with-lease. Die Variante bricht ab, sobald sich der Stand auf dem Server seit deinem letzten Holen verändert hat, und genau das ist der Fall, in dem sonst fremde Arbeit verschwindet.

Das Zusammenfassen bewusst einsetzen

Ein Beitrag aus fünfzehn Zwischenständen, von denen keiner für sich lauffähig ist, gehört beim Übernehmen zu einem Commit zusammengefasst. Ein Beitrag, der aus einer Umbenennung, einem Umbau und einer Funktion besteht, gehört das nicht, weil dann drei nachvollziehbare Schritte zu einem unlesbaren werden.

Die Voreinstellung für das Holen festlegen

git config --global pull.rebase true stellt git pull auf Rebase um und verhindert die Merge-Commits, die beim Holen des eigenen Branches sonst nebenbei entstehen. Wer das nicht will, setzt pull.ff only und führt bewusst zusammen. Was ihr auf keinen Fall wollt, ist ein Team, in dem die Einstellung auf jedem Rechner anders ist.

Die Nachricht wichtiger nehmen als die Form

Eine gerade Linie aus Commits mit der Nachricht fix bringt niemanden weiter. Ein Merge-Geflecht mit Nachrichten, die den Grund einer Änderung nennen und die Vorgangsnummer tragen, sehr wohl. Die Form ist die kleinere Hälfte der Frage.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Was ein Rebase tatsächlich tut

Ein Commit in Git ist unveränderlich. Seine ID ist ein Hash über den Inhalt, über die Metadaten und über den Vorgänger, und deshalb kann ein Commit nicht auf einen anderen Ausgangspunkt gesetzt werden. git rebase verschiebt also nichts, es erzeugt neue Commits mit demselben Inhalt und einem anderen Vorgänger. Die alten bleiben zunächst im Repository liegen, sie sind nur nicht mehr von einem Branch aus erreichbar.

Genau darin liegt der Ausweg, wenn ein Rebase schiefgeht. git reflog listet, wo dein Branch in den letzten Tagen überall stand, und mit git reset --hard und einem Eintrag daraus bist du wieder beim Stand vor dem Umbau. Das gilt auch nach einem interaktiven Rebase, bei dem etwas verlorengegangen zu sein scheint. Verloren ist es erst, wenn die Aufräumfunktion die unerreichbaren Objekte irgendwann entfernt, und das dauert.

Der zweite Punkt betrifft die Zeitstempel. Ein Commit trägt zwei davon, das Autorendatum und das Committerdatum. Beim Rebase bleibt das Autorendatum erhalten, das Committerdatum ist neu. Deshalb sieht eine per Rebase begradigte Historie in der Reihenfolge stimmig aus, während die Zeitstempel darin nicht mehr die tatsächliche Entstehungsreihenfolge abbilden. Für die Suche nach einer Ursache ist das selten ein Problem, für eine Auswertung, die Zeitstempel ernst nimmt, schon.

Warum Rebase auf geteilten Branches Ärger macht

Nach einem Rebase zeigt dein Branch auf neue Commits. Der Server kennt noch die alten, und weil die neuen keinen davon als Vorgänger haben, lehnt er den Push ab. Die Fehlermeldung nennt das non-fast-forward, also nicht vorspulbar, und gemeint ist: Du willst Geschichte ersetzen. Der Reflex ist git push --force, und der ersetzt alles auf dem Server durch das, was du hast. Was jemand anders in der Zwischenzeit dorthin geschoben hat, ist damit aus dem Branch verschwunden.

Wiederherstellbar ist das meist, weil die Plattform Reflogs führt und die betroffene Person ihren Stand noch lokal hat, angenehm ist es trotzdem nicht. Die vorbeugende Maßnahme ist eine Zeile Konfiguration: git config --global alias.pushf "push --force-with-lease" oder gleich die Gewöhnung daran, --force-with-lease auszuschreiben. Die Variante vergleicht vorher, ob der Server noch auf dem Stand steht, den du zuletzt geholt hast, und bricht sonst ab.

Die zweite Baustelle sind die anderen im Team. Wer den alten Stand bereits geholt hat und danach zieht, bekommt eine Historie mit denselben Änderungen zweimal, einmal als alte und einmal als neue Commits. Wer das mit einem Merge auflöst, hat den Beitrag doppelt im Verlauf stehen, und danach diskutiert das Team über Geisterkonflikte, die niemand mehr zuordnen kann. Der saubere Weg für die Betroffenen heißt git rebase --onto oder schlicht: den lokalen Branch wegwerfen und neu holen, wenn dort nichts Eigenes mehr liegt.

Was der Merge-Commit für die Fehlersuche wert ist

Der häufigste Vorwurf gegen Merge-Commits lautet, sie seien Rauschen. Das stimmt für den Blick auf den vollständigen Verlauf und ist falsch für den Blick, den man bei einer Störung tatsächlich braucht. git log --first-parent folgt nur der Hauptlinie und zeigt damit eine Liste der Beiträge, die in den Hauptstand gekommen sind, jeder als ein Eintrag. Das ist die Sicht, die eine Freigabe beschreibt, und ohne Merge-Commits gibt es sie nicht.

Beim Eingrenzen einer Ursache mit git bisect wirkt derselbe Unterschied in beide Richtungen. Über alle Commits zu suchen findet den genauen Schritt, führt aber durch Zwischenstände aus Branches, die nie gebaut haben. Entlang der ersten Elternlinie zu suchen findet den Beitrag statt des Schritts, dafür in wenigen, verlässlich lauffähigen Sprüngen. In der Praxis ist der zweite Weg der schnellere, und danach grenzt man innerhalb des gefundenen Beitrags weiter ein.

Der dritte Nutzen ist die Rücknahme. Ein Merge-Commit ist die einzige Stelle im Verlauf, an der ein ganzer Beitrag als eine Einheit greifbar ist. git revert -m 1 nimmt ihn zurück, ohne dass jemand die zugehörigen Einzelcommits heraussuchen muss. Bei einer Störung im Betrieb zählt genau das, weil unter Zeitdruck niemand Nachrichten durchliest.

Was die Plattform daraus macht

GitHub, GitLab und die übrigen Oberflächen bieten für das Übernehmen eines Beitrags drei Wege an, und sie heißen dort ungefähr so wie hier: einen Merge-Commit erzeugen, zusammenfassen, oder die Commits einzeln aufsetzen. Die Einstellung liegt am Repository und nicht bei der Person, die auf den Knopf drückt, und genau deshalb lohnt es sich, sie einmal bewusst zu setzen statt jedes Mal neu zu entscheiden.

Eine verbreitete Kombination ist, das Zusammenfassen für kleine Beiträge zu erlauben, den Merge-Commit für alles zu verlangen, was mehrere nachvollziehbare Schritte enthält, und das Aufsetzen der Commits einzeln ganz abzuschalten, weil es die Nachteile beider Wege verbindet: neue IDs ohne Klammer um den Beitrag. Dazu kommt der Schutz des Hauptbranches, der erzwungene Pushs dorthin verbietet und damit den gefährlichsten Fall von vornherein ausschließt.

Wichtig ist außerdem, was mit dem Quellbranch passiert. Nach dem Zusammenfassen und nach dem einzelnen Aufsetzen passt er nicht mehr zum Hauptstand, weil dort andere Commits liegen. Wer ihn weiterbenutzt, schleppt beim nächsten Beitrag die alten Änderungen erneut mit. Das automatische Löschen nach dem Übernehmen ist deshalb keine Ordnungsliebe, sondern verhindert eine ganze Klasse von Konflikten.

Eine Vereinbarung, die im Alltag trägt

Was sich in Teams bewährt hat, ist eine Mischung und keine Entscheidung für eine Seite. Auf dem eigenen Branch wird rebaset, damit der Beitrag gegen den aktuellen Stand geprüft wird und die Zwischenstände lesbar sind. In den Hauptstand hinein wird zusammengeführt, damit die Klammer erhalten bleibt. Zusammengefasst wird da, wo ein Beitrag ohnehin nur einen Schritt darstellt.

Dazu gehören zwei Zahlen, über die selten gesprochen wird. Die erste ist die Lebensdauer eines Branches: Je länger er lebt, desto größer wird der Konflikt am Ende, und zwar überproportional. Die zweite ist die Häufigkeit, mit der ihr den Hauptstand in laufende Beiträge holt. Wer das täglich tut, hat kleine Konflikte in kleinen Portionen; wer bis zum Schluss wartet, hat einen einzigen großen zu einem Zeitpunkt, an dem alle unter Termindruck stehen.

Und ein Satz zur Durchsetzung: Eine Vereinbarung, die nur im Wiki steht, hält nicht. Was hält, ist der Schutz des Hauptbranches auf der Plattform, eine einheitliche Voreinstellung für git pull und die Gewohnheit, im Review nicht nur den Inhalt zu prüfen, sondern auch, ob der Beitrag aus Schritten besteht, die jemand nacheinander lesen kann.

Dazu passende Kurse

Weil sich diese Handgriffe an Beispielen schlecht lernen lassen und im Ernstfall sitzen müssen, sind Git-Kurse, in denen Merge und Rebase am eigenen Repository durchgespielt werden der schnellste Weg zu einer gemeinsamen Vereinbarung im Team.

Wenn im Team noch mehr offen ist als der Umgang mit der Historie, decken die Grundlagen der Entwicklung im Team den Rahmen von Versionskontrolle bis Review ab.

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Einführung in die Programmierung für Programmiereinsteiger
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Groovy Grundkurs
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Unreal Engine Kurs für Einsteiger

Häufige Fragen

Ist ein Merge-Commit wirklich nur Rauschen?
Im vollständigen Verlauf ja, in der Sicht, die man bei einer Störung braucht, nein. git log --first-parent zeigt genau eine Zeile je übernommenem Beitrag, und git revert -m 1 nimmt einen Beitrag als Ganzes zurück. Beides gibt es ohne Merge-Commit nicht. Was tatsächlich Rauschen erzeugt, sind die Merge-Commits, die nebenbei beim Holen des eigenen Branches entstehen, und die verhindert ihr mit einer einheitlichen Einstellung für git pull.
Wann darf ich einen Force-Push machen?
Auf deinem eigenen Branch, solange ihn außer dir niemand geholt hat, und auf keinen Fall auf einem gemeinsamen Branch oder dem Hauptstand. Benutz dabei --force-with-lease statt --force: Die Variante prüft, ob sich der Stand auf dem Server seit deinem letzten Holen verändert hat, und bricht in genau dem Fall ab, in dem sonst fremde Arbeit verschwindet.
Ein Rebase ist mittendrin schiefgegangen, was jetzt?
Solange der Lauf noch offen ist, bringt dich git rebase --abort zurück auf den Stand davor, ohne Rückstände. Ist der Rebase schon durchgelaufen und das Ergebnis falsch, hilft git reflog: Dort steht, wo dein Branch vorher stand, und mit git reset --hard und dem passenden Eintrag bist du wieder dort. Die alten Commits sind nicht gelöscht, sie sind nur nicht mehr von einem Branch aus erreichbar.
Sollen wir git pull auf Rebase umstellen?
In den meisten Teams ja, weil es die unnötigen Merge-Commits verhindert, die beim Holen des eigenen Branches entstehen. Wichtig ist nur, dass alle dieselbe Einstellung haben, sonst sieht der Verlauf je nach Rechner anders aus. Die Alternative ist pull.ff only, das den Vorgang abbricht, wenn nicht einfach vorgespult werden kann, und die Entscheidung damit bewusst in deine Hand legt.
Zusammenfassen oder nicht?
Zusammenfassen ist richtig, wenn der Beitrag ohnehin einen Schritt darstellt und die Zwischenstände nur den Weg dorthin zeigen. Es ist falsch, wenn der Beitrag aus einer Umbenennung, einem Umbau und einer neuen Funktion besteht, denn dann werden drei getrennt lesbare Änderungen zu einer, in der später niemand mehr das Wesentliche vom Beiwerk trennen kann.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Die Handgriffe einmal am eigenen Repository durchspielen

Im Git-Kurs bei cmt baust du eine Historie absichtlich kaputt und holst sie mit reflog zurück. Danach ist der Unterschied zwischen Merge und Rebase keine Meinungsfrage mehr, sondern eine Entscheidung mit bekannten Folgen.