Commit-Historie von Git: Tipps und Tricks

In der Software-Entwicklung wird heutzutage meistens Git zur Versionsverwaltung eingesetzt. Viele Entwickler kennen die Grundfunktionen, wissen aber nicht, wie viel Git noch zu bieten hat und wie sie davon in ihren Projekten profitieren können. Dieser Artikel fokussiert sich auf die Commit-Historie von Git und zeigt, wie wichtig sie ist und wie man als Entwickler den meisten Nutzen daraus zieht. Zudem erklärt er, was bei den eigenen Commits zu beachten ist, damit eine saubere Historie entsteht, wie sich Fehler korrigieren lassen und welche Fallen man bedenken und meiden soll.
Git: Die Commit-Historie als Informationsquelle
Wenn Git im Projekt eingesetzt wird, protokolliert er jede Code-Änderung und speichert sie als neue Version. Zu jedem sogenannten Commit werden neben den Code-Änderungen noch Metadaten gespeichert. Zu den Metadaten zählen eine kurze Beschreibung, sogenannte Commit-Nachricht, Autor und Datum. Die so entstehende Commit-Historie ermöglicht, vergangene Commits zu sehen und die Code-Änderungen nachzuvollziehen. Entwickler arbeiten üblicherweise mit einem Git Client, der unter anderem die Commit-Historie visualisiert und damit eine Oberfläche für die Arbeit anbietet.
Die Commit-Historie ist besonders wichtig für große und ältere Projekte. Soll eine Code-Stelle geändert werden, die keiner im Entwicklerteam kennt, entstehen oft Fehler, wenn die Änderungen ohne eine Auseinandersetzung mit der ursprünglichen Implementierung und ihre Hintergründe gemacht werden. Die Commit-Historie von Git ist in solchen Fällen oft die einzige Informationsquelle für das Entwicklerteam. Sie macht es möglich, ohne einen Ansprechpartner die bisherigen Code-Änderungen und die Gründe dafür kennenzulernen. Dadurch wird die Fehlerwahrscheinlichkeit bei Bugfixes und Code-Umbauten deutlich reduziert.
Zwei Funktionen von Git kommen in diesem Zusammenhang zum Einsatz: Suche und Blame.
Die Suche in der Commit-Historie
Git bietet sehr flexible Suchmöglichkeiten. Am bekanntesten sind die Freitextsuche in den Commit-Nachrichten und die Einschränkungen nach Autor und Datum. Daneben lässt sich die Suche auf die letzten n Commits auf einen bestimmten Branch oder auf die Unterschiede zwischen zwei Branches einschränken. Es ist außerdem möglich, in den Differenzen (Diffs) zwischen den Versionen zu suchen. Die verschiedenen Git Clients unterstützen unterschiedliche Suchmöglichkeiten und je nach Bedarf können die parallele Benutzung mehrerer Git Clients oder der vorläufige Wechsel zur Git Commando-Zeile sinnvoll sein. Im Folgenden sind einige Beispiele für hilfreiche Suchanfragen und damit verbundene Best Practices aufgeführt.
Wird ein Ticket-System, wie zum Beispiel Jira oder Azure DevOps, für die Erfassung der Anforderungen benutzt, hat jede Anforderung an die Entwicklung eine eindeutige ID. Viele Teams haben die Vorgabe oder die interne Vereinbarung, diese ID in den Commit-Nachrichten einzutragen, damit sich die Commits den Aufgaben zuordnen lassen. Wird das konsistent gemacht, lassen sich mittels Freitextsuche in den Commit-Nachrichten alle Commits identifizieren, die zu einer bestimmten Aufgabe gehören. Das erleichtert wesentlich das Wiederfinden und die Überprüfung der Umsetzung im Fall von Bugs. Gleichzeitig lassen sich dadurch ähnliche Aufgaben sehr schnell umsetzen, weil die Entwickler das bereits funktionierende Beispiel finden und verfolgen können.
Werden die IDs der Aufgaben nicht in den Commit-Nachrichten eingetragen, ist die Identifikation der zugehörigen Commits deutlich schwieriger. Dennoch lassen sich über das Ticket-System der für die Aufgabe zuständige Entwickler und das ungefähre Datum der Umsetzung identifizieren. In Git kann man dann die Suche nach Autor und Datum einschränken. Damit findet man meistens einen oder mehrere Commits zu dieser Aufgabe, oft den Hauptteil der Umsetzung, aber selten die später umgesetzten Verbesserungen und Korrekturen.
Git erlaubt nicht nur die Suche in den Metadaten, wie Autor und Nachricht vom Commit, sondern auch die Suche in den gemachten Code-Änderungen selbst. Dabei werden die Diffs zwischen den Versionen betrachtet und es wird sowohl in den ursprünglichen als auch in den neuen Code-Zeilen gesucht. Diese Filtermöglichkeit macht es einfach zu identifizieren, zum Beispiel, wann und in welchem Zusammenhang eine bestimmte Anweisung entfernt oder verschoben worden ist. Außerdem können so die Unterschiede zwischen zwei Versionen, zum Beispiel zwischen zwei Releases oder zwischen Development Branch und Release Branch, genau betrachtet und durchsucht werden.
Zusammengefasst bietet die Git-Suche sehr viele verschiedene Filtermöglichkeiten, sowohl in Bezug auf die Commit-Metadaten als auch auf die Code-Änderungen selbst. Sie ermöglicht die Beschaffung von zusätzlicher Information über den Code, seine Historie und Hintergründe. Mit ihrer Hilfe lassen sich nicht nur Code-Änderungen schnell finden, sondern auch Umsetzungsfehler vermeiden.
Blame
Die Blame-Funktion von Git zeigt für jede Code-Zeile in einer Datei, durch welchen Commit sie zuletzt geändert worden ist. Sie gibt Auskunft darüber, warum der Code geschrieben oder geändert wurde, von wem und wann.
Die Blame-Funktion ist besonders wichtig, wenn der Verdacht besteht, dass eine Code-Zeile oder ein Code-Abschnitt nicht wie gewünscht funktioniert und korrigiert werden soll. In großen und alten Projekten kommt es zudem oft vor, dass niemand weiß, warum der jeweilige Abschnitt überhaupt da ist. Auch vor der Entfernung oder Verschiebung von Code ist es empfehlenswert, sich ausführlich über die Hintergründe für die gegenwärtige Umsetzung zu informieren.
Die gängigen Git Clients zeigen sämtliche Details zu den einzelnen Commits. Neben den Commit-Metadaten lassen sich schnell alle enthaltenen Code-Änderungen anzeigen. Oft wird zusätzlich noch die komplette Commit-Historie für die Datei aufgelistet. Das gibt zahlreiche Informationen über den Code und seine Geschichte. Ist eine Ticket-ID in der Commit-Nachricht enthalten, so lassen sich vom jeweiligen Ticket noch weitere Details entnehmen.
Die Blame-Funktion gibt somit, ähnlich wie die Suche, ausführliche Information über den Code und seine Hintergründe. Während bei der Suche Code-Änderungen gesucht werden und die gesamte Commit-Historie gefiltert wird, ist der Ausgangspunkt bei Blame eine konkrete Code-Zeile oder ein Code-Abschnitt, zu dem mehr Information benötigt wird. Beide Funktionen, je nach Bedarf separat oder zusammen angewendet, unterstützen den Entwickler dabei, den Code besser zu verstehen. Damit helfen sie ihm, die beste Lösung für seine Aufgabe zu finden und unerwünschte Fehler zu vermeiden.
Commits
Damit die Entwickler in einem Projekt von der Git-Historie profitieren können, sollte diese von allen gepflegt werden. Die Inhalte der Commits sind dabei genauso wichtig wie die Commit-Metadaten.
Commit-Inhalt
Die Code-Änderungen in einem Commit sollen überschaubar und nachvollziehbar sein. Ein Commit soll sich auf ein Thema beziehen, zum Beispiel ein Feature oder ein Bugfix. Änderungen, die nicht damit zusammenhängen, sind separat einzuchecken. Ist eine Aufgabe groß, so ist es oft sinnvoll, sie in mehrere kleinere Aufgaben mit jeweils eigenen Git Commits aufzuteilen. Diese Aufteilung sollte allerdings gut überlegt sein, so dass die einzelnen Commits trotzdem sinnvolle zusammenhängende Code-Änderungen enthalten. Wird ein Fehler in einem lokalen (noch nicht auf dem Server gepushten) Commit entdeckt, zum Beispiel im Laufe der ausführlichen Tests vor dem Pushen, so begründet das keinen neuen Commit für die Korrektur. Stattdessen soll der ursprüngliche Commit korrigiert werden. Das nächste Kapitel erläutert verschiedene Möglichkeiten dafür. Da in Git alles auf Commit-Ebene passiert und sich Commits sehr leicht zusammenfügen lassen, aber sehr schwer zu splitten sind, sind grundsätzlich kleinere Commits besser als größere, die Historie soll dennoch nicht unnötig fragmentiert und mit Korrektur-Commits überflutet werden.
Code-Umbauten
Die Aufteilung der Code-Änderungen in verschiedenen Commits ist besonders wichtig, wenn neben der Entwicklung von einem Feature noch Code umgebaut wird. Werden Properties und Methoden umbenannt oder Code verschoben, sind diese Änderungen nur nachvollziehbar, wenn sie nicht mit gleichzeitigen Änderungen vom Code vermischt sind. Wird eine Datei umbenannt und zugleich noch der Inhalt geändert, so löscht Git meistens die ursprüngliche Datei und fügt eine neue hinzu. Dabei geht die gesamte Commit-Historie der Datei verloren. Das lässt sich verhindern, wenn die Dateiumbenennung als eigenes Commit eingecheckt wird und die Änderungen vom Inhalt in einem separaten Commit gemacht werden. Wird zum Beispiel eine Klasse umbenannt und dabei erweitert, so dürfen die Umbenennung der Klasse und der Datei zusammen eingecheckt werden, weil für Git diese Änderung vom Dateiinhalt unwesentlich ist und nicht zum Löschen der Datei führt. Die Erweiterung der Klasse soll allerdings als separates Commit eingecheckt werden, damit die Historie der Datei erhalten bleibt.
Splitten von Dateien
Beim Splitten von Dateien ist besondere Vorsicht geboten. Während der Entwicklung einer Anwendung werden die Dateien immer mehr und größer. Wenn eine Datei zu groß wird, wird diese oft gesplittet. Die meisten Entwickler fügen in diesem Fall eine neue Datei hinzu und kopieren dorthin einen Teil vom ursprünglichen Code. Das hat den sehr großen Nachteil, dass dadurch die gesamte Git-Historie für diesen Code verloren geht und der Code als neuer Code gilt. Das ist selten sinnvoll.
In Git ist es möglich, die ursprüngliche Datei so zu kopieren, dass die Git-Historie mitkopiert wird und im Nachhinein sowohl für die alte Datei als auch für die Kopie zur Verfügung steht. Dafür sind folgende Schritte notwendig:
1. Datei (vorläufig) umbenennen, diese Umbenennung einchecken und Commit speichern.
git mv File.txt File1.txt git commit REV=`git rev-parse HEAD`
2. Zurück zum ursprünglichen Zustand wechseln.
git reset --hard HEAD^
3. Datei nochmals umbenennen und die Umbenennung einchecken – Damit wird eine Kopie der Datei erzeugt, diese kann wie gewünscht benannt und verschoben werden.
git mv File.txt File2.txt git commit
4. Den gespeicherten Commit mergen, alle Änderungen einchecken – Der Merge verursacht Konflikte, die mit dem Stagen aller Änderungen gelöst werden.
git merge $REV git commit -a
5. Wenn gewünscht, die Datei wieder zum ursprünglichen Namen umbenennen und einchecken.
Grafisch sieht das wie in Abb. 3 aus.
Die folgenden Punkte sind zu beachten:
- Die (vorläufige) Umbenennung der ursprünglichen Datei ist zwingend nötig. Soll der Name erhalten bleiben, so wird die Datei am Ende nochmals umbenannt.
- Beim Umbenennen und Kopieren der Datei (Schritte 1-3 oben) ist von Änderungen der Dateiinhalte abzusehen. Werden solche Änderungen gemacht, so ist der Merge (Schritt 4) deutlich schwieriger. Die Anpassung des Codes erst im Nachhinein macht das Splitten der Datei leichter und die Commit-Historie verständlicher.
Dieser Prozess ist zwar komplexer als das einfache Kopieren einer Datei im Explorer, er lässt sich aber in wenigen Minuten ausführen und sorgt dafür, dass die Git-Historie für die Zukunft erhalten bleibt.
Commit-Metadaten
Nach dem sorgfältigen Auswählen der Code-Änderungen für einen Commit folgt die Pflege der Commit-Metadaten. Zu jedem Commit soll ein Autor mit Namen und E-Mail-Adresse eingetragen sein. Dafür reicht es in der Regel, die eigenen Daten in den globalen Git-Einstellungen nach der Git-Installation zu hinterlegen. Einige Git Clients überschreiben allerdings diese Einstellungen, ohne den Benutzer darüber zu informieren. Deshalb sollten die Einstellungen nach jeder Client-Installation und jedem Update erneut überprüft werden.
Sehr entscheidend für die Git-Historie sind die Commit-Nachrichten. Zu jedem Commit soll eine sprechende kurze Zusammenfassung hinterlegt werden. Es ist empfehlenswert, diese mit der Ticket-ID zu beginnen, gefolgt von dem Programm-Modul oder Feature-Namen und einer kurzen Information über die enthaltene Code-Änderung. Üblicherweise werden die Commit-Nachrichten, so wie die Code-Kommentare und Dokumentation, auf Englisch geschrieben. Gute Beispiele für Commit-Nachrichten sind:
- EPS-23 About page – Removed code commented out
- EPS-23 About page – Updated list of contacts
Schlechte Beispiele sind:
- EPS-23 QA changes
- Added error messages.
Während bei den ersten Commit-Nachrichten sofort klar ist, in welchem Modul was und in welchem Zusammenhang geändert worden ist, geben die letzten Nachrichten keine ausreichende Information darüber. Somit sind sie nicht sprechend genug, um die Änderung nachvollziehbar zu machen. Die Nachteile der fehlenden Ticket-ID wurden im letzten Kapitel ausführlich diskutiert. Die Ticket-ID alleine ist allerdings nicht ausreichend, der Name des Moduls oder Features soll ebenfalls in der Commit-Nachricht enthalten sein. Nur so gibt die Commit-Nachricht ausreichende Information über die Code-Änderung, ohne eine Suche nach dem Ticket erforderlich zu machen. Das ist wichtig, zum Beispiel, damit das Feature als Filter für die Git-Suche benutzt werden kann oder ein Kollege bei seiner Suche in der Historie abschätzen kann, welche Commits für ihn relevant sind und welche nicht.
Commits überprüfen
Nach jedem Commit sollen die enthaltenen Code-Änderungen und Metadaten überprüft werden. Es kommt oft vor, dass Dateien beim Commit vergessen werden oder die durchgeführten Code-Änderungen nicht alle gespeichert worden sind und dadurch nicht alle im Commit enthalten sind. Zudem werden immer wieder unbewusst Code-Änderungen eingecheckt, die nichts mit der jeweiligen Aufgabe zu tun haben, zum Beispiel, weil der Entwickler parallel an mehreren Aufgaben arbeitet oder die Entwicklungsumgebung beim Öffnen von Dateien automatisch Code verschiebt oder umformatiert. Schließlich kann sich jeder beim Schreiben der Commit-Nachricht vertippen oder eine falsche Ticket-ID eintragen. Werden solche Fehler zeitnah nach dem Commit erkannt, lassen sie sich sehr leicht korrigieren. Spätestens vor dem Pushen von Commits auf dem Server sollten diese sorgfältig überprüft werden um sicherzustellen, dass die korrekten Code-Änderungen enthalten und anhand der Commit-Nachrichten sinnvoll in Git dokumentiert sind.
Korrekturen von lokalen Commits
Wird trotz der sorgfältigen Vorbereitung eines Commits im Nachhinein ein Fehler gefunden, sei es in den Code-Änderungen oder in den Commit-Metadaten, so bietet Git verschiedene Möglichkeiten, den Commit zu korrigieren. Dabei ist zwischen lokalen Commits und solchen, die bereits zum Server gepusht worden sind, zu unterscheiden.
Wird ein Fehler in einem lokalen Commit gefunden, so soll dieser in dem ursprünglichen Commit korrigiert werden. Dadurch bleiben die Commits nachvollziehbar und die Historie wird nicht unnötig fragmentiert.
Korrekturen vom letzten Commit
Während der Entwicklung kommt es häufig vor, dass Änderungen schnell eingecheckt und dabei neu erstellte Dateien vergessen werden, die in der Entwicklungsumgebung gemachten Änderungen erst nach dem Commit gespeichert werden oder ein Tippfehler bei der Commit-Nachricht unbemerkt bleibt. Oft fallen diese Fehler zeitnah auf, bevor weitere Commits gemacht werden. Für solche Fälle bietet Git die Amend-Option an. Sie ist bei den meisten Git Clients als Checkbox oder Button verfügbar. Wird sie ausgewählt, so werden die Code-Änderungen nicht als neuer Commit eingecheckt, sondern zum letzten Commit hinzugefügt. Dabei ist es unerheblich, ob die Änderungen Dateien hinzufügen oder entfernen, Code ändern oder eine im letzten Commit durchgeführte Änderung rückgängig machen. Die Commit-Metadaten lassen sich ebenfalls anpassen, um zum Beispiel Tippfehler in der Commit-Nachricht zu korrigieren.
Korrekturen von früheren Commits
Manchmal fallen die Fehler in einem Commit erst später auf, nach einem oder mehreren anderen Commits. Zum Beispiel, wenn die Kompilierung oder der Start einer Anwendung viel Zeit in Anspruch nimmt und die Code-Änderungen deshalb erst nach zwei bis drei Commits getestet werden, kann es nötig sein, einen Fehler im vorletzten Commit zu korrigieren. Während der Entwicklung eines Features liegt zudem der Fokus auf den Code-Änderungen und die Pflege der Git-Historie bleibt oft im Hintergrund. Es ist eine gute Praxis, die Commits vor einem Merge oder Push auf dem Remote-Server erneut zu überprüfen und aufzuräumen. Dabei lassen sich insbesondere kleinere, nicht nachvollziehbare Commits zu anderen Commits mergen und die Commit-Nachrichten korrigieren und ergänzen.
Wenn ein Fehler in einem früheren lokalen Commit gefunden oder allgemein die lokale Git-Historie aufgeräumt wird, bietet sich dafür die Rebase-Option von Git an. Damit lassen sich frühere Commits korrigieren, umsortieren, zusammenführen oder löschen. Zum Beispiel, wenn ein Fehler im vorletzten Commit gefunden wird, kann zuerst ein vorläufiger Korrektur-Commit eingecheckt werden, der später, beim Aufräumen der Commits, zu dem vorletzten Commit gemergt wird. Auch die Commit-Nachrichten lassen sich mit Rebase beliebig anpassen.
Der folgende Befehl auf der Git Bash triggert den Rebase vom aktiven Branch:
git rebase -i <commit>
Mit dem Befehlwird die interaktive Überarbeitung der Commits ab dem angegebenen Commit angestoßen. Der Commit kann zum Beispiel mit seinem Hash angegeben werden oder relativ zum letzten Commit. Der folgende Befehl triggert den Rebase der letzten zwei Commits auf dem Branch:
git rebase -i HEAD~2
Nach dem Ausführen des Befehls öffnet sich eine Textdatei mit der Liste der ausgewählten Commits in deren ursprünglicher Reihenfolge. Jeder Commit ist dort auf einer Zeile mit den folgenden Informationen aufgeführt:
- Aktion, die für den Commit angewendet wird
- Hash des Commits
- Commit-Nachricht.
Zum Beispiel:
pick 9364ee8 Git talk – Renamed split file back to its original name
pick 5a409e9 Git talk – Modified file content of split files
Das ist die sogenannte To-do-Liste für den Rebase. Wird die Liste ohne Änderungen ausgeführt, werden keine Änderungen der Commits vorgenommen. Soll ein Commit korrigiert werden, so soll die Aktion für diesen Commit angepasst werden.
Folgende Aktionen kommen meistens in Frage:
- Pick, p (vorausgewählt) – Übernimmt den Commit so, wie er ist.
- Reword, r – Übernimmt die Code-Änderungen so, wie sie sind, ermöglicht aber die Änderung der Commit-Nachricht. Die Commit-Nachricht wird nicht direkt in der To-do-Liste geändert, sondern in einer neuen Textdatei, die sich später, beim Ausführen vom Rebase, öffnet.
- Edit, e – Ermöglicht Änderungen von Code und Commit-Nachricht. Der Commit wird so übernommen, wie er ist und danach wird die Ausführung der To-do-Liste gestoppt. Mit Amend lassen sich anschließend beliebige Anpassungen von dem Commit machen, als ob dies der letzte Commit auf dem Branch wäre.
- Squash, s und Fixup, f – Mergt den Commit zum vorherigen Commit. Je nach Befehl wird entweder nur die erste Commit-Nachricht benutzt oder die Commit-Nachrichten werden verknüpft.
- Drop, d – Entfernt den Commit.
Neben diesen Commit-bezogenen Aktionen sind noch folgende Möglichkeiten von großer Bedeutung:
- Commits umsortieren – Ändert die Reihenfolge der Commits.
- Commit löschen – Entfernt den Commit.
- Break, b – Baut eine Pause ein. Das ermöglicht zum Beispiel das Hinzufügen von neuen Commits an dieser Stelle.
Im oben beschriebenen Fall von Korrekturbedarf des vorletzten Commits und vorläufigen Korrektur-Commits würde die Git-Historie folgendermaßen aussehen:
Commit 1: Feature A – Teil 1
Commit 2: Feature A – Teil 2
Commit 3: Feature A – Korrektur Teil 1
Vor dem Merge wäre hier ein Rebase mit dieser To-do-Liste empfehlenswert:
pick <Commit 1>
fixup <Commit 3>
pick <Commit 2>
Das würde den Korrektur-Commit (Commit 3) zum ersten Commit mergen und dabei die Commit-Nachricht von Commit 1 übernehmen und diese von Commit 3 verwerfen.
Nachdem die To-do-Liste gespeichert und die Datei geschlossen wird, fängt Git sofort mit der Abarbeitung von oben nach unten an. Soweit das möglich ist, werden die Aktionen automatisch ausgeführt. Bei Reword wird, wie oben erwähnt, der Texteditor mit der ursprünglichen Commit-Nachricht geöffnet, die dann beliebig angepasst werden kann. Bei Break oder nach Edit wird gestoppt, damit die gewünschten Änderungen ausgeführt werden. Bei Konflikten, zum Beispiel wegen Umsortierung der Commits, stoppt Git ebenfalls, damit die Konflikte gelöst werden. Sobald die gewünschten oder benötigten Änderungen gemacht werden, soll Git mit dem folgenden Befehl aufgefordert werden, die Ausführung der To-do-Liste fortzusetzen:
git rebase --continue
Neben diesem Befehl sind noch zwei weitere Aktionen bei Stopp möglich:
- git rebase --abort – Abbruch vom Rebase. Alle bisherigen Änderungen werden verworfen und der Stand vor dem Rebase wiederhergestellt.
- git rebase --edit-todo – Ermöglicht die Überarbeitung der To-do-Liste.
Um den Überblick zu behalten, ist es empfehlenswert, eine überschaubare Anzahl an Änderungen durchzuführen und bei Bedarf nach dem Rebase einen zweiten Rebase zu triggern. Wird der Überblick dennoch verloren, so hilft die Anzeige der To-do-Liste mit dem letzten Befehl von oben. Eine Änderung der Liste ist nicht zwingend notwendig, der Rebase kann auch mit der bisherigen To-do-Liste fortgesetzt werden. Wird ein Fehler beim Rebase gemacht, ist es unter Umständen sinnvoll, den Rebase abzubrechen und erneut anzustoßen. Das ist insbesondere dann der Fall, wenn ungewollt Commits zusammengeführt wurden, weil sich diese später nur sehr schwer wieder voneinander abtrennen lassen.
Zusammengefasst bietet die Rebase-Funktion von Git zahlreiche Möglichkeiten für die nachträgliche Korrektur von lokalen Commits. Damit lassen sich die fehlerhaften oder unvollständigen Commits korrigieren, was die Anzahl an Korrektur-Commits in der Git-Historie deutlich reduziert. Neben dem Code können auch die Commit-Metadaten und insbesondere die Commit-Nachrichten überarbeitet werden. Das sorgt für eine saubere Git-Historie auf dem Server, die bei Bedarf zahlreiche Informationen über den Code liefern kann.
Die in diesem Abschnitt aufgeführten Rebase-Beispiele beschränken sich auf lokale Commits ohne Merges von oder zu anderen Branches. Wie am Anfang erwähnt, ist es empfehlenswert, die Commit-Historie vor jedem Merge zu überprüfen und aufzuräumen. Sobald ein Merge gemacht wurde, ist eine Korrektur schwieriger. Zwar bietet Git auch dafür Rebase-Befehle an, es ist aber sehr vorsichtig abzuwägen, ob diese oder die Korrektur-Methoden aus dem nächsten Abschnitt besser für den konkreten Fall geeignet sind.
Korrekturen von Commits auf dem zentralen Server
Wenn ein Fehler erst dann auffällt, wenn der Commit schon zum zentralen Server gepusht wurde, sollte der ursprüngliche Commit nicht mehr korrigiert werden, sondern ein neuer mit der Korrektur durchgeführt werden. Das liegt daran, dass der ursprüngliche Commit unter Umständen bereits von anderen Entwicklern oder Systemen abgeholt und verwendet wird.
Wenn der entdeckte Fehler die Arbeit des Teams verhindert und keine zeitnahe Korrektur möglich ist, sollte als erstes ein Revert des fehlerhaften Commits gemacht werden. Das ist ein Commit, der genau die umgekehrten Änderungen des ursprünglichen Commits enthält. Die Funktion wird von Git und den Git Clients zur Verfügung gestellt. Danach kann das Team weiterarbeiten und der zuständige Entwickler ohne Zeitdruck den Fehler korrigieren. Das ist vor allem dann zu empfehlen, wenn die Korrektur zeitaufwändig oder der zuständige Entwickler nicht verfügbar ist, eine Übergabe nicht möglich oder sinnvoll ist oder aufgrund des gefundenen Fehlers die Vermutung besteht, dass der ursprüngliche Code komplett überarbeitet werden soll. Nach der Korrektur des Fehlers werden alle Code-Änderungen erneut eingecheckt.
In einigen seltenen Fällen müssen Administratoren Commits überarbeiten, die schon auf dem zentralen Server liegen. Das ist insbesondere dann der Fall, wenn aus Versehen Commits auf einem falschen (Release-)Branch gelandet sind und von dort entfernt werden müssen. Mit Revert kann zwar ein Commit mit den umgekehrten Änderungen hinzugefügt werden, allerdings ist das nicht dasselbe wie das Entfernen des ursprünglichen Commits. Zum einen ist dadurch die Git-Historie des Branches nicht mehr sauber und das Nachvollziehen der Code-Änderungen wird erschwert. Zum anderen ist das Verhalten von Git in Bezug auf den unerwünschten Commit in beiden Fällen unterschiedlich.
Insbesondere dann, wenn ein Commit nur vorläufig vom Branch entfernt werden soll und zu einem späteren Zeitpunkt, zum Beispiel nach dem nächsten Release, wieder hinzuzufügen ist, ist Revert nicht zu empfehlen. Der Merge von Git ist Commit-basiert und nicht Code-basiert. Das heißt, dass Git keinen Code-Vergleich macht, sondern für jeden Commit auf dem Quellbranch überprüft, ob er auf dem Zielbranch bereits vorhanden ist oder nicht. Nur wenn der Commit noch nicht auf dem Zielbranch vorhanden ist, wird er dorthin gemergt, sonst wird er übersprungen. Da Revert den ursprünglichen Commit nicht entfernt, sondern lediglich einen neuen Commit mit den umgekehrten Änderungen hinzufügt, kann also der ursprüngliche Commit nicht wieder zum Zielbranch gemergt werden. Die Code-Änderungen müssen auf anderem Wege, zum Beispiel mittels Cherry Pick, übernommen werden. Das Risiko, dass die fehlenden Änderungen bei einem größeren Merge unbemerkt bleiben, ist allerdings sehr hoch.
In solchen Fällen ist die mögliche Entfernung des unerwünschten Commits vom Server abzuwägen. Dafür wird der Branch lokal ausgecheckt und mittels Rebase geändert. Oft greifen die Entwickler für den Push danach auf den folgenden Befehl zurück:
git push --force
Das ist jedoch sehr gefährlich, weil dadurch zwischenzeitliche Änderungen auf dem Server ohne Warnung überschrieben werden. Deshalb sollte dieser Befehl nicht verwendet werden, wenn mehr als ein Entwickler mit dem Code arbeitet. Git bietet folgende Alternative:
git push --force-with-lease
Dieser Befehl hat dieselbe Funktion wie der obere, allerdings wird er von Git nur dann ausgeführt, wenn alle Commits vom remote Branch auch auf dem lokalen Branch vorhanden sind. Wurde also eine neue Änderung in der Zwischenzeit auf dem Server eingecheckt, lehnt Git den Push ab. Dadurch ist kein unbewusstes Überschreiben von Code-Änderungen möglich.
Zusammenfassung
Die Commit-Historie von Git ist eine sehr wertvolle Quelle für Hintergrundinformationen zum Code. Mit der Suche und mit Blame lassen sich der Autor und der Anlass für jede Zeile Code herausfinden. Dadurch kann der Entwickler den vorhandenen Code gründlich verstehen und auf dieser Basis bessere Lösungen für seine Aufgaben finden und Umsetzungsfehler vermeiden.
Um von der Git-Historie profitieren zu können, sollte diese von allen Entwicklern im Projekt gepflegt werden. Es ist wichtig, bei den eigenen Commits sowohl auf die Inhalte als auch die Commit-Metadaten zu achten. Jeder Commit soll sinnvolle zusammenhängende Code-Änderungen enthalten und sich nur auf ein Feature oder Bug beziehen. Code-Umbauten sind separat einzuchecken und größere Themen auf mehrere kleine und überschaubare Commits aufzuteilen. Bei der Umbenennung und Verschiebung von Dateien und insbesondere beim Splitten von Dateien ist darauf zu achten, dass die Git-Historie durch die Änderung nicht verloren geht. Neben den Commit-Inhalten ist die Commit-Nachricht entscheidend für die Qualität der Git-Historie. Um den größtmöglichen Nutzen davon zu haben, sollte sie mit der Ticket-ID beginnen, gefolgt von dem Modul- oder dem Feature-Namen und einer kurzen Zusammenfassung der gemachten Änderungen.
Jeder Commit sollte sorgfältig überprüft werden. Werden Fehler im Nachhinein gefunden, soll die Git-Historie nicht unnötig mit Korrektur-Commits fragmentiert werden. Bei lokalen Commits soll stattdessen der fehlerhafte Commit mit Amend oder Rebase korrigiert werden. Vor jedem Merge oder Push ist es empfehlenswert, nochmal alle lokalen Commits zu überprüfen und aufzuräumen. Fällt ein Fehler erst dann auf, wenn der Commit schon zu dem zentralen Server gepusht wurde, dann soll der Fehler entweder mit einem Korrektur-Commit korrigiert oder, wenn keine zeitnahe Korrektur möglich ist, ein Revert des fehlerhaften Commits gemacht werden, gefolgt von einem Commit mit den korrigierten Code-Änderungen. In Ausnahmefällen, wenn Administratoren einen Commit aus einem remote Branch entfernen müssen, soll das mit Rebase und Push mit Force with Lease geschehen. So bleibt die Commit-Historie trotz der Fehler so übersichtlich wie möglich und lässt sich dauerhaft als wertvolle Informationsquelle verwenden.