Git - Dépôts et répertoires de travail

dim. 08 juil. 2018 by Marmotte

Git est un système de gestion de versions décentralisé. Le côté décentralisé implique que chaque client télécharge une copie complète du dépôt, la plupart des opérations étant donc réalisées directement sur la machine de l'utilisateur.

Cet article fait partie d'une série expliquant quelques fonctionnalités avancées que j'utilise régulièrement.

  1. Généralités et notations
  2. Quelques configurations
  3. Notes diverses
  4. Dépôts et répertoires de travail
  5. L'historique local
  6. Retravailler l'historique des commits
  7. Trouver le commit introduisant une régression

Plusieurs répertoires de travail pour un même dépôt git

Lorsque l'on doit travailler sur plusieurs branches en même temps, par exemple deux versions majeures d'un projet, il devient vite nécessaire de changer de branche assez souvent.

Dans certains cas, le changement de branche peut poser problème. Par exemple, lorsqu'on doit corriger un bug sur la version X-1 pendant le développement d'une fonctionnalité de la version X, il peut être dérangeant de devoir mettre ses modifications de coté, pour corriger le bug, puis revenir sur la branche de développement. Il peut aussi arriver que l'on doive lancer un long traitement sur une branche, comme des tests unitaires. Cela empêcherait alors de travailler sur l'autre branche, puisque remplacer les fichiers dans le répertoire de travail fausserait l'exécution.

La fonctionnalité permettant de disposer de plusieurs répertoires de travail de git est très utile dans des cas comme ceux-ci. Cette commande crée un second répertoire, dans lequel une copie d'une autre branche que la branche courante est extraite. Tout se passe alors comme si l'on avait une seconde copie du dépôt git, à la différence que le répertoire .git n'est présent qu'une seule fois, et que les commandes exécutées dans les deux répertoires travaillent donc sur le même dépôt local.

Cela présente aussi d'autres avantages, comme un gain d'espace disque, puisque le répertoire .git n'est pas dupliqué, ou le fait que créer un commit dans l'un des répertoires de travail le rend immédiatement disponible dans l'autre. Il est donc possible de corriger un bug dans une version, puis de récupérer la correction dans l'autre version par un simple cherry-pick, sans autre opération intermédiaire.

Créer un nouveau répertoire de travail pour le dépôt git courant se fait au moyen de la commande git worktree add :

$ git worktree add ../<dir-name> <branch-name>
$ # Lister les répertoires de travails connus
$ git worktree list

Pour supprimer un répertoire de travail secondaire, il suffit de l'effacer, puis d'indiquer à git de nettoyer sa liste de répertoires de travail :

$ rm -r ../<dir-name>
$ git worktree prune

Intégration de plusieurs dépôts git en un

Il existe deux manières principales d'intégrer des dépôts git dans un dépôt plus gros. Chacune de ces deux méthodes a ses usage, et leurs effets ne sont pas tous directement comparables. Il n'y en a donc pas une qui soit meilleure que l'autre dans tous les cas.

Lorsque l'on veut utiliser l'une de ces deux commandes, le besoin de base est souvent le même : Regrouper plusieurs dépôts git en un seul, pour n'avoir qu'un seul dépôt à connaître et cloner, au lieu de devoir maintenir une liste de dépôts et leur emplacement. Dans les deux cas, le commit du dépôt secondaire est figé dans le dépôt global. Les différences se situent au niveau de la récupération et de la modification du contenu des sous-dépôts.

Les submodules

La commande git submodule permet de gérer une liste de dépôts git qui seront clonés dans des sous-répertoires du dépôt courant. La configuration d'un submodule contient son emplacement dans le dépôt global, l'URL du dépôt distant, le commit utilisé, et la branche distante à récupérer pour les mises à jour.

L'ajout d'un submodule se fait avec la commande :

$ git submodule add -b <branch-name> <repo-url> <local-path>
$ # Il est ensuite nécessaire de commiter la modification pour sauvegarder l'état du submodule dans le dépôt global
$ git commit -m "Add subrepo-name in local-path"

Pour les autres utilisateurs, récupérer les submodules présents se fait au moyen de la commande :

$ # La sous-commande sync permet de mettre à jour les informations d'URL et de branche à utiliser
$ git submodule sync
$ # Le paramètre --init permet de cloner les submodules, s'ils ne le sont pas déjà
$ git submodule update --init

L'utilisation de submodules nécessite que tous les utilisateurs aient accès aux dépôts distants désignés par les submodules. En effet, chaque submodule fonctionne comme un dépôt git séparé, cloné dans un sous-répertoire du dépôt global, dans lequel il est possible d'utiliser toutes les commandes de git, sans se soucier du dépôt le contenant. Il est donc aussi possible de développer directement dans le submodule, puis de pousser les modifications directement sur son dépôt distant. Après avoir changé l'état d'un submodule, il suffit d'ajouter la modification de commit du submodule au dépôt global au moyen de la commande git add, comme pour n'importe quel autre changement.

Ce fonctionnement permet aussi une mise à jour simple du contenu de ce sous-dépôt, au moyen de la commande git submodule update --remote, qui se chargera de récupérer les modifications de la branche distante automatiquement. Par contre, lorsqu'il devient nécessaire d'appliquer des modifications spécifiques, qui ne seront pas intégrées dans la branche suivie, il faut les pousser dans une autre branche, voire dans un autre dépôt distant, selon les droits d'accès et besoins. Si le dépôt distant est supprimé ou déplacé, il est nécessaire de mettre à jour le paramétrage du submodule associé dans le dépôt global.

Le fait que les informations sur le submodule soient contenues dans un simple fichier texte rend aussi la gestion des conflits de merge et rebase très simple. En effet, seul un fichier de moins de dix lignes est potentiellement modifié plutôt que le contenu complet du dépôt distant.

