Articles

Mastering git submodules (Français)

Submodules, step by step

Nous allons maintenant explorer chaque étape de l’utilisation des sous-modules dans un projet collaboratif, en nous assurant de mettre en évidence les comportements par défaut, les pièges et les améliorations disponibles.

afin de faciliter votre suivi, j’ai rassemblé quelques exemples de repos avec leurs « télécommandes” (en fait, juste des répertoires)., Vous pouvez décompresser l’archive où vous voulez, puis ouvrir un shell (ou Git Bash, si vous êtes sous Windows) dans le répertoire git-subs qu’il crée:

Télécharger l’exemple de dépôt

Vous y trouverez trois répertoires:

  • main agit comme le dépôt conteneur, local au premier collaborateur,
  • plugin agit comme le dépôt de maintenance central pour le module, et
  • remotes contient les les deux précédents repos.

dans l’exemple de commandes ci-dessous, l’invite affiche toujours dans quel dépôt nous sommes.,

ajout d’un sous-module

commençons par ajouter notre plugin en tant que sous-module à l’intérieur de notre conteneur (qui est en main). Le plugin lui-même a une structure simple:

.
├── README.md
├── lib
│ └── index.js
└── plugin-config.json

allons donc dans main et utilisons la commande git submodule add. Il prend L’URL de la télécommande et un sous-répertoire dans lequel « instancier” le sous-module.

parce que nous utilisons des chemins au lieu d’URL ici pour nos télécommandes, nous avons rencontré un problème étrange, bien que bien connu: les chemins relatifs pour les télécommandes sont interprétés par rapport à notre télécommande principale, non au répertoire racine de notre repo., C’est super bizarre, pas décrit nulle part, mais je l’ai vu arriver à chaque fois. Donc au lieu de dire ../ télécommandes / plugin, nous disons juste ../plugin.

main (master u=) $ git submodule add ../plugin vendor/plugins/demo
Cloning into 'vendor/plugins/demo'…
done.
main (master + u=) $

Cet ajout de certains paramètres dans nos locaux de configuration:

main (master + u=) $ cat .git/config


url = ../remotes/plugin

Et cela met également en scène deux fichiers:

Hein?! Qu’est ce que c’ .fichier gitmodules? Regardons-le:

main (master + u=) $ cat .gitmodules

path = vendor/plugins/demo
url = ../plugin

cela ressemble furieusement à notre configuration locale So alors pourquoi la duplication? Eh bien, précisément parce que notre configuration locale est local locale., Nos collaborateurs ne le verront pas (ce qui est parfaitement normal), ils ont donc besoin d’un mécanisme pour obtenir les définitions de tous les sous-modules qu’ils doivent configurer dans leurs propres dépôts. C’est ce qu’ .gitmodules est pour; il sera lu plus tard par la commande git submodule init, comme nous le verrons dans un instant.

pendant que nous sommes sur le statut, notez à quel point c’est minimaliste quand il s’agit de notre sous-module: il va juste avec un nouveau fichier trop générique au lieu de nous en dire plus sur ce qui se passe à l’intérieur., Notre sous-module a en effet été injecté dans le sous-répertoire:


└── vendor
└── plugins
└── demo
├── .git
├── README.md
├── lib
│ └── index.js
└── plugin-config.json

L’État, comme les logs et les diff, est limité au dépôt actif (en ce moment, le conteneur), pas aux sous-modules, qui sont des dépôts imbriqués. C’est souvent problématique (il est super facile de manquer une régression lorsqu’elle est limitée à cette vue), donc je vous recommande de configurer un statut conscient du sous-module une fois pour toutes:

git config --global status.submoduleSummary true

et maintenant:

Aaaah, c’est beaucoup mieux., Le statut étend ses informations de base pour ajouter que le sous-module présent chez vendor / plugins / demo a reçu 3 commits de nouvelles (comme nous venons de le créer, cela signifie que la branche distante n’avait que trois commits), le dernier étant un ajout (notez le support d’angle de pointage à droite >) avec

