Git
Chapters ▾ 2nd Edition

7.11 Git Tools - Submodule

Submodule

Es kommt oft vor, dass Sie während der Arbeit an einem Projekt ein anderes Projekt innerhalb des Projekts verwenden müssen. Möglicherweise handelt es sich dabei um eine Bibliothek, die von einem Dritten entwickelt wurde oder die Sie separat entwickeln und in mehreren übergeordneten Projekten verwenden wollen. In diesen Szenarien tritt ein typisches Problem auf: Sie möchten die beiden Projekte getrennt halten und dennoch das eine vom anderen aus nutzen können.

Dazu ein Beispiel. Angenommen, Sie entwickeln eine Website und erstellen Atom-Feeds. Statt Ihren eigenen Atom-generierenden Code zu schreiben, entscheiden Sie sich für die Verwendung einer Bibliothek. Wahrscheinlich müssen Sie entweder diesen Code aus einer gemeinsam genutzten Bibliothek wie einer CPAN-Installation bzw. RubyGems einbinden oder den Quellcode in Ihren eigenen Projektbaum kopieren. Die Schwierigkeit bei der Einbindung der Bibliothek besteht darin, dass es nicht einfach ist, die Bibliothek in beliebiger Form anzupassen und oft noch schwieriger ist, sie zu verteilen, da sichergestellt werden muss, dass jedem Kunden diese Bibliothek zur Verfügung steht. Das Problem mit dem Kopieren des Codes in Ihr eigenes Projekt ist, dass alle vorgenommenen eigenen Änderungen nur schwer gemergt werden können, wenn Änderungen in der vorgeschalteten Ebene verfügbar werden.

Git löst dieses Problem mit Hilfe von Submodulen. Submodule ermöglichen es Ihnen, ein Git-Repository als Unterverzeichnis eines anderen Git-Repositorys zu führen. Dadurch können Sie ein anderes Repository in Ihr Projekt klonen und Ihre Commits getrennt halten.

Erste Schritte mit Submodulen

Wir durchlaufen beispielhaft die Entwicklung eines einfachen Projekts, das in ein Hauptprojekt und mehrere Unterprojekte aufgeteilt wurde.

Beginnen wir mit dem Einfügen eines bestehenden Git-Repositorys als Submodul des in Arbeit befindlichen Repositorys. Ein neues Untermodul können Sie mit dem Befehl git submodule add und der absoluten oder relativen URL des zu trackenden Projekts hinzufügen. In diesem Beispiel fügen wir eine Bibliothek mit der Bezeichnung „DbConnector“ hinzu.

$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

Standardmäßig fügt der Befehl Submodul als Subprojekt in ein Verzeichnis mit dem gleichen Namen wie das Repository, hier „DbConnector“, ein. Sie können am Ende des Befehls einen anderen Pfad angeben, wenn Sie es woanders ablegen wollen.

Wenn Sie an dieser Stelle git status laufen lassen, werden Ihnen ein paar Dinge auffallen.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   .gitmodules
	new file:   DbConnector

Als erstes werden Sie die neue Datei .gitmodules bemerken. Das ist eine Konfigurationsdatei, die die Zuordnung zwischen der URL des Projekts und dem lokalen Unterverzeichnis, in das Sie es kopiert haben, speichert:

[submodule "DbConnector"]
	path = DbConnector
	url = https://github.com/chaconinc/DbConnector

Wenn Sie mehrere Submodule haben, werden Sie mehrere Einträge in dieser Datei haben. Beachten Sie bitte, dass diese Datei zusammen mit Ihren anderen Dateien, wie z.B. der .gitignore Datei, der Versionskontrolle unterliegen. Sie wird zusammen mit dem Rest Ihres Projekts gepusht und gepullt. So wissen auch andere Entwickler, die dieses Projekt klonen, wo sie die Submodul-Projekte beziehen können.

Note

Da die in der .gitmodules-Datei enthaltene URL der Ort ist, wo Andere zuerst versuchen werden, Ihr Reposority zu klonen oder zu holen, vergewissern Sie sich, dass Sie eine URL verwenden, auf die ein Zugriff möglich ist. Wenn Sie z.B. eine unterschiedliche URLs zum Pushen und zum Pullen verwenden, dann benutzen Sie die, auf die Andere Zugriff haben. Sie können diesen Wert lokal mit git config submodule.DbConnector.url PRIVATE_URL für den eigenen Einsatz überschreiben. Eine relative URL kann unter Umständen hilfreich sein.

Der andere Punkt in der Ausgabe von git status ist der Eintrag für den Projektordner. Wenn Sie darauf git diff ausführen, sehen Sie etwas Merkwürdiges:

$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

Obwohl DbConnector ein Unterverzeichnis in Ihrem Arbeitsverzeichnis ist, versteht es Git als ein Submodul und überwacht (engl. track) seinen Inhalt nicht, solange Sie sich nicht in diesem Verzeichnis befinden. Git sieht es vielmehr als einen besonderen Commit dieses Repositorys an.

