Git Submodules vs. Alternativen für Multi-Repo-Setups
git
HEAD
Git · Submodules · Multi-Repo · Composer
Git Submodules vs. Alternativen für Multi-Repo-Setups
Wann sich das Vendoring per Submodule wirklich lohnt

Git Submodules versprechen, mehrere Repositories sauber zu verzahnen, doch in der Praxis sorgen vergessene Updates, ein verwirrender Detached HEAD Zustand und divergierende Commit Pins im Team regelmäßig für Frust. Dieser Artikel erklärt, wie Submodules technisch funktionieren, vergleicht sie mit git subtree, Composer basierten Abhängigkeiten und Monorepos, und zeigt, welches Werkzeug für typische Magento Multi Repo Setups tatsächlich passt.

13 Min. Lesezeit Submodules · Subtree · Composer Magento 2 · Multi-Repo · CI/CD

1. Wie Git-Submodules technisch funktionieren

Ein Git-Submodule ist im Kern nichts anderes als ein vollständig eigenständiges Repository mit eigener Historie, eigenen Branches und eigenem Remote, das an einem bestimmten Pfad in den Working Tree eines übergeordneten (Parent-)Repositories eingehängt wird. Das Parent-Repository speichert dabei weder den Inhalt noch die Commits des Submodules selbst. Stattdessen legt Git im Verzeichnisbaum des Parent-Repos einen speziellen Tree-Eintrag vom Modus 160000 an, den sogenannten Gitlink. Dieser Eintrag verweist nicht auf einen Blob oder Tree, sondern direkt auf eine einzige, konkrete Commit-SHA im Submodule-Repository.

Ergänzt wird das durch die Datei .gitmodules im Wurzelverzeichnis des Parent-Repos, eine ganz normale, versionierte Textdatei im INI-Format, die für jeden Submodule-Pfad die Remote-URL und optional einen Branch hinterlegt. Wichtig ist die Trennung der Zuständigkeiten: .gitmodules sagt Git, woher ein Submodule stammt, während der Gitlink-Eintrag im Tree sagt, welcher exakte Commit dort aktuell ausgecheckt sein soll. Genau diese Kombination aus externem Repository plus fixiertem Commit-Zeiger macht Submodules zum nativen Git-Werkzeug für Vendoring, ist aber gleichzeitig die Wurzel fast aller im Folgenden beschriebenen Stolperfallen.

Wer git ls-tree HEAD in einem Repository mit Submodules ausführt, sieht neben normalen Blob- und Tree-Einträgen eine Zeile wie 160000 commit a1b2c3d... vendor/module-x. Der Modus 160000 existiert nur für Submodule-Einträge und markiert sie explizit als "commit", nicht als Datei oder Verzeichnis. Genau deshalb zeigt git diff bei einer Änderung im Submodule nur eine einzelne Zeile mit altem und neuem Commit-Hash, niemals den eigentlichen Dateiinhalt, denn Git betrachtet das gesamte Submodule als atomaren, undurchsichtigen Zeiger.

Die Datei .gitmodules ergänzt diese Struktur um die Herkunftsinformation. Sie wird selbst normal versioniert und committet, ist aber strikt von der eigentlichen Versionsfixierung getrennt. Ein häufiges Missverständnis: Ein git pull im Parent-Repository aktualisiert weder den Gitlink noch den Inhalt des Submodule-Verzeichnisses automatisch auf den neuesten Stand des Submodule-Remotes. Es holt lediglich die neue Historie des Parent-Repos inklusive eines möglicherweise geänderten Gitlink-Zeigers, der aber erst durch einen expliziten Submodule-Update-Befehl im Working Tree wirksam wird.


# .gitmodules: tracked in the parent repo, maps path to remote URL
[submodule "vendor/module-x"]
	path = vendor/module-x
	url = git@github.com:example-vendor/module-x.git
	branch = main

[submodule "deploy-config"]
	path = config/deploy-config
	url = git@gitlab.mironsoft.internal:ops/deploy-config.git
	# Shallow clone keeps checkout fast for a config-only repo
	shallow = true

3. Submodules einbinden, klonen und aktualisieren

Ein neues Submodule wird mit git submodule add <url> <pfad> eingebunden. Der Befehl klont das fremde Repository in den angegebenen Pfad, schreibt den passenden Eintrag in .gitmodules, erzeugt den Gitlink-Tree-Eintrag und staged beides für den nächsten Commit im Parent-Repository. Bis zu diesem Punkt ist noch nichts geteilt, außer der Referenz. Andere Teammitglieder, die den Parent-Repo klonen, erhalten zunächst nur einen leeren Ordner am Submodule-Pfad, denn ein normales git clone initialisiert Submodules standardmäßig nicht.