afin de vraiment ramener à la maison que nous traitons avec deux dépôts distincts ici, entrons dans le répertoire du sous-module:

le dépôt actif a changé, car un nouveau .,git prend le relais: dans le répertoire courant (demo, le répertoire du sous-module), A.git existe en effet, un seul fichier aussi, pas un répertoire. Regardons à l’intérieur:

demo (master u=) $ cat .git
gitdir: ../../../.git/modules/vendor/plugins/demo

encore une fois, depuis Git 1.7.8, Git ne laisse pas de répertoires repo dans le répertoire de travail du conteneur, mais les centralise dans celui du conteneur .répertoire git (à l’intérieur .git / modules), et utilise une référence gitdir dans les sous-modules.,

la raison d’être est simple: cela permet au dépôt de conteneur d’avoir des branches sans sous-module, sans avoir à supprimer le dépôt du sous-module du répertoire de travail et à le restaurer plus tard.

naturellement, lors de l’ajout du sous-module, vous pouvez choisir d’utiliser une branche spécifique, ou même un commit spécifique, en utilisant l’option-b CLI (comme d’habitude, la valeur par défaut est master). Notez que nous ne sommes pas, en ce moment, sur une tête détachée, contrairement à ce qui se passera plus tard: c’est parce que Git a vérifié master, pas un SHA1 spécifique. Nous aurions dû spécifier un SHA1 à-b pour obtenir une tête détachée dès le départ.,

donc, revenons au dépôt de conteneur, et finalisons l’ajout du sous-module et poussons-le à la télécommande:

demo (master u=) $ cd -
main (master + u=) $ git commit -m "Ajout submodule plugin demo"
main (master u+1) $ git push

saisir un dépôt qui utilise des sous-modules

afin d’illustrer les problèmes de collaboration sur un dépôt qui utilise des sous-modules, nous allons diviser les personnalités et agir comme notre collègue, qui clone la télécommande du conteneur pour commencer à travailler avec nous. Nous allons cloner cela dans un répertoire de collègues, afin que nous puissions immédiatement dire quelle casquette de personnalité nous avons à un moment donné.,

la première chose à remarquer est que notre sous-module est absent du répertoire de travail; seul son répertoire de base est ici:

vendor
└── plugins
└── demo

comment cela s’est-il produit? C’est simplement dû au fait que, jusqu’à présent, notre nouveau repo (collègue) n’est pas au courant de notre sous-module encore: l’information est nulle part dans sa configuration locale (vérifier son .git/config si vous ne me croyez pas). Nous devrons remplir cela, en fonction de quoi .gitmodules a à dire, ce qui est précisément ce que fait git submodule init:

notre .git/config est maintenant au courant de notre sous-module., Cependant, nous ne l’avons toujours pas récupéré de sa télécommande, sans parler de l’avoir présent dans notre répertoire de travail. Et pourtant, notre statut apparaît comme propre!

voyez, nous devons saisir manuellement les commits pertinents. Ce n’est pas quelque chose que notre clone initial a fait, nous devons le faire à chaque traction. Nous y reviendrons dans une minute, car il s’agit d’un clone de comportement qui peut réellement automatiser, lorsqu’il est correctement appelé.,

en pratique, lorsque vous utilisez des dépôts sous-modules, Nous regroupons généralement les deux commandes (init et update) en une seule:

colleague (master u=) $ git submodule update --init

Il est toujours dommage que Git vous fasse faire tout cela vous-même. Imaginez, sur de plus grands projets de fil dentaire, quand les sous-modules ont leurs propres sous-modules, et ainsi de suite et ainsi de suite This cela deviendrait rapidement un cauchemar.

il se trouve que Git fournit une option CLI pour cloner automatiquement git submodule update — init récursivement juste après le clonage: l’option-recursive plutôt bien nommée.,

alors essayons le tout à nouveau:

maintenant c’est mieux! Notez que nous sommes maintenant sur une tête détachée à l’intérieur du sous-module (comme nous le serons à partir de Maintenant):

git-subs $ cd colleague/vendor/plugins/demo
demo ((master)) $

