Git 🌙
Chapters ▾ 2nd Edition

7.11 Utilitaires Git - Sous-modules

Sous-modules

Il arrive souvent lorsque vous travaillez sur un projet que vous deviez utiliser un autre projet comme dépendance. Cela peut être une bibliothèque qui est développée par une autre équipe ou que vous développez séparément pour l’utiliser dans plusieurs projets parents. Ce scénario provoque un problème habituel : vous voulez être capable de gérer deux projets séparés tout en utilisant l’un dans l’autre.

Voici un exemple. Supposons que vous développez un site web et que vous créez des flux Atom. Plutôt que d’écrire votre propre code de génération Atom, vous décidez d’utiliser une bibliothèque. Vous allez vraisemblablement devoir soit inclure ce code depuis un gestionnaire partagé comme CPAN ou Ruby gem, soit copier le code source dans votre propre arborescence de projet. Le problème d’inclure la bibliothèque en tant que bibliothèque externe est qu’il est difficile de la personnaliser de quelque manière que ce soit et encore plus de la déployer, car vous devez vous assurer de la disponibilité de la bibliothèque chez chaque client. Mais le problème d’inclure le code dans votre propre projet est que n’importe quelle personnalisation que vous faites est difficile à fusionner lorsque les modifications du développement principal arrivent.

Git gère ce problème avec les sous-modules. Les sous-modules vous permettent de gérer un dépôt Git comme un sous-répertoire d’un autre dépôt Git. Cela vous laisse la possibilité de cloner un dépôt dans votre projet et de garder isolés les commits de ce dépôt.

Démarrer un sous-module

Détaillons le développement d’un projet simple qui a été divisé en un projet principal et quelques sous-projets.

Commençons par ajouter le dépôt d’un projet Git existant comme sous-module d’un dépôt sur lequel nous travaillons. Pour ajouter un nouveau sous-module, nous utilisons la commande git submodule add avec l’URL du projet que nous souhaitons suivre. Dans cette exemple, nous ajoutons une bibliothèque nommée « DbConnector ».

$ git submodule add https://github.com/chaconinc/DbConnector
Clonage dans 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Dépaquetage des objets: 100% (11/11), fait.
Vérification de la connectivité... fait.

Par défaut, les sous-modules ajoutent le sous-projet dans un répertoire portant le même nom que le dépôt, dans notre cas « DbConnector ». Vous pouvez ajouter un chemin différent à la fin de la commande si vous souhaitez le placer ailleurs.

Si vous lancez git status à ce moment, vous noterez quelques différences.

$ git status
Sur la branche master
Votre branche est à jour avec 'origin/master'.

Modifications qui seront validées :
  (utilisez "git reset <fichier>..." pour désindexer)

	nouveau fichier :   .gitmodules
	nouveau fichier :   DbConnector

Premièrement, un fichier .gitmodules vient d’apparaître. C’est le fichier de configuration qui stocke la liaison entre l’URL du projet et le sous-répertoire local dans lequel vous l’avez tiré.

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

Si vous avez plusieurs sous-modules, vous aurez plusieurs entrées dans ce fichier. Il est important de noter que ce fichier est en gestion de version comme vos autres fichiers, à l’instar de votre fichier .gitignore. Il est poussé et tiré comme le reste de votre projet. C’est également le moyen que les autres personnes qui clonent votre projet ont de savoir où récupérer le projet du sous-module.

Note

Comme l’URL dans le fichier .gitmodules est ce que les autres personnes essaieront en premier de cloner et de tirer, assurez-vous que cette URL est effectivement accessible par les personnes concernées. Par exemple, si vous utilisez une URL différente pour pousser que celle que les autres utiliseront pour tirer, utilisez l’URL à laquelle les autres ont accès. Vous pouvez surcharger cette URL localement pour votre usage propre avec la commande git config submodule.DbConnector.url PRIVATE_URL.

L’autre information dans la sortie de git status est l’entrée du répertoire du projet. Si vous exécutez git diff, vous verrez quelque chose d’intéressant :

$ 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

