Révisions GIT: épisode 5

J’ai terminé l’article précédent avec quelques questionnements sur la méthodologie. En effet, si gérer son code reste relativement simple pour un développeur isolé, les choses se compliquent un peu dans le cadre d’un travail en équipe. Il s’agit, d’une part, de ne pas perdre de temps dans la gestion des fusions (git merge), et d’autre part, de ne pas perdre de code, ou de ne pas pousser en production du code buggé. Afin d’éviter les multiples écueils possibles, il existe plusieurs méthodes de travail avec GIT, que l’on appelle flux de travail (workflow). Je vais vous présenter quelques-unes de ces méthodes.

Révisions GIT: épisode 5

Pourquoi un workflow ?

Lorsque nous commençons à utiliser GIT, nous avons tendance à nous concentrer sur les commandes, et leurs conséquences. Cependant, maîtriser les commandes ne suffit pas à éliminer intégralement toutes les erreurs, surtout lorsque nous travaillons à plusieurs sur le même code. Il faut donc une méthodologie, dont les finalités principales sont (pêle-mêle)

  1. Optimiser le travail de chaque développeur,
  2. Favoriser les développements en parallèle,
  3. Permettre, et faciliter le travail en équipe,
  4. Eviter les conflits lors des merges (merge hell),
  5. Produire un historique de développement clair, facilement lisible par vos collègues (et par vous-même),
  6. Rapprocher le développement et la gestion de projet.

Un workflow doit également faciliter le cycle de vie du développement, et du déploiement des applications:

  • Faciliter les revues de code,
  • Permettre l’enchaînement des développements, des tests, et des mises en production,
  • Le tout de façon complètement automatisée.

Figure 1: Cycle de développement standard
Figure 1: Cycle de développement standard

Préambule

Avant de démarrer la description de nos workflows, je vais aborder quelques concepts qui nous serviront un peu plus tard pour comprendre les subtilités de certaines méthodes.

Etiquettage / les tags

GIT offre la possibilité d’attacher des étiquettes à un commit. Cela permet, entre autre, de marquer des états de publication. Commençons par le plus simple: lister des étiquettes

$ git tag           # donne la liste des toutes les étiquettes
1.0.0
1.1.0
1.2.0
1.3.0
$
$ git tag "v1.2*"   # donne la liste de toutes les étiquettes commençant par "v1.2"
v1.2.1
v1.2.2
v1.2.2-rc1
v1.2.2-rc2
v1.2.3

Il existe deux types d’étiquettes: les étiquettes légères (lightweight), et les étiquettes annotées (annotated).

  • Une étiquette légère n’est qu’un pointeur vers un état du code (un commit),
  • Une étiquette annotée contient un certain nombre d’informations comme le nom de l’auteur, son adresse mail, une date, un message, … En outre, ces étiquettes peuvent être « signées », et vérifiées avec GPG (GNU Privacy Guard).

Pour créer une étiquette légère, il suffit de spécifier l’étiquette

$ git tag <étiquette>
$ git tag "v1.1"

La création d’une étiquette annotée se fait de la façon suivante:

$ git tag -a <étiquette> [-m <message>] [commit]
$ git tag -a "v1.0" -m 'First release'
$ git tag -a "v1.0" -m 'First release' 0b7434d

Nous pouvons afficher une étiquette annotée avec la commande git show

$ git show v1.1

tag v1.1
Tagger: Emmanuel Georjon <mail@emmanuelgeorjon.com>
Date:   Mon Dec 13 10:32:16 2021 

version 1.1
commit 0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc
Author: Emmanuel Georjon <mail@emmanuelgeorjon.com>
Date:   Sat Dec 11 09:15:22 2021 

    Update Bootstrap to 5.1.3
...

Par défaut, la commande git push ne propage pas les étiquettes d’un dépôt local vers un dépôt central. Il faut donc le faire explicitement:

$ git push origin <étiquette>
$ git push origin "v1.1.0"

Si vous avez plusieurs tags à partager, la méthode la plus simple est d’utiliser

$ git push origin --tags

Les versions

