Différences entre les versions de « Monorepo Git »
(++pour les git submodules) |
|||
(2 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 31 : | Ligne 31 : | ||
== Briques de solution == | == Briques de solution == | ||
== Plusieurs repos indépendants == | === 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. | 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. | ||
Ligne 78 : | Ligne 78 : | ||
* Le support des sous-modules évolue (en s’améliorant amha) mais certaines commandes ou options de configuration n’étaient pas disponibles à l’origine | * 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 | *: 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 === | |||
<syntaxhighlight lang="sh"> | |||
git init | |||
git remote add <url> <dossier> | |||
git branch <sousrepo1> … | |||
git submodule add -b <sousrepo1> ./ <sousdossier1> | |||
git commit | |||
</syntaxhighlight> | |||
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 [https://git-scm.com/docs/git-clone/fr] (cela éviterait que la commande classique "git branch" n’affiche par défaut les "sous-repo"). |
Version actuelle datée du 31 juillet 2021 à 22:46
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
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").