Même si DbConnector est un sous-répertoire de votre répertoire de travail, Git le voit comme un sous-module et ne suit pas son contenu (si vous n’êtes pas dans ce répertoire). En échange, Git l’enregistre comme un commit particulier de ce dépôt.

Si vous souhaitez une sortie diff plus agréable, vous pouvez passer l’option --submodule à 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)

Au moment de valider, vous voyez quelque chose comme :

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

Remarquez le mode 160000 pour l’entrée DbConnector. C’est un mode spécial de Git qui signifie globalement que vous êtes en train d’enregistrer un commit comme un répertoire plutôt qu’un sous-répertoire ou un fichier.

Enfin, poussez ces modifications :

$ git push origin master

Cloner un projet avec des sous-modules

Maintenant, vous allez apprendre à cloner un projet contenant des sous-modules. Quand vous récupérez un tel projet, vous obtenez les différents répertoires qui contiennent les sous-modules, mais encore aucun des fichiers :

$ git clone https://github.com/chaconinc/MainProject
Clonage dans 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Dépaquetage des objets: 100% (14/14), fait.
Vérification de la connectivité... fait.
$ 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
$

Le répertoire DbConnector est présent mais vide. Vous devez exécuter deux commandes : git submodule init pour initialiser votre fichier local de configuration, et git submodule update pour tirer toutes les données de ce projet et récupérer le commit approprié tel que listé dans votre super-projet :

$ git submodule init
Sous-module 'DbConnector' (https://github.com/chaconinc/DbConnector) enregistré pour le chemin 'DbConnector'
$ git submodule update
Clonage dans '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'

Votre répertoire DbConnector est maintenant dans l’état exact dans lequel il était la dernière fois que vous avez validé.

Il existe une autre manière plus simple d’arriver au même résultat. Si vous passez l’option --recurse-submodules à la commande git clone, celle-ci initialisera et mettra à jour automatiquement chaque sous-module du dépôt.

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject
Clonage dans 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Dépaquetage des objets: 100% (14/14), fait.
Vérification de la connectivité... fait.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Clonage dans 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Dépaquetage des objets: 100% (11/11), fait.
Vérification de la connectivité... fait.
chemin du sous-module 'DbConnector' : 'c3f01dc8862123d317dd46284b05b6892c7b29bc' extrait

Si vous avez déjà cloné le projet et oublié --recurse-submodules, vous pouvez combiner les étapes git submodule init et git submodule update en lançant git submodule update --init. Pour initialiser, récupérer et extraire aussi tous les sous-modules récursivement, vous pouvez utiliser la commande complète git submodule update --init --recursive.

Travailler sur un projet comprenant des sous-modules

Nous avons à présent une copie d’un projet comprenant des sous-modules, et nous allons collaborer à la fois sur le projet principal et sur le projet du sous-module.

Tirer des modifications amont

Le modèle le plus simple d’utilisation des sous-modules est le cas de la simple consommation d’un sous-projet duquel on souhaite obtenir les mises à jour de temps en temps mais auquel on n’apporte pas de modification dans la copie de travail. Examinons un exemple simple.

Quand vous souhaitez vérifier si le sous-module a évolué, vous pouvez vous rendre dans le répertoire correspondant et lancer git fetch puis git merge de la branche amont pour mettre à jour votre code local.

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

Si vous revenez maintenant dans le projet principal et lancez git diff --submodule, vous pouvez remarquer que le sous-module a été mis à jour et vous pouvez obtenir une liste des commits qui y ont été ajoutés. Si vous ne voulez pas taper --submodule à chaque fois que vous lancez git diff, vous pouvez le régler comme format par défaut en positionnant le paramètre de configuration diff.submodule à la valeur « log ».

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

Si vous validez à ce moment, vous fixez la version du sous-module à la version actuelle quand d’autres personnes mettront à jour votre projet.

Il existe aussi un moyen plus facile, si vous préférez ne pas avoir à récupérer et fusionner manuellement les modifications dans le sous-répertoire. Si vous lancez la commande git submodule update --remote, Git se rendra dans vos sous-modules et réalisera automatiquement le fetch et le merge.

$ 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)
Dépaquetage des objets: 100% (4/4), fait.
Depuis https://github.com/chaconinc/DbConnector
   3f19983..d0354fc  master     -> origin/master