La notion de release (version) n’existe pas dans GIT, mais j’en parle ici rapidement, car elle est parfois confondue avec la notion d’étiquette. Une version est une façon de rendre votre développement disponible auprès des utilisateurs, sous une forme « packagée »

Sans cette notion de release, un utilisateur souhaitant utiliser vos développements, devra télécharger le code. Avec la notion de release, l’utilisateur peut

  • Télécharger le code,
  • Télécharger des fichiers contenant une version “compilée” de votre application (quelque chose de prêt à l’emploi),
  • Accèder à une description de la version (release note).

Les versions s’appuient sur les étiquettes pour désigner le code concerné.

Figure 2: Liens entre version, étiquettes et code
Figure 2: Liens entre version, étiquettes et code

La création d’une version s’effectue facilement, via l’interface de GITHUB.

Etape 1: dans l’écran principal de votre dépôt, cliquer sur Release

Figure 3: Ecran principal et bouton release
Figure 3: Ecran principal et bouton release

Etape 2: Github affiche alors la liste des versions disponibles dans le dépôt.

Figure 4: Liste des versions
Figure 4: Liste des versions

Etape 3: Cliquer sur Draft a new release

Figure 5: Démarrer la création d’une version
Figure 5: Démarrer la création d’une version

Etape 4: Choisir un tag, ou en créer un nouveau

Figure 6: Choisir un tag existant
Figure 6: Choisir un tag existant

Figure 7: ou créer un nouveau tag
Figure 7: ou créer un nouveau tag

Etape 5: Compléter les informations requises

  1. Le nom de la release,
  2. Une description (la release note),
  3. Ajouter des binaires si besoin (compilation de votre code, par exemple)
  4. Si l’utilisateur doit considérer cette version comme une version de production ou non,

Figure 8: Compléter les informations de la release
Figure 8: Compléter les informations de la release

Etape 6: Publier cette release ou la sauvegarder en mode brouillon (point de la figure 8)

Les pull requests

Comme pour les releases, les pull requests ne sont pas un concept GIT à proprement parler. Cette fonction a été popularisée par GitHub, puis adoptée par d’autres outils, comme GitLab (merge-request).

Un pull request est un outil permettant à des développeurs (appelés contributeurs), d’apporter des modifications à un projet sous le contrôle d’un groupe de personnes chargées de gérer le projet (les mainteneurs).

Un pull request n’est pas une simple commande équivalente à un git merge. C’est un ensemble d’outils permettant

  • de faire une demande d’intégration de certaines modifications dans une branche spécifique,
  • de discuter de ces modifications, de gérer les conflits possibles,
  • de faire “valider” cette demande,
  • et, finalement, de réaliser le merge.

Dans l’image suivante (figure 9):

  1. La demande du développeur, avec les explications, le lien vers le code (le commit),
  2. Les vérifications automatiques de Github, avec le résultat (ici pas de conflit),
  3. La zone de dialogue entre les validateurs, et les développeurs.

Figure 9: Exemple de Pull Request
Figure 9: Exemple de Pull Request

Nomenclature

Dans tout le reste de l’article, j’utiliserai la nomenclature, et le code couleur suivant:

Figure 10: Nomenclature
Figure 10: Nomenclature

Gitflow

GitFlow est un méthode créée par Vincent Driessen en 2010. Même si elle ne semble plus être dans l’air du temps, cette méthode est toujours très largement utilisée aujourd’hui, avec de multiples variantes.

Principe

Gitflow fait un usage intensif des branches, en suivant les étapes de développement d’une application.

Pour expliquer la méthode, je vais reprendre le plan de l’article de Vincent Driessen (en anglais) que je vous invite à consulter.

La méthode s’appuie sur deux branches principales (figure 11):

  • La branche principale (main ou master) pointe sur le code tel qu’il est en production,
  • La branche développement (develop) pointe sur les dernières évolutions du code, qui seront utilisées pour la construction des prochaines versions. Cette branche peut être également appelée branche d’intégration. C’est à partir de cette branche que sont effectuées les contructions / compilations quotidiennes (nightly build).

La fusion de la branche développement vers la branche principale correspond par définition à une nouvelle version (new release).

Figure 11: Les branches principale et développement
Figure 11: Les branches principale et développement