Wenn Sie eine etwas informativere diff-Ausgabe wollen, können Sie an git diff die Option --submodule übergeben.

$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+       path = DbConnector
+       url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

Wenn Sie einen Commit durchführen, sehen Sie etwa das:

$ git commit -am 'Add DbConnector module'
[master fb9093c] Add DbConnector module
 2 files changed, 4 insertions(+)
 create mode 100644 .gitmodules
 create mode 160000 DbConnector

Beachten Sie den Modus 160000 für den Eintrag DbConnector. Das ist ein spezieller Modus in Git, der im Prinzip bedeutet, dass Sie einen Commit als Verzeichniseintrag und nicht als Unterverzeichnis oder Datei erfassen.

Schließlich sollten Sie diese Änderungen pushen:

$ git push origin master

Ein Projekt mit Submodulen klonen

Jetzt klonen wir ein Projekt mit einem enthaltenen Submodul. Wenn Sie ein solches Projekt klonen, erhalten Sie standardmäßig die Verzeichnisse, die Submodule enthalten, aber keine der darin enthaltenen Dateien:

$ git clone https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
$ cd MainProject
$ ls -la
total 16
drwxr-xr-x   9 schacon  staff  306 Sep 17 15:21 .
drwxr-xr-x   7 schacon  staff  238 Sep 17 15:21 ..
drwxr-xr-x  13 schacon  staff  442 Sep 17 15:21 .git
-rw-r--r--   1 schacon  staff   92 Sep 17 15:21 .gitmodules
drwxr-xr-x   2 schacon  staff   68 Sep 17 15:21 DbConnector
-rw-r--r--   1 schacon  staff  756 Sep 17 15:21 Makefile
drwxr-xr-x   3 schacon  staff  102 Sep 17 15:21 includes
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 scripts
drwxr-xr-x   4 schacon  staff  136 Sep 17 15:21 src
$ cd DbConnector/
$ ls
$

Das Verzeichnis DbConnector ist vorhanden, aber leer. Sie müssen deshalb zwei Befehle ausführen: git submodule init, um Ihre lokale Konfigurationsdatei zu initialisieren und git submodule update, um alle Daten dieses Projekts zu fetchen und den entsprechenden Commit prüfen, ob diese Ihrem Hauptprojekt aufgelistet sind:

$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

Nun hat Ihr Unterverzeichnis DbConnector exakt den aktuellen Inhalt, den es bei Ihrem letzten Commit hatte.

Es gibt noch eine andere Möglichkeit, die etwas unkomplizierter ist. Wenn Sie dem Befehl git clone die Option --recurse-submodules zuweisen, wird jedes Submodul im Repository automatisch initialisiert und aktualisiert, einschließlich verschachtelter Submodule, falls eines der Submodule im Repository selbst Submodule hat.

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

Wenn Sie das Projekt bereits geklont und --recurse-submodules vergessen haben, können Sie die Schritte git submodule init und git submodule update zur Aktualisierung auch kombinieren, indem Sie git submodule update --init aufrufen. Damit auch verschachtelte Submodule initialisiert, gefetcht und ausgecheckt werden, können Sie das narrensichere git submodule update --init --recursive verwenden.

Arbeiten an einem Projekt mit Submodulen

Wir haben jetzt eine Projektkopie mit Submodulen und werden sowohl beim Haupt- als auch beim Submodulprojekt mit unseren Teamkollegen zusammenarbeiten.

Übernehmen von Upstream-Änderungen aus dem Remote-Submodul

Das Einfachste bei der Verwendung von Submodulen in einem Projekt, ist ein Subprojekt nur zu benutzen und gelegentlich Aktualisierungen zu erhalten, die aber nichts an Ihrem Checkout ändern. Gehen wir ein einfaches Beispiel durch.

Wenn Sie in einem Untermodul nach neuen Inhalten suchen wollen, können Sie in das Verzeichnis gehen und git fetch und git merge auf dem Upstream-Branch ausführen, um den lokalen Code zu aktualisieren.

$ git fetch
From https://github.com/chaconinc/DbConnector
   c3f01dc..d0354fc  master     -> origin/master
$ git merge origin/master
Updating c3f01dc..d0354fc
Fast-forward
 scripts/connect.sh | 1 +
 src/db.c           | 1 +
 2 files changed, 2 insertions(+)

Wenn Sie nun zurück in das Hauptprojekt gehen und git diff --submodule ausführen, können Sie sehen, dass das Submodul aktualisiert wurde und erhalten eine Liste der Commits, die ihm hinzugefügt wurden. Um nicht jedes Mal, wenn Sie git diff ausführen, --submodule einzugeben, können Sie es als Standardformat festlegen, indem Sie den Konfigurationswert diff.submodule auf „log“ setzen.

$ git config --global diff.submodule log
$ git diff
Submodule DbConnector c3f01dc..d0354fc:
  > more efficient db routine
  > better connection routine