chemin du sous-module 'DbConnector': checked out 'd0354fc054692d3906c85c3af05ddce39a1c0644' extrait

Cette commande considère par défaut que vous souhaitez mettre à jour la copie locale vers la branche master du dépôt du sous-module. Vous pouvez, cependant, indiquer une autre branche. Par exemple, si le sous-module DbConnector suit la branche stable du dépôt amont, vous pouvez l’indiquer soit dans votre fichier .gitmodules (pour que tout le monde le suive de même) ou juste dans votre fichier local .git/config. Voyons ceci dans le cas du fichier .gitmodules :

$ 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)
Dépaquetage des objets: 100% (4/4), fait.
Depuis https://github.com/chaconinc/DbConnector
   27cf5d3..c87d55d  stable -> origin/stable
chemin du sous-module 'DbConnector' : 'c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687' extrait

Si vous ne spécifiez pas la partie -f .gitmodules, la commande ne fera qu’une modification locale, mais il semble plus logique d’inclure cette information dans l’historique du projet pour que tout le monde soit au diapason.

Quand vous lancez git status, Git vous montrera que nous avons de nouveaux commits (« new commits ») pour le sous-module.

$ git status
Sur la branche master
Votre branche est à jour avec 'origin/master'.

Modifications qui ne seront pas validées :
  (utilisez "git add <fichier>..." pour mettre à jour ce qui sera validé)
  (utilisez "git checkout -- <fichier>..." pour annuler les modifications dans la copie de travail)

  modifié :   .gitmodules
  modifié :   DbConnector (new commits)

aucune modification n'a été ajoutée à la validation (utilisez "git add" ou "git commit -a")

Si vous activez le paramètre de configuration status.submodulesummary, Git vous montrera aussi un résumé des modifications dans vos sous-modules :

$ git config status.submodulesummary 1

$ git status
Sur la branche master
Votre branche est à jour avec 'origin/master'.

Modifications qui ne seront pas validées :
  (utilisez "git add <fichier>..." pour mettre à jour ce qui sera validé)
  (utilisez "git checkout -- <fichier>..." pour annuler les modifications dans la copie de travail)

	modifié :   .gitmodules
	modifié :   DbConnector (new commits)

Sous-modules modifiés mais non mis à jour :

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

Ici, si vous lancez git diff, vous pouvez voir que le fichier .gitmodules a été modifié mais aussi qu’il y a un certain nombre de commits qui ont été tirés et sont prêts à être validés dans le projet du sous-module.

$ 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

C’est une information intéressante car vous pouvez voir le journal des modifications que vous vous apprêtez à valider dans votre sous-module. Une fois validées, vous pouvez encore visualiser cette information en lançant git log -p.

$ 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

Par défaut, Git essaiera de mettre à jour tous les sous-modules lors d’une commande git submodule update --remote, donc si vous avez de nombreux sous-modules, il est préférable de spécifier le sous-module que vous souhaitez mettre à jour.

Tirer des modifications amont depuis le serveur distant

Glissons-nous maintenant dans les habits d’un collaborateur, qui a son propre clone local du dépôt ProjetPrincipal. Lancer simplement git pull pour obtenir les nouvelles modifications validées ne suffit plus :

$ git pull
From https://github.com/chaconinc/ProjetPrincipal
   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")

Par défaut, la commande git pull récupère les modifications des sous-modules récursivement, comme nous pouvons le voir sur la première ligne ci-dessus. Cependant, elle ne met pas les sous-modules à jour. C’est affiché à la sortie de la commande git status, qui indique que le sous-module est « modified », et a des nouveaux commit (new commits). De plus, les chevrons indiquant les nouveaux commits pointent à gauche (<), ce qui signifie que ces commits sont enregistrés dans ProjetPrincipal, mais ne sont pas présents dans l’extraction locale de DbConnector. Pour finaliser la mise à jour, vous devez lancer git submodule update :

$ 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