En plus des branches développement et principale, la méthode propose trois autres types de branches

  • Les branches fonctionnalités (features),
  • Les branches versions (releases),
  • Les branches correction (hotfix).

Les branches fonctionnalités (features) sont dédiées au développement d’une ou plusieurs fonctionnalités, et sont amenées à être fusionnées dans la branche développement. L’article indique que ces branches peuvent rester au niveau des dépôts locaux, et ne sont pas poussées dans le dépôt central (Feature branches typically exist in developer repos only, not in origin).

L’option --no-ff est recommandée lors du git merge pour garder un historique de l’opération.

Figure 12: Les branches fonctionnalités
Figure 12: Les branches fonctionnalités

Si une fonctionnalité est développée par plusieurs intervenants, les branches correspondantes doivent être partagées (comme pour la figure 13). L’intérêt de cette variante est de gérer la fusion de ces branches feature, à travers un pull request.

Figure 13: Les branches fonctionnalités, partagées
Figure 13: Les branches fonctionnalités, partagées

Les branches versions (releases) sont utilisées pour la préparation d’une nouvelle version de production. Ces branches

  • sont créées à partir de la branche développement (develop), lorsque cette branche reflète ce que l’on veut mettre en production,
  • ne doivent servir qu’à réaliser de petits ajustements, avant la mise en production.

La création d’une branche version correspond

  • A l’attribution d’un numéro de version (tagging),
  • A l’arrêt des développements pour cette version (sauf ajustements mentionnés précédemment),
  • Au début des développements de la version suivante (dans la branche développement).

Figure 14: Les branches version
Figure 14: Les branches version

Les branches correction (hotfix) sont utilisées pour intervenir rapidement sur une version de production pour corriger un bug urgent. Cette branche est créée à partir de la branche principale. La création d’une branche spécifique à la gestion des bugs urgents permet

  • de démarrer rapidement la correction à partir du code déployé en production,
  • de ne pas interférer sur les développements en cours ( branche développement).

Figure 15: Les branches hotfix
Figure 15: Les branches hotfix

Une fois les corrections prêtes, elles doivent être ré-intégrées dans

  • la branche principale pour corriger la version actuellement en production,
  • la branche développement pour inclure la correction de bugs dans le développement en cours,
  • ET, dans la ou les branches versions si elles existent au moment de la correction.

Le flow global

Dans sa globalité, la méthode se présente sous la forme suivante (figure 16):

Figure 16: Gitflow avec l’ensemble des branches
Figure 16: Gitflow avec l’ensemble des branches

Motivation

La méthode Gitflow multiplie les branches, afin d’isoler chacun des flux de travail

  • Le travail de correction ne se fait pas dans la branche de développement,
  • Le travail de mise en production s’effectue dans des branches spécifiques (release), et pas dans la branche develop, ni dans la branche main.

L’avantage est que chacun de ces flux peut se faire en parallèle: des équipes peuvent travailler sur de nouvelles fonctionnalités, sans interférer avec celles qui travaillent à la mise en production de la prochaine version.

La méthode présente deux inconvénients potentiels:

  • La complexité liée à la gestion des branches, qui peut générer un historique de commits difficilement lisible,
  • Les problèmes de merge: plus les développements « silottés » sont longs, plus les fusions sont complexes (merge hell).

Feature branch

Principe

Comme son nom l’indique cette méthode consiste à créer une branche par fonctionnalité à développer. Cette méthode peut-être considérée comme une version simplifiée de la méthode Gitflow. La méthode systématise le partage des branches features, et l’utilisation des pull-request pour gérer la fusion entre ces branches, et la branche principale.

Chaque développeur

  • Clone le projet sur son poste,
  • Crée une branche pour développer une nouvelle fonctionnalité,
  • Effectue des commits sur la branche créee,
  • Envoie ces commits vers le dépôt cental (git push), sur la branche créée précédemment,
  • Crée un pull request en demandant la fusion entre la branche liée à la fonctionnalité, et la branche principale.

Le (ou les) validateur

  • Traite les pull request,
  • Fait intervenir des membres de l’équipe pour des revues de code, ou des tests,
  • Gère les conflits qui peuvent apparaître lors de la fusion des deux branches.

