welche Strategie für welchen Anwendungsfall wirklich passt
Sobald ein Projekt Code aus einem anderen Repository einbinden muss, stehen zwei eingebaute Git-Mechanismen zur Auswahl: Submodule und Subtree. Beide lösen dasselbe Grundproblem, externe Historie im eigenen Repository referenzierbar zu machen, aber mit vollkommen unterschiedlichen Konsequenzen für Checkout, CI-Pipelines und die Frage, wer welchen Code wo pflegt. Dieser Artikel vergleicht beide Ansätze anhand konkreter Workflows und ordnet ein, wann welche Variante in einem Magento-Kontext sinnvoll ist.
Inhaltsverzeichnis
- 1. Das Grundproblem: externer Code im eigenen Repository
- 2. Wie Git Submodule intern funktioniert
- 3. Wie Git Subtree intern funktioniert
- 4. Praktischer Alltag mit Submodulen
- 5. Praktischer Alltag mit Subtree, inklusive Rücksynchronisation
- 6. Auswirkungen auf CI-Pipelines und Checkout-Zeit
- 7. Subtree und Submodule in Magento-Projekten
- 8. Entscheidungshilfe: welche Strategie wann
- 9. Von Submodule zu Subtree wechseln und umgekehrt
- 10. Zusammenfassung
- 11. FAQ
1. Das Grundproblem: externer Code im eigenen Repository
Fast jedes größere Projekt erreicht irgendwann den Punkt, an dem Code aus einem anderen Repository eingebunden werden muss, sei es eine gemeinsam genutzte Bibliothek, ein Theme oder ein internes Toolset. Die naive Lösung, den fremden Code einfach zu kopieren und ins eigene Repository einzuchecken, funktioniert kurzfristig, verliert aber sofort den Bezug zur Originalhistorie und macht spätere Updates zur manuellen Fleissarbeit.
Git bringt für genau dieses Problem zwei native Mechanismen mit: Submodule verwaltet einen Verweis auf einen exakten Commit eines fremden Repositories, Subtree kopiert den fremden Code inklusive Historie direkt in einen Unterordner des eigenen Repositories. Beide Ansätze sind seit Jahren stabiler Bestandteil von Git, unterscheiden sich aber so grundlegend in ihrer Arbeitsweise, dass die Wahl selten beliebig ist.
2. Wie Git Submodule intern funktioniert
Ein Submodule ist technisch gesehen kein Ordner voller Dateien im üblichen Sinn, sondern ein spezieller Verweis-Eintrag, der auf einen konkreten Commit-Hash eines fremden Repositories zeigt. Die Datei .gitmodules im Wurzelverzeichnis speichert dabei den Pfad und die Remote-URL jedes eingebundenen Submodules, während das Hauptrepository selbst nur den referenzierten Commit-Hash mitführt, nicht den Inhalt.
Das hat einen entscheidenden Vorteil: Das Hauptrepository bleibt klein, weil die eigentliche Historie des fremden Codes getrennt bleibt. Der Nachteil zeigt sich beim Checkout, denn ein normales git clone lädt Submodule standardmäßig nicht mit, sondern legt nur einen leeren Ordner an. Wer den vollständigen Stand braucht, muss explizit git submodule update --init --recursive ausführen, ein Schritt, der in CI-Pipelines gerne vergessen wird und dann zu rätselhaften fehlenden Dateien führt.
# Submodule einbinden
git submodule add https://example.com/shared-library.git vendor/shared-library
# Nach einem frischen Clone: Submodule-Inhalte nachladen
git clone https://example.com/hauptprojekt.git
cd hauptprojekt
git submodule update --init --recursive
# Submodule auf den neuesten Commit im referenzierten Branch bringen
git submodule update --remote vendor/shared-library
3. Wie Git Subtree intern funktioniert
Subtree geht den entgegengesetzten Weg: Statt eines Verweises wird der komplette Inhalt des fremden Repositories per Merge in einen Unterordner des Hauptrepositories geschrieben. Nach dem Einbinden liegen alle Dateien direkt im Arbeitsverzeichnis, so als wären sie von Anfang an Teil des Projekts gewesen. Es gibt keine separate .gitmodules-Datei und keinen zweiten Checkout-Schritt.
Der Preis dafür ist eine größere Historie im Hauptrepository, weil die komplette Commit-Historie des fremden Projekts beim Einbinden mit hereinkommt, sofern man nicht mit --squash arbeitet. Für die meisten Teams überwiegt der praktische Vorteil: Jeder, der klont, hat sofort den vollständigen Code, ohne zusätzliche Befehle, und Werkzeuge, die Submodule nicht kennen, funktionieren trotzdem einwandfrei.
# Fremdes Repository als Subtree einbinden, Historie komprimiert
git subtree add --prefix=vendor/shared-library \
https://example.com/shared-library.git main --squash
# Aenderungen aus dem Original-Repository nachziehen
git subtree pull --prefix=vendor/shared-library \
https://example.com/shared-library.git main --squash
4. Praktischer Alltag mit Submodulen
Der tägliche Umgang mit Submodulen bringt eine zusätzliche mentale Ebene mit sich, denn ein Submodule-Ordner befindet sich fast immer in einem sogenannten Detached-HEAD-Zustand: Er zeigt auf einen konkreten Commit, nicht auf einen Branch. Wer im Submodule Aenderungen vornimmt, muss dort explizit auf einen Branch wechseln, committen und pushen, bevor der Verweis im Hauptrepository überhaupt aktualisiert werden kann.
Vergisst jemand im Team, den aktualisierten Submodule-Verweis im Hauptrepository zu committen, arbeiten andere Entwickler unbemerkt mit einem veralteten Stand weiter. In der Praxis hilft git status im Hauptrepository dabei, solche Abweichungen sichtbar zu machen, weil Git einen veränderten Submodule-Commit als modifizierte Datei ausweist.
5. Praktischer Alltag mit Subtree, inklusive Rücksynchronisation
Weil Subtree-Code wie normaler Projektcode aussieht, können Entwickler direkt darin ändern, ohne an Submodule-Besonderheiten zu denken. Die eigentliche Herausforderung liegt darin, solche lokalen Aenderungen später zurück in das Original-Repository zu bringen. Dafür existiert git subtree push, das die relevanten Commits aus dem Unterordner herausfiltert und in das fremde Repository schiebt.
Bei Repositories mit vielen parallelen Aenderungen kann dieser Rücksynchronisationsschritt spürbar langsamer werden, weil Git die komplette Historie nach relevanten Aenderungen im jeweiligen Prefix durchsuchen muss. Für Projekte, die nur gelegentlich Updates aus der Quelle ziehen und selten selbst zurückliefern, ist das kein praktisches Problem, bei sehr aktiver bidirektionaler Zusammenarbeit lohnt sich ein Blick auf dedizierte Werkzeuge wie git-subtree-Wrapper oder alternative Monorepo-Strategien.
# Lokale Aenderungen im Subtree-Ordner zurück ins Original-Repository schieben
git subtree push --prefix=vendor/shared-library \
https://example.com/shared-library.git feature/lokale-anpassung
6. Auswirkungen auf CI-Pipelines und Checkout-Zeit
In CI-Umgebungen macht sich der Unterschied besonders deutlich bemerkbar. Ein Standard-Checkout ohne zusätzliche Konfiguration reicht bei Subtree vollständig aus, weil der Code physisch im Repository liegt. Bei Submodulen muss die Pipeline-Konfiguration explizit wissen, dass rekursiv initialisiert werden muss, sei es über GIT_SUBMODULE_STRATEGY: recursive in GitLab CI oder eine entsprechende Checkout-Action-Option bei GitHub Actions.
Wird dieser Schritt vergessen, scheitert der Build oft erst spät und mit einer schwer zu deutenden Fehlermeldung über fehlende Dateien, nicht mit einem offensichtlichen Git-Fehler. Teams, die Submodule einsetzen, sollten diesen Schritt deshalb explizit in der Pipeline-Dokumentation und in Onboarding-Anleitungen für neue Entwickler festhalten.
7. Subtree und Submodule in Magento-Projekten
In der Magento-Welt taucht die Frage typischerweise bei eigenen Hyvä-Kindthemes, gemeinsam genutzten Modulen über mehrere Mandanten hinweg oder bei internen Composer-Paketen auf, die noch nicht über ein privates Packagist-Repository verteilt werden. Für ein Theme, das mehrere Shops mit identischer Basis, aber kleinen Store-spezifischen Anpassungen nutzen, bietet sich Subtree an, weil Entwickler direkt im Theme-Ordner arbeiten können, ohne sich um Detached-HEAD-Zustände zu kümmern.
Für klar abgegrenzte, selten geänderte Bibliotheken, etwa eine gemeinsame PHP-Utility-Klassensammlung, die von mehreren unabhängigen Magento-Installationen referenziert wird, ist Submodule oft die sauberere Wahl, weil der Verweis auf eine konkrete, getestete Version explizit im Hauptrepository sichtbar bleibt. In der Praxis ersetzt bei echten PHP-Abhängigkeiten aber meist ein privates Composer-Repository beide Ansätze, Subtree und Submodule bleiben dann Werkzeuge für Grenzfälle wie Themes oder Build-Konfiguration.
8. Entscheidungshilfe: welche Strategie wann
Wer häufig ändert, wenig Historie-Ballast will und möchte, dass jeder Entwickler nach einem einfachen Clone sofort arbeitsfähig ist, fährt mit Subtree in der Regel besser. Wer dagegen eine klare, versionierte Trennung zwischen eigenem Code und einer als extern behandelten Abhängigkeit braucht und bereit ist, den zusätzlichen Checkout-Schritt in Kauf zu nehmen, ist mit Submodule gut bedient.
Eine dritte Option, die in vielen Fällen beide Werkzeuge überflüssig macht, ist ein echter Paketmanager: Für PHP-Code ist das Composer mit einem privaten Repository, für JavaScript npm mit einem privaten Registry-Scope. Subtree und Submodule bleiben dann für die Fälle reserviert, in denen kein Paketmanager passt, etwa bei ganzen Theme-Verzeichnissen oder Build-Tooling, das nicht als Paket modelliert werden soll.
9. Von Submodule zu Subtree wechseln und umgekehrt
Ein bestehendes Submodule lässt sich in ein Subtree überführen, indem der Submodule-Eintrag entfernt und der Code anschliessend frisch per git subtree add eingebunden wird, allerdings ohne automatische Uebernahme der ursprünglichen Commit-Historie im Hauptrepository. Der umgekehrte Weg, von Subtree zurück zu Submodule, ist aufwendiger, weil die bereits kopierten Dateien zunächst entfernt und durch einen sauberen Submodule-Verweis ersetzt werden müssen.
In beiden Richtungen empfiehlt es sich, den Wechsel in einem eigenen Commit mit klarer Beschreibung durchzuführen und alle Teammitglieder vorab zu informieren, weil ein solcher Strukturwechsel bestehende lokale Arbeitskopien betrifft und im schlimmsten Fall zu Verwirrung über verschwundene oder verdoppelte Ordner führt.
# Bestehendes Submodule entfernen, bevor es als Subtree neu eingebunden wird
git submodule deinit -f vendor/shared-library
git rm -f vendor/shared-library
rm -rf .git/modules/vendor/shared-library
git commit -m "Submodule vendor/shared-library entfernt"
git subtree add --prefix=vendor/shared-library \
https://example.com/shared-library.git main --squash
| Merkmal | Submodule | Subtree |
|---|---|---|
| Speicherung im Hauptrepository | Nur Verweis auf Commit-Hash |
Vollständiger Code kopiert |
| Checkout nach git clone | Zusätzlicher Schritt submodule update --init nötig |
Sofort vollständig, kein Zusatzschritt |
| CI-Konfiguration | Muss rekursiven Checkout explizit aktivieren | Funktioniert mit Standard-Checkout |
| Aenderungen zurückliefern | Im Submodule direkt committen und pushen | git subtree push, kann bei grosser Historie langsam sein |
| Historie im Hauptrepository | Bleibt schlank | Wächst, besonders ohne --squash |
Mironsoft
Git-Workflows, Branching-Strategien und CI-Hooks
Chaotische Git-Historie und unklare Branching-Regeln im Team?
Wir richten saubere Git-Workflows ein, klären Branching-Strategien fürs Team und automatisieren Qualitätschecks über Git-Hooks und CI-Pipelines, damit die Historie nachvollziehbar bleibt.
Workflow-Audit
Bestehende Branching-Strategie und Merge-Praxis auf Schwachstellen prüfen.
Hook-Automatisierung
Pre-Commit- und Pre-Push-Hooks für Linting, Tests und Commit-Konventionen einrichten.
Team-Schulung
Rebase, Cherry-Pick und Konfliktauflösung im Team praxisnah vermitteln.
10. Zusammenfassung
Subtree vs. Submodule: Das Wichtigste auf einen Blick
Empfehlung
Subtree für häufig geänderte, gemeinsam genutzte Themes, Submodule für stabile, versionierte Abhängigkeiten
CI-Aufwand
Submodule braucht expliziten rekursiven Checkout, Subtree funktioniert ohne Zusatzkonfiguration
Historie
Submodule hält das Hauptrepository schlank, Subtree kopiert Historie mit hinein
Magento-Praxis
Für echte PHP-Abhängigkeiten meist Composer, Subtree/Submodule bleiben für Themes und Grenzfälle