Ausgangslage
Zu Beginn wurden alle Features in einem einzigen, großen Feature-Branch implementiert und nach vollständiger Entwicklung in den develop
Branch integriert. Ein üblicher Pull-Request hatte dabei den Umfang von ca. 60 geänderten Dateien und oft mehr als 3000 hinzugefügte Zeilen Code bzw. Markup. Da zu diesem Zeitpunkt allerdings nur zwei bis drei EntwicklerInnen gleichzeitig an dem Projekt arbeiteten, kam es zu wenigen Merge-Konflikten und der Integrationsaufwand hielt sich in Grenzen. Die Größe der Pull-Requests war zu diesem Zeitpunkt schon ein Problem, allerdings hätte in dieser Situation, auf Grund der geringen Verfügbarkeit an Personen, eine höhere Frequenz an kleinen Pull-Requests keinen Mehrwert gebracht.
Verbesserung 1: Aufteilung eines Features in Schnittstelle, Frontend und Backend
Nachdem das Team gewachsen war, wurde der Aufwand für Integration und Review der Features langsam größer und die Übersichtlichkeit der Pull-Requests schwand noch mehr, da die Anforderungen ebenfalls umfangreicher wurden. Der erste Ansatz zur Bewältigung dieser Herausforderungen war, ein fachliches Feature in die drei Bestandteile „Schnittstelle“, „Frontend“ und „Backend“ zu teilen, welche dann jeweils in einem eigenen Branch entwickelt wurden. Die „Schnittstelle“ beinhaltete dabei alle Modellklassen, d.h. Fachobjekte wie Leseprojektionen, sowie das Service-Interface. In „Frontend“ befanden sich die Web-Oberflächen, Controller und Validierungen, im „Backend“ hingegen die Anbindung an die Datenbank und die Implementierung der Service-Methoden. Dadurch wurde die damals kritischste Stelle hinsichtlich der Merge-Konflikte, nämlich das Interface zwischen Front- und Backend, schon weitestgehend stabil bereitgestellt.
Als Folge dieser Maßnahme sank der Umfang der einzelnen Pull-Requests im Verhältnis nur geringfügig, da der meiste Code in Front- und Backend erzeugt wurde. So hatten die Pull-Request für Frontend und Backend einen Umfang von jeweils ca. 25 geänderten Dateien und zwischen 1000 und 1600 hinzugefügte Zeilen Code. Die Größe eines Schnittstellen Pull-Requests konnte allerdings, abhängig von der Anzahl der verarbeiteten Fachobjekte, sehr stark variieren und dadurch auch wieder sehr umfangreich werden. Zudem entstand eine starke zeitliche Abhängigkeit zwischen der Umsetzung „Schnittstelle“ und den darauf aufbauenden Bestandteilen „Frontend“ und „Backend“, welche die Zusammenarbeit sehr erschwerte.
Ver(schlimm)besserung 2: Langlebiger Feature-Branch mit kleinen Sub-Branches
Im Laufe der Projektdauer wurde das Team aufgeteilt und erweitert, so dass zu Spitzenzeiten zehn Personen an der gleichen Codebasis arbeiteten. Dadurch stieg die Menge an Commits, und damit auch Merge-Konflikten, enorm an, da einige wenige Klassen von beiden Teams gemeinsam verwendet wurden. Um diese Konflikte wieder zu minimieren, sollte folgendes Modell getestet werden: Vom develop
Branch wurde ein Branch für die Entwicklungszeit des ganzen Features, also „Schnittstelle“, „Frontend“ und „Backend“ abgezweigt, welcher als Integrationspunkt für die kleineren Pull-Requests dienen sollte. Ebenso sollte es dadurch möglich werden, auch die drei Hauptbestandteile zu zerteilen.
Leider führte dieses Modell zu folgendem Problem: Durch die lange Laufzeit und die hohe Anzahl an Änderungen auf dem develop
war es oft notwendig, den Feature-Branch zu rebasen und damit auch alle davon abzweigenden Branches. Dies konnte sehr schnell zu weiteren Konflikten führen, wenn die Reihenfolge des Rebase nicht strikt eingehalten wurde. Dadurch war das eigentliche Ziel, eine Vereinfachung der Arbeitsweise und Verminderung der Merge-Konflikte zu schaffen, total verfehlt. Allerdings zeigte sich, dass das Aufteilen eines Features in teils sehr kleine Pull-Requests äußerst effizient sein konnte.
Der Endstand: Kleine Branches direkt auf develop mergen
Die Erkenntnis aus den vorherigen Ansätzen war, dass das größte Problem die Menge an Änderungen im Basisbranch über die Laufzeit eines Feature-Branches geworden ist. Da allerdings das Aufteilen eines Features in sehr kleine Pull-Requests mittlerweile einen großen Mehrwert brachte, konnte folgendes Branching-Modell erfoglreich aufgesetzt werden: Vom develop
Branch werden direkt die kleinen Bestandteile eines Features abgezweigt, beispielsweise nur das Bereitstellen des Datenmodells, anstelle der gesamten Schnittstelle. Sobald dann dieser Teilaspekt fertig implementiert ist, wird er über einen Pull-Request direkt in den develop
zurückgemerged.
Durch dieses Vorgehen minimierten sich die Lebenszeit eines Branches und die Wahrscheinlichkeit von Merge-Konflikten. Ebenso wurden die Pull-Requests übersichtlicher und die Motivation der Beteiligten, diese zu reviewen, stieg enorm. Zum Vergleich: ein Pull-Request bei diesem Vorgehen hatte im Schnitt fünf geänderte Dateien und etwa 500 bis 600 hinzugefügte Zeilen Code. Als einziger Nachteil kann das Vorhandensein von Dummy-Implementierungen angesehen werden, welche im Rahmen einzelner Teilaspekte eines Features schon im develop
Branch angelegt werden mussten. Da jedoch die Web-Oberflächen als Feature-Toggle fungierten, hatte dies keine Auswirkung auf die Benutzer der Anwendung.
Fazit
Dieses Beispiel zeigt, dass es schwierig sein kann, das passende Branching-Modell für ein Projekt zu finden. Möchte man jedoch in einem großen Team auf einer kleinen Codebasis effizient arbeiten, kann es sinnvoll sein, komplexe Branching-Modelle abzulegen und auf sehr kurzlebige Branches auszuweichen. Wie dann mit den unvollständigen Features umgegangen wird, kommt wiederum auf den Release- bzw. Deploymentprozess an. Denkbar wären hier Feature-Toggles oder, wie in diesem Fall, die UI einfach als letzte Komponente fertigzustellen.