Notez que pour rester en sécurité, vous devriez lancer git submodule update avec le drapeau --init au cas où les commits de ProjetPrincipal que vous venez de tirer ont ajouté des nouveaux sous-modules, et le drapeau --recursive si certains sous-module ont des sous-modules imbriqués.

Si vous souhaitez automatiser ce processsus, vous pouvez ajouter le drapeau --recurse-submodules à la commande git pull (depuis Git 2.14). Cela forcera Git à lancer git submodule update juste après le tirage, de manière à mettre à jour les sous-modules dans l’état correct. De plus, si vous voulez que Git tire toujours avec --recurse-submodules, vous pouvez régler l’option de configuration submodule.recurse à true (cela marche pour git pull depuis Git 2.15). Cette option forcera Git à utiliser le drapeau --recurse-submodules pour toutes les commandes qui le supportent (à part clone).

Il y a une situation spéciale qui peut arriver lors du tirage depuis le super-projet ; le dépôt amont peut avoir modifié l’URL du sous-module dans le fichier .gitmodules dans un des commits que vous tirez. Cela peut arriver si le projet du sous-module change de plate-forme d’hébergement. Dans ce dernier cas, il est possible pour git pull --recurse-submodules, or git submodule update, d’échouer si le super-projet fait référence à un commit d’un sous-module qui n’est pas trouvé dans le serveur distant du sous-module configuré localement dans votre dépôt. Pour corriger cette situation, la commande git submodule sync est nécessaire :

# copier la nouvelle URL dans votre config locale
$ git submodule sync --recursive
# mettre à jour le sous-module depuis la nouvelle URL
$ git submodule update --init --recursive

Travailler sur un sous-module

Il y a fort à parier que si vous utilisez des sous-modules, vous le faites parce que vous souhaitez en réalité travailler sur le code du sous-module en même temps que sur celui du projet principal (ou à travers plusieurs sous-modules). Sinon, vous utiliseriez plutôt un outil de gestion de dépendances plus simple (tel que Maven ou Rubygems).

De ce fait, détaillons un exemple de modifications réalisées dans le sous-module en même temps que dans le projet principal et de validation et de publication des modifications dans le même temps.

Jusqu’à maintenant, quand nous avons lancé la commande git submodule update pour récupérer les modifications depuis les dépôts des sous-modules, Git récupérait les modifications et mettait les fichiers locaux à jour mais en laissant le sous-répertoire dans un état appelé « HEAD détachée ». Cela signifie qu’il n’y pas de branche locale de travail (comme master, par exemple) pour y valider les modifications. Donc, toutes les modifications que vous y faites ne sont pas suivies non plus.

Pour rendre votre sous-module plus adapté à la modification, vous avez besoin de deux choses. Vous devez vous rendre dans chaque sous-module et extraire une branche de travail. Ensuite vous devez dire à Git ce qu’il doit faire si vous avez réalisé des modifications et que vous lancez git submodule update --remote pour tirer les modifications amont. Les options disponibles sont soit de les fusionner dans votre travail local, soit de tenter de rebaser le travail local par dessus les modifications distantes.

En premier, rendons-nous dans le répertoire de notre sous-module et extrayons une branche.

$ git checkout stable
Basculement sur la branche 'stable'

Attaquons-nous au choix de politique de gestion. Pour le spécifier manuellement, nous pouvons simplement ajouter l’option --merge à l’appel de update. Nous voyons ici qu’une modification était disponible sur le serveur pour ce sous-module et qu’elle a été fusionnée.

$ 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)
Dépaquetage des objets: 100% (4/4), fait.
Depuis https://github.com/chaconinc/DbConnector
   c87d55d..92c7337  stable     -> origin/stable
Mise à jour de c87d55d..92c7337
Avance rapide
 src/main.c | 1 +
 1 file changed, 1 insertion(+)
chemin du sous-module 'DbConnector': fusionné dans '92c7337b30ef9e0893e758dac2459d07362ab5ea'

Si nous nous rendons dans le répertoire DbConnector, les nouvelles modifications sont déjà fusionnées dans notre branche locale stable. Voyons maintenant ce qui arrive si nous modifions localement la bibliothèque et que quelqu’un pousse une autre modification en amont dans le même temps.

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

