Monorepo Git
Cette page est une étude pour trouver la meilleure solution de faire un monorepo Git, ou plus exactement, idéalement, un repo Git qui serait à la fois :
- un monorepo (qu’on puisse télécharger en une seule fois)
- un multi-repo où certains dossiers serait des repo Git ayant leur historique autonome.
Dans les contraintes imposées, il faut :
- que les solutions potentielles soient possibles en l’état actuel de Git ou, a maximum, un programme Bourne shell d’une taille relativement faible (afin d’être compatible sur un maximum de plate-formes et qu’il reste lisible et compréhensible),
- il peut y avoir une forme d’équivalence entre deux états "monorepo" et "multirepo" évenutuellement disticts mais switchable avec une commande,
- les commandes standard Git devraient fonctionner, au moins dans un certain état "monorepo"/"multirepo" s’ils sont distincts,
- la performance doit rester acceptable (je sais, c’est vague et ça dépend des situations).
Cas d’usage
Les cas d’usage que je prévoie sont :
- pour Archéo Lex : pouvoir aggréger l’ensemble des lois françaises dans un seul monorepo (au niveau stockage) mais en ayant la possibilité de télécharger seulement des sous-ensembles, par exemples les décrets,
- pour MediaWiki : pouvoir avoir le logiciel de base (un repo) avec les extensions (chacun dans un repo) où le déploiement est un monorepo comportant la version de base + les extensions déployées dans une certaine version, éventuellement patchée.
Briques de base
Sous-modules Git (voir gitsubmodules(7))
alternates (voir gitrepository-layout(5))
Espaces de noms Git (voir gitnamespaces(7))
Dépôt distant de type ext (voir git-remote-ext(1))
Ilôts delta (delta islands) (voir git-pack-objects(1))
Les options de configuration, par exemple "diff.submodule"
Briques de solution
Plusieurs repos indépendants
Ceci n’est pas une solution au problème exposé car il n’y a jamais de monorepo, au sens que la version globale et unifiée d’un repo et de ses sous-repos n’est jamais présente. C’est toutefois une partie d’une éventuelle solution, intégrée à une autre brique de solution.
Méthode :
- Faire un repo Git « parent »
- Ajouter les repos Git « enfants » avec des `git clone` dans des dossiers du repo parent
- Faire des `git commit` dans chacun des enfants et du parent
…
Sous-modules classiques
Méthode :
- Faire un repo Git « parent »
- Ajouter les repos Git « enfants »
- Ajouter les sous-modules avec `git submodule add|init|update`
- Ajouter un repo Git normal dans un dossier, puis, dans le repo parent, faire un git-add(1) (il y a un avertissement, c’est normal), puis git-submodule(1) (absorbgitdirs) sur ce dossier pour transférer le dossier <enfant>/.git dans <parent>/.git/modules/<enfant>
- `git commit` dans le repo parent pour enregistrer la version
- `git submodule deinit` pour retirer le sous-module
Avantages :
- Très standard depuis longtemps, au moins pour l’organisation et les commandes de base
- Gestion de la récursion (enfants d’enfants)
- En local, tout l’historique est dans le dossier <parent>/.git
Inconvénients :
- Les repo enfants ne font pas partie du repo parent :
- En cas d’accès distant, il faut rendre les repos enfants accessibles
- En cas de clone du repo parent, seul le repo parent est téléchargé par défaut
- Cela peut être changé au cas par cas avec `git clone --recurse-submodules` ou configuré de façon permanente avec "submodule.recurse", "fetch.recurseSubmodules" et "submodule.<name>.fetchRecurseSubmodules"
- Lors d’un push, il faut pousser aussi les repos enfants
- Cela peut être changé au cas par cas avec `git push --recurse-submodules=check|on-demand|no` configuré de façon permanente avec "submodule.recurse" et "push.recurseSubmodules"
- La compression est donc dégradée par rapport à un object store global
- La perfomance lors d’un clone est dégradée par rapport à un clone global
- En cas de changement de branche (pour un repo ayant un work tree) :
- il y a des avertissements si les branches source et destination ont des repos enfants ayant des statuts "enregistré dans un commit"/"non-enregistré dans un commit" différents (dans le 2e cas, ça peut être un dossier "non suivi" ou "ignoré")
- il faut synchroniser a posteriori les repos enfants : si on oublie, on est alors dans un état intermédiaire entre deux commits
- L’algorithme de fusion ne fusionne pas à l’intérieur des sous-modules
- Il faut fusionner chacun des sous-modules puis, lors de la fusion du repo parent, résoudre le conflit de fusion en préférant les commits fusionnés des repos enfants
- Il n’y a pas d’ordre canonique dans le fichier .gitmodules
- Les commandes `git submodule add` ajoutent à la fin du fichier les nouveaux sous-modules
- En cas de fusion, même si deux fichiers .gitmodules sont sémantiquement identiques (mêmes sous-modules, mêmes paramètres pour chaque sous-module), il peut y avoir un conflit de fusion sur ce fichier
- Lors du checkout, la référence HEAD des sous-modules pointe vers un commit et non vers la branche indiquée dans le fichier .gitmodules
- Il est moins aisé de mettre à jour en masse les sous-modules (par exemple dans MediaWiki, dans le principal dépôt d’extensions (Gerrit), les extensions MediaWiki sont dans des branches correspondant à la version majeure de MediaWiki (par exemple REL1_35 pour la branche majeure 1.35) : par défaut il n’est pas possible de faire un `git submodule foreach git fetch` ou `git iterate -- fetch` (programme complémentaire git-iterate)
- Le support des sous-modules évolue (en s’améliorant amha) mais certaines commandes ou options de configuration n’étaient pas disponibles à l’origine
- Cela amoindrit la facilité d’utilisation pour les anciennes versions de Git