Genau hier liegt der mit Abstand häufigste praktische Stolperstein: Ohne git clone --recurse-submodules beim initialen Klonen oder git submodule update --init --recursive danach bleibt das Submodule-Verzeichnis leer, während Build-Skripte oder Composer-Autoloader auf Dateien verweisen, die schlicht nicht existieren. Dasselbe Problem tritt nach jedem git pull erneut auf, sobald sich der Gitlink-Zeiger im Parent-Repo geändert hat: Der neue Commit-Hash ist zwar lokal bekannt, das Submodule-Arbeitsverzeichnis zeigt aber weiterhin auf den alten Stand, bis erneut aktualisiert wird.


# Clone a parent repo including all submodules in one step
$ git clone --recurse-submodules git@github.com:mironsoft/magento-shop.git

# Forgot --recurse-submodules? Initialize and fetch afterwards
$ git submodule update --init --recursive

# Pull parent changes AND move submodules to their newly pinned commits
$ git pull --recurse-submodules

# Make git pull always recurse into submodules for this repo
$ git config submodule.recurse true

# Check status of all submodules including nested ones
$ git submodule status --recursive
 a1b2c3d vendor/module-x (heads/main)
+e4f5a6b config/deploy-config (heads/main)
# leading "+" means the checked-out commit differs from the pinned SHA

4. Das Detached-HEAD-Problem bei Submodules

Nach jedem git submodule update befindet sich das Submodule standardmäßig nicht auf einem Branch, sondern im Detached-HEAD-Zustand, direkt auf dem gepinnten Commit. Das ist technisch korrekt und beabsichtigt, denn Git muss exakt den referenzierten Commit auschecken, unabhängig davon, welcher Branch dort gerade zeigt. Für Entwickler, die es gewohnt sind, dass jeder Checkout automatisch auf einem Branch landet, fühlt sich das jedoch wie ein Fehler an, besonders wenn sie versehentlich direkt im Submodule-Verzeichnis arbeiten.

Das eigentliche Risiko entsteht, wenn im Detached-HEAD-Zustand committet wird: Die neuen Commits existieren zwar im lokalen Objektspeicher des Submodules, hängen aber an keinem Branch. Wird anschließend im Submodule ein anderer Branch ausgecheckt oder erneut git submodule update im Parent-Repo ausgeführt, werden diese Commits für Git faktisch unerreichbar und laufen Gefahr, von der nächsten Garbage Collection entfernt zu werden. Die Faustregel lautet deshalb: Vor jeder Änderung im Submodule zuerst explizit git checkout -b feature/x oder zumindest auf einen bekannten Branch wechseln, niemals direkt im Detached-HEAD-Zustand weiterarbeiten.

5. Divergierende Commit-Pins im Team und CI-Komplexität

Weil jeder Gitlink einen exakten Commit fixiert, driften Submodule-Stände im Team schnell auseinander, sobald nicht jeder Entwickler diszipliniert nach jedem Pull aktualisiert. Ein Entwickler arbeitet gegen Commit A, ein anderer noch gegen Commit B, ein dritter hat lokal sogar unbewusst einen neueren Commit C ausgecheckt, ohne ihn im Parent-Repo zu committen. Tests laufen dann scheinbar zufällig unterschiedlich durch, obwohl der Code im Parent-Repository für alle identisch aussieht, weil die tatsächliche Ursache unsichtbar im Submodule-Zeiger liegt und von git status im Parent-Repo nur als vage "modified content" Zeile angezeigt wird.

In CI-Pipelines potenziert sich das Problem: Ein Runner, der frisch aus dem Parent-Repository klont, benötigt zwingend einen expliziten Schritt für Submodule-Initialisierung, sonst schlagen Build oder Tests mit kryptischen "file not found" Fehlern fehl, die nichts mit dem eigentlichen Code zu tun haben. Private Submodule-Repositories erfordern zusätzlich, dass der CI-Runner über eigene Zugangsdaten oder SSH-Deploy-Keys verfügt, getrennt von denen des Parent-Repos, was Secret-Management und Berechtigungskonzepte in mehrstufigen Pipelines spürbar verkompliziert.