Maintenant, si nous mettons à jour notre sous-module, nous pouvons voir ce qui arrive lors d’un rebasage de deux modifications concurrentes.

$ git submodule update --remote --rebase
Premièrement, rembobinons head pour rejouer votre travail par-dessus...
Application : unicode support
chemin du sous-module 'DbConnector': rebasé dans '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94'

Si vous oubliez de spécifier --rebase ou --merge, Git mettra juste à jour le sous-module vers ce qui est sur le serveur et réinitialisera votre projet à l’état « HEAD détachée ».

$ git submodule update --remote
chemin du sous-module 'DbConnector' : '5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94' extrait

Si cela arrive, ne vous inquiétez pas, vous pouvez simplement revenir dans le répertoire et extraire votre branche (qui contiendra encore votre travail) et fusionner ou rebaser origin/stable (ou la branche distante que vous souhaitez) à la main.

Si vous n’avez pas validé vos modifications dans votre sous-module, et que vous lancez une mise à jour de sous-module qui causerait des erreurs, Git récupérera les modifications mais n’écrasera pas le travail non validé dans votre répertoire de sous-module.

$ 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)
Dépaquetage des objets: 100% (4/4), fait.
Depuis https://github.com/chaconinc/DbConnector
   5d60ef9..c75e92a  stable     -> origin/stable
error: Vos modifications locales seraient écrasées par checkout:
	scripts/setup.sh
Please, commit your changes or stash them before you can switch branches.
Aborting
Impossible d'extraire 'c75e92a2b3855c9e5b66f915308390d9db204aca' dans le chemin du sous-module 'DbConnector'

Si vous avez réalisé des modifications qui entrent en conflit avec des modifications amont, Git vous en informera quand vous mettrez à jour.

$ git submodule update --remote --merge
Auto-merging scripts/setup.sh
CONFLIT (contenu): Conflit de fusion dans scripts/setup.sh
La fusion automatique a échoué ; réglez les conflits et validez le résultat
Impossible de fusionner 'c75e92a2b3855c9e5b66f915308390d9db204aca' dans le chemin du sous-module 'DbConnector'

Vous pouvez vous rendre dans le répertoire du sous-module et résoudre le conflit normalement.

Publier les modifications dans un sous-module

Nous avons donc des modifications dans notre répertoire de sous-module, venant à la fois du dépôt amont et de modifications locales non publiées.

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

Si nous validons dans le projet principal et que nous le poussons en amont sans pousser les modifications des sous-modules, les autres personnes qui voudront essayer notre travail vont avoir de gros problèmes vu qu’elles n’auront aucun moyen de récupérer les modifications des sous-modules qui en font partie. Ces modifications n’existent que dans notre copie locale.

Pour être sur que cela n’arrive pas, vous pouvez demander à Git de vérifier que tous vos sous-modules ont été correctement poussés avant de pouvoir pousser le projet principal. La commande git push accepte un argument --recurse-submodules qui peut avoir pour valeur « check » ou « on-demand ». L’option « check » fera échouer push si au moins une des modifications des sous-modules n’a pas été poussée.

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

Comme vous pouvez le voir, il donne aussi quelques conseils utiles sur ce que nous pourrions vouloir faire ensuite. L’option simple consiste à se rendre dans chaque sous-module et à pousser manuellement sur les dépôts distants pour s’assurer qu’ils sont disponibles publiquement, puis de réessayer de pousser le projet principal.

L’autre option consiste à utiliser la valeur « on-demand » qui essaiera de faire tout ceci pour vous.

$ git push --recurse-submodules=on-demand
Pushing submodule 'DbConnector'
Décompte des objets: 9, fait.
Delta compression using up to 8 threads.
Compression des objets: 100% (8/8), fait.
Écriture des objets: 100% (9/9), 917 bytes | 0 bytes/s, fait.
Total 9 (delta 3), reused 0 (delta 0)
To https://github.com/chaconinc/DbConnector
   c75e92a..82d2ad3  stable -> stable
