Git
Chapters ▾ 2nd Edition

7.11 Git Tools - Submodules

Submodules

Het gebeurt vaak dat terwijl je aan het werk bent op het ene project, je van daar uit een ander project moet gebruiken. Misschien is het een door een derde partij ontwikkelde library of een die je zelf elders aan het ontwikkelen bent en die je gebruikt in meerdere ouder (parent) projecten. Er ontstaat een veelvoorkomend probleem in deze scanario’s: je wilt de twee projecten als zelfstandig behandelen maar ondertussen wel in staat zijn om de een vanuit de ander te gebruiken.

Hier is een voorbeeld. Stel dat je een web site aan het ontwikkelen bent en daar Atom feeds maakt. In plaats van je eigen Atom-genererende code te schrijven, besluit je om een library te gebruiken. De kans is groot dat je deze code moet insluiten vanuit een gedeelde library zoals een CPAN installatie of een Ruby gem, of de broncode naar je projecttree moet kopieëren. Het probleem met de library insluiten is dat het lastig is om de library op enige manier aan te passen aan jouw wensen en vaak nog moeilijker om het uit te rollen, omdat je jezelf moet verzekeren dat elke gebruikende applicatie deze library beschikbaar moet hebben. Het probleem met het inbouwen van de code in je eigen project is dat elke eigen aanpassing het je moeilijk zal maken om te mergen als er stroomopwaarts wijzigingen beschikbaar komen.

Git adresseert dit probleem met behulp van submodules. Submodules staan je toe om een Git repository als een subdirectory van een andere Git repository op te slaan. Dit stelt je in staat om een andere repository in je eigen project te clonen en je commits apart te houden.

Beginnen met submodules

We zullen een voorbeeld nemen van het ontwikkelen van een eenvoudig project die is opgedeeld in een hoofd project en een aantal sub-projecten.

Laten we beginnen met het toevoegen van een bestaande Git repository als een submodule van de repository waar we op aan het werk zijn. Om een nieuwe submodule toe te voegen gebruik je het git submodule add commando met de URL van het project dat je wilt gaan tracken. In dit voorbeeld voegen we een library genaamd “DbConnector” toe.

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

Standaard zal submodules het subproject in een directory met dezelfde naam als de repository toevoegen, in dit geval “DbConnector”. Je kunt aan het eind van het commando een ander pad toevoegen als je het ergens anders wilt laten landen.

Als je git status op dit moment aanroept, zullen je een aantal dingen opvallen.

$ 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

Het eerste wat je zal opvallen is het nieuwe .gitmodules bestand. Dit is een configuratie bestand waarin de relatie wordt vastgelegd tussen de URL van het project en de lokale subdirectory waar je het in gepulled hebt:

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

Als je meerdere submodules hebt, zal je meerdere van deze regels in dit bestand hebben. Het is belangrijk om op te merken dat dit bestand onder versiebeheer staat samen met je andere bestanden, zoals je .gitignore bestand. Het wordt met de rest van je project gepushed en gepulled. Dit is hoe andere mensen die dit project clonen weten waar ze de submodule projecten vandaan moeten halen.

Note

Omdat de URL in het .gitmodules bestand degene is waarvan andere mensen als eerste zullen proberen te clonen of fetchen, moet je je er bij voorkeur van verzekeren dat ze er wel bij kunnen. Bijvoorbeeld, als je een andere URL gebruikt om naar te pushen dan waar anderen van zullen pullen, gebruik dan degene waar anderen toegang toe hebben. Je kunt deze waarde lokaal overschrijven met git config submodule.DbConnector.url PRIVATE_URL voor eigen gebruik.

De andere regel in de git status uitvoer is de entry voor de project folder. Als je git diff daarop aanroept, zal je iets opvallends zien:

$ 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

Alhoewel DbConnector een subdirectory is in je werk directory, zie Git het als een submodule en zal de inhoud ervan niet tracken als je niet in die directory staat. In plaats daarvan ziet Git het als een specifieke commit van die repository.

Als een een iets betere diff uitvoer wilt, kan je de --submodule optie meegeven aan git diff.

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

Als je commit, zal je iets als dit zien:

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

Merk de 160000 mode op voor de DbConnector entry. Dat is een speciale mode in Git wat gewoon betekent dat je een commit opslaat als een directory entry in plaats van een subdirectory of een bestand.

Een project met submodules clonen