Voir le double ensemble de parenthèses dans mon invite, au lieu d’un seul ensemble?, Si votre invite n’est pas configurée comme la mienne, pour afficher la tête détachée comme décrit (avec le script d’invite intégré de Git, vous devrez définir la variable D’environnement GIT_PS1_DESCRIBE_STYLE=branch), vous verrez plutôt quelque chose comme ceci:

demo ((fe64799...)) $

en tout cas, l’état confirme où nous en sommes:

demo ((master)) $ git status
HEAD detached at fe64799
nothing to commit, working directory clean

obtenir une mise à jour à partir de la télécommande du sous-module

OK, maintenant que nous avons notre propre repo (principal) et notre « collègue” (collègue) tous configurés pour collaborer, entrons dans la peau d’une troisième personne: celle qui maintient le plugin., Ici, passons à cela:

maintenant, ajoutons deux pseudo-commits et publions-les sur la télécommande:

enfin, remettons notre bouchon « premier développeur”:

plugin (master u=) $ cd ../main
main (master u=) $

supposons que nous voulions maintenant obtenir ces deux commits dans notre sous-module. Pour ce faire, nous devons mettre à jour son dépôt local, en commençant par se déplacer dans son répertoire de travail afin qu’il devienne notre dépôt actif.

sur une note de côté, Je ne recommanderais pas d’utiliser pull pour ce type de mise à jour., Pour obtenir correctement les mises à jour dans le répertoire de travail, cette commande nécessite que vous soyez sur la branche active appropriée, ce que vous n’êtes généralement pas (vous êtes sur une tête détachée la plupart du temps). Vous devriez commencer par une caisse de cette branche. Mais plus important encore, la branche distante aurait très bien pu aller plus loin puisque le commit que vous souhaitez définir, et un pull injecterait des commits que vous ne voudrez peut-être pas dans votre base de code locale.,

Par conséquent, je recommande de diviser le processus manuellement: d’abord git fetch pour obtenir toutes les nouvelles données de la télécommande dans le cache local, puis connectez-vous pour vérifier ce que vous avez et vérifiez le SHA1 souhaité. En plus d’un contrôle plus fin, cette approche présente l’avantage supplémentaire de fonctionner quel que soit votre état actuel (branche active ou tête détachée).

OK, donc nous sommes Bons, Pas de commit étranger., Quoi qu’il en soit, définissons explicitement celui qui nous intéresse (évidemment, vous avez un SHA1 différent):

demo (master u-2) $ git checkout -q 0e90143

(le-q n’est là que pour nous épargner git blabler sur la façon dont nous nous retrouvons sur une tête détachée. Habituellement, ce serait un rappel salutaire, mais sur ce que nous savons ce que nous faisons.)

maintenant que notre sous-module est mis à jour, nous pouvons voir le résultat dans l’état du dépôt de conteneur:

dans la partie « classique” de l’état, nous voyons un nouveau type de changement de commits, ce qui signifie que le commit référencé a changé., Une autre possibilité (qui pourrait être ajoutée à celle-ci) est le nouveau contenu, ce qui signifierait que nous avons apporté des modifications locales au répertoire de travail du sous-module.

la partie inférieure, activée par notre statut.submoduleSummary = true réglage plus tôt, indique explicitement les commits introduits (car ils utilisent un angle de pointage à droite >) depuis notre dernier commit de conteneur qui avait touché le sous-module.

dans la famille des” comportements par défaut terribles », git diff laisse beaucoup à désirer:

Qu’est — ce que le -?, Il y a une option CLI qui nous permet de voir quelque chose de plus utile:

main (master * u=) $ git diff --submodule=log
Submodule vendor/plugins/demo fe64799..0e90143:
> Pseudo-commit #2
> Pseudo-commit #1

Il n’y a pas d’autres changements locaux en ce moment en plus du commit référencé du sous-module Notice notez que cela correspond presque exactement à la partie inférieure de notre affichage d’état git amélioré.