# .gitlab-ci.yml: explicit submodule handling for a Magento CI pipeline
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  # Separate deploy token, submodule repo is private and not covered
  # by the parent repo's default CI credentials
  GIT_SUBMODULE_FORCE_HTTPS: "true"

build:
  stage: build
  before_script:
    - git submodule sync --recursive
    - git submodule update --init --recursive
  script:
    - bin/composer install --no-dev --optimize-autoloader
    - bin/magento setup:di:compile

6. git subtree: Historie verschmelzen statt referenzieren

git subtree löst dasselbe Grundproblem, Fremdcode in ein Repository integrieren, mit einem fundamental anderen Ansatz: Statt eine Referenz auf ein externes Repository zu speichern, kopiert und verschmilzt Subtree die komplette Historie des fremden Projekts direkt in einen Unterordner des Parent-Repos. Nach einem git subtree add ist der Code einfach da, als wäre er von Anfang an Teil des Repositories gewesen. Es gibt kein .gitmodules, keinen Gitlink, kein separates Update-Kommando, keinen Detached-HEAD-Zustand, ein frischer git clone reicht vollständig aus.

Der Preis dafür ist eine deutlich unübersichtlichere Commit-Historie: Jeder Subtree-Merge fügt potenziell hunderte fremde Commits in den git log des Parent-Repos ein, was git blame und Bisect-Sessions erschwert. Aktualisierungen erfolgen über git subtree pull, das erneut den kompletten aktuellen Stand des fremden Repos hereinmerged, und eigene Änderungen lassen sich mit git subtree push zurück in das Original-Repository schieben, allerdings nur, wenn dessen Historie kompatibel bleibt. Für Teams, die kein zusätzliches Init-Kommando nach dem Klonen tolerieren wollen, ist Subtree oft die pragmatischere Wahl als Submodules.


# Add a third-party repo as a subtree, squashing its history into one commit
$ git subtree add --prefix=vendor/module-x \
    git@github.com:example-vendor/module-x.git main --squash

# Pull upstream changes into the subtree later on
$ git subtree pull --prefix=vendor/module-x \
    git@github.com:example-vendor/module-x.git main --squash

# Push local changes made inside vendor/module-x back upstream
$ git subtree push --prefix=vendor/module-x \
    git@github.com:example-vendor/module-x.git contribution-branch

# No .gitmodules, no init step: a plain clone already has everything
$ git clone git@github.com:mironsoft/magento-shop.git

7. Composer-Abhängigkeiten: private Packagist, Satis und VCS-Repositories

Für PHP- und Magento-Projekte ist die naheliegendste Alternative zu Submodules oft gar keine Git-Technik, sondern der Paketmanager, der ohnehin schon im Einsatz ist. Statt einen rohen Commit-Hash über einen Gitlink zu fixieren, referenziert Composer versionierte Releases über Semantic-Versioning-Constraints wie ^2.3. Ein privates Magento-Modul lässt sich über ein vcs-Repository direkt aus dem Git-Remote laden, über eine selbstgehostete Satis-Instanz als eigenes Paketarchiv bereitstellen, oder über privates Packagist mit vollem Release-Management und Team-Zugriffsrechten verwalten.

Der entscheidende Vorteil gegenüber Submodules: composer install ist bereits Teil jedes Magento-Deployments und jeder CI-Pipeline, es gibt also keinen zusätzlichen Init-Schritt und kein Risiko eines vergessenen Updates. Versionen werden explizit in composer.lock fixiert, was reproduzierbare Builds genauso garantiert wie ein Gitlink, aber mit lesbaren Versionsnummern statt kryptischer Hashes und mit den vertrauten Werkzeugen composer update, composer why und composer outdated für Abhängigkeitsanalyse. Der Nachteil: Jede Änderung am privaten Modul erfordert einen echten Release-Tag, was den Iterationszyklus während aktiver Parallelentwicklung spürbar verlangsamt.


{
  "repositories": [
    {
      "type": "vcs",
      "url": "git@github.com:mironsoft/module-seosuite.git"
    },
    {
      "type": "composer",
      "url": "https://satis.mironsoft.internal"
    }
  ],
  "require": {
    "mironsoft/module-seosuite": "^2.3",
    "mironsoft/module-core": "^1.8"
  },
  "config": {
    "allow-plugins": {
      "magento/*": true
    }
  }
}

8. Monorepo: ein Repository für alles

