Git
Chapters ▾ 2nd Edition

7.11 Git Tools - Submodule

Submodule

Es kommt oft vor, dass du während der Arbeit an einem Projekt ein anderes Projekt verwenden musst. Möglicherweise handelt es sich dabei um eine Bibliothek, die von einem Dritten entwickelt wurde oder die du selber entwickelst und in mehreren übergeordneten Projekten verwenden willst. In diesen Szenarien tritt ein typisches Problem auf: Du möchtest die beiden Projekte getrennt halten und dennoch das eine vom anderen aus nutzen können.

Dazu ein Beispiel. Angenommen, du entwickelst eine Website und erstellst Atom-Feeds. Statt deinem eigenen Atom-generierenden Code zu schreiben, entscheidest du dich für die Verwendung einer Bibliothek. Wahrscheinlich musst du entweder den Code aus einer gemeinsam genutzten Bibliothek wie eine CPAN-Installation bzw. RubyGems einbinden oder den Quellcode in deinem eigenen Projektbaum kopieren. Die Schwierigkeit beim Einbinden von Bibliotheken besteht darin, dass es nicht einfach ist, die Bibliothek in anzupassen. Noch schwieriger ist es, sie zu deployen, da sichergestellt werden muss, dass jedem Kunden diese Bibliothek zur Verfügung steht. Das Problem mit dem Kopieren des Codes in dein eigenes Projekt ist, dass alle von dir vorgenommenen Änderungen nur schwer gemerged werden können, wenn Änderungen im Upstream verfügbar werden.

Git löst dieses Problem mit Hilfe von Submodulen. Submodule ermöglichen es dir, ein Git-Repository als Unterverzeichnis eines anderen Git-Repositorys zu führen. Dadurch kannst du ein anderes Repository in dein Projekt klonen und deine 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 kannst du 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. Du kannst am Ende des Befehls einen anderen Pfad angeben, wenn du es woanders ablegen willst.

Wenn du an dieser Stelle git status ausführst, werden dir 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 wirst du die neue Datei .gitmodules bemerken. Das ist eine Konfigurationsdatei, die die Zuordnung zwischen der URL des Projekts und dem lokalen Unterverzeichnis, in das du es kopiert hast, speichert:

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

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

Anmerkung

Da die in der .gitmodules-Datei enthaltene URL der Ort ist, wo Andere zuerst versuchen werden, das Submodul zu erreichen, vergewissere dich, dass du eine URL verwendest, auf die ein Zugriff möglich ist. Wenn du z.B. eine unterschiedliche URLs zum Pushen und als Andere zum Pullen verwendest, dann benutze die, auf die auch die Andere Zugriff haben. Du kannst 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 du darauf git diff ausführst, siehst du 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 deinem Arbeitsverzeichnis ist, versteht es Git als ein Submodul und überwacht (engl. track) seinen Inhalt nicht, solange du dich nicht in diesem Verzeichnis befindest. Git sieht es vielmehr als einen besonderen Commit dieses Repositorys an.

Wenn du eine etwas informativere diff-Ausgabe benötigst, kannst du 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 du einen Commit durchführst, siehst du folgendes:

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

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

Schließlich solltest du diese Änderungen pushen:

$ git push origin master

Ein Projekt mit Submodulen klonen

Jetzt klonen wir ein Projekt mit einem enthaltenen Submodul. Wenn du ein solches Projekt klonst, erhältst du 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. Du musst deshalb zwei Befehle ausführen: git submodule init, um deine lokale Konfigurationsdatei zu initialisieren und git submodule update, um alle Daten dieses Projekts zu fetchen und die entsprechenden Commits auszuchecken, die in deinem 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 dein Unterverzeichnis DbConnector exakt denselben Inhalt, als du es zum letzten Mal committed hast.

Es gibt noch eine andere, unkompliziertere Möglichkeit. Wenn du dem Befehl git clone die Option --recurse-submodules übergibst, 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 du das Projekt bereits geklont und --recurse-submodules vergessen hast, kannst du die Schritte git submodule init und git submodule update zur Aktualisierung auch kombinieren, indem du git submodule update --init aufrufst. Damit auch verschachtelte Submodule initialisiert, gefetcht und ausgecheckt werden, kannst du 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 deinem Checkout ändern. Gehen wir ein einfaches Beispiel durch.