devoir taper ce type d’option CLI à chaque fois (ce qui, soit dit en passant, n’apparaît pas dans les offres d’achèvement actuelles de Git) est plutôt difficile à manier. Heureusement, il existe un paramètre de configuration correspondant:

il ne nous reste plus qu’à effectuer le commit de conteneur qui finalise la mise à jour de notre sous-module., Si vous deviez toucher le code du conteneur pour le faire fonctionner avec cette mise à jour, validez-le naturellement. D’autre part, évitez de mélanger les modifications liées au sous-module et d’autres choses qui ne concerneraient que le code du conteneur: en séparant soigneusement les deux, les migrations ultérieures vers d’autres approches de réutilisation de code sont facilitées (également, comme d’habitude, Atomic commits FTW).

alors que nous sommes sur le point de récupérer cette mise à jour du sous-module dans le dépôt de notre collègue, nous pousserons juste après la validation (ce qui n’est pas une bonne pratique générale).

main (master * u=) $ git commit -am "Setting submodule on PC2"
main (master u+1) $ git push

tirer un sous-module-en utilisant repo

Cliquez!, « Collègue” cap sur!

Nous tirons donc des mises à jour de la télécommande du dépôt de conteneurs

(vous pourriez ne pas avoir le « rebasé et mis à jour avec succès.” et voir une « fusion faite par la stratégie ‘récursive’” à la place. Si oui, mon cœur va vers vous, et vous devriez immédiatement apprendre pourquoi tire devrait rebase).

notez la seconde moitié de cet affichage: il s’agit du sous-module, en commençant par « récupérer le sous-module…”.

ce comportement est devenu la valeur par défaut avec Git 1.7.5, avec le paramètre de configuration fetch.,recurseSubmodules maintenant par défaut à la demande: si un projet de conteneur obtient des mises à jour pour les validations de sous-modules référencés, ces sous-modules sont récupérés automatiquement. (Rappelez-vous que la récupération est la première partie de la traction.)

toujours, et c’est critique: Git récupère automatiquement, mais ne met pas à jour automatiquement. Votre cache local est à jour avec la télécommande du sous-module, mais le répertoire de travail du sous-module est resté dans son ancien contenu. Au moins, vous pouvez fermer cet ordinateur portable, monter dans un avion et aller de l’avant une fois hors ligne., Bien que cette récupération automatique soit limitée aux sous-modules déjà connus: les nouveaux, pas encore copiés dans la configuration locale, ne sont pas récupérés automatiquement.

git récupère automatiquement, mais ne met pas à jour automatiquement.

l’invite actuelle, avec son astérisque (*), fait allusion aux modifications locales, car notre WD n’est pas synchronisé avec l’index, ce dernier étant conscient des commits du sous-module nouvellement référencé. Découvrez l’état:

remarquez comment les équerres pointent vers la gauche (<)?, Git voit que le DEO actuel n’a pas ces deux commits, contrairement aux attentes du projet container.

c’est le danger énorme: si vous ne mettez pas explicitement à jour le répertoire de travail du sous-module, votre prochain commit de conteneur régressera le sous-module. C’est une de premier ordre piège.

est donc obligatoire que vous finalisiez la mise à jour:

tant que nous essayons de former de bonnes habitudes génériques, la commande préférée ici serait un sous — module git update — init-recursive, afin d’initialiser automatiquement tout nouveau sous-module, et de les mettre à jour récursivement si nécessaire.,

Il y a un autre cas de bord: si L’URL distante du sous-module a changé depuis la dernière utilisation (peut-être l’un des collaborateurs l’a changé dans le .gitmodules), vous devez mettre à jour manuellement votre configuration locale pour correspondre à cela. Dans une telle situation, avant la mise à jour du sous-module git, vous devez exécuter une synchronisation du sous-module git.

je devrais mentionner, par souci d’exhaustivité, que même si git submodule update vérifie par défaut le SHA1 référencé, vous pouvez changer cela pour, par exemple, rebaser tout travail de sous-module local (nous en parlerons très bientôt)., Pour ce faire, définissez le paramètre de configuration de mise à jour de votre sous-module pour qu’il soit rebasé dans la configuration locale de votre conteneur.