Figure 17: Le flow Feature-branch
Figure 17: Le flow Feature-branch

Motivation

Comme pour GitFlow, le principe général de la méthode Feature Branch est de paralléliser les développements des différentes fonctionnalités pour les intégrer au moment voulu.

Variante

Il existe une variante de cette méthode, utilisant une branche supplémentaire appelée développement, afin de « protéger » la branche principale. Cette variante se rapproche encore plus de la méthode Gitflow.

Figure 18: Le flow Feature-branch avec une branche de développement
Figure 18: Le flow Feature-branch avec une branche de développement

Trunk-Based Development (TBD)

Motivation

La méthode Trunk-Based Development (TDB) part de deux principaux constats:

  • Le travail par branche à tendance à « isoler » les différentes équipes de développement: s’il y a bien une collaboration entre les développeurs d’une même équipe, une communication entre les équipes n’est pas obligatoire / nécessaire. Il n’y a donc pas d’échanges entre les équipes.
  • Effectuer un développement long au sein d’une branche, rend les fusions plus complexes (merge hell).

Principe

Les concepts liés à la méthode TDB sont donc

  • Réduire autant que possible l’utilisation des branches,
  • Réduire la taille des « lots » de développement,
  • Augmenter la fréquence de livraison (l’intégration des nouveautés à la branche principale).

Dans sa version la plus réduite, la méthode est la suivante (figure 19):

Figure 19: Méthode Trunk-Based dans sa version simplifiée
Figure 19: Méthode Trunk-Based dans sa version simplifiée

Chaque développeur

  • Clone le projet sur son poste,
  • Effectue des commits fréquents sur la branche principale,
  • Envoie ces commits vers le dépôt cental (git push), toujours sur la branche principale.

Le gestionnaire de version

  • Est le seul a pouvoir travailler en dehors de la branche principale,
  • Eventuellement créer des versions (release),
  • Génère des branches par version (release branch).

A noter que pour cette version simplifiée, les développeurs poussent directement les modifications sur la branche principale:

  • Il n’y a donc pas de pull-request, donc pas de contrôle de ce que les développeurs intégrent,
  • Chacun des développeurs doit gérer, avec ces collègues, les conflits potentiels, lors des git push.

Derrière les principes généraux décrits un peu plus haut, se cachent donc d’autres principes permettant à la méthode de fonctionner:

  • Collaboration / synchronisation étroite entre développeurs,
  • Il faut des développements courts (pas plus de quelques heures), des commits et des push fréquents,
  • Le code dans la branche principale doit être, à tout moment, être utilisable pour la production: Il faut s’assurer qu’aucun commit ne génère de problème lors des phases de compilation (never break the build, keep the trunk release ready)

Variante

Au quotidien, la méthode décrite précédemment fonctionne parfaitement pour des équipes réduites de quelques personnes. Pour des équipes de développeurs plus importantes, ou si les équipes souhaitent organiser des revues de code, la méthode autorise l’utilisation de branches, similaires aux branches de fonctionnalités (feature branches), déjà évoquées avec les deux méthodes précédentes. Pour respecter l’esprit de la méthode TBD, la durée de vie des branches doit rester courte.

Figure 20: Le flow trunk-based, avec des branches de fonctionnalités
Figure 20: Le flow trunk-based, avec des branches de fonctionnalités

Forking workflow

Cette méthode diffère des autres méthodes citées jusqu’à présent. Pour ces méthodes, nous avons en effet, utiliser une approche basée sur la gestion des branches (voir chapitre 4 de cette série).

  • Nous avons un dépôt central,
  • Les développeurs clonent ce dépôt sur leur poste de travail (dépôt locaux),
  • Ils peuvent créer des commits, et des branches,
  • Qu’ils poussent ensuite sur le dépôt central.

Dans le cas de projets vastes et complexes, le contrôle d’accès aux branches est complexe. Dans certains cas, les développeurs n’ont pas d’accès en écriture au dépôt. Les méthodes précédentes ne sont donc pas applicables.

L’idée est donc de passer par un dépôt intermédiaire:

Figure 21: Méthode standard vs fork
Figure 21: Méthode standard vs fork