Sollten Sie zu diesem Zeitpunkt einen Commit ausführen, dann wird für das Submodul festgelegt, dass es in allen Repositorys den neuen Code enthalten soll. Das bedeutet, wenn andere Personen in Klonen Ihres Repositorys git submodule update ausführen, bekommen sie auch den neuen Code des Submoduls.

Es gibt auch einen leichteren Weg, das zu erreichen. Falls Sie es vorziehen, nicht manuell in das Unterverzeichnis zu fetchen und mergen. Wenn Sie git submodule update --remote ausführen, wird Git in Ihre Submodule hineingehen und die Aktualisierung für Sie abholen und durchführen.

$ git submodule update --remote DbConnector
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
Submodule path 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644'

Dieser Befehl geht standardmäßig davon aus, dass Sie den Checkout auf den Branch master des Submodul-Repositorys aktualisieren wollen. Sie können es, sofern Sie möchten, auch auf etwas anderes umstellen. Wenn Sie beispielsweise möchten, dass das DbConnector-Submodul den Branch „stable“ dieses Repositorys tracken soll, dann können Sie es entweder in Ihrer .gitmodules Datei eintragen (so jeder es auch trackt) einfach in Ihrer lokalen .git/config Datei setzen. Lassen Sie es uns in der .gitmodules Datei einrichten:

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
Submodule path 'DbConnector': checked out 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687'

Wenn Sie die Option -f .gitmodules weglassen, wird die Änderung nur für Sie vorgenommen. Es ist aber wahrscheinlich sinnvoller, diese Informationen mit dem Repository zu protokollieren, damit alle anderen es genauso machen.

Wenn wir hier git status ausführen, wird Git uns anzeigen, dass wir „neue Commits“ im Submodul haben.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

  modified:   .gitmodules
  modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

Wenn Sie die Konfigurationseinstellung status.submodulesummary setzen, zeigt Ihnen Git auch eine kurze Übersicht der Änderungen in den Submodulen an:

$ git config status.submodulesummary 1

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   .gitmodules
	modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c3f01dc...c87d55d (4):
  > catch non-null terminated lines

Sobald Sie jetzt git diff ausführen, können Sie erkennen, dass sowohl unsere .gitmodules Datei modifiziert wurde und dass es eine Reihe von Commits gibt, die wir gepullt haben und die bereit sind, an unser Submodul-Projekt committet zu werden.

$ git diff
diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
 Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

Das ist ziemlich beeindruckend, denn wir können das Log-Protokoll der Commits sehen, die wir in unserem Submodul vornehmen wollen. Nach dem erfolgten Commit können Sie diese Informationen auch nachträglich anzeigen lassen, indem Sie git log -p aufrufen.

$ git log -p --submodule
commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae
Author: Scott Chacon <schacon@gmail.com>
Date:   Wed Sep 17 16:37:02 2014 +0200

    updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules
index 6fc0b3d..fd1cc29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
 [submodule "DbConnector"]
        path = DbConnector
        url = https://github.com/chaconinc/DbConnector
+       branch = stable
Submodule DbConnector c3f01dc..c87d55d:
  > catch non-null terminated lines
  > more robust error handling
  > more efficient db routine
  > better connection routine

Standardmäßig wird Git versuchen, alle Submodule zu aktualisieren, wenn Sie git submodule update --remote ausführen. Wenn Sie viele Submodule haben, sollten Sie also den Namen genau des Submoduls angeben, das Sie gerade aktualisieren möchten.

Upstream-Änderungen des Projekts vom Remote aus pullen

Lassen Sie uns aus Sicht Ihres Mitarbeiters agieren, der einen eigenen lokalen Klon des Hauptprojekt-Repositorys besitzt. Einfach nur git pull auszuführen, um die von Ihnen eingereichten Änderungen abzurufen, wird nicht ausreichen:

$ git pull
From https://github.com/chaconinc/MainProject
   fb9093c..0a24cfc  master     -> origin/master
Fetching submodule DbConnector
From https://github.com/chaconinc/DbConnector
   c3f01dc..c87d55d  stable     -> origin/stable
Updating fb9093c..0a24cfc
Fast-forward
 .gitmodules         | 2 +-
 DbConnector         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c87d55d...c3f01dc (4):
  < catch non-null terminated lines
  < more robust error handling
  < more efficient db routine
  < better connection routine

no changes added to commit (use "git add" and/or "git commit -a")

Der Befehl git pull holt standardmäßig rekursiv die Änderungen der Submodule, wie wir in der Ausgabe des ersten Befehls oben sehen können. Er aktualisiert jedoch nicht die Submodule. Dies wird durch die Ausgabe des git status Befehls gezeigt, aus dem hervorgeht, dass das Submodul „modifiziert“ ist und „neue Commits“ hat. Außerdem zeigen die spitzen Klammern der neuen Commits nach links (<) und bedeuten, dass diese Commits im MainProject aufgezeichnet werden, aber nicht im lokalen Checkout von DbConnector vorhanden sind. Um das Update abzuschließen, müssen Sie git submodule update ausführen:

$ git submodule update --init --recursive
Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'

$ git status
 On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

Sicherheitshalber sollten Sie git submodule update mit dem --init Flag ausführen. Für den Fall, dass das MainProject einen Commit durchführt, bei dem Sie gerade neue Submodule hinzugefügt haben. Wenn ein Submodul verschachtelte Submodule hat, sollten Sie das --recursive Flag setzen.

Wenn Sie diesen Prozess automatisieren möchten, können Sie den Flag --recurse-submodules zum Befehl git pull hinzufügen (seit Git 2.14). Dadurch wird Git dazu veranlasst git submodule update direkt nach dem Pull-Kommando zu starten, wodurch die Submodule in die korrekte Version versetzt werden. Wenn Sie in Git immer mit dem Flag --recurse-submodules pullen wollen, können Sie die Konfigurations-Option submodule.recurse auf true setzen (dies funktioniert für git pull seit Git 2.15). Diese Option bewirkt, dass Git den Flag --recurse-submodules für alle Befehle verwendet, die ihn unterstützen (außer clone).

Es gibt eine besondere Situation, die beim Abrufen von Aktualisierungen des Hauptprojekts auftreten kann: Es könnte sein, dass das Upstream-Repository die URL des Submoduls in der Datei .gitmodules in einem der von Ihnen abgerufenen Commits geändert hat. Das kann zum Beispiel passieren, wenn das Submodul-Projekt seine Hosting-Plattform ändert. In diesem Fall ist es möglich, dass git pull --recurse-submodules oder git submodule update fehlschlägt, wenn das Hauptprojekt auf einen Submodul-Commit verweist, der nicht in dem lokal konfigurierten Submodul in Ihrem Repository gefunden wird. Um diese Situation zu beheben, ist der Befehl git submodule sync erforderlich:

# copy the new URL to your local config
$ git submodule sync --recursive
# update the submodule from the new URL
$ git submodule update --init --recursive

An einem Submodul arbeiten

Wahrscheinlich verwenden Sie Submodule, weil Sie gleichzeitig am Code im Submodul und im Hauptprojekt (oder modulübergreifend in mehreren Submodulen) arbeiten wollen. Andernfalls würden Sie wahrscheinlich stattdessen ein einfacheres System zur Verwaltung von Abhängigkeiten (wie Maven oder Rubygems) verwenden.

Betrachten wir nun ein Beispiel, bei dem Änderungen am Submodul gleichzeitig mit dem Hauptprojekt vorgenommen werden und diese Änderungen zur gleichen Zeit committet und veröffentlicht werden.

Wenn wir bisher den Befehl git submodule update ausgeführt haben, um Änderungen aus den Submodul-Repositorys zu holen, würde Git die Änderungen erhalten und die Dateien im Unterverzeichnis aktualisieren, aber das Sub-Repository in einem sogenannten "detached HEAD" Status belassen. Das bedeutet, dass es keinen lokalen Arbeits-Branch (wie z.B. den master) gibt, der die Änderungen trackt. Ohne einen Arbeitszweig, der Änderungen nachverfolgt, d.h. selbst wenn Sie Änderungen an das Untermodul committen, gehen diese Änderungen möglicherweise verloren, wenn Sie das nächste Mal git submodule update ausführen. Sie müssen einige zusätzliche Schritte ausführen, wenn Sie wollen, dass diese Änderungen in einem Submodul getrackt werden.

Um Ihr Submodul so einzurichten, dass Sie leichter einsteigen und sich einbinden können, müssen Sie zwei Dinge tun. Sie müssen in jedes Submodul gehen und einen Branch auschecken, an dem Sie arbeiten wollen. Dann müssen Sie Git mitteilen, was zu tun ist, wenn Sie Änderungen vorgenommen haben. Anschließend pullt der Befehl git submodule update --remote neue Daten vom Upstream. Die Optionen sind, dass Sie entweder diese in Ihre lokale Bearbeitung mergen oder versuchsweise die Änderungen in Ihrer lokalen Bearbeitung zu rebasen.

Gehen wir zunächst in unser Submodul-Verzeichnis und wechseln in einen Branch.

$ cd DbConnector/
$ git checkout stable
Switched to branch 'stable'

Versuchen wir, unser Submodul mit der Option „merge“ zu aktualisieren. Wenn Sie es manuell starten möchten, können wir einfach die Option --merge zu unserem update Aufruf hinzufügen. Wir erkennen hier, dass es eine Änderung auf dem Server für dieses Submodul gegeben hat und es wird zusammengeführt.

$ cd ..
$ git submodule update --remote --merge
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Updating c87d55d..92c7337
Fast-forward
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
Submodule path 'DbConnector': merged in '92c7337b30ef9e0893e758dac2459d07362ab5ea'