et je suis désolé mais non, il n’y a pas de paramètre de configuration locale, ni même d’option CLI d’ailleurs, qui peut se mettre à jour automatiquement lors de la traction. Pour automatiser de telles choses, vous devez utiliser des alias, des scripts personnalisés ou des hooks locaux soigneusement conçus., Voici un exemple d’alias spull (ligne unique, divisé ici pour l’affichage):

git config --global alias.spull '!git pull && git submodule sync --recursive && git submodule update --init --recursive'

Si vous souhaitez conserver la possibilité de passer des arguments personnalisés à git pull, vous pouvez soit définir une fonction à la volée et l’appeler, soit utiliser un script personnalisé. La première approche ressemblerait à ceci (encore une fois, une seule ligne):

pas très lisible, hein? Je préfère l’approche de script personnalisé., Disons que vous mettriez un fichier de script git-spull quelque part dans votre chemin (j’ai un répertoire ~/perso/bin dans mon chemin juste pour de telles choses):

#! /bin/bash
git pull "$@" &&
git submodule sync --recursive &&
git submodule update --init --recursive

Nous lui donnons alors les droits d’exécution:

chmod +x git-spull

et maintenant nous pouvons l’utiliser comme

mise à jour d’un sous-module en place dans le conteneur

c’est le cas d’utilisation le plus difficile, et vous devriez rester à l’écart autant que possible, préférant la maintenance via le repo central dédié.,

cependant, il peut arriver que le code du sous-module ne puisse pas être testé, ou même compilé, en dehors du code du conteneur. De nombreux thèmes et plugins ont de telles contraintes.

la première chose à comprendre est, parce que vous allez faire des commits, vous devez commencer à partir d’une base appropriée, qui sera une pointe de branche. Vous devez donc vérifier que les derniers commits de la branche ne « casseront” pas votre projet de conteneur. S’ils le font, Eh bien, créer votre propre branche spécifique au conteneur dans le sous-module semble tentant, mais ce chemin conduit à un couplage fort entre le sous-module et le conteneur, ce qui n’est pas conseillé., Vous voudrez peut-être arrêter de « submoduler” ce code dans ce projet particulier, et simplement l’intégrer comme n’importe quel contenu régulier à la place.

admettons que vous pouvez, en toute conscience, ajouter à la branche principale actuelle du sous-module. Commençons par synchroniser notre état local sur celui de la télécommande:

Une autre façon de procéder serait, à partir du dépôt de conteneur, de synchroniser explicitement la branche locale du sous — module sur sa branche distante suivie (ligne unique en haut, dernière suivie d’espaces):

Nous pouvons maintenant éditer le code, le faire fonctionner, le tester, etc., Une fois que nous sommes tous prêts, nous pouvons ensuite effectuer les deux commits et les deux pushs nécessaires (c’est super facile, et en pratique trop fréquent, d’oublier une partie de cela).

ajoutons simplement du faux travail et faisons les deux commits associés, au niveau du sous-module et du conteneur:

à ce stade, le danger majeur est d’oublier de pousser le sous-module. Vous revenez au projet de conteneur, le validez et ne poussez que le conteneur. C’est une erreur facile à faire, surtout à l’intérieur d’une ou D’une interface graphique. Lorsque vos collègues essaient d’obtenir des mises à jour, tout l’enfer se déchaîne., Regardez la première étape:

Il n’y a absolument aucune indication que Git n’a pas pu récupérer le commit référencé à partir de la télécommande du sous-module. Le premier indice de ceci est dans le statut:

notez l’avertissement: apparemment, le commit nouvellement référencé pour le sous-module est introuvable. En effet, si nous essayons de mettre à jour le répertoire de travail du sous-module, nous obtenons:

main (master * u=) $ git submodule update
fatal: reference is not a tree: 12e3a529698c519b2fab790630f71bd531c45727
Unable to checkout '12e3a529698c519b2fab790630f71bd531c45727' in submodule path 'vendor/plugins/demo'