Cette méthode, très utilisée dans le domaine des projets opensource, consiste à mettre en place la structure de dépôts, de la façon suivante:

  • Créer un nouveau dépôt (dépôt de fork), copie du dépôt d’origine (dépôt de référence),
  • Cloner ce nouveau dépôt sur son poste de travail,
  • Ajouter au dépôt local, le dépôt de référence comme dépôt distant,

La création du dépôt de fork, peut se faire, par l’intermédiaire de l’interface de Github, ou Gitlab

Figure 22: Créer un dépôt de fork
Figure 22: Créer un dépôt de fork

Figure 23: Créer un dépôt de fork (suite et fin)
Figure 23: Créer un dépôt de fork (suite et fin)

L’ajout du dépôt distant se fait par la commande

$ git remote add upstream http://github.com/ref-org/ref-repo

Le mode d’utilisation de ces dépôts est la suivante:

  • Mises à jour régulières du dépôt local, à partir du dépôt de référence (git fetch / git pull),
  • Création de branches et de commits (en local),
  • Pousser de ces branches du dépôt local, vers le dépôt de fork (git push),
  • Lancer un pull request pour demander l’incorporation des branches dans le dépôt de référence.

Figure 24: Le flux de travail du Forking workflow
Figure 24: Le flux de travail du Forking workflow

Prenons un exemple avec un de mes dépôts:

Figure 25: Une fois le fork effectué, vue du dpôt de fork
Figure 25: Une fois le fork effectué, vue du dpôt de fork

A partir de là, je crée un fichier supplémentaire dans le dépôt de fork (dans la branche main), et j’effectue un commit

Figure 26: Nouveau fichier commité dans le dépôt de fork
Figure 26: Nouveau fichier commité dans le dépôt de fork

Pour demander l’incorporation de ce fichier dans le dépôt de référence, je crée un pull request

Figure 27: Ouverture du pull request
Figure 27: Ouverture du pull request

Le pull request vient d’être ouvert. Il apparaît dans le dépôt de fork

Figure 28: Pull request ouvert, depuis le dépôt de fork
Figure 28: Pull request ouvert, depuis le dépôt de fork

Le pull request apparaît également dans le dépôt de référence

Figure 29: Pull request ouvert, depuis le dépôt de référence
Figure 29: Pull request ouvert, depuis le dépôt de référence

Motivation

Comme je l’ai déjà expliqué, la méthode Forking permet de contribuer à un projet, sans avoir les droits sur le dépôt correspondant. Elle permet un contrôle strict des modifications du code. Cette méthode est surtout utilisée dans le cadre de projet opensource.

Conclusion

La comparaison des différentes méthodes fait l’objet de nombreux débats. Pour ma part, je ne ferai pas de comparaison, parce que je n’ai pas encore assez de recul sur le sujet. Je ferai simplement quelques commentaires.

En mettant à part la méthode Forkflow très spécifique,

  • Le choix d’une méthode ou d’une autre dépend beaucoup du contexte (application/solution à développer, type de projet, nombre et taille des équipes),
  • Quels que soient les arguments des uns et des autres, à la fin, toutes les méthodes utilisent le même outil, et les mêmes séquences: git pull, git add, git commit, git push, pull-request et/ou git merge. Les développeurs font donc face aux mêmes problèmes.
  • Les questions portant sur les fréquences de commits, et sur les difficultés de merge sont assez subjectives et dépendent beaucoup de la discipline des équipes. Avec la méthode TBD (Trunk-Based), rien ne garantie qu’un développeur « pousse » son code suffisamment régulièrement pour éviter les problèmes de merge, et réciproquement, avec la méthode GitFlow, rien n’empêche un développeur de faire des branches courtes, et de délivrer du code à fréquence élevée.

Pour moi, le choix d’une méthode ou d’une autre se base, avant tout, sur des considérations d’organisation:

  • Nombre et taille des équipes: plus le projet est imposant, plus il faut de la coordination,
  • A quel « moment » veut-on gérer les conflits de fusion ?
  • Quel contrôle veut-on mettre en place (pull-request, ou pas, à quel moment)?

Références

Commentaires