Das Verzeichnis DbConnector hat die neuen Änderungen bereits in unserem lokalen stable Branch verschmolzen. Jetzt wollen wir beobachten, was passiert, wenn wir unsere eigene lokale Änderung an der Bibliothek vornehmen und jemand anderes gleichzeitig eine andere Änderung im Upstream-Bereich vornimmt.

$ cd DbConnector/
$ vim src/db.c
$ git commit -am 'Unicode support'
[stable f906e16] Unicode support
 1 file changed, 1 insertion(+)

Bei der Aktualisierung unseres Submoduls erfahren wir jetzt, was passiert, wenn wir eine lokale Änderung vorgenommen haben und auch im Upstream-Bereich eine Änderung vorliegt, die wir einbauen müssen.

$ cd ..
$ git submodule update --remote --rebase
First, rewinding head to replay your work on top of it...
Applying: Unicode support
Submodule path 'DbConnector': rebased into '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

Wenn Sie --rebase oder --merge vergessen haben, wird Git einfach das Submodul auf das aktualisieren, was auch immer auf dem Server vorhanden ist und Ihr Projekt in einen abgekoppelten (engl. detached) HEAD-Zustand zurücksetzen.

$ git submodule update --remote
Submodule path 'DbConnector': checked out '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

Wenn das passiert, brauchen Sie sich keine Sorgen zu machen, Sie können einfach ins Verzeichnis zurückgehen und Ihren Branch wieder auschecken (der immer noch Ihre Arbeit enthält) und origin/stable (oder welchen Remote-Branch auch immer) manuell mergen oder rebasieren.

Sollten Sie Ihre Änderungen nicht in Ihrem Submodul committet haben und ein Submodul-Update ausführen, das Probleme verursachen würde, holt Git die gemachten Änderungen, überschreibt aber nicht die ungesicherte Arbeit in Ihrem Submodul-Verzeichnis.

$ git submodule update --remote
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0)
Unpacking objects: 100% (4/4), done.
From https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Your local changes to the following files would be overwritten by checkout:
	scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Unable to checkout 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

Wenn Sie Änderungen vorgenommen haben, die mit einer Änderung im Upstream-Bereich in Konflikt stehen, wird Git Sie darüber informieren, sobald Sie das Update ausführen.

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLICT (content): Merge conflict in scripts/setup.sh
Recorded preimage for 'scripts/setup.sh'
Automatic merge failed; fix conflicts and then commit the result.
Unable to merge 'c75e92a2b3855c9e5b66f915308390d9db204aca' in submodule path 'DbConnector'

Sie können in das Submodul-Verzeichnis wechseln und den Konflikt wie gewohnt beheben.

Änderungen am Submodul veröffentlichen

Wir haben jetzt einige Änderungen in unserem Submodul-Verzeichnis gemacht. Einige davon wurden von unseren Updates aus dem Upstream-Bereich eingebracht, andere wurden lokal vorgenommen und stehen noch niemandem zur Verfügung, da wir sie noch nicht gepusht haben.

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > Update setup script
  > Unicode support
  > Remove unnecessary method
  > Add new option for conn pooling

Wenn wir im Hauptprojekt einen Commit machen und ihn nach oben pushen, ohne gleichzeitig die Änderungen an den Submodulen zu pushen, werden andere Entwickler, die versuchen, unsere Änderungen zu überprüfen, in Schwierigkeiten geraten, da sie keine Möglichkeit haben, die davon abhängigen Änderungen an den Submodulen zu erhalten. Diese Änderungen werden nur auf unserer lokalen Kopie existieren.

Um sicherzustellen, dass dies nicht passiert, können Sie Git auffordern, zu überprüfen, ob alle Ihre Submodule ordnungsgemäß gepusht wurden, bevor Sie das Hauptprojekt hochladen. Der Befehl git push übernimmt das Argument --recurse-submodules, das entweder auf „check“ oder „on-demand“ (dt. bei Bedarf) gesetzt werden kann. Die Option „check“ lässt push einfach fehlschlagen, wenn eine der eingereichten Submodul-Änderungen noch nicht gepusht wurde.

$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
  DbConnector

Please try

	git push --recurse-submodules=on-demand

or cd to the path and use

	git push

to push them to a remote.

Wie Sie feststellen können, erhalten wir dadurch auch einige hilfreiche Tipps, was wir als nächstes tun könnten. Die einfache Lösung besteht darin, in jedes Submodul zu wechseln und manuell auf den Remote zu pushen. So stellen Sie sicher, dass die Submodule extern verfügbar sind. Danach kann man diesen Push erneut versuchen. Wenn Sie möchten, dass dieses Verhalten für alle Pushs durchgeführt wird, können Sie dieses Vorgehen zur Standardeinstellung machen. Dazu müssen Sie den Befehl git config push.recurseSubmodules check ausführen.

Die andere Möglichkeit ist die Verwendung der Option „on-demand“, die das für Sie erledigt.

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/MainProject
   3d6d338..9a377d1  master -> master

