Git Submodule
Eine Einführung in Git-Submodule: Hinzufügen, Klonen, Aktualisieren, Pushen und Entfernen von Submodulen mit allen wichtigen Befehlen.
Ein Submodul ermöglicht es, ein Git-Repository in ein anderes einzubetten und dabei deren Historien vollständig getrennt zu halten. Diese Seite erklärt, was ein Submodul ist, wann es das richtige Werkzeug ist, und den vollständigen Lebenszyklus der Befehle: Hinzufügen, Klonen, Pullen, Aktualisieren, Pushen und Entfernen eines Submoduls – inklusive der häufigen Fallstricke, die Submodule knifflig machen.
Was ist ein Submodul
Sehr häufig ist ein Code-Repository auf externen Code aus anderen Repositories angewiesen. Man kann diesen externen Code direkt kopieren und in das Haupt-Repository einfügen oder das Paketverwaltungssystem einer Programmiersprache nutzen. Beide Methoden haben jedoch den Nachteil, dass Änderungen am externen Repository nicht nachverfolgt werden.
Git ermöglicht es, andere Git-Repositories – sogenannte Submodule – in ein einzelnes Repository einzubinden. Ein Submodul befindet sich an einem bestimmten Pfad im Arbeitsverzeichnis des übergeordneten Repositories und ist selbst ein vollständiger Klon eines anderen Repositories mit seiner eigenen .git-Historie.
Der Kerngedanke ist, dass ein Submodul auf genau einen bestimmten Commit festgelegt ist, nicht auf einen Branch oder einen Tag. Das übergeordnete Repository speichert nur den Pfad des Submoduls, die URL und den SHA des erwarteten Commits. So kann man externen Code zu einem bekannten, reproduzierbaren Zeitpunkt einbinden.
Zwei Dateien verwalten diese Beziehung:
.gitmodules— eine versionierte Datei im Wurzelverzeichnis des übergeordneten Repositories. Sie ordnet den Pfad jedes Submoduls der Remote-URL zu (und optional einem Branch). Diese Datei wird committet und mit allen geteilt..git/configund der Gitlink-Eintrag im Tree — lokale, klonspezifische Buchführung, die den tatsächlich ausgecheckten Commit speichert (der Gitlink erscheint mit dem Modus160000).
Submodule unterstützen das Hinzufügen, Synchronisieren, Aktualisieren und Klonen. Da das übergeordnete Repository jedoch nur einen Commit-SHA speichert, ist das Aktualisieren eines Submoduls immer ein bewusster, zweistufiger Vorgang – niemals automatisch.
Wann Submodule verwendet werden sollten
Die Arbeit mit Submodulen ist anspruchsvoll. Daher empfehlen wir folgende optimale Anwendungsfälle:
- Wenn sich das Teilprojekt zu schnell ändert oder bevorstehende Änderungen die API brechen würden, sperrt man den Code auf einen bestimmten Commit, um sicher zu gehen.
- Wenn eine Komponente nicht sehr häufig aktualisiert wird und man sie als Vendor-Abhängigkeit verfolgen möchte.
- Wenn man einen Teil des Projekts an eine dritte Partei übergibt und deren Arbeit zu einem bestimmten Zeitpunkt integrieren möchte (funktioniert nur, wenn Updates nicht zu häufig erfolgen).
- Wenn der technologische Kontext Pakete und formale Abhängigkeitsverwaltung erlaubt, sollte man Paketmanager statt Submodule verwenden.
- Wenn die Codebasis sehr groß ist und man sie nicht jedes Mal vollständig herunterladen möchte, ermöglichen Submodule, dass Mitarbeiter nur die benötigten Teile herunterladen.
Ein Submodul hinzufügen
Zunächst erstellt man das Repository, das das Submodul enthalten soll (oder wechselt in ein vorhandenes):
mkdir git-submodule-demo
cd git-submodule-demo/
git initInitialized empty Git repository in /Users/example/git-submodule-demo/.git/Ein Submodul wird mit git submodule add hinzugefügt, indem die URL des einzubettenden Repositories übergeben wird:
git submodule add https://somehost/example/textexampleCloning into '/Users/example/git-submodule-demo/textexample'...
remote: Counting objects: 8, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (8/8), done.Git klont das textexample-Repository sofort in einen gleichnamigen Ordner und erstellt eine .gitmodules-Datei. Um das Submodul an einem anderen Pfad zu platzieren, fügt man diesen als letztes Argument hinzu, z. B. git submodule add <url> vendor/textexample.
Nun prüft man den Zustand des Repositories mit git status:
git statusOn branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitmodules
new file: textexampleZu beachten ist, dass textexample als einzelner Eintrag gestagt wird, nicht als die einzelnen Dateien darin. Das übergeordnete Repository verfolgt nur den Commit-Zeiger des Submoduls. Beide Dateien werden mit git add und git commit committet:
git add .gitmodules textexample
git commit -m "Add textexample submodule"[master (root-commit) d5002d0] Add textexample submodule
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 textexampleDer Modus 160000 ist besonders: Er kennzeichnet textexample als Gitlink (einen Zeiger auf einen Commit) und nicht als normales Verzeichnis.
Submodul-Status prüfen
Bevor man etwas ändert, sollte man sich ansehen, wo sich jedes Submodul aktuell befindet:
git submodule status a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 textexample (v1.2.0)Das führende Zeichen ist aussagekräftig: Ein Leerzeichen bedeutet, dass das Submodul am erwarteten Commit ist; ein + bedeutet, dass es auf einem anderen Commit als dem vom übergeordneten Repository gespeicherten ausgecheckt ist; ein - bedeutet, dass es noch nicht initialisiert wurde.
Submodule aktualisieren
Teammitglieder müssen den Submodul-Code aktualisieren, wenn er anderswo geändert wurde. Man kann sich nicht allein auf git pull verlassen, da das Pullen des übergeordneten Repositories nur den gespeicherten Submodul-Commit ändert – die im Submodul ausgecheckten Dateien werden nicht berührt. Um den vom übergeordneten Repository erwarteten Commit auszuchecken, führt man folgenden Befehl aus:
git submodule updateOhne das Flag --remote checkt dieser Befehl den im übergeordneten Repository gespeicherten Commit aus und holt keine neuen Upstream-Änderungen.
Um stattdessen den neuesten Commit vom verfolgten Branch des Submoduls zu holen (main standardmäßig oder der in .gitmodules festgelegte branch), verwendet man --remote:
git submodule update --remote textexampleDadurch wird der Upstream des Submoduls abgerufen und vorwärts bewegt. Das übergeordnete Repository sieht nun einen neuen Commit-Zeiger, daher muss man das Submodul mit git add stagen und committen, um die Änderung zu speichern.
Um ein Update für jedes Submodul durchzuführen, einschließlich verschachtelter, fügt man --init --recursive hinzu:
git submodule update --init --recursiveWenn sich die .gitmodules-Datei ändert (z. B. wenn die URL des Submoduls verschoben wird), führt man git submodule sync aus, um die neuen URLs in die lokale .git/config zu kopieren, bevor man aktualisiert:
git submodule sync --recursiveGit-Submodule klonen
Um ein Projekt mit Submodulen zu klonen, verwendet man den Befehl git clone. Standardmäßig klont er das übergeordnete Repository, lässt aber Submodul-Verzeichnisse leer. Danach muss man git submodule init und git submodule update ausführen. Ersteres aktualisiert die lokale .git/config mit den Zuordnungen aus .gitmodules, während letzteres die Submodul-Daten abruft und den gespeicherten Commit auscheckt.
Wenn man ohne --recurse-submodules geklont hat, füllt man die Submodule nachträglich:
git clone /url/to/repo/with/submodules
git submodule init
git submodule updateDie Abkürzung git submodule update --init kombiniert diese letzten beiden Befehle. Noch besser ist es, alles in einem Schritt mit --recurse-submodules zu klonen:
git clone --recurse-submodules /url/to/repo/with/submodulesDadurch werden das übergeordnete Repository geklont und alle Submodule automatisch initialisiert und ausgecheckt, sodass der Arbeitsbaum sofort vollständig ist.
Den Code des Submoduls pullen
Wenn man ein übergeordnetes Repository pullt, das neue Submodule erhalten hat, kommen diese Submodule uninitialisiert an. Zunächst holt man den neuesten Stand des übergeordneten Repositories:
git pullWenn neue Submodule aufgelistet sind, initialisiert man sie und holt sie in einem Schritt:
git submodule update --init --recursivegit submodule init allein kopiert nur die Zuordnungen in die lokale .git/config; es wird kein Code heruntergeladen. Der update-Schritt ruft das Submodul tatsächlich ab und checkt den gespeicherten Commit aus. Man kann git pull so konfigurieren, dass dies automatisch geschieht:
git config submodule.recurse trueUpdates in einem Submodul pushen
Ein Submodul ist ein vollständiges, unabhängiges Git-Repository, daher committet und pusht man innerhalb seines Verzeichnisses genau wie überall sonst:
cd textexample
git checkout main
# ...edit files...
git commit -am "Fix typo in textexample"
git push
cd ..Nachdem man innerhalb des Submoduls committet hat, zeigt das übergeordnete Repository noch auf den alten Commit. Das Ausführen von git status im übergeordneten Repository zeigt nun:
modified: textexample (new commits)Den neuen Zeiger speichert man, indem man den Submodul-Pfad im übergeordneten Repository stagt, committet und dann pusht:
git add textexample
git commit -m "Bump textexample to latest"
git pushUm zu vermeiden, dass das übergeordnete Repository gepusht wird, bevor seine Submodul-Commits auf dem Remote existieren (was Teammitglieder mit einem kaputten, unerreichbaren Zeiger zurücklassen würde), pusht man alles zusammen:
git push --recurse-submodules=on-demandDies pusht zuerst alle noch nicht gepushten Submodul-Commits und dann das übergeordnete Repository.
Ein Submodul entfernen
Das manuelle Löschen des Ordners reicht nicht aus – Git behält seine Buchführung bei. Ein Submodul wird sauber entfernt mit:
git submodule deinit -f textexample
git rm textexample
git commit -m "Remove textexample submodule"deinit hebt die Registrierung des Submoduls auf und entfernt seinen Arbeitsbaum, git rm löscht den Gitlink und seinen .gitmodules-Eintrag, und der Commit speichert die Entfernung.
Zusammenfassung
Submodule sind eine gute Möglichkeit, Projekte in separaten Repositories zu halten und sie gleichzeitig als Ordner im Arbeitsverzeichnis eines anderen Repositories zu referenzieren. Das Grundprinzip ist einfach: Das übergeordnete Repository speichert einen Commit-Zeiger, jedes Update ist bewusst, und --recurse-submodules schützt vor halb gefüllten Klonen. Für häufig wechselnde Abhängigkeiten ist ein echter Paketmanager in der Regel die bessere Wahl. Für weiterführende Informationen zu verwandten Workflows siehe git clone, git pull und git status.