Die radikalste Alternative verzichtet komplett auf mehrere Repositories: Im Monorepo-Ansatz leben Shop, gemeinsame Module und Deployment-Konfiguration in genau einem Git-Repository, typischerweise in getrennten Verzeichnissen. Das eliminiert Submodules, Gitlinks und Versionsfixierung als Konzept vollständig, weil es schlicht nur eine einzige, gemeinsame Historie gibt. Ein Commit kann gleichzeitig eine Änderung an einem Modul und die dazu passende Anpassung im Shop-Code enthalten, atomar und ohne Koordination über mehrere Repository-Grenzen hinweg. Genau dieser Vorteil macht Monorepos besonders attraktiv für eng gekoppelten, gemeinsam entwickelten Code.

Der Preis ist strukturell: Mit wachsender Anzahl an Projekten wächst auch die Repo-Größe, Clone-Zeiten steigen, und ohne zusätzliches Tooling wie Sparse-Checkout oder Partial-Clone laden Entwickler oft mehr Code herunter, als sie für ihre eigentliche Aufgabe brauchen. CI-Pipelines müssen zudem lernen, selektiv nur die tatsächlich betroffenen Teilbereiche zu bauen und zu testen, sonst wächst die Pipeline-Laufzeit linear mit der Gesamtgröße des Repositories, selbst wenn nur eine einzelne Zeile in einem kleinen Modul geändert wurde.

9. Submodules, Subtree, Composer und Monorepo im Vergleich

Keine der vier Optionen ist pauschal die "richtige" Lösung, jede löst ein anderes Grundproblem unterschiedlich gut. Die folgende Tabelle stellt sie entlang der praxisrelevantesten Kriterien gegenüber.

Kriterium Submodules Subtree Composer Monorepo
Setup-Komplexität Init-Schritt nach jedem Clone nötig Kein Init-Schritt, normaler Clone reicht composer install, ohnehin Standard Kein zusätzlicher Schritt
Historien-Klarheit Sauber getrennt, eigene Historie Fremde Commits vermischen sich mit eigenen Sauber getrennt über Pakete Eine Historie, thematisch vermischt
Lernkurve im Team Hoch, Detached HEAD verwirrt Einsteiger Mittel, wenige Zusatzbefehle Niedrig, Standardworkflow aller PHP-Devs Niedrig, aber Tooling-Reife nötig
CI-Aufwand Zusätzliche Init- und Auth-Konfiguration Keine Extra-Konfiguration nötig Bereits im Standard-Build enthalten Selektive Build-Strategie erforderlich
Passt gut für Fixierte Vendor-Forks, Config-Repos Einmalige Vendoring-Situationen Versionierte, wiederverwendbare Module Eng gekoppelte Teams, ein Produkt

Für die meisten Magento-Multi-Repo-Setups gilt eine einfache Faustregel: Sobald ein Stück Code als eigenständiges, wiederverwendbares Modul mit klaren Versionsgrenzen gedacht ist, etwa eine SEO-Extension, die in mehreren Kundenprojekten läuft, ist Composer über ein privates VCS-Repository oder Satis fast immer die bessere Wahl. Submodules bleiben sinnvoll für Sonderfälle wie einen gepinnten Fork eines Drittanbieter-Moduls, der bewusst nicht über Composer aktualisiert werden soll, oder ein zentrales, git-verwaltetes Deployment-Config-Repository, das mehrere Projekte referenzieren, aber niemals automatisch aktualisieren sollen. Monorepos wiederum lohnen sich vor allem dann, wenn Shop und Module ohnehin nur von einem einzigen, eng abgestimmten Team weiterentwickelt werden.

Mironsoft

Repo-Strategien, Composer-Migrationen und CI/CD-Pipelines für PHP- und Magento-Teams

Multi-Repo-Setup professionell strukturieren?

Wir analysieren euer aktuelles Submodule- oder Multi-Repo-Setup, zeigen konkrete Alternativen auf und begleiten die Migration zu Composer, Subtree oder Monorepo, ohne Build- und Deployment-Prozesse zu unterbrechen.

Repo-Strategie-Audit

Bestehende Submodule- und Multi-Repo-Setups analysieren und bewerten

Composer-Migration

Private Packagist, Satis oder VCS-Repositories für Magento-Module aufsetzen

CI/CD für Multi-Repo

Pipelines für Submodule-, Composer- oder Monorepo-Workflows zuverlässig aufsetzen

10. Zusammenfassung