Les subtrees

La commande git subtree permet d'intégrer le contenu d'un dépôt dans un autre dépôt. Contrairement à l'utilisation de submodules, c'est bien le contenu lui même qui est ajouté dans le dépôt global, pas seulement un fichier contenant les informations permettant de cloner un autre dépôt.

L'ajout d'un subtree peut se faire de trois façons :

$ # Ajout depuis l'URL d'un dépôt distant
$ git subtree add --prefix=<local-path> <repo-url> <branch-name>
$ # Ajout depuis un dépôt distant connu du dépôt courant
$ git subtree add --prefix=<local-path> <remote-name> <branch-name>
$ # Ajout depuis un commit déjà présent dans le dépôt courant
$ git subtree add --prefix=<local-path> <commit>
$ # La commande git subtree fait elle-même le commit, nous n'avons donc pas à le faire manuellement

Note : La commande git subtree ne fait pas partie de la suite standard de commandes de git. Cependant, son fonctionnement correspond juste à une automatisation de traitements faisables en utilisant les commandes de base git merge et git read-tree.

Note : Il est possible d'ajouter le paramètre --squash, pour fusionner tous les commits du dépôt distant en un seul. Cela permet d'avoir un historique du dépôt global plus clair, et évite de le faire grossir inutilement, puisque seul un commit est conservé, le dépôt global ne contiendra donc pas les blobs de tout l'historique du dépôt ajouté en tant que subtree.

Comme c'est le contenu qui est ajouté, les autres utilisateurs n'ont pas besoin de déclencher la mise à jour du contenu des subtrees, puisqu'il fait partie intégrante du dépôt global, comme tout autre fichier contenu dans ce dépôt. Ils n'ont donc pas non plus besoin d'avoir accès aux dépôts secondaires. Un avantage de ce fonctionnement est que si l'un des dépôts secondaires est supprimé ou déplacé, il n'y a rien à modifier dans le dépôt global. Cela permet aussi de s'assurer que le contenu du dépôt secondaire sera toujours accessible tant que le dépôt global existe.

Pour mettre à jour le contenu du subtree par rapport au dépôt d'origine, il est par contre nécessaire de connaître son emplacement, et de préciser quelle branche utiliser. La mise à jour se fait à l'aide de l'une de ces commandes :

$ # Mise à jour depuis un dépôt distant par URL
$ git subtree pull --prefix=<local-path> <repo-url> <branch-name>
$ # Mise à jour depuis un dépôt distant connu du dépôt courant
$ git subtree pull --prefix=<local-path> <remote-name> <branch-name>
$ # Mise à jour depuis un commit déjà présent dans le dépôt courant
$ git subtree merge --prefix=<local-path> <commit>

Note : Si le subtree a été ajouté en utilisant le paramètre --squash, il ne sera possible de le mettre à jour qu'en utilisant ce paramètre aussi pour les commandes git subtree pull et git subtree merge.

L'utilisation de subtrees n'empêche pas de modifier directement le code, et permet même, contrairement aux submodules, de versionner les modifications spécifiques directement dans le dépôt global, sans devoir créer de nouvelle branche ou dépôt pour les héberger. La mise à jour du contenu du subtree étant un simple merge, les modifications spécifiques sont conservées, et fusionnées aux modifications du dépôt distant comme n'importe quel autre changement. Cela peut donc faire apparaître des conflits, de la même manière que si l'on avait fusionné une branche contenant notre modification avec une branche mise à jour du dépôt distant lui-même.

Il reste aussi possible de pousser les modifications du subtree vers son dépôt d'origine, au moyen de la commande :

$ # Pousser les modifications d'un subtree dans son dépôt d'origine par URL
$ git subtree push --prefix=<local-path> <repo-url> <branch-name>
$ # Pousser les modifications d'un subtree dans son dépôt d'origine connu du dépôt courant
$ git subtree push --prefix=<local-path> <remote-name> <branch-name>

Note : Il est à noter que cela génère un nouvel objet commit, puisque le commit parent et le tree utilisé ne sont pas les mêmes. Le commit présent dans le dépôt global et le commit poussé seront donc différents, ce qui peut provoquer des conflits lors d'une prochaine mise à jour du contenu du subtree. Je conseille donc d'effectuer un rebase après l'utilisation de git subtree push, lorsque c'est possible, pour éviter tout risque de conflit et d'historique incohérent.

Note : La commande git subtree est basée sur le mécanisme de merge en utilisant la stratégie subtree. Cette stratégie génère des commits dont les parents référencent des objets tree n'ayant pas la même racine, ce qui peut poser problème dans certains cas bien précis. La solution à l'un de ces cas problématiques est expliquée dans l'article sur la commande git rebase.

Extraction d'un répertoire dans dépôt séparé

La commande git subtree permet aussi de faire l'inverse du fonctionnement décrit dans le paragraphe précédent : Générer un nouveau dépôt en partant d'un répertoire contenu dans un dépôt git existant.

Par exemple, si une partie d'un logiciel doit être extrait pour former une bibliothèque autonome, il est possible de créer un nouveau dépôt contenant uniquement le répertoire correspondant. Dans ce nouveau dépôt, le contenu sera bien entendu placé à la racine, et les commits seront filtrés automatiquement, pour ne faire référence qu'aux fichiers présents dans le nouveau dépôt généré.

$ # Génération d'une branche contenant le subtree
$ git subtree split --prefix=<local-path> -b <branch-name>
$ # Envoi de cette branche dans un dépôt séparé par URL
$ git push <repo-url> <branch-name>:<remote-branch-name>
$ # Envoi de cette branche dans un dépôt séparé connu du dépôt courant
$ git push <remote-name> <branch-name>:<remote-branch-name>