Wie Sie oben sehen können, ist Git in das DbConnector-Modul gewechselt und hat es gepusht, bevor es das Hauptprojekt gepusht hat. Wenn dieser Push des Submoduls aus irgendeinem Grund fehlschlägt, wird auch der Push des Hauptprojekts fehlschlagen. Sie können dieses Verhalten zur Standardeinstellung machen, indem Sie git config push.recurseSubmodules on-demand ausführen.

Änderungen an den Submodulen mergen

Wenn Sie die Referenz eines Submoduls gleichzeitig mit anderen Personen ändern, könnten Sie auf Probleme stoßen. Sollten die Historien der Submodule divergieren und Sie dorthin zu einem Hauptprojekt mit divergierenden Branches committet haben, kann es ein Stück Arbeit erfordern, das zu beheben.

Ist einer der Commits ein direkter Vorfahre des anderen (ein fast-forward Merge), dann wählt Git einfach den letzteren für den Merge aus, so dass das gut funktioniert.

Git wird nicht einmal einen trivialen Merge versuchen. Wenn die Submodule Commits divergieren und gemergt werden müssen, erhalten Sie ähnliche Informationen wie diese:

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Im Wesentlichen ist hier Folgendes passiert: Git hat herausgefunden, dass die beiden Branches, im Verlauf der Submodule, Einträge aufzeichnen, die voneinander abweichen und gemergt werden müssen. Es erklärt es als „merge following commits not found“ (Merge nach Commits nicht gefunden), was verwirrend ist, aber wir werden gleich erklären, warum das so passiert.

Um das Problem zu lösen, müssen Sie ermitteln, in welchem Status sich das Submodul befinden sollte. Merkwürdigerweise liefert Git hier nicht wirklich viele Informationen, die Ihnen helfen können, nicht einmal die SHA-1s der Commits beider Seiten der Historie. Glücklicherweise ist es aber ganz leicht, das herauszufinden. Wenn Sie git diff ausführen, können Sie die SHA-1s der Commits der beiden Branches, die Sie zusammenführen wollten, anzeigen lassen.

$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

In diesem Fall ist eb41d76 der Commit in unserem Submodul, den wir hatten, und c771610 ist der Commit, den der Upstream hatte. Wenn wir in unser Submodul-Verzeichnis gehen, sollte es bereits eb41d76 enthalten, da es durch den Merge nicht angetastet wurde. Sollte das aus irgendeinem Grund nicht der Fall sein, können Sie einfach einen neuen Branch erstellen und auschecken, der auf dieses Verzeichnis zeigt.

Wichtig ist der SHA-1 des Commits von der Gegenseite. Das werden Sie mergen und lösen müssen. Sie können entweder direkt versuchen, den Merge mit dem SHA-1 durchzuführen oder Sie können einen Branch dafür erstellen und dann versuchen, diesen zu verschmelzen. Wir empfehlen letzteres – und sei es nur, um eine bessere Merge-Commit-Meldung zu erhalten.

Wir werden also in unser Submodul-Verzeichnis wechseln, einen Branch namens „try-merge“, basierend auf diesem zweiten SHA-1 aus git diff erstellen und manuell mergen.

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610

$ git merge try-merge
Auto-merging src/main.c
CONFLICT (content): Merge conflict in src/main.c
Recorded preimage for 'src/main.c'
Automatic merge failed; fix conflicts and then commit the result.

Wir haben hier einen echten Merge-Konflikt. Wenn wir diesen lösen und beheben, dann können wir das Hauptprojekt einfach mit dem Resultat updaten.

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'merged our changes'
Recorded resolution for 'src/main.c'.
[master 9fd905e] merged our changes

$ cd .. (2)
$ git diff (3)
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit eb41d764bccf88be77aced643c13a7fa86714135
 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d
++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a
$ git add DbConnector (4)

$ git commit -m "Merge Tom's Changes" (5)
[master 10d2c60] Merge Tom's Changes
  1. Zuerst lösen wir den Konflikt

  2. Dann wechseln wir zurück zum Hauptprojekt-Verzeichnis

  3. Wir könnten den SHA-1 noch einmal überprüfen

  4. Wir lösen den Konflikt im Submodul-Eintrag

  5. Wir committen unseren Merge

Es kann etwas verwirrend sein, aber es ist eigentlich nicht sehr schwer.

Interessanterweise gibt es einen weiteren Prozess, den Git bearbeiten kann. Wenn ein Merge-Commit im Submodul-Verzeichnis existiert, der beide Commits in seinem Verlauf enthält, wird Git Ihnen diesen als mögliche Abhilfe vorschlagen. Es sieht, dass irgendwann im Submodul-Projekt jemand Branches verschmolzen hat, die diese beiden Commits enthalten. Möglicherweise möchten Sie also diesen einen haben wollen.

Deshalb lautete die Fehlermeldung von vorhin „merge following commits not found“, weil dies nicht möglich war. Es ist verwirrend, denn wer würde erwarten, dass es das versucht?

