Lorsque l’on s’intéresse de près ou de loin au merveilleux monde du développement, on ne peut pas passer à côté de Git (merci Linus Torvald) et des commits. Et un jour, on tombe sur ces 2 mots commit atomique
. On se demande alors à quoi cela pourrait faire référence. Ici, je vais vous donner mon interprétation de ce type de commit et pourquoi il est important de les utiliser au quotidien.
Une petite histoire pour un peu de contexte
Actuellement à Indy, on utilise GitHub, donc chaque commit sur master est en théorie relié à une PR. Cette PR est alors normalement reliée à une tâche Jira, donc on peut avoir un historique assez simplement.
Ainsi, si on tombe sur un commit qui a comme seul nom fix
, on va voir la PR, même si le message de PR est vide lui aussi, on a accès aux autres commits compris dans cette PR, et on peut comprendre à quoi correspond ce “fix” et avoir un peu de contexte (et encore, on vient de perdre 5min).
Maintenant, imaginons que cette PR contient uniquement ce commit, il est alors nécessaire d’aller à la tâche Jira rattachée et cette fois, on comprend ce qu’il se passe parce qu’on a du contexte. Mais seulement si une tâche Jira est rattachée à la PR !
Bon ça, c’est dans le meilleur des cas en fait.
Imaginons, qu’on touche à du legacy et qu’on essaie de comprendre ce qu’il y a bien pu se passer. Sauf que ce legacy date d’il y a 5 ans. Et qu’à l’époque on n’était pas sous GitHub, mais GitLab et qu’on n’a donc aucun historique des PRs/MRs, et qu’à l’époque on n’avait pas Jira mais Trello (instance de Trello qui n’existe plus bien sûr). Et, bien entendu, la ligne de code qu’on essaie de modifier est rattaché à un seul commit qui s’appelle fix
et qui fait 600 lignes. Clairement, personne n’a envie d’arriver dans cet état-là. Donc, on modifie en croisant les doigts, on rajoute des tests comme on peut, et on fait des incantations pour que notre patch ne casse pas d’autres choses tout en se disant “J’aurai dû être éleveur de lamas dans le Périgord”.
Alors clairement, qu’est-ce que c’est un commit atomique ?
Pour faire simple, c’est un commit qui ne dépend que de lui.
En d’autre mot, il contient plusieurs caractéristiques :
- Très simple, peu de lignes modifiées (lorsque cela est possible) et doit pouvoir se lire facilement
- Ses tests associés (👋 TDD)
- Un titre simple expliquant le but. Pour t’aider, tu peux utiliser Conventional Commits et Conventional Comments.
- Une description claire, et qui répond à plusieurs questions :
- Pourquoi ?
- Qu’est-ce qu’on cherche à faire ?
- Quelle est la suite ? (S’il y en a une)
- Il ne doit pas dépendre d’un autre commit pour être fonctionnel en production
- Un lien vers une tâche Jira ou autre (en bonus, le titre de la tache, car un titre est plus parlant qu’un simple numéro)
Enfin, quelques règles supplémentaires :
- Son message de commit doit être aussi conséquent que son nombre de lignes modifiées (ça ne se justifie pas toujours bien sûr)
- Il serait possible de revenir sur n’importe quel commit, compiler le projet, le lancer et il fonctionnera toujours. Dans les faits, il faut prendre en compte les montées de version des outils externes comme NPM, node, ou autres librairies qui peuvent compliquer la tâche, aussi le fait que 2 commits peuvent dépendre l’un de l’autre, etc.
Un petit exemple est disponible en fin de page pour mieux comprendre
Ok c’est bien beau, mais à la fin le résultat sera le même : On aura du code !
Oui. Je suis totalement d’accord avec toi. Mais, j’ai 4 contre-exemples à te donner :
- Il ne faut pas oublier une chose : ton code n’est pas parfait ! Un jour, un bug sera introduit sans t’en rendre compte (parce qu’un bug est rarement introduit consciemment …) et ça sera durant cette phase de debugging que tu seras content d’avoir un peu de contexte grâce aux messages de commits. Et ainsi te rendre compte que ce n’est pas un bug, mais une feature ! (la fameuse feature)
- Tu touches à un code pas si vieux que ça, et tu dois introduire une nouvelle fonctionnalité. Comme tu es quelqu’un de minutieux, tu as mis de beaux messages de commits là où il le fallait. Ainsi, tu gagnes en temps, car tu n’auras pas besoin d’aller voir la PR ni Jira pour comprendre le contexte de chaque ligne et pourquoi elles ont été introduites/modifiées, tout se fera directement dans ton IDE. Tu n’auras pas besoin de lire tout le code pendant 10min pour te remettre dans le contexte de l’époque et poser la question “Pourquoi ils ont fait ça ??” toutes les 3 minutes.
- Durant la phase de relecture, ton copain relecteur sera content d’avoir un peu de contexte s’il ne bosse pas sur la feature en même temps que toi. Ainsi, il saura ce que tu veux faire et où tu comptes aller par la suite, et il pourra t’aiguiller s’il sent que tu vas dans le mur. Aussi, lire un commit qui fait plus de 1000 lignes n’est jamais agréable, c’est très long et fastidieux, donc plus le commit est petit, plus la revue ira vite et sera un moment de plaisir pour le relecteur.
- Qui te dit que tu seras encore dans l’entreprise dans 2 ans ? Qui te dit que le projet ne sera pas repris par une autre équipe par la suite ? Qui te dit que l’entreprise utilisera les PRs et GitHub toute sa vie ? À vrai dire, personne ne le sait à ce moment-là, la seule chose qui est sûre, c’est que le logiciel sera encore existant, et ses commits aussi (jusqu’à qu’un nouvel outil de versionning plus performant que Git arrive).
Tu pourrais aussi te dire “Je mets un commentaire dans mon code, ça fait la même chose”. Et c’est là que je te sors ma carte (Yu-Gi-Oh) fétiche.
Un message de commit correspond à ce que tu as voulu faire à un moment T, il est relié à des modifications de fichiers. Il est uniquement là pour te donner du contexte et comprendre plus rapidement ce que tu faisais et ce que tu cherchais à faire à ce moment précis.
Un commentaire est quant à lui volatile/modifiable, il est là, relié à une fonction ou un fichier, mais cette fonction a changé de signature ou de comportement, et on a oublié de modifier le commentaire, et dans le doute, on le laisse, se disant qu’il doit servir à quelqu’un (combien de // TODO
sont là depuis 2 ans alors que personne n’y a touché ?) mais en fait non. Du coup, on a un commentaire relié à une méthode et ils n’ont plus rien en commun. Alors qu’un message de commit, lui, sera toujours relié à ses fichiers modifiés.
Petite synthèse
Les commits atomiques sont composés de 3 choses importantes : le message, le code et les tests.
Le message permet de mettre en avant le pourquoi. Le code permet de dire le comment. Et les tests pour s’assurer que ton quoi répond au pourquoi.
Il n’est pas nécessaire de passer du temps sur le message s’il n’y a aucune utilité. Par exemple, fix: Button should be display in red when alert
, est assez compréhensible pour être seul.
Par contre, lorsque notre commit modifie ou introduit une archi un peu complexe, il peut être intéressant d’expliquer le but de cette architecture, de mettre un lien vers un ADR si celui-ci existe, etc. Idem pour les breaking change
d’API par exemple.
Donc perdre 5min à un moment T peut être lourd et compliqué à prendre, mais sera synonyme de rapidité dans 6 mois. Ainsi, un projet n’est plus relié à un gestionnaire de projet quelconque ou un service de référentiel. Mais uniquement à ses commits, permettant un gain de temps et de compréhension.
Ce n’est pas parce que tu comprends ton code aujourd’hui que tu le comprendras dans 1 an, et imagine ce que ressentent ceux qui n’ont jamais touché à ton code 🙃
Petit exemple concret
On introduit une nouvelle feature sur le paiement SEPA et cela correspond à une grosse fonctionnalité. La première bonne pratique est alors de la découper en 3 ou 4 commits.
Le premier commit :
refactor: Move files to pay by card
SEPA payment must be added as a payment option. Because many users ask for it, and some users cannot pay by credit card.
In order to add SEPA payment, it is necessary to move some functions in order to DRY. This commit must not break the initial behavior of the CB payment.
Part of JIRA-3015 Pouvoir payer par SEPA
Le deuxième commit :
feat: User can select SEPA mode
SEPA payment is not yet in place, only the UI part is available. A new feature flag ‘payment_sepa_activated’ is introduced so user does not see this part. It is now possible to select the SEPA payment and a new form is displayed to enter all the details. This form is currently disabled. In next patch, the back-end will be built to allow SEPA payment.
Part of JIRA-3015 Pouvoir payer par SEPA
Le troisième commit :
feat: Pay by SEPA or CB
The user can choose between SEPA and CB. In both cases, a form is displayed and the payment should be successful in both cases.
The feature flag ‘payment_sepa_activated’ is now deleted.
Closes JIRA-3015 Pouvoir par SEPA
Références :