Vous pouvez clairement voir à quel point il est important de se rappeler d’avoir poussé le sous-module aussi, idéalement avant de pousser le conteneur., Faisons cela en collègue et tentons à nouveau la mise à jour:

je devrais noter qu’il existe une option CLI qui vérifiera si les validations de sous — module actuellement référencées doivent également être poussées, et si oui les poussera: c’est git push-recurse-submodules=on-demand (une bouchée, certes). Il doit cependant avoir quelque chose au niveau du conteneur pour fonctionner: seuls les sous-modules ne le couperont pas.

de plus, (il n’y a pas de paramètre de configuration pour cela, vous devrez donc standardiser les procédures autour d’un alias, par exemple spush:) — à partir de Git 2.7.0, il y a maintenant un push.,configuration de recurseSubmodules que vous pouvez définir (à la demande ou vérifier).

git config --global alias.spush 'push --recurse-submodules=on-demand'

suppression d’un sous-module

Il y a deux situations où vous voudriez « supprimer” un sous-module:

  • vous voulez juste effacer le répertoire de travail (peut-être avant d’archiver le WD du conteneur) mais vous voulez conserver la possibilité de le restaurer plus tard (il doit doncgitmodules et .git/modules);
  • Vous souhaitez supprimer définitivement de la branche courante.

– voyons chaque cas à son tour.,

suppression temporaire d’un sous-module

la première situation est facilement gérée par git submodule deinit. Voyez par vous-même:

cela n’a aucun impact sur l’état du conteneur. Le sous-module n’est plus connu localement (il est parti de .git / config), donc son absence du répertoire de travail passe inaperçue. Nous avons toujours le répertoire vendor / plugins / demo mais il est vide; nous pourrions le supprimer sans conséquence.

le sous — module ne doit pas avoir de modifications locales lorsque vous faites cela, sinon vous devrez forcer l’appel.,

toute Sous-commande ultérieure du sous-module git ignorera béatement ce sous-module jusqu’à ce que vous l’initialisiez à nouveau, car le sous-module ne sera même pas dans la configuration locale. Ces commandes incluent update, foreach et sync.

d’autre part, le sous-module reste défini .gitmodules: un init suivi d’une mise à jour (ou un seul update — init) le restaurera comme nouveau:

suppression permanente d’un sous-module

cela signifie que vous voulez vous débarrasser définitivement du sous-module: un git rm régulier fera l’affaire, comme pour toute autre partie du répertoire de travail., Cela ne fonctionnera que si votre sous-module utilise un gitfile (A.git qui est un fichier, pas un répertoire), ce qui est le cas à partir de Git 1.7.8. Sinon, vous devrez gérer à la main (je vais vous dire comment à la fin).

en plus de retirer le sous-module du répertoire de travail, la commande mettra à jour le .le fichier gitmodules ne fait donc plus référence au sous-module. Voici:

naturellement, les informations d’état avancées se déplacent ici, car le fichier gitfile du sous-module a disparu (en fait, tout le répertoire de démonstration a disparu).,

ce qui est étrange cependant, c’est que la configuration locale conserve les informations du sous-module, contrairement à ce qui se passe lorsque vous deinit. Donc, pour une suppression complète, je vous recommande de faire les deux, en séquence, afin de finir correctement nettoyé (cela ne fonctionnerait pas après notre commande précédente, car il s’est effacé .gitmodules déjà):

git submodule deinit path/to/module # ensure local config cleanup
git rm path/to/module # clean WD and .gitmodules

quelle que soit votre approche, le dépôt du sous-module reste présent dans .git / modules/vendor/plugins / demo, mais vous êtes libre de tuer cela quand vous le souhaitez.

Si vous avez besoin de supprimer un sous-module qui a été configuré avant Git 1.7.,8, et intègre donc son .répertoire git directement dans le répertoire de travail du conteneur (au lieu de compter sur un gitfile), vous devrez sortir le bulldozer: les deux commandes précédentes doivent être précédées d’une suppression manuelle du dossier, par exemple RM-FR vendor/plugins/demo, car ces commandes refuseront toujours de supprimer un dépôt réel.