Wenn Git einen einzelnen, akzeptablen Merge-Commit findet, werden Sie ungefähr folgendes sehen:

$ git merge origin/master
warning: Failed to merge submodule DbConnector (not fast-forward)
Found a possible merge resolution for the submodule:
 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes
If this is correct simply add it to the index for example
by using:

  git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion.
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Der von Git vorgeschlagene Befehl wird den Index aktualisieren, als ob Sie git add (was den Konflikt aufhebt) und dann commit ausführen würden. Allerdings sollten Sie das nicht tun. Sie können genauso einfach in das Submodul-Verzeichnis wechseln, den Unterschied prüfen, zu diesem Commit springen, ihn ordnungsgemäß testen und ihn dann committen.

$ cd DbConnector/
$ git merge 9fd905e
Updating eb41d76..9fd905e
Fast-forward

$ cd ..
$ git add DbConnector
$ git commit -am 'Fast forward to a common submodule child'

Damit wird dasselbe erreicht, aber zumindest können Sie auf diese Weise überprüfen, ob es wirklich funktioniert und Sie haben den Code in Ihrem Submodul-Verzeichnis, wenn Sie fertig sind.

Tipps für Submodule

Es gibt ein paar Dinge, die Sie tun können, um sich die Arbeit mit den Untermodulen ein wenig zu erleichtern.

Submodul Foreach

Es gibt das Submodul-Kommando foreach, um in jedem Submodul ein beliebiges Kommando auszuführen. Das kann wirklich hilfreich sein, wenn Sie mehrere Submodule im gleichen Projekt haben.

Nehmen wir zum Beispiel an, wir wollen ein neues Feature starten oder einen Bugfix durchführen und arbeiten an mehreren Submodulen. Wir können leicht die gesamte Arbeit in all unseren Submodulen stashen.

$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

Dann können wir einen neuen Branch erstellen und von allen unseren Submodulen zu diesem wechseln.

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

Sie verstehen die Idee dahinter? Eine wirklich sinnvolle Methode, die Ihnen hilft, ein gutes, einheitliches Diff zwischen den Änderungen in Ihrem Hauptprojekt und all Ihren Subprojekten zu erstellen.

$ git diff; git submodule foreach 'git diff'
Submodule DbConnector contains modified content
diff --git a/src/main.c b/src/main.c
index 210f1ae..1f0acdc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -245,6 +245,8 @@ static int handle_alias(int *argcp, const char ***argv)

      commit_pager_choice();

+     url = url_decode(url_orig);
+
      /* build alias_argv */
      alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1));
      alias_argv[0] = alias_string + 1;
Entering 'DbConnector'
diff --git a/src/db.c b/src/db.c
index 1aaefb6..5297645 100644
--- a/src/db.c
+++ b/src/db.c
@@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len)
        return url_decode_internal(&url, len, NULL, &out, 0);
 }

