Git : Placez le rebase au centre de votre stratégie de versioning
Wait… what ?
Proposer une méthodo Git en 2020, ça sent le réchauffé, et pourtant nous ne sommes pas tous égaux face à la gestion du versioning de nos projets.
Si on y réfléchit bien, ce que l’on fait le plus au quotidien en tant que développeur web ce n’est pas d’écrire de l’HTML / CSS, ni du code dans son langage préféré, mais bel et bien des actions Git afin de gérer du code source et éviter les conflits.
Git est un outil stratégique qui est au centre des développements et s’il est mal utilisé, il peut occasionner des problèmes importants et ralentir le flux des développements. En revanche, s’il est bien utilisé et bien maîtrisé par chaque personne de l’équipe, il s’avère d’une redoutable efficacité.
La méthodologie Git présentée ci-dessous privilégie le rebase.
Je l’ai mise en place chez l’un de mes clients au sein d’une équipe d’une vingtaine de développeurs qui travaille sur plusieurs dizaines de dépôts Git.
Elle a été testée et ajustée en fonction des retours sur plusieurs semaines, j’espère qu’elle pourra vous servir également.
Elle mentionne l’utilisation de Gitlab, mais tout est faisable sur Github de la même manière.
Il est très important que les développeurs aient un outil pour voir à tout moment l’arborescence Git afin de comprendre les conséquences de leurs actions dans l’arbre des commits. Cette méthodo parle de Gitkraken, mais libre à vous de choisir le client Git qui vous convient le mieux.
Les objectifs de cette méthodo sont :
- Produire une arborescence Git claire et simple, quel que soit le projet.
- Eviter les sessions de merges impossibles.
- Eviter de devoir faire du nettoyage.
- Gagner du temps.
- Améliorer la qualité des revues de code.
- Améliorer le code.
- Eviter les régressions.
Master ? Develop ? Quelle branche par défaut ?
Ce guide part du principe qu’un dépôt bien configuré contient :
- Une branche master : c’est la version la plus proche de la version de production de l’application.
- Une branche develop : c’est la dernière version stable des développements en cours. Cette branche est définie comme branche par défaut au niveau des paramètres du dépôt sur Gitlab.
Le principe est ensuite le suivant :
- Les différentes branches de features et fixes sont créées à partir de la branche develop et mergées dans cette branche une fois le développement terminé.
- A chaque nouvelle version de l’application un tag est posé sur le dernier commit correspondant, en respectant la syntaxe semver (format de numéro de version : MAJOR.MINOR.PATCH).
- Lors de la livraison d’une nouvelle version de production, la branche master est déplacée sur le commit correspondant à la version de production (utilisation de la commande “git reset”). Sur ce même commit, un tag indiquant la version est normalement déjà présent (cf point précédent).
Selon ces principes, la suite de ce guide prendra comme branche de référence la branche develop.
Pour les projets qui nécessitent de maintenir plusieurs versions de production en parallèle, il est nécessaire de créer des branches develop et master par version de production.
Exemple : develop-v1, develop-v2, master-v1, etc…
Prendre les bons réflexes
Avant d’entrer dans le détail des conventions sur les branches, les commits et les merges, voici quelques règles à suivre.
A ne pas faire
NE PAS merger develop dans sa branche.
C’est une mauvaise pratique.
La bonne pratique est de faire un rebase régulier de sa branche sur develop pour récupérer les dernières modifications de l’équipe et diminuer très fortement le risque de régressions et de conflits.
En travaillant à plusieurs sur un dépôt, si chacun fait un merge de develop dans sa branche, l’arbre Git devient très vite illisible et le risque d’erreur augmente fortement.
A faire
Utiliser “git pull” avec l’option “rebase”
Ce n’est pas grand chose et cela permet de garder l’arborescence Git lisible.
Si vous utilisez Gitkraken, cliquez sur la petite flèche du bouton “Pull” pour sélectionner “Pull (rebase)”.
Pour le faire en ligne de commande :
Toujours rebaser la branche à merger sur la branche cible avant de merger via Gitlab.
Lorsqu’une merge request a été approuvée sur Gitlab et qu’il est temps de merger : avant de cliquer sur le bouton “merge”, bien penser à rebaser la branche à merger sur la branche cible (celle sur laquelle le merge va être fait) pour récupérer les nouveaux commits qui ont pu être fait pendant que la merge request était active.
Voici les actions à réaliser dans l’ordre :
- Avec Git ou Gitkraken :
- Fetch origin
- Rebase de la branche à merger sur la branche cible
Pour plus de détails sur comment réaliser le rebase, consultez la rubrique “Mise en pratique du rebase” ci-dessous.
- Push (force si nécessaire) - Sur Gitlab :
- Cliquer sur le bouton “merge” de la merge request - Avec Git ou Gitkraken :
- Fetch origin
- Supprimer la branche distante si Gitlab ne l’a pas déjà fait
Supprimer la branche distante
Bien penser à supprimer la branche distante une fois que la branche locale a été mergée. Cela permet d’éviter de prévoir une session de nettoyage ultérieurement.
Différence entre une bonne et une mauvaise arborescence Git
Moins subtile que la différence entre le bon et le mauvais chasseur, la différence entre une bonne et une mauvaise arborescence Git tient dans notre capacité à la lire et à la comprendre rapidement.
Exemple :
Dans l’exemple ci-dessus, les deux arborescences correspondent exactement aux mêmes manipulations. A gauche, la méthode du “merge dans sa branche” a été utilisée, à droite celle du rebase.
Au premier coup d’œil, l’arborescence de gauche demande quelques secondes d’attention alors que celle de droite est comprise instantanément.
Lorsque plusieurs développeurs travaillent en parallèle avec Git, l’arborescence de droite restera toujours verticale, alors que celle de gauche se complexifiera d’avantage horizontalement.
Les branches
Créer une branche par bug ou évolution
- Une branche doit être créée pour chaque évolution ou nouvelle fonctionnalité (feature).
Le nom de la branche doit être explicite et contenir un identifiant permettant de la relier à l’application de gestion des bugs et évolutions (Jira, Mantis, etc…).
Exemple :
feat/123456-my-new-feature - Une branche doit être créée pour chaque correctif (fix).
Le nom de la branche doit être explicite et mentionner l’identifiant du bug correspondant.
Exemple :
fix/123456-my-bug-fix - Si une évolution ou un correctif concerne plusieurs dépôts, créer une branche dans chaque dépôt avec le même nom, correspondant à l’évolution ou au correctif.
- Faire un rebase régulier de votre branche sur la branche develop (tous les matins par exemple).
Cela permet :
- de récupérer les dernières modifications effectuées par les autres membres de l’équipe
- de faciliter le futur merge de votre branche dans develop
- de résoudre les conflits éventuels au fur et à mesure du développement
Convention de nommage des branches
Le nom des nouvelles branches créées doit respecter les règles suivantes :
- Commencer par un préfixe précisant le type : “feat/” ou “fix/”
Le “slash” est important. - Contenir l’identifiant de l’application de gestion des bugs et évolutions
- Les mots doivent être écrits en lettres minuscules
- Les mots doivent être séparés par un tiret : “-”
- Idéalement, utiliser l’anglais
Exemples :
feat/123456-my-new-feature
fix/123456-my-bug-fix
Pourquoi mettre un “slash” après “feat” ou “fix” ?
Cela permet d’avoir un nom de branche lisible qui ressemble à une arborescence de répertoire. Certains logiciels comme Gitkraken utilisent cette arborescence pour regrouper les features et les fixes.
Pourquoi des tirets plutôt que des underscores ?
Dans les normes de nommage les plus utilisées (pour les fichiers par exemple), c’est le tiret qui est privilégié. On reproduit donc ces normes dans le nom des branches.
Les commits
Faire des petits commits
Il est optimal de découper son travail en petites unités (chaque unité correspondant à un commit) afin de pouvoir retravailler ses commits librement si besoin, en :
- regroupant les commits via un rebase interactif
Exemple :
git rebase -i HEAD~3 - supprimant un commit sans impacter le fonctionnement de la branche, via le rebase interactif.
- effectuant un cherry pick d’un ou plusieurs commits sur une autre branche
Exemple :
git cherry-pick [id commit] - etc…
Faire beaucoup de petits commits plutôt que quelques gros commits permet également de faciliter la relecture du code et d’éviter des résolutions de conflits complexes.
Chaque commit doit être atomique, c’est à dire contenir l’ensemble des modifications correspondant à une unité.
Si l’unité est par exemple l’ajout d’un champ “date de naissance” dans un formulaire, le commit atomique correspond à la modification dans la vue et le contrôleur (voir également dans le service et/ou le model associé).
Si on se place avant ce commit, le formulaire ne contient pas le champ “date de naissance”, si on se place dessus ou après, le formulaire contient le champ “date de naissance” fonctionnel.
Convention de nommage des commits
A chaque message de commit doit être associé un préfixe :
- “feat” pour une nouvelle fonctionnalité / une évolution
- “fix” pour la correction d’un bug
- “docs” pour la mise à jour d’une documentation
- “style” pour les changements qui n’affectent pas le fonctionnement du code (espaces, formattage, point-virgule manquants, etc…)
- “refactor” pour une modification du code qui n’est ni un correctif, ni une évolution
- “perf” pour une modification de code qui améliore les performances
- “test” pour ajouter ou compléter un test
- “chore” pour les changements du process de build ou d’une librairie / d’un outil tiers
Juste après le préfixe, il est possible de préciser le nom de la fonctionnalité impactée entre parenthèses (non obligatoire).
Puis le séparateur “:” doit apparaître.
Et enfin vous devez entrer un message de commit.
Exemples de messages de commit respectant ces conventions :
feat(profile form): add birthdate field
fix(profile form): fix birthdate validity check
refactor: remove console.log
En paramétrant les modules husky, commitlint et commitizen sur votre dépôt, vous pourrez vérifier ce format de message de commit et bloquer le commit si le format n’est pas validé. Pour en savoir plus, reportez-vous à la section “Bien configurer son projet” à la fin de cet article.
Commiter un WIP
La règle : ne pas conserver dans l’historique Git un Work in Progress (WIP).
L’historique Git final ne doit pas contenir de WIP. Il ne doit être composé que de branches mergées contenant des commits atomiques. Git, utilisé dans la durée, ne sert pas à sauvegarder son travail mais à versionner le code pour pouvoir revenir à différents états d’une application.
Il est possible de faire un push d’un WIP sur sa branche, mais seulement dans le but de sauvegarder temporairement son travail en cours et à condition de revenir en arrière le lendemain, pour remplacer ce WIP par les commits permettant de terminer le correctif ou l’évolution concernée.
Les merge requests
Chaque merge d’une branche dans une autre doit passer par une merge request via Gitlab.
Cela permet aux collègues de faire une revue de code avant merge mais aussi d’avoir une trace des commits sur Gitlab en cas de “push force” malheureux.
Pour créer une merge request, effectuez les actions suivantes :
- Connectez-vous sur Gitlab, sur le dépôt concerné
- Accédez à la rubrique “Merge request” via le menu de navigation gauche
- Créez une merge request de sa branche dans la branche cible
- Sélectionnez un template de merge request si disponible sur le projet.
Sinon remplissez le champ description en expliquant à minima l’objet de la merge request et comment tester la modification. - Renseignez le titre de la merge request au format
“feat: “ ou “fix: “ + ID BUG/EVO + “-” + description générale des changements.
Exemple :
feat: 123456 — Add birthdate field in edit profile form - Sélectionnez des relecteurs
- Cochez la case : “Remove source branch when merge request is accepted.”
- Cliquez sur “Submit merge request”.
Les relecteurs vont recevoir un e-mail les invitant à relire le code et faire des commentaires. L’auteur de la merge request effectue les ajustements suite aux échanges afin qu’elle puisse être validée et mergée.
Pour effectuer le merge via Gitlab :
- Vérifier le titre de la merge request, il doit respecter le format décrit précédemment.
- Vérifier que la case “Remove source branch” est bien cochée.
- Avant de cliquer sur le bouton “Merge” de Gitlab, rebaser la branche à merger sur la branche cible. Si vous avez un doute, rendez-vous à la section “Mise en pratique du rebase” ci-dessous.
- Cliquer sur “Merge” pour dire à Gitlab d’effectuer le merge.
Mise en pratique du rebase
Cette méthodologie Git repose sur la technique du rebase.
Nous allons voir dans cette section comment réaliser un rebase en ligne de commande et avec Gitkraken.
Ici, les branches feat/feature et feat/feature2 ont été créées à partir de develop lorsque celle-ci était sur le commit “add index.js”.
La branche feat/feature a été mergée dans develop.
A présent, il faut faire un rebase de feat/feature2 sur develop pour bénéficier des modifications ajoutées sur cette dernière.
L’action de rebase va appliquer les commits de feat/feature2 sur develop dans l’ordre dans lequel ils ont été commités, puis placer la branche feat/feature2 sur le commit le plus récent.
Rebaser avec Gitkraken
Si vous utilisez Gitkraken, faire un rebase est très simple et se fait de la façon suivante :
- Double cliquez sur la branche feat/feature2 pour effectuer un checkout
- Cliquez avec le bouton droit sur la branche develop et choisir “Rebase feat/feature2 onto develop”
- Si la branche feat/feature2 a une branche distante associée, il est nécessaire de resynchroniser la branche locale feat/feature2 (qui s’est déplacée avec le rebase) avec la branche distante correspondante.
Pour ce faire, il suffit de cliquer sur le bouton “push” de la barre d’action Gitkraken.
Ce dernier vous demandera si vous souhaitez faire un “force push”. Validez après avoir vérifié que cela n’aura pas d’impact sur vos modifications en cours. - Vous avez terminé :)
Rebaser avec git en ligne de commande
En ligne de commande, le rebase s’effectue en 3 commandes :
Comment effectuer un hotfix ?
Un hotfix est un correctif à appliquer sur la dernière version de production car il est bloquant pour le bon fonctionnement de l’application. Pour gérer un hotfix, le plus simple est de :
- Créer une branche “hotfixes-[version]/[ID Bug]-[description]” à partir du commit correspondant à la version de production à fixer.
- Effectuer le ou les commits de hotfix.
- Relivrer l’application à partir de cette branche.
Si nécessaire, il faut faire un cherrypick du/des commit(s) créé(s) sur la brancher develop pour que les développements en cours bénéficient également du hotfix.
A noter
Pourquoi ne pas utiliser Gitflow ?
Après expérimentation, il s’avère que Gitflow n’apporte pas suffisamment de valeur ajoutée pour justifier son utilisation dans cette méthodo.
Tous les avantages de Gitflow sont pris en compte, y compris la gestion des hotfixes expliquée dans la section : “Comment gérer un hotfix ?”
Commande git pour rebaser un groupe de commits d’une branche sur une autre branche
Exemple :
- une branche 1 contient les commits : A, B, C et D
- une branche 2 contient les commits : C’, D’, E et F
Les commits C’ et D’ sont les mêmes commits que C et D, mais ils n’ont pas le même id. On souhaite faire un rebase de la branche 2 sur la branche 1 à partir du commit E, pour que la branche 2 commence sur le commit D et contiennent les commits E et F.
La commande suivante permet de le faire :
git rebase -i — onto [Nom branche 1] [Id commit E]^ [Nom branche 2]
Cette commande lance un rebase interactif de la branche 2 sur la branche 1, à partir du parent du commit E (la présence du “chapeau” après l’identifiant du commit E signifie que l’on veut prendre le parent de ce commit).
Après la commande, on obtient :
- la branche 1 avec les commits : A, B, C, D
- la branche 2 qui démarre au dessus de la branche 1 (sur le commit D), donc qui contient les commits : A, B, C, D, E et F
Bien configurer son poste de travail
Régler sa configuration Git en local, sur Gitlab et sur Gitkraken permet de faire apparaître correctement votre Prénom et votre Nom dans l’historique Git.
Configuration locale : .gitconfig
S’assurer de la bonne configuration de Git sur son poste de travail en exécutant ces 2 commandes :
Configuration Gitlab
Se connecter à Gitlab et cliquer sur votre profil en haut à droite de la page, puis choisir la rubrique :
Settings > Main settings
Mettre votre Prénom et votre Nom dans le champ “Full name”.
Configuration Gitkraken
Editer votre profil Gitkaken et remplir votre Prénom et votre Nom dans les champs “Profile Name” et “Name”.
Bien configurer son projet
Ajouter un linter de commits
Installer un linter de commits permet d’aider les développeurs à bien respecter les conventions précisées au paragraphe “Convention de nommage des commits”.
L’exemple ci-dessous utilise des modules Node.js, mais il existe certainement des équivalents si vous utilisez une autre stack.
Pour le mettre en place, il faut installer les modules : commitlint, commitizen, cz-conventional-changelog et husky.
Dans le répertoire du projet, exécutez les commandes suivantes :
Dans le fichier package.json, ajoutez la configuration :
A noter que si vous souhaitez exécuter un linter de code sur le hook “pre-commit”, ajoutez la ligne suivante dans le noeud husky > hooks :
“pre-commit”: “npm run lint”
La commande “npm run lint” fait référence à un script défini dans le noeud “scripts” du fichier package.json, mais vous pouvez spéficier une autre commande qui n’est pas un script (par exemple : “gradlew lint”), du moment qu’elle est valide par rapport à votre environnement de travail.
Enfin, créez un fichier “commitlint.config.js” à la racine de votre projet.
Il doit contenir :
Lors de vos prochains commits, le hook Git se mettra en place pour vérifier votre message de commit.
Conclusion
J’espère que toutes ces propositions et exemples vous auront inspirés.
N’hésitez pas à réagir sur cet article si vous avez des suggestions ou des retours d’expérience intéressants.
La stratégie de versioning sur un projet est similaire à son code : c’est une bonne chose de la challenger et de l’améliorer au fil du temps en fonction des nouvelles problématiques rencontrées.
Elle ne doit pas être une contrainte mais un outil pour améliorer le quotidien des développeurs.
Et maintenant, à vous de jouer :).