Qu'est-ce que les sous-modules <span class="css-span">git</span> ?
Un sous-module c'est un dépôt <span class="css-span">git</span>… dans un autre dépôt <span class="css-span">git</span>.
Cela permet d'avoir un lien, unilatéral, entre deux dépôts de code.
Nous pouvons imaginer un cas simple d'utilisation : un produit A utilise une librairie commune à plusieurs produits A, B et C.
Pour réaliser ceci nous pouvons utiliser des sous-modules. Les produits A, B et C vont tous avoir un sous-module contenant le code de la librairie commune.
Évidemment, une autre approche serait d'utiliser un gestionnaire de paquets, mais nous en reparlerons plus tard.
Structure d'un sous-module
Un sous-module c'est un dépôt <span class="css-span">git</span> dans un autre dépôt <span class="css-span">git</span>, mais il y a quand même certaines spécificités et malheureusement aussi certains pièges.
Déjà, il est important de toujours savoir dans quel dépôt nous nous trouvons au moment où nous exécutons une commande.
Dans le dépôt parent, <span class="css-span">git</span> gère les sous-modules comme des liens symboliques vers un commit spécifique.
Par contre, une fois dans le sous-module (ou dépôt enfant) toutes les opérations <span class="css-span">git</span> habituelles peuvent être exécutées.
Travailler avec des sous-modules <span class="css-span">git</span>
Pour commencer, nous avons deux dépôts <span class="css-span">git</span> : <span class="css-span">depo_1</span> et <span class="css-span">depo_2</span> qui contiennent chacun un fichier <span class="css-span">README.md</span>.
~ $ git clone git@github.com:Younup/depo_1.git
~ $ git clone git@github.com:Younup/depo_2.git
~ $ tree
.
├── depo_1
│ └── README.md
└── depo_2
└── README.md
Ajouter un sous-module <span class="css-span">git</span>
Nous allons maintenant ajouter <span class="css-span">depo_2</span> comme sous-module de <span class="css-span">depo_1</span> :
~ $ cd depo_1
~/depo_1 --> depo_1.git@main $ git submodule add git@github.com:Younup/depo_2.git
~/depo_1 --> depo_1.git@main $ git commit -m "Add submodule"
~/depo_1 --> depo_1.git@main $ git push
~ $ tree
.
├── depo_1
│ ├── depo_2
│ │ └── README.md
│ └── README.md
└── depo_2
└── README.md
Récupérer un dépôt qui contient des sous-modules
Pour simuler la récupération de <span class="css-span">depo_1</span> avec les sous-modules, nous allons le supprimer puis le re-cloner :
~ $ rm -fr depo_1
~ $ git clone git@github.com:Younup/depo_1.git
~ $ cd repo_1
Et là, nous constatons que le sous-module n'est pas présent…
~/depo_1 --> depo_1.git@main $ tree
.
├── depo_2
└── README.md
Par défaut, <span class="css-span">git</span> ne clone pas les sous-modules. En effet, toutes les opérations sur les sous-modules se font via la commande <span class="css-span">git submodule</span>.
Il faut donc dire à <span class="css-span">git</span> de récupérer aussi les sous-modules de notre dépôt.
~/depo_1 --> depo_1.git@main $ git submodule init
~/depo_1 --> depo_1.git@main $ git submodule update
~/depo_1 --> depo_1.git@main $ tree
.
├── depo_2
│ └── README.md
└── README.md
NOTE : si les sous-modules ont aussi des sous-modules, il faut ajouter l'option <span class="css-span">--recursive</span>.
Push
Nous allons maintenant effectuer une modification dans <span class="css-span">depo_2</span> puis faire un <span class="css-span">push</span> dans le sous-module.
État initial du depo_2 :
Nous allons :
- créer une nouvelle branche feature dans <span class="css-span">depo_2</span> et se placer sur celle-ci ;
- ajouter les modifications avec <span class="css-span">git add</span> ;
- faire un <span class="css-span">commit</span> ;
- et enfin effectuer un <span class="css-span">push</span> sur <span class="css-span">depo_2</span>.
NOTE : pour cette dernière opération, il faut spécifier <span class="css-span">--set-upstream origin feature</span> lors du premier push sur cette branche, afin de spécifier la branche remote qui sera rattachée à notre branche locale.
~/depo_1/depo_2 depo_2@(HEAD detached at 2e8bd03) $ git checkout -b feature
Switched to branch 'feature'
~/depo_1/depo_2 --> depo_2.git@feature $ git add *
~/depo_1/depo_2 --> depo_2.git@feature $ git commit -m "Modif on feature"
[feature 0b0b9f2] Modif on feature
~/depo_1/depo_2 --> depo_2.git@feature $ git push --set-upstream origin feature
Nous avons effectué les modifications dans <span class="css-span">depo_2</span> en local et remote.
Mais qu'en est-il pour <span class="css-span">depo_1</span> ?
~/depo_1 --> depo_1.git@main $ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
modified: depo_2 (new commits)
Comme vous pouvez le voir dans le résultat de la commande <span class="css-span">git status</span> ci-dessus, <span class="css-span">depo_1</span> contient des modifications relatives au sous-module <span class="css-span">depo_2</span>.
Le graphique suivant représente l'historique de <span class="css-span">depo_2</span> avec des tags qui indiquent où <span class="css-span">depo_1</span> pointe en local et remote.
sous-module depo_2 et référence de depo_1 :
À ce stade, <span class="css-span">depo_1</span> ne dispose pas encore de nos modifications en remote. Si quelqu'un d'autre le récupère, le sous-module pointera sur le tag _depo_1_remote_.
Pour partager nos modifications dans <span class="css-span">depo_1</span>, il faut simplement faire un <span class="css-span">push</span> comme si nous avions modifié un fichier :
~/depo_1 --> depo_1.git@main $ git add depo_2/
~/depo_1 --> depo_1.git@main $ git commit -m "Update submodule"
~/depo_1 --> depo_1.git@main $ git push
sous-module depo_2 :
ATTENTION : si vous clonez le dépôt principal, le sous-module ne sera pas sur une branche, seulement sur une révision "sans tête" (headless).
Pull
C'est un peu pareil pour faire un <span class="css-span">pull</span>.
Supposons que nous soyons sur le premier commit de <span class="css-span">depot_1</span> et que nous voulions récupérer les modifications faites dans le paragraphe précédent.
sous-module depo_2 :
Comme d'habitude, commençons par faire un <span class="css-span">fetch</span> et un <span class="css-span">pull</span>.
~/depo_1_2/depo_1 --> depo_1.git@main $ git fetch
From github.com:Younup/depo_1
d698556..8076465 main -> origin/main
Fetching submodule sub/depo_2
From github.com:Younup/depo_2
0b0b9f2..81a3d90 feature -> origin/feature
~/depo_1_2/depo_1 --> depo_1.git@main $ git pull
Updating d698556..8076465
Fast-forward
sub/depo_2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Seulement, nous constatons, une fois de plus, que les fichiers du sous-module n'ont pas été mis à jour.
Et, comme dans les cas précédents, cela sera résolu en appelant <span class="css-span">git submodule</span>.
~/depo_1_2/depo_1 --> depo_1.git@main $ git submodule update
Submodule path 'sub/depo_2': checked out '81a3d90051dfe142f29a746730be82403b304d53'
sous-module depo_2 :
Workflow avec les sous-modules
Voyons maintenant comment gérer les conflits avec les sous-modules.
Imaginons que deux développeurs aient chacun modifié le sous-module <span class="css-span">depo_2</span> depuis <span class="css-span">depo_1</span>, chacun dans une branche différente :
Sur le graphique précédent, le <span class="css-span">depo_1</span> du premier développeur pointe sur le commit <span class="css-span">main_1</span> du <span class="css-span">depo_2</span>. De même le <span class="css-span">depo_1</span> du deuxième développeur pointe sur le commit <span class="css-span">main_2</span> du <span class="css-span">depo_2</span>.
Dans cette situation, <span class="css-span">git</span> ne vous laissera pas faire de <span class="css-span">merge</span> ou de <span class="css-span">rebase</span> directement depuis <span class="css-span">depo_1</span>.
C'est là une différence par rapport au <span class="css-span">merge</span> depuis le même dépôt. Puisque le sous-module pointe uniquement vers une révision et pas vers une branche, il n'est pas possible de résoudre les conflits depuis <span class="css-span">depo_1</span>.
Mais il y a, bien sûr, une solution assez simple : il suffit de faire le <span class="css-span">merge</span> ou <span class="css-span">rebase</span> depuis <span class="css-span">depo_2</span> vers sa branche <span class="css-span">main</span>, puis de faire pointer <span class="css-span">depo_1</span> vers le commit ainsi créé.
Sous-modules ou gestionnaires de paquets
REMARQUE Ce paragraphe est teinté de mon expérience en tant que développeur <span class="css-span">C/C++</span>.
Tout cela peut paraître un peu complexe si vous avez l'habitude de travailler avec un langage qui incorpore un gestionnaire de paquets :
- <span class="css-span">Java</span> utilise <span class="css-span">Maven</span> ou <span class="css-span">Gradle</span>,
- <span class="css-span">Python</span> utilise <span class="css-span">Pip</span> et
- <span class="css-span">Rust</span> utilise <span class="css-span">Cargo</span>.
Cependant il y a une différence entre les deux, c'est qu'un manageur de paquet gère des binaires alors qu'un sous module <span class="css-span">git</span> gère du code.
Choisir l'un ou l'autre va dépendre de vos préférences et des contraintes spécifiques au projet. Il n'y a donc pas de règles strictes.
Dépendances internes ou externes
Si vous n'êtes pas le propriétaire de la librairie et que vous ne prévoyez pas d'y ajouter des fonctionnalités, préférez un gestionnaire de paquet.
Hébergement des paquets
Un gestionnaire de paquet a besoin d'un service d'hébergement de paquet pour pouvoir fonctionner.
Il en existe des publiques mais ce n'est peut-être pas souhaitable pour votre projet (confidentialité, propriété, licence, ...).
On n'y pense pas forcément mais configurer et maintenir un service d'hébergement de paquets privé cela représente du travail (infrastructure, maintenance, intégration continue, ...). Pour une petite équipe, ce n'est pas toujours pertinent et se satisfaire uniquement de <span class="css-span">git</span> peut être préférable.
Besoin du code ou cycles de release liés
Si vous avez besoin du code d'une dépendance (consultation, test, debug, instrumentation, apporter des modifications, ...) alors préférez les sous-modules.
De même si vous sortez une nouvelle version de votre librairie pour chaque release de votre projet principal, cela signifie que les deux sont liés. Dans ce cas les sous-modules sont probablement plus adaptés. Ils vous permettront, par exemple, de modifier les deux simultanément.
Aide à la prise de décision
Si vous hésitez toujours, voici un résumé pour vous aider :
Conclusion
En conclusion, les sous-modules <span class="css-span">git</span> offrent une solution efficace pour gérer des dépendances de code.
Bien que cette approche puisse paraître moins flexible qu'un gestionnaire de paquets, elle présente des avantages dans certains contextes, notamment pour gérer du code source plutôt que des binaires et lorsque les cycles de développement entre le projet principal et ses dépendances sont étroitement liés.
Toutefois, leur utilisation nécessite une bonne maîtrise des commandes <span class="css-span">git</span>.
Choisir entre sous-modules et gestionnaires de paquets dépend donc des besoins spécifiques du projet : confidentialité, infrastructure, nécessité d'accéder au code, ou simple besoin de gestion de binaires. Il n'y a pas de réponse unique, et chaque équipe devra évaluer les avantages et inconvénients en fonction de ses contraintes.
Rien ne vous empêche, aussi, d'utiliser les deux simultanément, pour pouvoir profiter des avantages de chacun.
Sous-modules pour les librairies internes et gestionnaires de paquet pour les libraires externes ?
Annexes
Liens
Documentation de <span class="css-span">git</span> sur les sous-modules
Afficher le nom de votre dépôt <span class="css-span">git</span> et sa branche dans votre prompt
Dans votre <span class="css-span">.bashrc</span> (ou équivalent), remplacez la définition de la variable <span class="css-span">PS1</span> par :
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'
}
get_git_repo_name() {
basename $(git remote get-url origin 2>/dev/null) 2>/dev/null
}
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w --> $(get_git_repo_name)@$(parse_git_branch) \$ '
Ici nous affichons : <span class="css-span">dossier --> depo@branche $</span>
Mermaid
Les diagrammes ont été faits avec Mermaid.
Hugo est un éternel idéaliste.
Il rêve du jour où les humains travailleront en harmonie avec les machines.
En attendant, il programme en C/C++ sur système embarqué et quand il lui reste du temps, il dessine ou joue aux jeux vidéo.
Et au cas où les machines se rebelleraient, il apprend le MMA, on ne sait jamais…