Décompte des objets: 2, fait.
Delta compression using up to 8 threads.
Compression des objets: 100% (2/2), fait.
Écriture des objets: 100% (2/2), 266 bytes | 0 bytes/s, fait.
Total 2 (delta 1), reused 0 (delta 0)
To https://github.com/chaconinc/ProjetPrincipal
   3d6d338..9a377d1  master -> master

Comme vous pouvez le voir, Git s’est rendu dans le module DbConnector et l’a poussé avant de pousser le projet principal. Si la poussée du sous-module échoue pour une raison quelconque, la poussée du projet principal sera annulée.

Fusion de modifications de sous-modules

Si vous changez la référence d’un sous-module en même temps qu’une autre personne, il se peut que cela pose problème. Particulièrement, si les historiques des sous-modules ont divergé et sont appliqués à des branches divergentes dans un super-projet, rapprocher toutes les modifications peut demander un peu de travail.

Si un des commits est un ancêtre direct d’un autre (c’est-à-dire une fusion en avance rapide), alors Git choisira simplement ce dernier pour la fusion et cela se résoudra tout seul.

Cependant, Git ne tentera pas de fusion, même très simple, pour vous. Si les commits d’un sous-module divergent et doivent être fusionnés, vous obtiendrez quelque chose qui ressemble à ceci :

$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Dépaquetage des objets: 100% (2/2), fait.
From https://github.com/chaconinc/ProjetPrincipal
   9a377d1..eb974f8  master     -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Fusion automatique de DbConnector
CONFLIT (sous-module): Conflit de fusion dans DbConnector
La fusion automatique a échoué ; réglez les conflits et validez le résultat.

Donc, ce qui s’est passé en substance est que Git a découvert que les deux points de fusion des branches à fusionner dans l’historique du sous-module sont divergents et doivent être fusionnés. Il l’explique par « merge following commits not found » (fusion suivant les commits non trouvée), ce qui n’est pas clair mais que nous allons expliquer d’ici peu.

Pour résoudre le problème, vous devez comprendre l’état dans lequel le sous-module devrait se trouver. Étrangement, Git ne vous donne pas d’information utile dans ce cas, pas même les SHA-1 des commits des deux côtés de l’historique. Heureusement, c’est assez facile à comprendre. Si vous lancez git diff, vous pouvez obtenir les SHA-1 des commits enregistrés dans chacune des branches que vous essayiez de fusionner.

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

Donc, dans ce cas, eb41d76 est le commit dans notre sous-module que nous avions et c771610 est le commit amont. Si nous nous rendons dans le répertoire du sous-module, il devrait déjà être sur eb41d76 parce que la fusion ne l’a pas touché. S’il n’y est pas, vous pouvez simplement créer et extraire une branche qui pointe dessus.

Ce qui importe, c’est le SHA-1 du commit venant de l’autre branche. C’est ce que nous aurons à fusionner. Vous pouvez soit essayer de fusionner avec le SHA-1 directement ou vous pouvez créer une branche à partir du commit puis essayer de la fusionner. Nous suggérons d’utiliser cette dernière méthode, ne serait-ce que pour obtenir un message de fusion plus parlant.

Donc, rendons-nous dans le répertoire du sous-module, créons une branche basée sur ce second SHA-1 obtenu avec git diff et fusionnons manuellement.

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

Nous avons eu un conflit de fusion ici, donc si nous le résolvons et validons, alors nous pouvons simplement mettre à jour le projet principal avec le résultat.

$ vim src/main.c (1)
$ git add src/main.c
$ git commit -am 'fusion de nos modifications'
[master 9fd905e] fusion de nos modifications

$ 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 "Fusion du travail de Tom" (5)
[master 10d2c60] Fusion du travail de Tom
  1. Nous résolvons le conflit

  2. Ensuite, nous retournons dans le projet principal

  3. Nous pouvons revérifier les SHA-1

  4. Nous résolvons l’entrée en conflit dans le sous-module

  5. Enfin, nous validons la résolution.

Cela peut paraître compliqué mais ce n’est pas très difficile.

Curieusement, il existe un autre cas que Git gère seul. Si un commit de fusion existe dans le répertoire du sous-module qui contient les deux commits dans ses ancêtres, Git va le suggérer comme solution possible. Il voit qu’à un certain point de l’historique du projet du sous-module, quelqu’un a fusionné les branches contenant ces deux commits, donc vous désirerez peut-être utiliser celui-ci.

