Git und Projektabhängigkeiten

Denke über die folgenden Fragen nach:
Wie gehst du in Git mit Projektabhängigkeiten um?
Unser Projekt besteht aus mehreren voneinander abhängigen Repositorys. Derzeit verwalten wir diese mit svn:externals. Wie gehen wir am besten vor, wenn wir dafür Git verwenden möchten?
Wie teilt man ein großes Repository mithilfe von Git in kleinere Komponenten auf?
Das sind einige unserer am häufigsten gestellten Fragen.
Dieses Thema bereitet vielen Softwareteams bei der Einführung von Git Kopfzerbrechen. Dieser Artikel soll Licht ins Dunkel bringen.
Projekt-Abhängigkeiten und Build-Infrastruktur greifen offenbar ineinander, was auch intern bei Atlassian eine Diskussion über die "Zukunft von Builds" entfacht hat.
Separate Repositorys statt einem einzigen können so manches erschweren. Trotzdem ist dies aus mindestens zwei wichtigen Gründen ein relativ natürlicher und manchmal auch zwingend notwendiger Schritt während der Weiterentwicklung eines Softwareprojekts: steigende Build-Zeiten und gemeinsame Abhängigkeiten zwischen Projekten.
Ein Überblick in groben Zügen: Richtlinien und suboptimale Lösungen
Zurück zur Frage: Wie kannst du Projektabhängigkeiten mit git verfolgen und verwalten?
Am besten gar nicht!
Spaß beiseite. Ich werde diese Frage zuerst mal allgemein beantworten und später ins Detail gehen. Allerdings gibt es für all die Schwierigkeiten mit Projektabhängigkeiten keine Patentlösung, weder in Git noch auf andere Weise.
Sobald ein Projekt eine gewisse Größe erreicht hat, ist es sinnvoll, es logisch zu unterteilen. Warte aber nicht, bis du in einem Repository schon Millionen von Zeilen an Code verfasst hast. Die folgenden Hinweise sollen dir lediglich helfen, deinen eigenen Ansatz zu finden.
Erste Möglichkeit: Verwendung eines geeigneten Build-/Abhängigkeitstools statt Git
Ein Abhängigkeitsmanagementtool ist derzeit die von mir empfohlene Methode zum Umgang mit den wachsenden Problemen und den Build-Zeiten größerer Projekte.
Halte deine Module in individuellen Repositorys separat und verwalte ihre Interdependenzen mit einem Tool, das hierfür konzipiert wurde. Es ist für (nahezu) jeden Technologie-Stack eines auf dem Markt erhältlich. Einige Beispiele:
Npm für Node-Apps
Bower, Component.io etc., wenn du JavaScript verwendest (neu!)
Pip und requirements.txt, falls du mit Python arbeitest
NuGet für .NET
CocoaPods für Cocoa-iOS-Apps
Die Build-/Abhängigkeits-Infrastruktur von Go ist ein Stück weit in die Programmiersprache integriert (wobei eine umfassendere Lösung entwickelt wird, siehe godep). Für unseren Git-Server (Bitbucket) nutzen wir sowohl Maven als auch Bower. Wenn alles zum Builden bereit ist, zieht sich das ausgewählte Tool die richtigen Versionen über die Abhängigkeiten, sodass der Build für dein Main-Projekt laufen kann. Einige dieser Tools haben Einschränkungen und gehen nicht von optimalen Annahmen aus. Sie haben sich aber bewährt und sind für diesen Zweck gut geeignet.
Die Probleme durch das Aufteilen eines Projekts
Zur Vereinfachung fasst man zu Beginn eines Projekts alles in einem Build zusammen. Mit zunehmender Größe des Projekts wird dein Build jedoch zu langsam und du musst "cachen". Jetzt kommt das Abhängigkeits-Management ins Spiel. Daher eignen sich Untermodule (im Folgenden mehr dazu) z. B. sehr gut für dynamische Programmiersprachen. Die meisten müssen sich irgendwann über Build-Zeiten Gedanken machen, daher denke ich, dass ein Tool zum Management von Abhängigkeiten sinnvoll ist.
Das Aufteilen von Komponenten in separate Repositorys bringt einige ernsthafte Schwierigkeiten mit sich. In unbestimmter Reihenfolge:
Zum Ändern einer Komponente ist ein Release erforderlich.
Es ist zeitaufwendig und kann aus den unerfindlichsten Gründen schiefgehen.
Man kommt sich bei kleinen Änderungen dumm vor.
Neue Builds müssen für jede Komponente manuell erstellt werden.
Das Finden von Repositorys wird erschwert.
Wenn nicht die gesamte Quelle in einem einzigen Repository zur Verfügung steht, wird refaktoriert.
In einigen Umgebungen (wie in unserer) ist für die Aktualisierung von APIs ein Meilenstein-Release des Produkts und dann des Plug-ins und anschließend wieder des Produkts erforderlich. Ich habe bestimmt einige Punkte vergessen, aber ich denke, du hast jetzt eine gute Vorstellung davon, dass eine perfekte Lösung für das Problem sicherlich anders aussieht.
Zweite Möglichkeit: git submodule
Wenn du kein Tool zum Abhängigkeits-Management nutzen willst oder kannst, bietet Git dir einen einfachen Weg, mit Untermodulen zu arbeiten. Untermodule können vor allem bei dynamischen Programmiersprachen praktisch sein. Langsame Builds ersparen sie dir allerdings nicht unbedingt. Ich habe ein paar Anhaltspunkte und Tipps dazu notiert und auch Alternativen entdeckt. Im Internet findet man jedoch größtenteils Argumente, die dagegensprechen.
1-zu1-Entsprechung von svn:external in Git
ACHTUNG! Wenn du auf der Suche nach einer gleichwertigen Lösung für svn:externals in Git bist, kannst du submodules verwenden. Mit submodules werden nur Release Branches und keine zufälligen Commits nachverfolgt.
Dritte Möglichkeit: Andere Tools für Build- und Stack-übergreifende Abhängigkeiten
Nicht immer hast du das Glück, an einem völlig homogenen Projekt zu arbeiten, das mit einem einzigen Tool fertiggestellt und zusammengesetzt werden kann. Zum Beispiel müssen bei einigen mobilen Projekten Abhängigkeiten von Java und C++ in Einklang gebracht oder anbieterspezifische Tools zur Erstellung von Ressourcen eingesetzt werden. In komplexeren Fällen kannst du Git auch noch erweitern. Ein tolles Beispiel hierfür ist das Repository von Android.
Andere Build-Tools, die einen näheren Blick wert sind:
Fazit und weiterführende Artikel
Weitere lesenswerte Inhalte zum Thema Build-Infrastruktur (und Maven) von Charles O'Farrell:
und der nachfolgende Blogpost über Mavens grundlegende Schwächen
Zum Schluss noch ein großartiges Zitat aus dem letzten der oben angegebenen Artikel. Es bezieht sich zwar auf Maven, gilt aber ebenso für andere Build- und Abhängigkeitstools:
Ein Cache hat keine andere Funktion, als für mehr Geschwindigkeit zu sorgen. Man könnte den Cache komplett entfernen und das betroffene System würde genauso weiterlaufen, nur eben langsamer. Ein Cache hat auch keine Nebenwirkungen. Unabhängig davon, was du mit einem Cache zuvor gemacht hast, wird dieselbe Anfrage an den Cache immer denselben Wert zurückgeben.
Das Erlebnis mit Maven unterscheidet sich stark von dem, was ich beschreibe! Maven-Repositorys werden wie Caches verwendet, haben aber nicht die Eigenschaften von Caches. Wenn du etwas aus einem Maven-Repository abrufst, ist es absolut entscheidend, was zuvor passiert ist. Es gibt das Letzte zurück, was man hineingelegt hat. Wenn du etwas abrufst, bevor du es deinem Repository hinzugefügt hast, kann der Abruf sogar fehlschlagen.
Für dich empfohlen
Bitbucket-Blog
DevOps-Lernpfad
Weitere Informationen zu Git
Weitere Git-Anleitungen und Ressourcen findest du in diesem Hub.