Comprendre le processus Git

Traduction libre de votre serviteur de l’article Understanding the Git Workflow par Benjamin Sandofsky

Comprendre le processus Git

Si vous ne comprenez pas ce qui a poussé au design actuel de Git, vous allez souffrir en l’utilisant. Avec toutes ses options, vous arriverez à forcer Git à agir comme vous pensez qu’il doit agir au lieu de le laisser agir comme il le souhaite. C’est comme utiliser un tournevis à la place d’un marteau, vous arriverez au résultat souhaité sauf que cela sera mal fait, ça prendra du temps et ça abîmera le tournevis.

Regardez comment le processus classique de Git peut s’écrouler.

Créer une branche depuis Master, coder, fusionner la branche dans Master quand vous avez fini.

La plupart du temps vous obtiendrez le comportement attendu car Master n’aura pas changé depuis la création de la branche. Plus tard, vous allez fusionner la branche dans Master mais Master n’aura pas divergé. Au lieu de créer un commit de fusion, Git va simplement faire pointer Master sur le dernier commit de la branche (ce qu’on appelle un «fast forward», une avance rapide, cf. ce diagramme).

Malheureusement, votre branche contenait des commits d’étapes, des commits qui permettent de sauvegarder votre progression mais qui fixent un code non stabilisé. Maintenant ces commits sont confondus avec les commits stables de la branche Master. Vous pouvez facilement provoquer une pagaille en revenant en arrière.

Donc vous vous créez une nouvelle règle : «Quand je fusionne une branche, j’utilise l’option –no-ff pour forcer un nouveau commit.». Affaire résolue, on continue.

Et puis un beau jour vous découvrez un bug critique en production et vous avez besoin de revenir en arrière pour trouver quand il a été introduit. Vous utilisez bisect mais vous arrivez toujours sur des commits d’étapes. Vous abandonnez et vous finissez par chercher «à la main».

Vous arrivez à localiser le bug dans un fichier et vous utilisez blame pour voir comment il a évolué dans les dernières 48 heures. Vous savez que c’est impossible pourtant blame vous rapporte que le fichier n’a pas été modifié depuis des semaines. Il se trouve que blame rapporte les changements à la date du commit initial et pas à la fusion. Vos changements ont été fusionnés aujourd’hui mais votre premier commit d’étapes a modifié ce fichier il y a des semaines.

Entre le plâtre no-ff, bisect inutilisable et blame incompréhensible, nous avons tous les symptômes d’un tournevis utilisé comme un marteau.

Repenser le contrôle de révision

Le contrôle des révisions existe pour deux raisons.

La première est d’aider l’acte d’écrire du code. Vous avez besoin de synchroniser des changements avec vos collègues et de régulièrement sauvegarder votre travail. Non, envoyer des zips de vos fichiers ne tient pas la montée en charge.

La seconde est le management de configuration. Cela inclut le management en parallèle de plusieurs axes de développement comme travailler sur la prochaine version majeure tout en appliquant des corrections de bug sur la version existante en production. Le management de configuration est aussi utilisé pour comprendre exactement quand quelque chose a changé, ce qui en fait un outil de débogage qui n’a pas de prix.

Traditionnellement ces deux raisons sont en conflit.

Quand on maquette une fonctionnalité, on fait des commits d’étapes réguliers et souvent ces commits empêchent la compilation. Alors que dans un monde parfait, chaque changement dans votre historique de révision est succinct et stable. Il n’y a pas de bruit créé par des commits d’étapes. Il n’y a pas de commit géant de 10 000 lignes. Un historique propre facilite l’annulation de changement ou l’opération de cherry-pick entre les branches. Un historique propre facilite également l’analyse, l’inspection à posteriori. Cependant, maintenir un historique propre signifie attendre que le code soit parfait avant de l’enregistrer.

Donc quelle approche choisir ? Commit régulier ou historique propre ?

Si vous vous êtes à la veille de lancer votre mini startup, un historique propre ne va pas être un avantage décisif. Vous pouvez commiter à tout va et déployer quand bon vous chante.

Mais en conséquence d’une augmentation du nombre des modifications, que ce soit du à l’élargissement de l’équipe de développement ou à la taille de votre base d’utilisateurs, vous allez avoir besoin d’outils et de techniques pour maintenir les choses sous contrôle. Cela inclut les tests automatiques, les revues de code … et un historique propre.

Les branches de fonctionnalités peuvent sembler une solution équilibrée. Elles résolvent les problèmes de base du développement parallèle. Vous pensez à l’intégration au moment le moins opportun, quand vous écrivez du code, mais ça vous rattrapera. Quand le projet aura assez grossi, le simple processus branche/commit/fusion va s’effondrer. Finis les rafistolages, vous aurez besoin d’un historique de révision propre.

Git est révolutionnaire car il vous donne le meilleur des deux mondes. Vous pouvez maquetter une solution avec des commits réguliers mais également délivrer un historique propre lorsque vous avez fini. Et si c’est bien votre but, les choix par défaut de Git prennent alors tous leurs sens.

Le processus

Penser aux branches en les divisant en deux catégories : publique et privée.

Les branches publiques représentent l’autorité historique du projet. Dans une branche publique, chaque commit doit être succinct, atomique et un message de commit correctement rédigé. Une branche publique doit être la plus linéaire possible et non modifiable. Master et les branches de releases sont des branches publiques.