Wenn du in einem Untermodul nach neuen Inhalten suchen willst, kannst du ins entsprechende 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 du nun zurück in das Hauptprojekt gehst und git diff --submodule ausführst, kannst du sehen, dass das Submodul aktualisiert wurde und erhältst eine Liste der Commits, die ihm hinzugefügt wurden. Um nicht jedes Mal, wenn du git diff ausführst, --submodule einzugeben, kannst du es als Standardformat festlegen, indem du 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

Wenn du nun committest, zwingst du das submodule neuen code zu zu beinhalten, wenn andere Personen ihn updaten.

Es gibt auch einen einfacheren Weg, das zu erreichen. Falls du es vorziehst, nicht manuell in das Unterverzeichnis zu fetchen und zu mergen. Wenn du git submodule update --remote ausführst, wird Git in dein Submodul wechseln und die Aktualisierung 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 du den Checkout auf den default branch des entfernten Submodul-Repositorys aktualisieren möchtest (derjenige, auf den HEAD auf remote zeigt). Du kannst es, sofern du möchtest, auch anders konfigurieren. Wenn du beispielsweise möchtest, dass das DbConnector-Submodul den Branch „stable“ dieses Repositorys tracken soll, dann kannst du es entweder in deiner .gitmodules Datei eintragen (damit jeder andere es auch trackt) oder es einfach in deiner lokalen .git/config Datei setzen. Lass 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 du die Option -f .gitmodules weglässt, wird die Änderung nur für dich vorgenommen. Es ist aber wahrscheinlich sinnvoller, diese Informationen im Repository zu speichern, 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 du die Konfigurationseinstellung status.submodulesummary setzt, zeigt dir 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 du jetzt git diff ausführst, kannst du 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 kannst du diese Informationen auch nachträglich anzeigen lassen, indem du git log -p aufrufst.

$ 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 du git submodule update --remote ausführst. Wenn du viele Submodule hast, solltest du also den genauen Namen des Submoduls angeben, das du gerade aktualisieren möchtest.

Upstream-Änderungen des Projekts vom Remote aus pullen

Lass uns nun aus Sicht deiner Mitstreiters agieren, der einen eigenen lokalen Klon des Hauptprojekt-Repositorys besitzt. Einfach nur git pull auszuführen, um die von dir 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 die Submodule jedoch nicht. Dies wird durch die Ausgabe des git status Befehls gezeigt, aus dem hervorgeht, dass das Submodul „modifiziert“ ist und „neue Commits“ enthält. Außerdem zeigen die spitzen Klammern der neuen Commits nach links (<) und bedeuten, dass diese Commits im Hauptprojekt, aber nicht im lokalen Checkout von DbConnector vorhanden sind. Um das Update abzuschließen, musst du 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 solltest du git submodule update mit dem --init Flag ausführen. Für den Fall, dass das Hauptprojekt einen Commit durchführt, bei dem du gerade neue Submodule hinzugefügt hast. Wenn ein Submodul verschachtelte Submodule enthält, solltest du das --recursive Flag setzen.

Wenn du diesen Prozess automatisieren möchtest, kannst du das 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 du in Git immer mit dem Flag --recurse-submodules pullen willst, kannst du die Konfigurations-Option submodule.recurse auf true setzen (dies funktioniert für git pull seit Git 2.15). Diese Option bewirkt, dass Git das Flag --recurse-submodules für alle Befehle verwendet, die es 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 dir 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 deinem 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 verwendest du Submodule, weil du gleichzeitig am Code im Submodul und im Hauptprojekt (oder modulübergreifend in mehreren Submodulen) arbeiten willst. Andernfalls würdest du 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 committed 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 Arbeits-Branch verändert sich die Nachverfolgung, d.h. selbst wenn du Änderungen an das Untermodul committest, gehen diese Änderungen möglicherweise verloren, wenn du das nächste Mal git submodule update ausführst. Du musst einige zusätzliche Schritte ausführen, wenn du willst, dass diese Änderungen in einem Submodul getrackt werden.

Um dein Submodul so einzurichten, dass du es leichter kannst, musst du zwei Dinge tun. Du musst in jedes Submodul gehen und einen Branch auschecken, an dem du arbeiten willst. Dann musst du Git mitteilen, was zu tun ist, wenn du Änderungen vorgenommen hast. Anschließend pullt der Befehl git submodule update --remote neue Daten vom Upstream. Die Optionen sind, dass du diese entweder in deine lokale Bearbeitung mergst oder du kannst deine lokale Arbeit auf die neuen Änderungen 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 du es manuell starten möchtest, 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 gemerged. 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 du --rebase oder --merge vergessen hast, wird Git einfach das Submodul auf das aktualisieren, was auch immer auf dem Server vorhanden ist und dein Projekt in einen abgekoppelten (engl. detached) HEAD-Zustand zurücksetzen.

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

Wenn das passiert, brauchst du dir keine Sorgen zu machen. Du kannst einfach ins Verzeichnis zurückgehen und deinen Branch wieder auschecken (der immer noch deine Arbeit enthält) und origin/stable (oder welchen Remote-Branch auch immer) manuell mergen oder rebasen.

Solltest du deine Änderungen nicht in deinem Submodul committed haben und ein Submodul-Update ausführen, das Probleme verursachen würde, holt Git die gemachten Änderungen, überschreibt aber nicht die ungesicherte Arbeit in deinem 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 du Änderungen vorgenommen hast, die mit einer Änderung im Upstream-Bereich in Konflikt stehen, wird Git dich darüber informieren, sobald du das Update ausführst.

$ 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'

Du kannst 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 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, kannst du Git auffordern, zu überprüfen, ob alle deine Submodule ordnungsgemäß gepusht wurden, bevor du das Hauptprojekt pushst. 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 du feststellen kannst, 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 stellst du sicher, dass die Submodule extern verfügbar sind. Danach kann man diesen Push erneut versuchen. Wenn du möchtest, dass dieses Verhalten für alle Pushs durchgeführt wird, kannst du dieses Vorgehen zur Standardeinstellung machen. Dazu musst du den Befehl git config push.recurseSubmodules check ausführen.

Die andere Möglichkeit ist die Verwendung der Option „on-demand“, die das für dich 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 du oben sehen konntest, ist Git in das DbConnector-Modul gewechselt und hat es gepusht, bevor das Hauptprojekt gepusht wurde. Wenn dieser Push des Submoduls aus irgendeinem Grund fehlschlägt, wird auch der Push des Hauptprojekts fehlschlagen. Du kannst dieses Verhalten zur Standardeinstellung machen, indem du git config push.recurseSubmodules on-demand ausführst.

Änderungen an den Submodulen mergen

Wenn du die Referenz eines Submoduls gleichzeitig mit anderen Personen änderst, könntest du auf Probleme stoßen. Sollten die Historien der Submodule divergieren und du dorthin zu einem Hauptprojekt mit divergierenden Branches committed haben, kann es schwierig sein, das zu beheben.

Ist einer der Commits ein direkter Vorgänger 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 gemerged werden müssen, erhältst du folgende Ausgabe:

$ 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 gemerged werden müssen. Es erklärt es als „merge following commits not found“ (Merge nach Commits nicht gefunden), was erstmal verwirrend erscheint. Wir werden gleich erklären, warum das so passiert.

Um das Problem zu lösen, musst du ermitteln, in welchem Status sich das Submodul befinden sollte. Merkwürdigerweise liefert Git hier nicht wirklich viele Informationen, die dir helfen können, nicht einmal die SHA-1s der Commits beider Seiten der Historie. Glücklicherweise ist es aber ganz leicht, das herauszufinden. Wenn du git diff ausführst, kannst du die SHA-1s der Commits der beiden Branches, die du zusammenführen willst, 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, kannst du einfach einen neuen Branch erstellen und auschecken, der auf dieses Verzeichnis zeigt.

Wichtig ist der SHA-1 des Commits von der Gegenseite. Diesen wirst du mergen und auflösen müssen. Du kannst entweder direkt versuchen, den Merge mit dem SHA-1 durchzuführen oder du kannst einen Branch dafür erstellen und dann versuchen, diesen zu mergen. 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 Fall, den Git handhaben kann. Wenn ein Merge-Commit im Submodul-Verzeichnis existiert, der beide Commits in seinem Verlauf enthält, wird Git dir dies als mögliche Lösung vorschlagen. Git sieht, dass irgendwann im Submodul-Projekt jemand Branches gemerged hat, die diese beiden Commits enthalten. Möglicherweise möchtest du 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, siehst du wahrscheinlich Folgendes:

$ 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 du git add (was den Konflikt aufhebt) und dann commit ausführen würdest. Allerdings solltest du das nicht tun. Du kannst 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 kannst du auf diese Weise überprüfen, ob es wirklich funktioniert und du hast den Code in deinem Submodul-Verzeichnis, wenn du fertig bist.

Tipps für Submodule

Es gibt ein paar Dinge, die du tun kannst, um dir 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 du mehrere Submodule im gleichen Projekt hast.

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'

Du verstehst die Idee dahinter? Eine wirklich sinnvolle Methode, die dir hilft, ein gutes, einheitliches Diff zwischen den Änderungen in deinem Hauptprojekt und all deinen 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 kannst du sehen, dass wir eine Funktion in einem Submodul definieren und sie im Hauptprojekt aufrufen. Das ist natürlich ein vereinfachtes Beispiel, aber es gibt dir hoffentlich eine Vorstellung davon, wie hilfreich das sein könnte.

Nützliche Aliase

Vielleicht möchtest du Aliase für manche dieser Befehle einrichten, da sie ziemlich lang sein können. Du kannst 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 ein Beispiel dafür, was du vielleicht einrichten solltest, wenn du viel mit Submodulen in Git arbeiten willst.

$ 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 kannst du einfach git supdate ausführen, wenn du deine Submodule aktualisieren willst. Oder du kannst git spush nutzen, 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) etwas knifflig sein. Wenn du einen neuen Branch erstellst, dort ein Submodul hinzufügst und dann wieder zu einem Branch ohne dieses Submodul wechselst, hast du 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, was darin enthalten ist. Wenn du es entfernst und dann wieder zum Branch wechselst, der dieses Submodul besitzt, musst du 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 auch das 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 du auf mehreren Branches im Hauptprojekt arbeitest, wobei jedes deiner Submodule auf unterschiedliche Commits zeigt. Wenn du zwischen den Branches wechselst, die das Submodul bei verschiedenen Commits enthalten, 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 dein Projekt Submodule hat. Für ältere Git-Versionen, die das Flag --recurse-submodules nicht kennen, verwende nach dem Auschecken git submodule update --init --recursive, um die Submodule in den richtigen Zustand zu versetzen.

Glücklicherweise kannst du Git (≥2.14) anweisen, immer das Flag --recurse-submodules zu verwenden, indem du die Konfigurationsoption submodule.recurse setzt 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).

Von Unterverzeichnissen zu Submodulen wechseln

Die andere Falle, in die viele Anwender tappen, ist der Wechsel von Unterverzeichnissen zu Submodulen. Wenn du Dateien in deinem Projekt getrackt hast und sie in ein Submodul verschieben willst, musst du vorsichtig sein. Anderenfalls wird Git dir das nicht verzeihen. Angenommen, du hast Dateien in einem Unterverzeichnis deines Projekts und willst sie in ein Untermodul verschieben. Wenn du das Unterverzeichnis löschen und dann submodule add ausführst, wird Git dich in etwa so ausschimpfen:

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

Du musst zuerst das CryptoLibrary Verzeichnis aus der Staging-Area unstagen. Danach kkannst du 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, du hast das in einem Branch getan. Wenn du versuchst, wieder zu einem Branch zu wechseln, in der sich diese Dateien noch im aktuellen Verzeichnisbaum und nicht in einem Submodul befinden, erhältst du 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

Du kannst den Wechsel mit checkout -f erzwingen, aber achte darauf, dass dort keine ungesicherten Änderungen enthalten sind, da diese mit dem Befehl überschrieben werden könnten.

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

Wenn du zurückwechselst, erhältst du aus bestimmten Gründen ein leeres CryptoLibrary Verzeichnis. Auch ein git submodule update kann es dann nicht beheben. Möglicherweise musst du in dein Submodul-Verzeichnis gehen und git checkout . ausführen, um alle deine Dateien zurück zu bekommen. Du kannst das mit einem submodule foreach Skript erledigen, um es für mehrere Submodule anzuwenden.

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

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

scroll-to-top