Hier zullen we een project met een submodule erin gaan clonen. Als je zo’n project cloont, krijg je standaard de directories die submodules bevatten, maar nog geen bestanden die erin staan:

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

De DbConnector directory is er wel, maar leeg. Je moet twee commando’s aanroepen: git submodule init om het lokale configuratie bestand te initialiseren, en git submodule update om alle gegevens van dat project te fetchen en de juiste commit uit te checken die in je superproject staat vermeld:

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

Nu is je DbConnector subdirectory in precies dezelfde staat als het was toen je het eerder committe.

Er is echter een andere, iets eenvoudiger, manier om dit te doen. Als je --recursive doorgeeft aan het git clone commando zal het automatisch elke submodule in de repository initialiseren en updaten.

$ git clone --recursive 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'

Werken aan een project met submodules

Nu hebben we een kopie van een project met submodules erin en gaan we met onze teamgenoten samenwerken op zowel het hoofdproject als het submodule project.

Wijzigingen van stroomopwaarts pullen

De eenvoudigste werkwijze bij het gebruik van submodules in een project zou zijn als je eenvoudigweg een subproject naar binnentrekt en de updates ervan van tijd tot tijd binnen haalt maar waarbij je niet echt iets wijzigt in je checkout. Laten we een eenvoudig voorbeeld doornemen.

Als je wilt controleren voor nieuw werk in een submodule, kan je in de directory gaan en git fetch aanroepen en git merge gebruiken om de wijzigingen uit de branch stroomopwaarts in de lokale code in te voegen.

$ 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(+)

Als je nu terugspringt naar het hoofdproject en git diff --submodule aanroept kan je zien dat de submodule is bijgewerkt en je krijgt een lijst met commits die eraan is toegevoegd. Als je niet elke keer --submodule wilt intypen voor elke keer dat je git diff aanroept, kan je dit als standaar formaat instellen door de diff .submodule configuratie waarde op “log” te zetten.

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

Als je nu gaat committen zal je de nieuwe code in de submodule insluiten als andere mensen updaten.

Er is ook een makkelijker manier om dit te doen, als je er de voorkeur aan geeft om niet handmatig te fetchen en mergen in de subdirectory. Als je git submodule update --remote aanroept, zal Git naar je submodules gaan en voor je fetchen en updaten.

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

Dit commando zal standaard aannemen dat je de checkout wilt updaten naar de master branch van de submodule repository. Je kunt echter dit naar iets anders wijzigen als je wilt. Bijvoorbeeld, als je de DbConnector submodule de “stable” branch van die repository wilt laten tracken, kan je dit aangeven in de .gitmodules bestand (zodat iedereen deze ook trackt), of alleen in je lokale .git/config bestand. Laten we het aangeven in het .gitmodules bestand:

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

Als je de -f .gitmodules weglaat, zal het de wijziging alleen voor jou maken, maar het is waarschijnlijk zinvoller om die informatie bij de repository te tracken zodat iedereen dat ook zal gaan doen.

Als we nu git status aanroepen, zal Git ons laten zien dat we “new commits” hebben op de submodule.

$ 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")

Als je de configuratie instelling status.submodulessummary instelt, zal Git je ook een korte samenvatting van de wijzigingen in je submodule laten zien.

$ 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

Als je op dit moment git diff aanroept kunnen we zowel zien dat we onze .gitmodules bestand hebben gewijzigd en daarbij dat er een aantal commmit is die we omlaag hebben gepulled en die klaar staan om te worden gecommit naar ons submodule project.

$ 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

Dit is best wel handig omdate we echt de log met commits kunnen zien waarvan we op het punt staan om ze in onze submodule te committen. Eens gecommit, kan je deze informatie ook achteraf zien als je git log -p aanroept.

$ 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

Git zal standaard proberen alle submodules te updaten als je git submodule update --remote aanroept, dus als je er hier veel van hebt, is het wellicht aan te raden om de naam van alleen die submodule door te geven die je wilt updaten.

Werken aan een submodule

Het is zeer waarschijnlijk dat als je submodules gebruikt, je dit zult doen omdat je echt aan de coe in die submodule wilt werken tegelijk met het werken aan de code in het hoofdproject (of verspreid over verschillende submodules). Anders zou je waarschijnlijk een eenvoudiger afhankelijkheids beheer systeem (dependency management system) hebben gebruikt (zoals Maven of Rubygems).

Dus laten we nu eens een voorbeeld behandelen waarin we gelijkertijd wijzigingen aan de submodule en het hoofdproject maken en deze wijzigingen ook gelijktijdig committen en publiceren.

Tot dusverre, als we het git submodule update commando aanriepen om met fetch wijzigen uit de repositories van de submodule te halen, ging Git de wijzigingen ophalen en de files in de subdirectory updaten, maar zou het de subdirectory laten in een staat die bekend staat als “detached HEAD”. Dit houdt in dat er geen lokale werk branch is (zoals “master”, bijvoorbeeld) waar de wijzigingen worden getracked. Dus de wijzigingen die je maakt worden niet echt goed bijgehouden.

Om de submodule in te richten zodate het eenvoudiger is om erin te werken, moet je twee dingen doen. Je moet in elke submodule gaan en een branch uitchecken om in te werken. Daarna moet je Git vertellen wat het moet doen als je wijzigingen hebt gemaakt en daarna zal git submodule update --remote nieuw werk van stroomopwaarts pullen. Je hebt nu de keuze om dit in je lokale werk te mergen, of je kunt proberen je nieuwe lokale werk te rebasen bovenop de nieuwe wijzigingen.

Laten we eerst in onze submodule directory gaan en een branch uitchecken.

$ git checkout stable
Switched to branch 'stable'

Laten we het eens proberen met de “merge” optie. Om dit handmatig aan te geven kunnen we gewoon de --merge optie in onze update aanroep toevoegen. Hier zullen we zien dat er een wijziging op de server was voor deze submodule en deze wordt erin gemerged.

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

Als we in de DbConnector directory gaan, hebben we de nieuwe wijzigingen al in onze lokale stable branch gemerged. Laten we eens kijken wat er gebeurt als we onze lokale wijziging maken aan de library en iemand anders pushed tegelijk nog een wijziging stroomopwaarts.

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

Als we nu onze submodule updaten kunnen we zien wat er gebeurt als we een lokale wijziging maken en er stroomopwaarts ook nog een wijziging is die we moeten verwerken.

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

Als je de --rebase of --merge bent vergeten, zal Git alleen de submodule updaten naar wat er op de server staat en je lokale project in een detached HEAD status zetten.

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

Maak je geen zorgen als dit gebeurt, je kunt eenvoudigweg teruggaan naar deze directory en weer je branch uitchecken (die je werk nog steeds bevat) en handmatig origin/stable mergen of rebasen (of welke remote branch je wilt).

Als je jouw wijzigingen aan je submodule nog niet hebt gecommit en je roept een submodule update aan die problemen zou veroorzaken, zal Git de wijzigingen ophalen (fetchen) maar het nog onbewaarde werk in je submodule directory niet overschrijven.

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

Als je wijzigingen hebt gemaakt die conflicteren met wijzigingen die stroomopwaarts zijn gemaakt, zal Git je dit laten weten als je de update aanroept.

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

Je kunt in de directory van de submodule gaan en de conflicten oplossen op dezelfde manier zoals je anders ook zou doen.

Submodule wizigingen publiceren

We hebben nu een aantal wijzigingen in onze submodule directory. Sommige van deze zijn van stroomopwaarts binnengekomen via onze updates en andere zijn lokaal gemaakt en zijn nog voor niemand anders beschikbaar omdat we ze nog niet hebben gepushed.

$ git diff
Submodule DbConnector c87d55d..82d2ad3:
  > Merge from origin/stable
  > updated setup script
  > unicode support
  > remove unnecessary method
  > add new option for conn pooling

Als we het hoofdproject committen en deze pushen zonder de submodule wijzigingen ook te pushen, zullen andere mensen die willen zien wat onze wijzigingen inhouden problemen krijgen omdat er geen enkele manier is voor hen om de wijzigingen van de submodule te pakken krijgen waar toch op wordt voortgebouwd. Deze wijzigingen zullen alleen in onze lokale kopie bestaan.

Om er zeker van te zijn dat dit niet gebeurt, kan je Git vragen om te controleren dat al je submodules juist gepushed zijn voordat het hoofdproject wordt gepushed. Het git push commando leest het --recurse-submodules argument die op de waardes “check” of “on-demand” kan worden gezet. De “check” optie laat een push eenvoudigweg falen als een van de gecomitte submodule wijzigingen niet is gepushed.

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

Zoals je kunt zien, geeft het ook wat behulpzame adviezen over wat he vervolgens kunnen doen. De eenvoudige optie is om naar elke submodule te gaan en handmatig naar de remotes te pushen om er zeker van te zijn dat ze extern beschikbaar zijn en dan deze push nogmaals te proberen.

De andere optie is om de “on-demand” waarde te gebruiken, wat zal proberen dit voor je te doen.

$ 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

Zoals je hier kunt zien, ging Git in de DbConnector module en heeft deze gepushed voordat het hoofdproject werd gepushed. Als die push van de submodule om wat voor reden ook faalt, zal de push van het hoofdproject ook falen.

Submodule wijzigingen mergen

Als je een submodule referentie wijzigt op hetzelfde moment als een ander, kan je in enkele problemen geraken. In die zien, dat wanneer submodule histories uitelkaar zijn gaan lopen en naar uitelkaar lopende branches in het superproject worden gecommit, zal het wat extra werk van je vergen om dit te repareren.

Als een van de commits een directe voorouder is van de ander (een fast-forward merge), dan zal git eenvoudigweg de laatste voor de merge kiezen, dus dat werkt prima.

Git zal echter niet eens een triviale merge voor je proberen. Als de submodule commits uiteen zijn gaan lopen en ze moeten worden gemerged, zal je iets krijgen wat hier op lijkt:

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

Dus wat er hier eigenlijk gebeurd is, is dat Git heeft achterhaald dat de twee branches punten in de historie van de submodule hebben opgeslagen die uiteen zijn gaan lopen en die gemerged moeten worden. Het legt dit uit als “merge following commits not found” (merge volgend op commits niet gevonden), wat verwarrend is, maar we leggen zo uit waarom dit zo is.

Om dit probleem op te lossen, moet je uit zien te vinden in welke staat de submodule in zou moeten zijn. Vreemdgenoeg geeft Git je niet echt veel informatie om je hiermee te helpen, niet eens de SHA-1 getallen van de commits van beide kanten van de historie. Gelukkig is het redelijk eenvoudig om uit te vinden. Als je git diff aanroept kan je de SHA-1 getallen van de opgeslagen commits uit beide branches die je probeerde te mergen krijgen.

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

Dus in dit geval, is eb41d76 de commit in onze submodule die wij hebben en c771610 is de commit die stroomopwaarts aanwezig is. Als we naar onze submodule directory gaan, moet het al aanwezig zin op eb41d76 omdate de merge deze nog niet zal hebben aangeraakt. Als deze om welke reden dan ook er niet is, kan je eenvoudigweg een branch die hiernaar wijst aanmaken en uit checken.

Wat nu een belangrijke rol gaat spelen is de SHA-1 van de commit van de andere kant. Dit is wat je in zult moeten mergen en oplossen. Je kunt ofwel de merge met de SHA-1 gewoon proberen, of je kunt een branch hiervoor maken en dan deze proberen te mergen. We raden het laatste aan, al was het maar om een mooiere merge commit bericht te krijgen.

Dus, we gaan naar onze submodule directory, maken een branch gebaseerd op die tweede SHA-1 van git diff en mergen handmatig.

$ cd DbConnector

$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610
(DbConnector) $ 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.

We hebben een echte merge conflict, dus als we deze oplossen en committen, dan kunnen we eenvoudigweg het hoofdproject updaten met het resultaat.

$ 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. Eerst lossen we het conflict op

  2. Dan gaan we terug naar de directory van het hoofdproject

  3. We controleren de SHA-1 getallen nog een keer

  4. Lossen de conflicterende submodule entries op

  5. Committen onze merge.

Dit kan nogal verwarrend overkomen, maar het is niet echt moeilijk.

Interessant genoeg, is er een ander geval die Git aankan. Als er een merge commit bestaat in de directory van de submodule die beide commits in z’n historie bevat, zal Git je deze voorstellen als mogelijke oplossing. Het ziet dat op een bepaald punt in het submodule project iemand branches heeft gemerged met daarin deze twee commits, dus wellicht wil je die.

Dit is waarom de foutboodschap van eerder “merge following commits not found” was, omdat het dit niet kon doen. Het is verwarrend omdat, wie verwacht er nu dat Git dit zou proberen?

Als het een enkele acceptabele merge commit vindt, zal je iets als dit zien:

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

Wat hier gesuggereerd wordt om te doen is om de index te updaten alsof je git add zou hebben aangeroepen, wat het conflict opruimt, en dan commit. Echter, je moet dit waarschijnlijk niet doen. Je kunt net zo makkelijk naar de directory van de submodule gaan, kijken wat het verschil is, naar deze commit fast-forwarden, het goed testen en daarna committen.

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

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

Dit bereikt hetzelfde, maar op deze manier kan je verfiëren dat het werkt en je hebt de code in je submodule als je klaar bent.

Submodule Tips

Er zijn een aantal dingen die je kunt doen om het werken met submodules iets eenvoudiger te maken.

Submodule Foreach

Er is een foreach submodule commando om een willekeurig commando aan te roepen in elke submodule. Dit kan echt handig zijn als je een aantal submodules in hetzelfde project hebt.

Bijvoorbeeld, stel dat we een nieuwe functie willen beginnen te maken of een bugfix uitvoeren en we hebben werkzaamheden in verscheidene submodules onderhanden. We kunnen nu makkelijk al het werk in al onze submodules 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

Daarna kunnen we een nieuwe branch maken en ernaar switchen in al onze submodules.

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

Je begrijpt het idee. Een heel nuttig ding wat je kunt doen is een mooie unified diff maken van wat er gewijzigd is in je hoofdproject alsmede al je subprojecten.

$ 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 kunnen we zien dat we een functie aan het definieren zijn in een submodule en dat we het in het hoofdproject aanroepen. Dit is duidelijk een versimpeld voorbeeld, maar hopelijk geeft het je een idee wanneer dit handig kan zijn.

Bruikbare aliassen

Je wilt misschien een aantal aliassen maken voor een aantal van deze commando’s omdat ze redelijk lang kunnen zijn en je geen configuratie opties voor de meeste van deze kunt instellen om ze standaard te maken. We hebben het opzetten van Git aliassen in Git aliassen behandeld, maar hier is een voorbeeld van iets wat je misschien zou willen opzetten als je van plan bent veel met submodules in Git te werken.

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

Op deze manier kan je eenvoudig git supdate aanroepen als je je submodules wilt updaten, of git spush om te pushen met controle op afhankelijkheden op de submodule.

Problemen met submodules

Submodules gebruiken is echter niet zonder nukken.

Bijvoorbeeld het switchen van branches met daarin submodulen kan nogal listig zijn. Als je een nieuwe branch maakt, daar een submodule toevoegt, en dan terug switcht naar een branch zonder die submodule, heb je de submodule directory nog steeds als een untracked directory.

$ 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 'adding crypto library'
[add-crypto 4445836] adding 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)

Die directory weghalen is niet moeilijkm maar het kan nogal verwarrend zijn om hem daar te hebben. Als je het weghaalt en dan weer terug switcht naar de branch die deze submodule heeft, zal je submodule update --init moeten aanroepen om het weer te vullen.

$ git clean -fdx
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

Alweer, niet echt moeilijk, maar het kan wat verwarring scheppen.

Het andere grote probleem waar veel mensen tegenaan lopen betreft het omschakelen van subdirectories naar submodules. Als je files aan het tracken bent in je project en je wilt ze naar een submodule verplaatsen, moet je voorzichtig zijn omdat Git anders erg boos op je gaat worden. Als je de subdirectory verwijdert en dan submodule add aanroept, zal Git tegen je roepen:

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

Je moet de CryptoLibrary directory eerst unstagen. Dan kan je de submodule toevoegen:

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

Stel je nu voor dat je dit in en branch zou doen. Als je naar een branch terug zou switchen waar deze bestanden nog steeds in de actuele tree staan in plaats van in een submodule - krijg je deze fout:

$ 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

Je kunt forceren om de switch te maken met checkout -f, maar wees voorzichtig dat je geen onbewaarde gegevens daar hebt staan omdat deze kunnen worden overschreven met dit commando.

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

Daarna, als je weer terug switched, krijg je om de een of andere reden een lege CryptoLibrary directory en git submodule update zou hier ook geen oplossing voor kunnen bieden. Je zou misschien naar je submodule directory moeten gaan en een git checkout . aanroepen om al je bestanden terug te krijgen. Je zou dit in een submodule foreach script kunnen doen om het voor meerdere submodules uit te voeren.

Het is belangrijk om op te merken dat submodules vandaag de dag al hun Git data in de .git directory van het hoogste project opslaan, dus in tegenstelling tot oudere versies van Git, leidt het vernietigen van een submodule directory niet tot verlies van enig commit of branches die je had.

Met al deze gereedschappen, kunnen submodules een redelijk eenvoudig en effectieve manier zijn om een aantal gerelateerde maar toch aparte projecten tegelijk te ontwikkelen.