C’est pourquoi le message d’erreur précédent s’intitulait « merge following commits not found », parce que justement, il ne pouvait pas trouver le commit de fusion. C’est déroutant car qui s’attendrait à ce qu’il essaie de le chercher ?

S’il trouve un seul commit de fusion acceptable, vous verrez ceci :

$ 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.
Fusion automatique de DbConnector
CONFLIT (submodule): Conflit de fusion dans DbConnector
La fusion automatique a échoué ; réglez les conflits et validez le résultat.

Ce qu’il suggère de faire est de mettre à jour l’index comme si vous aviez lancé git add, ce qui élimine le conflit, puis de valider. Vous ne devriez cependant pas le faire. Vous pouvez plus simplement vous rendre dans le répertoire du sous-module, visualiser la différence, avancer en avance rapide sur le commit, le tester puis le valider.

$ cd DbConnector/
$ git merge 9fd905e
Mise à jour eb41d76..9fd905e
Avance rapide

$ cd ..
$ git add DbConnector
$ git commit -am 'Avance rapide sur un fils commun dans le sous-module'

Cela revient au même, mais de cette manière vous pouvez au moins vérifier que ça fonctionne et vous avez le code dans votre répertoire de sous-module quand c’est terminé.

Trucs et astuces pour les sous-modules

Il existe quelques commandes qui permettent de travailler plus facilement avec les sous-modules.

Submodule foreach

Il existe une commande submodule foreach qui permet de lancer une commande arbitraire dans chaque sous-module. C’est particulièrement utile si vous avez plusieurs sous-modules dans le même projet.

Par exemple, supposons que nous voulons développer une nouvelle fonctionnalité ou faire un correctif et que nous avons déjà du travail en cours dans plusieurs sous-modules. Nous pouvons facilement remiser tout le travail en cours dans tous les sous-modules.

$ 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

Ensuite, nous pouvons créer une nouvelle branche et y basculer dans tous nos sous-modules.

$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Basculement sur la nouvelle branche 'featureA'
Entering 'DbConnector'
Basculement sur la nouvelle branche  'featureA'

Vous comprenez l’idée. Une commande vraiment utile permet de produire un joli diff unifié des modifications dans le projet principal ainsi que dans tous les sous-projets.

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

Ici, nous pouvons voir que nous définissons une fonction dans un sous-module et que nous l’appelons dans le projet principal. C’est un exemple exagérément simplifié, mais qui aide à mieux comprendre l’utilité de cette commande.

Alias utiles

Vous pourriez être intéressé de définir quelques alias pour des commandes longues pour lesquelles vous ne pouvez pas régler la configuration par défaut. Nous avons traité la définition d’alias Git dans Les alias Git, mais voici un exemple d’alias que vous pourriez trouver utiles si vous voulez travailler sérieusement avec les sous-modules de Git.

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

De cette manière, vous pouvez simplement lancer git supdate lorsque vous souhaitez mettre à jour vos sous-module ou git spush pour pousser avec une gestion de dépendance de sous-modules.

Les problèmes avec les sous-modules

Cependant, utiliser des sous-modules ne se déroule pas sans accroc.

Commuter des branches

Commuter des branches qui contiennent des sous-modules peut également s’avérer difficile. Si vous créez une nouvelle branche, y ajoutez un sous-module, et revenez ensuite à une branche dépourvue de ce sous-module, vous aurez toujours le répertoire de ce sous-module comme un répertoire non suivi :

$ git --version
git version 2.12.2

$ git checkout -b add-crypto
Basculement sur la nouvelle branche '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
Basculement sur la branche 'master'
Votre branche est à jour avec 'origin/master'.

$ git status
Sur la branche master
Votre branche est à jour avec 'origin/master'.

Fichiers non suivis :
  (utilisez "git add <fichier>..." pour inclure dans ce qui sera validé)

	CryptoLibrary/

aucune modification ajoutée à la validation mais des fichiers non suivis sont présents (utilisez "git add" pour les suivre)