Git Submodules fixieren ein externes Repository über einen Gitlink-Tree-Eintrag und die Datei .gitmodules auf einen exakten Commit. Das ist technisch sauber, bringt aber praktische Fallstricke mit sich: vergessene git submodule update --init --recursive-Aufrufe, ein verwirrender Detached-HEAD-Zustand nach jedem Update, divergierende Commit-Pins im Team und zusätzlicher Konfigurationsaufwand in jeder CI-Pipeline. git subtree vermeidet den Init-Schritt und den Detached-HEAD-Zustand vollständig, erkauft das aber mit einer unübersichtlicheren, vermischten Commit-Historie.

Für die meisten PHP- und Magento-Projekte ist Composer über private Packagist-, Satis- oder VCS-Repositories die pragmatischere Wahl, weil Versionsverwaltung und Installation bereits Teil des Standard-Workflows sind. Ein Monorepo eliminiert das Problem strukturell, indem es mehrere Repositories komplett überflüssig macht, verlangt dafür aber wachsende Investition in Tooling und selektive CI-Strategien. Submodules bleiben die richtige Wahl für klar abgegrenzte Sonderfälle wie gepinnte Vendor-Forks oder geteilte Deployment-Config-Repositories, die bewusst nicht automatisch aktualisiert werden sollen.

Git Submodules vs. Alternativen: Das Wichtigste auf einen Blick

Submodules

Gitlink + .gitmodules fixieren einen exakten Commit. Init-Schritt und Detached HEAD sind die größten Stolperfallen.

Subtree

Verschmilzt fremde Historie direkt ins Repo. Kein Init-Schritt, dafür unübersichtlicherer git log.

Composer

Versionierte Releases über VCS-Repository, Satis oder privates Packagist statt rohem Commit-Pin.

Monorepo

Ein Repository, atomare Commits über Modulgrenzen hinweg, dafür wachsende Repo-Größe und CI-Komplexität.

11. FAQ: Git Submodules vs. Alternativen

1Was ist ein Git-Submodule technisch gesehen?
Ein eigenständiges Repository, das über einen Gitlink-Tree-Eintrag (Modus 160000) und .gitmodules auf einen exakten Commit fixiert wird. Der Parent-Repo speichert weder Inhalt noch Historie selbst.
2Warum ist mein Submodule-Ordner nach dem Klonen leer?
git clone initialisiert Submodules standardmäßig nicht. Mit --recurse-submodules klonen oder danach git submodule update --init --recursive ausführen.
3Was bedeutet Detached HEAD bei Submodules und ist es gefährlich?
HEAD zeigt direkt auf den gepinnten Commit statt auf einen Branch. Gefährlich nur bei Commits dort: Sie hängen an keinem Branch und können bei einem erneuten Update verloren gehen.
4Was ist der Unterschied zwischen git submodule und git subtree?
Submodules referenzieren nur einen fixierten Commit im externen Repo. Subtree verschmilzt die komplette fremde Historie ins Parent-Repo, ohne Init-Schritt, aber mit unübersichtlicherem Log.
5Wann Composer statt Submodules für ein privates Modul nutzen?
Wenn das Modul ein eigenständiges, wiederverwendbares Paket mit klaren Versionsgrenzen ist. Composer bringt Standard-Tooling ohne zusätzliche Init-Befehle mit.
6Was ist der Unterschied zwischen Satis und privatem Packagist?
Satis ist eine selbstgehostete, statische Paketliste ohne Release-Management. Privates Packagist ist ein vollwertiger, gehosteter Paket-Server mit Team-Zugriffsrechten.
7Wie aktualisiere ich alle Submodules auf den neuesten Remote-Stand?
Mit git submodule update --remote --merge, danach den neuen Gitlink-Stand im Parent-Repository committen, damit er für das Team sichtbar wird.
8Warum divergieren Submodule-Stände im Team so leicht?
Weil git pull das Submodule-Arbeitsverzeichnis nicht automatisch aktualisiert. Ohne konsequentes submodule update arbeiten Teammitglieder unbemerkt gegen unterschiedliche Commit-Stände.
9Was sind die Nachteile eines Monorepos?
Wachsende Repo-Größe und Clone-Zeit, sowie die Notwendigkeit einer selektiven CI-Build-Strategie, damit die Pipeline-Laufzeit nicht mit der Gesamtgröße mitwächst.
10Wann sind Submodules trotzdem die richtige Wahl?
Bei gepinnten Vendor-Forks mit lokalen Patches oder zentralen, git-verwalteten Deployment-Config-Repositories, die bewusst nicht über Composer versioniert werden sollen.