Une branche privée est personnelle. C’est votre brouillon quand vous travaillez sur un problème.

Il est plus sûr de garder locale une branche privée. Si vous avez besoin de la faire remonter sur dépôt, pour synchroniser vos ordinateurs professionnel et personnel par exemple, prévenez vos collègues que cette branche est privée et qu’ils n’ont pas besoin de travailler dessus.

Vous ne devriez jamais fusionner une branche privée directement dans une branche publique avec un merge de base. Vous devez d’abord nettoyer votre branche avec des outils comme reset, rebase, squash merges, et l’amendement de commit.

Mettez vous dans la peau d’un écrivain et prenez chaque commit comme le chapitre d’un livre. Les écrivains ne publient pas leurs premiers brouillons, Michael Crichton a dit «Les grands livres ne sont pas écris, ils sont réécris.».

Si vous venez d’autres systèmes, modifier l’historique peut vous sembler tabou. Vous avez été conditionnés pour considérer que les commits sont écris dans le marbre mais avec cette logique nous devrions désactiver la fonction «Annuler» dans nos éditeurs de textes.

Les gens pragmatiques prennent soin des changements jusqu’à ce que les changements ne soient que du bruit. Pour la gestion des configurations, nous prenons soin des changements importants. Les commits d’étapes ne sont que des brouillons sauvegardés dans le nuage.

Si vous traitez votre historique public comme une lady, les fusions avec avances rapides sont non seulement plus sûres mais nécessaires. Elles conservent l’historique linéaire et sont simples à suivre.

Le seul argument restant pour –no-ff c’est «la documentation». On peut utiliser les commits de fusion pour représenter la dernière version déployée du code en production. C’est un antipattern, utilisez les tags.

Lignes directrices et exemples

J’utilise trois approches de base en fonction de la taille de mes changements, du temps consacré à ce travail et de l’état de divergence de la branche

Travail à courte durée de vie

Dans la majorité des cas, mon nettoyage consiste seulement à une fusion avec squash.

Imaginons que je crée une branche de fonctionnalité et une série de commits d’étapes pendant la prochaine heure :

<code>git checkout -b branche_privee_fonctionnalite
touch file1.txt
git add file1.txt
git commit -am "WIP"</code>

Quand j’ai fini, au lieu d’un merge de base, j’utilise :

<code>git checkout master
git merge --squash branche_privee_fonctionnalite
git commit -v </code>

Et je prends une minute pour écrire un message de commit détaillé.

Travail plus conséquent

Parfois une fonctionnalité s’étale sur un projet de plusieurs jours avec des douzaines de petits commits.

Si je décide que ma modification doit être séparée en plusieurs modifications plus petites, squash est un peu trop bourrin (comme métrique, je me demande toujours «Est ce que ce code serait facile à faire valider ?»)

Si mes commits d’étapes suivent une progression logique, j’utilise le mode interactif de rebase. C’est très puissant, vous pouvez l’utiliser pour éditer un vieux commit, les séparés, changer l’ordre et dans ce cas utiliser squash sur quelques uns.

Sur ma branche de fonctionnalité :

<code>git rebase --interactive master</code>

Cela ouvre un éditeur avec une liste des commits. Sur chaque ligne, on retrouve l’opération à effectuer, le SHA1 du commit et le message de commit actuel. Une légende de la liste des commandes possibles est affichée.

Par défaut, chaque commit utilise «pick» qui ne modifie pas le commit.

<code>pick ccd6e62 Travail sur le bouton retour
pick 1c83feb Résolutions de bogues
pick f9d0c33 Début du travail sur la barre d'outils</code>

Je change l’opération en «squash» ce qui va fusionner le deuxième commit dans le premier.

<code>pick ccd6e62 Travail sur le bouton retour
squash 1c83feb Résolutions de bogues
pick f9d0c33 Début du travail sur la barre d'outils</code>

Quand je sauvegarde et ferme l’éditeur, un nouvel éditeur va s’afficher pour écrire le message de commit des commits fusionnés.
Et c’est fini.

Travail sur une branche en faillite

Peut être que ma branche de fonctionnalité existe depuis très longtemps et que j’ai du fusionner plusieurs branches dans ma branche de fonctionnalité pour la garder à jour tout en travaillant dessus. L’historique est en vrille. Le plus simple est de récupérer les différences brutes et de créer une nouvelle branche.

<code>git checkout master
git checkout -b branche_propre
git merge --squash branche_privee_fonctionnalite
git reset</code>

J’ai maintenant un répertoire de travail avec toutes mes modifications sans traîner les valises de ma branche précédente. Je peux alors ajouter manuellement mes modifications et faire mes commits

Résumé

Si vous vous battez avec les choix par défaut de Git, demandez vous « pourquoi ? ».

Traiter l’historique public comme immuable, atomique et facile à suivre. Traiter l’historique privé comme jetable et malléable.

Le processus prévu est :

  1. Créer un branche privée depuis une branche publique.
  2. Commiter régulièrement sur cette branche privée.
  3. Une fois que votre code est parfait, nettoyer son historique.
  4. Fusionner cette branche nettoyée vers la branche publique.

L’auteur remercie particulièrement @joshaber et @jbarnette pour leurs retours sur les premiers brouillons.
Le traducteur remercie Mizu no kokoro pour sa lecture attentive d’un texte « totalement incompréhensible mais tellement poétique, hermétique et ésotérique ».

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *