Dieser Beitrag ist eine Übersetzung des Artikels „A successful Git branching model“ von Vincent Driessen. Alle Bezüge auf „ich“, „wir“ usw. gelten daher aus seiner Perspektive.
In diesem Beitrag möchte ich das Entwicklungsmodell vorstellen, das ich vor ungefähr einem Jahr für alle meine Projekte (sowohl berufliche als auch private) eingeführt habe und das sich als sehr erfolgreich erwiesen hat. Ich wollte schon seit einiger Zeit darüber berichten, habe bisher jedoch nie wirklich die Zeit gefunden, das auch gründlich zu tun. Ich werde dabei die Projektdetails auslassen und stattdessen nur über die Branching-Strategie und das Release-Management schreiben.
Hier wird der Schwerpunkt auf Git als Tool zur Versionierung unseres gesamten Quellcodes gelegt.
Warum Git?
Für eine ausführliche Diskussion der Vor- und Nachteile von Git im Vergleich zu zentralisierten Quellcode-Kontrollsystemen informiert Euch bitte im Netz. Dort gibt es zahlreiche Flame Wars. Als Entwickler gebe ich Git vor allen anderen Tools, die aktuell angeboten werden, den Vorzug. Git hat die Art und Weise, wie Entwickler über das Mergen und Branchen denken, nachhaltig verändert. In der klassischen CVS-/Subversion-Welt, aus der ich ursprünglich stamme, hatte man immer etwas Angst vor dem Mergen/Branchen („Achtung vor Merge-Konflikten, sie können beißen!“) und es galt als etwas, das man nur ab und zu tut.
Bei Git sind diese Aktionen allerdings äußerst günstig und einfach und sie gelten tatsächlich als wesentliche Bestandteile in Euren täglichen Arbeitsschritten. In Büchern zu CVS/Subversion wird das Branchen und Mergen beispielsweise erst in den hinteren Kapiteln besprochen (für fortgeschrittene Nutzer), während beide Themen in jedem normalen Git-Buch bereits in Kapitel 3 (Grundlagen) behandelt werden.
Aufgrund ihrer Einfachheit und Häufigkeit muss man keine Angst mehr vor dem Branchen und Mergen haben. Tools zur Versionskontrolle gelten beim Branchen/Mergen als größere Hilfe als jegliches andere Hilfsmittel.
Doch jetzt genug zu den Tools. Gehen wir nun über zum Entwicklungsmodell. Das Modell, das ich hier vorstellen möchte, ist im Grunde nur eine Reihe von Verfahren, an die sich jedes Teammitglied halten muss, um einen verwalteten Softwareentwicklungsprozess zu erhalten.
Dezentralisiert und trotzdem zentralisiert
Den Repository-Aufbau, den wir verwenden und der bei diesem Branching-Modell gut funktioniert, ist ein Aufbau mit einem zentralen „wahren“ Repo. Ihr müsst jedoch beachten, dass dieses Repo nur wie das zentrale Repository aussieht (da es sich bei Git um ein DVCS handelt, gibt es auf technischer Ebene kein zentrales Repo). Auf dieses Repo kommen wir als origin
noch einmal zurück, da diese Bezeichnung allen Git-Nutzern geläufig ist.
origin
. Neben den Push-/Pull-Beziehungen kann jeder Entwickler jedoch auch Änderungen von anderen Kollegen pullen, um untergeordnete Teams zu bilden. Das könnte beispielsweise bei der Zusammenarbeit mit einem oder mehreren Entwicklern an einem neuen großen Feature hilfreich sein, bevor die laufenden Arbeiten frühzeitig nach origin
gepusht werden. In der oben stehenden Abbildung haben wir die untergeordneten Teams Alice und Bob, Alice und David sowie Clair und David.
Aus technischer Sicht bedeutet das nichts weniger, als dass Alice einen Git-Remote mit dem Namen bob
definiert hat, der auf Bobs Repository hinweist und umgekehrt.
Die Haupt-Branches
Im Kern ist das Entwicklungsmodell von bereits bestehenden Modellen stark beeinflusst worden. Das zentrale Repo besitzt zwei Haupt-Branches mit unbegrenzter Lebensdauer:
master
develop
Der master
-Branch auf origin
sollte jedem Git-Nutzer ein Begriff sein. Parallel zum master
-Branch gibt es mit develop
noch einen weiteren Branch.
Wir betrachten origin/master
als Haupt-Branch, in welchem der Quellcode des HEAD
immer in produktiv-bereitem Zustand ist.
Wir betrachten origin/develop
als Haupt-Branch, in welchem der Quellcode des HEAD
immer den Zustand mit den neuesten bereitgestellten Entwicklungsänderungen für den nächsten Release widerspiegelt. Einige würden dies als „Integrations-Branch“ bezeichnen. Hieraus werden jegliche automatischen Nightly Builds gebaut.
Erreicht der Quellcode im develop
-Branch einen stabilen Punkt und ist er für den Release bereit, sollten alle Änderungen irgendwie nach master
zurückgemergt und mit einer Release-Nummer versehen werden. Wie das genau geschieht, wird später erklärt.
Daher stellt es per Definition jedes Mal dann einen neuen Produktions-Release dar, wenn Änderungen nach master
zurückgemergt werden. Wir sind hierbei in der Regel sehr streng, so dass wir theoretisch ein Git hook-Skript verwenden könnten, um unsere Software jedes Mal dann automatisch zu bauen und auf unsere Produktions-Server auszuliefern, wenn es einen Commit an den master
gab.
Unterstützende Branches
Neben den Haupt-Branches master
und develop
nutzt unser Entwicklungsmodell eine Vielzahl unterstützender Branches, um die parallele Entwicklung zwischen Teammitgliedern zu unterstützen, um das Tracken von Features zu erleichtern, um Produktions-Releases vorzubereiten und um dabei zu helfen, Probleme auf der aktiven Produktivumgebung schnell zu beheben. Im Gegensatz zu den Haupt-Branches haben diese Branches immer eine begrenzte Lebensdauer, da sie irgendwann wieder entfernt werden.
Zu den verschiedenen Arten der von uns verwendeten Branches gehören:
- Feature-Branches
- Release-Branches
- Hotfix-Branches
Jede dieser Branches hat einen spezifischen Zweck und ist an strenge Regeln hinsichtlich der Frage gebunden, welche Branches ihre Ursprungs-Branch sein können und welche Branches ihre Merge-Ziele sein müssen. Wir werden diese gleich gemeinsam durchgehen.
Diese Branches sind aus technischer Sicht keineswegs „besonders“. Die Branch-Arten werden danach unterteilt, wie wir sie nutzen. Im Kern sind es natürlich herkömmliche Git-Branches.
Feature-Branches
Kann gebrancht werden von: develop
Muss gemergt werden nach: develop
Vorgabe zur Bezeichnung des Branches: alles außer master
, develop
, release-*
oder hotfix-*
Feature-Branches (manchmal auch Topic-Branches genannt) werden für die Entwicklung neuer Features für einen anstehenden Release oder einen Release in weiter Zukunft verwendet. Zu Beginn der Entwicklung eines Features mag der Ziel-Release, in den das Feature eingefügt wird, zu diesem Zeitpunkt noch unbekannt sein. Das Wesentliche eines Feature-Branches besteht darin, dass er so lange besteht, wie das Feature noch entwickelt wird, dass er irgendwann aber zurück nach develop
gemergt (um das neue Feature abschließend zum anstehenden Release hinzuzufügen) oder verworfen wird (bei einem nicht zufriedenstellenden Versuch).
Feature-Branches gibt es üblicherweise nur in Entwickler-Repos und nicht im origin
.
Erstellung eines Feature-Branches
Ihr müsst zu Beginn der Arbeit an einem neuen Feature vom develop
-Branch branchen
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"
Einbindung eines fertiggestellten Features in develop
Fertiggestellte Features können zum develop
-Branch gemergt werden, um sie abschließend zum anstehenden Release hinzuzufügen:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop
Das Flag --no-ff
führt dazu, dass durch das Mergen immer ein neues Commit-Objekt erstellt wird, auch wenn das Mergen im fast-forward-Modus durchgeführt werden könnte. So wird verhindert, dass Informationen zum historischen Vorkommen eines Feature-Branches verloren gehen und es werden dadurch alle Commits, die zusammen das Feature ergänzt haben, zusammengefasst. Vergleiche hierzu:
--no-ff
verwendet wurde.
Es ist richtig, dass dabei einige (leere) Commit-Objekte mehr erstellt werden, der Nutzen ist jedoch deutlich größer als der Kostenaufwand.
Leider habe ich noch nicht herausbekommen, wie man aus --no-ff
die Standardeinstellung git merge
machen kann, dies sollte aber geschehen.
Anm. d. Red.: Dies kann man inzwischen mit git config --global merge.ff false
einstellen. Jedoch lassen sich dann überhaupt keine Fast-Forward-Merges mehr durchführen.
Release-Branches
Kann gebrancht werden von: develop
Muss gemergt werden nach: develop
und master
Vorgabe zur Bezeichnung des Branches: release-*
Release-Branches unterstützen die Erstellung eines neuen Produktions-Releases. Sie sorgen dafür, dass die Buchstaben „i“ und „t“ in allerletzter Sekunde jeweils ihren Punkt bzw. ihren Strich erhalten. Außerdem ermöglichen sie geringfügige Fehlerbehebungen und die Erstellung von Metadaten für einen Release (Versionsnummer, Erstellungsdaten usw.). Durch all das wird die develop
-Branch geleert, um Platz für neue Features für den nächsten großen Release zu machen.
Der entscheidende Zeitpunkt zum Branchen eines neuen Release-Branches von develop
ist dann, wenn develop
(fast) den gewünschten Status des neuen Releases anzeigt. Zu diesem Zeitpunkt müssen zumindest alle Features, die für den zu bauenden Release geplant werden, nach develop
gemergt worden sein. Mit Features, die für zukünftige Releases geplant werden, darf das nicht geschehen – sie müssen bis zu dem Zeitpunkt warten, nach der Release-Branch gebrancht wurde.
Der anstehende Release erhält genau zu Beginn einer Release-Branch eine Versionsnummer und nicht früher. Bis zu diesem Zeitpunkt hat der develop
-Branch die Änderungen für den „nächsten Release“ angezeigt. Es ist jedoch so lange unklar, ob dieser „nächste Release“ irgendwann zu 0.3 oder 1.0 werden wird, bis der Release-Branch in Betrieb genommen wurde. Diese Entscheidung erfolgt zu Beginn des Release-Branches und wird gemäß den Projektrichtlinien bezüglich der Versionsnummerierung vorgenommen.
Erstellung eines Release-Branches
Release-Branches werden aus dem develop
-Branch erstellt. Nehmen wir zum Beispiel an, dass es sich bei Version 1.1.5 um den aktuellen Produktions-Release handelt und ein großer Release ansteht. Der Stand in develop
ist bereit für den „nächsten Release“ und wir haben festgelegt, dass dieser Release die Versionsnummer 1.2 erhalten soll (statt 1.1.6 oder 2.0). Wir branchen also und geben dem Release-Branch eine Bezeichnung, welche die neue Versionsnummer anzeigt:
$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)
Nach der Erstellung eines neuen Branches und der Umstellung auf diesen neuen Branch erhöhen wir die Versionsnummern. Hierbei ist bump-version.sh ein fiktionales Shell-Skript, das einige Dateien in der Arbeitskopie ändert, um die neue Version anzuzeigen (natürlich kann dies auch durch eine manuelle Änderung erfolgen – wichtig ist nur, dass sich wenigstens einige Dateien ändern). Anschließend muss man die erhöhte Versionsnummer commiten.
Dieser neue Branch kann eine Zeit lang bestehen, bis der Release endgültig eingeführt wird. Währenddessen können in diesem Branch Fehlerbehebungen vorgenommen werden (anstatt im develop
-Branch). Das Hinzufügen großer neuer Features an dieser Stelle ist streng verboten. Sie müssen nach develop
gemergt werden und daher ist es besser, dafür auf den nächsten großen Release zu warten.
Fertigstellung eines Release-Branches
Ist der Zustand im Release-Branch soweit bereit, ein tatsächlicher Release zu werden, müssen noch einige Schritte durchgeführt werden. Zuerst wird die Release-Branch nach master
gemergt (da jeder Commit auf master
per Definition einen neuen Release darstellt, nicht vergessen!). Als nächstes muss dieser Commit auf master
getaggt werden um später einfach darauf zugreifen zu können. Und schließlich müssen die auf dem Release-Branch vorgenommenen Änderungen zurück nach develop
gemergt werden, damit zukünftige Releases ebenfalls diese Fehlerbehebungen enthalten.
Die ersten beiden Schritte in Git:
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2
Der Release ist nun erfolgt und wurde für zukünftige Verweise getaggt.
Bearbeitung: Ihr wollt vielleicht auch die Flags -s
oder -u
<key> verwenden, um Eure Tags kryptografisch zu unterschreiben.
Um die Änderungen in der Release-Branch behalten zu können, müssen wir diese jedoch zurück auf develop
mergen. In Git:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
Dieser Schritt kann unter Umständen zu einem Merge-Konflikt führen (wahrscheinlich nur, weil die Versionsnummer geändert wurde). Trifft das zu, behebt das und commitet.
Jetzt sind wir wirklich fertig und der Release-Branch kann entfernt werden, da wir ihn nun nicht mehr benötigen:
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).
Hotfix-Branches
Kann gebrancht werden von: master
Muss gemergt werden nach: develop
und master
Vorgabe zur Bezeichnung des Branches: hotfix-*
Hotfix-Branches ähneln in der Hinsicht stark den Release-Branches, dass sie ebenfalls auf einen neuen Produktiv-Release vorbereiten sollen, auch wenn dieser nicht geplant ist. Sie entstehen durch die Notwendigkeit, bei einem unerwünschten Zustand einer Live-Produktionsversion sofort zu reagieren. Muss ein kritischer Fehler in einer Produktionsversion umgehend behoben werden, kann ein Hotfix-Branch vom entsprechenden Tag auf dem Master-Branch, der die Produktiv-Version markiert, gebrancht werden.
Der Beweggrund dafür ist, dass die Arbeit der Teammitglieder (an der develop
-Branch) weitergehen kann, während eine andere Person einen schnellen Fix für den Produktiv-Stand erstellt.
Erstellung des Hotfix-Branches
Hotfix-Branches werden aus der master
-Branch erstellt. Nehmen wir zum Beispiel an, dass es sich bei Version 1.2 um den aktuellen Produktionsrelease handelt, der live läuft und aufgrund eines schwerwiegenden Fehlers Probleme macht. Änderungen auf develop
sind jedoch instabil. Wir können nun einen Hotfix-Branch branchen und mit der Behebung des Problems beginnen:
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)
Vergesst nicht, die Versionsnummer nach dem branchen zu erhöhen!
Behebt anschließend den Fehler und commitet die Behebung in einem oder mehreren separaten Commits.
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)
Abschließen eins Hotfix-Branches
Seid ihr damit fertig, muss die Fehlerbehebung zurück nach master
gemergt werden. Sie muss jedoch auch zurück nach develop
gemergt werden, um sicherzustellen, dass die Fehlerbehebung beim nächsten Release ebenfalls enthalten ist. Dieses Vorgehen ähnelt vollständig der Fertigstellung von Release-Branches.
Aktualisiert zunächst den master
und taggt den Release.
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1
Bearbeitung: Ihr wollt vielleicht auch die Flags -s
oder -u
<key> verwenden, um Eure Tags kryptografisch zu unterschreiben.
Fügt als nächstes auch die Fehlerbehebung in develop
ein:
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
Die einzige Ausnahme von der Regel besteht darin, dass, die Hotfix-Änderungen anstelle von develop
zu diesem Release-Branch gemergt werden müssen, wenn aktuell ein Release-Branch besteht. Das Zurückmergen der Fehlerbehebung zum Release-Branch führt irgendwann dazu, dass die Fehlerbehebung auch nach develop
gemergt wird, wenn der Release-Branch fertiggestellt wurde (verlangt die Arbeit in develop
diese Fehlerbehebung sofort und kann man nicht so lange warten, bis der Release-Branch fertiggestellt wurde, könnt ihr die Fehlerbehebung auch jetzt bereits sicher nach develop
mergen).
Entfernt schließlich den vorläufigen Branch:
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).
Zusammenfassung
Obwohl dieses Branching-Modell keine absolute Neuheit darstellt, hat sich die zusammenfassende Abbildung, mit der dieser Beitrag begonnen hat, als ungemein hilfreich für unsere Projekte erwiesen. Sie stellt ein elegantes Denkmodell dar, das leicht verständlich ist und durch das Teammitglieder ein gemeinsames Verständnis für die Branching- und Releasing-Prozesse entwickeln können.
Eine PDF-Version der Abbildung in hoher Qualität findet ihr hier. Druckt es Euch aus und hängt es auf, damit ihr jederzeit einen schnellen Blick drauf werfen könnt.
Aktualisierung: Auf vielfachen Wunsch: hier ist die gitflow-model.src.key des Hauptdiagramms (Apple Keynote).
0 Kommentare