Monorepo Git

De Wiki Seb35
Aller à la navigation Aller à la recherche

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 »
    1. Ajouter les sous-modules avec `git submodule add|init|update`
    2. 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 :
    1. En cas d’accès distant, il faut rendre les repos enfants accessibles
    2. 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"
    3. 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"
    4. La compression est donc dégradée par rapport à un object store global
    5. 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

Monorepo

Méthode :

  • Faire un repo Git « parent »
  • Ajouter les repos Git « enfants » avec des `git clone` ou `git submodule add|init` dans des dossiers du repo parent
  • Faire un `git add .` dans le repo parent
  • Retirer les dossiers <enfant>/.git et <parent>/.git/modules
  • Faire des `git commit` dans le parent

Avantages :

  • L’ensemble des fichiers est toujours cohérent (pas d’état intermédiaire comparé aux sous-modules, par exemple lorsqu’on change de branche sans faire un `git submodule update`)
  • Un `git clone` télécharge l’ensemble des fichiers

Inconvénients :

  • Les historiques d’origine des enfants sont perdus
    Les liens avec les repos enfants étant perdus, ces dossiers vont évoluer de façon différente du repo "officiel", créant de facto des forks

Monorepo avec des repos enfants via des espaces de noms et remote-ext

Méthode :

  • dans les repos enfants, créer des remote de type 'ext' pointant vers des namespaces refs/namespaces/gitmodules/refs/<enfant> dans le repo parent, faire un `git fetch` [à détailler]
  • dans le repo parent, ajouter des .git/objects/info/alternates pour qu’il n’y ait pas de transferts physiques entre les objectstore enfant et l’objectstore parent
  • ajouter de la glue pour que quand on commit dans un repo enfant, ça commit aussi le monorepo (possible?)

Inconvénients :

  • Les remote de type 'ext' ne sont pas autorisés par défaut (voir git-remote-ext(1) et git-config(1) protocol.allow)

Multi-repo stocké dans un monorepo

git init
git remote add <url> <dossier>
git branch <sousrepo1> …
git submodule add -b <sousrepo1> ./ <sousdossier1>
git commit

Cela crée un sous-repo via un sous-module stocké dans une branche du monorepo, ce qui est une solution quasi-parfaite (dans ses grandes lignes) à mon sens :

  • tout est stocké et téléchargé via le monorepo
  • chaque sous-repo a un historique autonome (lié au repo parent par le numéro de commit comme classiquement avec les sous-modules)

Pour aller légèrement plus loin, j’aurais aimé qu’on puisse appeler, à la place d’une branche du repo parent, une référence du repo parent, mais ça ne semble pas possible [1] (cela éviterait que la commande classique "git branch" n’affiche par défaut les "sous-repo").