+char *url_decode(const char *url)
+{
+       return url_decode_mem(url, strlen(url));
+}
+
 char *url_decode_parameter_name(const char **query)
 {
        struct strbuf out = STRBUF_INIT;

Hier können Sie sehen, dass wir eine Funktion in einem Submodul definieren und sie im Hauptprojekt aufrufen. Das ist natürlich ein vereinfachtes Beispiel, aber es gibt Ihnen hoffentlich eine Vorstellung davon, wie hilfreich das sein könnte.

Nützliche Aliase

Vielleicht möchten Sie Aliase für manche dieser Befehle einrichten, da sie ziemlich lang sein können und Sie können für die meisten dieser Befehle keine Konfigurationsoptionen festlegen, um sie zu Standardeinstellungen zu machen. Wir haben die Einrichtung von Git-Aliasen in Git Aliases behandelt, hier ist aber ein Beispiel dafür, was Sie vielleicht einrichten sollten, wenn Sie viel mit Submodulen in Git arbeiten wollen.

$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

Auf diese Weise können Sie einfach git supdate ausführen, wenn Sie Ihre Submodule aktualisieren wollen, oder git spush, um einen Push mit der Überprüfung der Submodul-Abhängigkeiten durchzuführen.

Probleme mit Submodulen

Die Verwendung von Submodulen ist jedoch nicht ohne Schwierigkeiten.

Branches wechseln

So kann beispielsweise das Wechseln von Branches mit darin enthaltenen Submodulen bei älteren Git-Versionen (vor Git 2.13) ebenfalls knifflig sein. Wenn Sie einen neuen Branch erstellen, dort ein Submodul hinzufügen und dann wieder zu einem Branch ohne dieses Submodul wechseln, haben Sie darin das Submodul-Verzeichnis immer noch als ungetracktes Verzeichnis:

$ git --version
git version 2.12.2

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

Das Entfernen des Verzeichnisses ist nicht schwierig, aber es kann etwas verwirrend sein, das darin enthalten zu haben. Wenn Sie es entfernen und dann wieder zu dem Branch wechseln, der dieses Submodul besitzt, müssen Sie submodule update --init ausführen, um es neu zu befüllen.

$ git clean -ffdx
Removing CryptoLibrary/

$ git checkout add-crypto
Switched to branch 'add-crypto'

$ ls CryptoLibrary/

$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'

$ ls CryptoLibrary/
Makefile	includes	scripts		src

Auch das ist nicht wirklich schwierig, aber es kann ein wenig verwirrend sein.

Neuere Git-Versionen (ab Git 2.13) vereinfachen das alles, indem sie das Flag --recurse-submodules zum Befehl git checkout hinzufügen, der sich darum kümmert, die Submodule in den richtigen Zustand für den Branch zu bringen, auf den wir wechseln.

$ git --version
git version 2.13.3

$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...

$ git commit -am 'Add crypto library'
[add-crypto 4445836] Add crypto library
 2 files changed, 4 insertions(+)
 create mode 160000 CryptoLibrary

$ git checkout --recurse-submodules master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working tree clean

Die Verwendung des Flags --recurse-submodules mit git checkout kann auch praktisch sein, wenn Sie auf mehreren Branches im Hauptprojekt arbeiten, wobei jedes Ihrer Submodule auf unterschiedliche Commits zeigt. Tatsächlich, wenn Sie zwischen den Branches wechseln, die das Submodul bei verschiedenen Commits erfassen, wird das Submodul bei der Ausführung von git status als „modifiziert“ erscheinen und „neue Commits“ anzeigen. Das liegt daran, dass der Submodul-Status beim Wechseln der Branches standardmäßig nicht mit übertragen wird.

Das kann äußerst verwirrend sein, deshalb ist es besser, immer den Befehl git checkout --recurse-submodules zu verwenden, wenn Ihr Projekt Submodule hat. Für ältere Git-Versionen, die das Flag --recurse-submodules nicht kennen, verwenden Sie nach dem Auschecken git submodule update --init --recursive, um die Submodule in den richtigen Zustand zu versetzen.

Glücklicherweise können Sie Git (≥2.14) anweisen, immer das Flag --recurse-submodules zu verwenden, indem Sie die Konfigurationsoption submodule.recurse setzen mit: git config submodule.recurse true. Wie oben schon erwähnt, wird das auch dazu führen, dass Git in Submodulen jeden Befehl entsprechend umwandelt, der eine --recurse-submodules Option hat (außer bei git clone).

Wechseln von Unterverzeichnissen zu Submodulen

Die andere Warnung, die viele Anwender zu beachten haben, ist der Wechsel von Unterverzeichnissen zu Submodulen. Wenn Sie Dateien in Ihrem Projekt getrackt haben und sie in ein Submodul verschieben wollen, müssen Sie vorsichtig sein, sonst wird Git Ihnen das nicht verzeihen. Angenommen, Sie haben Dateien in einem Unterverzeichnis Ihres Projekts und wollen es in ein Untermodul verschieben. Wenn Sie das Unterverzeichnis löschen und dann submodule add ausführen, wird Git Sie etwa so anbrüllen:

$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index

Sie müssen zuerst das CryptoLibrary Verzeichnis aus der Staging-Area entfernen (engl. unstage). Danach können Sie das Submodul hinzufügen:

$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

Nehmen wir jetzt an, Sie hätten das in einem Branch getan. Wenn Sie versuchen, wieder zu einem Branch zu wechseln, in der sich diese Dateien noch im aktuellen Verzeichnisbaum und nicht in einem Submodul befinden, erhalten Sie diesen Fehler:

$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
  CryptoLibrary/Makefile
  CryptoLibrary/includes/crypto.h
  ...
Please move or remove them before you can switch branches.
Aborting

Sie können den Wechsel mit checkout -f erzwingen, aber achten Sie darauf, dass dort keine ungesicherten Änderungen enthalten sind, weil diese mit dem Befehl überschrieben werden könnten.

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'

Dann, wenn Sie zurückwechseln, erhalten Sie aus bestimmten Gründen ein leeres CryptoLibrary Verzeichnis, und git submodule update kann es auch nicht beheben. Möglicherweise müssen Sie in Ihr Submodul-Verzeichnis gehen und git checkout . ausführen, um alle Ihre Dateien zurück zu bekommen. Sie könnten das mit einem submodule foreach Skript erledigen, um es für mehrere Submodule anzuwenden.

Es ist wichtig zu wissen, dass Submodule heute alle ihre Git-Daten im .git Verzeichnis des Hauptprojekts speichern, so dass im Gegensatz zu vielen älteren Git-Versionen, mit dem Löschen eines Submodul-Verzeichnisses keine Commits oder Branches verloren gehen, die Sie zuvor schon hatten.

Mit diesen Werkzeugen können Submodule eine ziemlich einfache und effektive Methode sein, um an mehreren verwandten, aber dennoch unterschiedlichen Projekten gleichzeitig zu entwickeln.