Supprimer le répertoire n’est pas difficile, mais sa présence est assez déroutante. Si vous le supprimez puis que vous rebasculez sur la branche qui contient le sous-module, vous devrez lancer submodule update --init pour le réalimenter.

$ git clean -ffdx
Suppression de CryptoLibrary/

$ git checkout add-crypto
Basculement sur la branche 'add-crypto'

$ ls CryptoLibrary/

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

$ ls CryptoLibrary/
Makefile	includes	scripts		src

Une fois de plus, ce n’est pas réellement difficile, mais cela peut être déroutant.

Les nouvelles versions de Git (Git >= 2.13) simplifie tout ceci en ajoutant le drapeau --recurse-submodules à la commande git checkout, qui s’occupe de placer les sous-modules dans le bon état pour la branche sur laquelle nous commutons.

$ git --version
git version 2.13.3

$ git checkout -b add-crypto
Basculement sur la branche 'add-crypto'

$ git submodule add https://github.com/chaconinc/CryptoLibrary
Clonage dans '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

Utiliser le drapeau --recurse-submodules de git checkout peut aussi être utile quand vous travaillez sur plusieurs branches dans les super-projet, chacune aillant votre sous-module pointant sur des commits différents. En fait, si vous commutez de branches qui enregistrent le sous-module à différents commits, à l’exécution de git status le sous-module apparaîtra comme « modified », et indique « new commits » (nouveaux commits). C’est parce que l’état du sous-module n’est pas géré par défaut lors du basculement de branches.

Cela peut être vraiment déroutant, donc c’est toujours une bonne idée de toujours lancer git checkout --recurse-submodules quand votre projet a des sous-modules. Pour les versions anciennes de Git qui n’ont pas de drapeau --recurse-submodules, après l’extraction, vous pouvez utiliser git submodule update --init --recursive pour placer les sous-modules dans le bon état.

Par chance, vous pouvez indiquer à Git (>=2.14) de toujours utiliser le drapeau --recurse-submodules en paramétrant l’option de configuration submodule.recurse : git config submodule.recurse true. Comme noté ci-dessus, cela forcera auss Git à parcourir récursivement les sous-modules pour toute commande qui accepte l’option --recurse-submodules (excepté git clone).

Basculer d’un sous-répertoire à un sous-module

Une autre difficulté commune consiste à basculer de sous-répertoires en sous-modules. Si vous suiviez des fichiers dans votre projet et que vous voulez les déplacer dans un sous-module, vous devez être très prudent ou Git sera inflexible. Présumons que vous avez les fichiers dans un sous-répertoire de votre projet, et que vous voulez les transformer en un sous-module. Si vous supprimez le sous-répertoire et que vous exécutez submodule add, Git vous hurle dessus avec :

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

Vous devez d’abord supprimer le répertoire CryptoLibrary de l’index. Vous pourrez ensuite ajouter le sous-module :

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

Maintenant, supposons que vous avez fait cela dans une branche. Si vous essayez de basculer dans une ancienne branche où ces fichiers sont toujours dans l’arbre de projet plutôt que comme sous-module, vous aurez cette erreur :

$ 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

Vous pouvez le forcer à basculer avec checkout -f, mais soyez attentif à ce qu’il soit propre ou les modifications seraient écrasées.

$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Basculement sur la branche 'master'

Ensuite, lorsque vous rebasculez, vous aurez un répertoire CryptoLibrary vide et git submodule update pourrait ne pas le remettre en état. Vous allez devoir vous rendre dans le répertoire de votre sous-module et lancer git checkout . pour retrouver tous vos fichiers. Vous pouvez lancer ceci dans un script submodule foreach dans le cas de multiples sous-modules.

Il est important de noter que depuis les versions de Git récentes, les sous-modules conservent leurs données Git dans le répertoire .git du projet principal, ce qui à la différence des versions antérieures, permet de supprimer le dossier du sous-module sans perdre les commits et les branches qu’il contenait.

Avec ces outils, les sous-modules peuvent être une méthode assez simple et efficace pour développer simultanément sur des projets connexes mais séparés.

scroll-to-top