Ce que la garde-robe de Barack Obama a en commun avec une bonne histoire de git

Un jour d'été dans la chaleur de fin août 2014, le président de l'époque, Barack Obama, a pris une décision qui allait choquer la nation : il portait un costume différent. La « controverse sur le costume de bronzage » qui en a résulté a dominé un cycle d'actualités et s'est propagée pour diverses raisons, mais elle a finalement été déclenchée par la nouveauté du costume lui-même. Comme les cols roulés noirs de Steve Jobs et les chemises grises de Mark Zuckerberg, Obama s'en tenait généralement aux mêmes costumes bleus ou gris chaque jour.
Le dénominateur commun derrière ce comportement partagé est le concept psychologique de fatigue décisionnelle : que même les plus petites décisions que nous prenons chaque jour peuvent épuiser la quantité finie de cerveau dont nous disposons pour prendre des décisions et bien les prendre. Une stratégie adoptée par ces individus pour préserver cette précieuse ressource consiste à éliminer le plus de décisions mineures possible : porter les mêmes choses, manger les mêmes choses, suivre le même horaire, etc. Cela vous permet de concentrer votre énergie mentale sur les décisions qui comptent vraiment.
Alors, qu'est-ce que tout cela a à voir avec le système de contrôle de version préféré de tout le monde, git ? Comme pour tant de choses en programmation, il n'y a pas de « bonne » façon de structurer un commit git ou de gérer l'historique git d'un projet ; vous devez simplement choisir un principe directeur et organiser vos modèles autour de lui. Personnellement, je crois au choix d'une stratégie qui réduit la fatigue décisionnelle (et la "fatigue mentale" plus largement) pour tous les différents "consommateurs" d'un commit. J'entrerai plus en détail sur la façon de procéder ci-dessous (et n'hésitez pas à passer à cette liste numérotée si vous le souhaitez), mais je pense qu'il est très important d'expliquer d'abord pourquoi je crois cela. Et nous devons commencer par savoir à qui s'adresse réellement un commit.
À qui appartient l'histoire ?
Il faut souligner qu'au cours du cycle de vie complet d'un projet moyen, le nombre de personnes qui consultent un commit sans l' avoir écrit sera bien supérieur à celui qui l'a fait. Ces autres personnes auront également la connaissance la moins intime de ce à quoi sert réellement un commit et de la manière dont il est censé fonctionner. En pratique, alors, construire une bonne histoire de git devrait vraiment être pour eux . Et, avec suffisamment de temps, même le code que vous avez écrit vous-même peut un jour vous sembler étranger. Donc, garder une bonne histoire peut également aider votre futur moi.
Gardant cela à l'esprit, il convient de noter qu'il existe deux grandes catégories de personnes qui, à un moment donné, verront un commit particulier :
- Réviseurs de code
Ceux qui consultent le commit avant qu'il ne soit fusionné dans l'historique via le processus de révision du code. Ces personnes sont généralement connues sous le nom de "code reviewers". Dans une bonne équipe, tout le monde sera, à un moment donné, un réviseur de code et il peut y en avoir plusieurs pour chaque ensemble de nouvelles modifications de code. - Détectives de code
Ceux qui consultent le commit après sa fusion. Ce sont généralement des personnes qui remontent l'historique pour essayer de comprendre pourquoi quelque chose a été ajouté ou quand un bogue a été introduit. Faute d'un meilleur nom, j'appellerai ces personnes des "détectives de code" pour les distinguer des personnes ci-dessus.
Les détectives de code ont tous ces mêmes défis en plus d'autres : ils ne savent peut-être pas toujours ce qu'ils recherchent et même lorsqu'ils le trouvent, ils peuvent manquer de contexte vital pour le comprendre. Souvent, ils n'ont même pas l'avantage de pouvoir parler avec l'auteur original du code. Pour cette raison, une grande partie de ce que fait un détective de code consiste à essayer de déduire l'intention du code existant sans pouvoir réellement poser de questions à ce sujet ou donner suite à ses soupçons.
Les tâches de ces deux groupes sont légèrement différentes, mais elles impliquent toutes deux une série de décisions qui doivent être prises, ligne par ligne, pour répondre à la question ultime : que fait ce code ? Selon la façon dont les commits sont structurés par l'auteur, cela peut être soit relativement simple, soit une tâche pénible avec des barrages routiers inutiles et des faux-fuyants.
Ah, prendre des décisions
Voyons maintenant quels types de décisions entrent dans la compréhension d'un commit. Nous devons d'abord noter que chaque ligne d'un commit peut être classée comme l'une des deux choses suivantes : une ligne de code "ajoutée" ou une ligne "supprimée".
Sans aucun contexte supplémentaire, les décisions suivantes doivent être prises lors de l'affichage d'une seule ligne de code « ajouté » :
- Est-ce une toute nouvelle ligne de code ?
- S'il ne s'agit pas d'une nouvelle ligne de code, s'agit-il d'une ligne de code existante qui a simplement été déplacée d'un autre endroit ?
- S'il ne s'agit pas d'une nouvelle ligne de code et qu'elle n'a pas été déplacée, s'agit-il d'une modification triviale d'une ligne existante (comme un changement de formatage) ou s'agit-il d'un changement logique légitime ?
- S'il s'agit soit d'une ligne de code entièrement nouvelle, soit d'une modification entraînant un changement logique, pourquoi le fait-on ? Est-ce fait correctement ? Peut-il être simplifié ou amélioré ?
Nous pouvons voir un processus similaire pour chaque ligne de code "supprimée":
- Cette ligne est-elle entièrement supprimée ?
- S'il n'est pas entièrement supprimé, est-il déplacé ou modifié ?
- S'il n'est pas entièrement supprimé et qu'il n'est pas simplement déplacé, est-ce le résultat d'une modification triviale (ex : formatage) ou le résultat d'un changement logique ?
- S'il s'agit en fait d'une modification logique, pourquoi est-elle modifiée ? Est-ce fait correctement ?
- Si la ligne est entièrement supprimée, pourquoi n'est-elle plus nécessaire ?
Donc, cela nous ramène finalement à la fatigue de la décision :
Comment pouvons-nous organiser les commits de manière à éliminer ces premiers choix triviaux et permettre aux téléspectateurs de se concentrer sur les plus importants ?
Vous ne voulez pas que votre équipe passe son cerveau et son temps limités à décider, par exemple, qu'un morceau de code vient d'être déplacé d'un module à un autre sans modification, puis rate les erreurs de codage présentes dans le nouveau code réel. Multipliez cela entre les équipes d'une grande organisation et cela peut entraîner une perte de productivité mesurable.
Donc, maintenant que nous avons discuté des raisons pour lesquelles je pense que nous devrions suivre cette stratégie, discutons enfin de la manière dont je plaide pour la mettre en pratique.
1. Placer des modifications triviales dans leurs propres commits
La chose la plus simple et la plus importante à faire est de séparer les modifications triviales dans leurs propres commits. Voici quelques exemples :
- Modifications de la mise en forme du code
- Renommer fonction / variable / classe
- Réorganisation des fonctions / variables / importations au sein d'une classe
- Suppression du code inutilisé
- Déplacement des emplacements de fichiers
Considérez le commit suivant mélangeant des modifications triviales avec des modifications non triviales :
Message de validation : "Mettre à jour la liste des fruits valides"

Combien de temps vous a-t-il fallu pour repérer les changements non triviaux ? Voyons maintenant ce qui se passe lorsque ces deux modifications sont divisées en deux commits distincts :
Message de validation : "Mettre à jour le formatage valide de la liste de fruits"

Message de validation : "Ajouter des dates à la liste de fruits valide"

Le commit "formatage uniquement" peut essentiellement être ignoré et les ajouts de code peuvent être découverts immédiatement en un coup d'œil.
2. Placer les refactors de code dans leurs propres commits
Les refactorisations de code impliquent des modifications de la structure de certains codes mais pas de leur fonction. Parfois, cela est fait pour le plaisir, mais souvent par nécessité : pour s'appuyer sur du code existant, il est parfois nécessaire de le refactoriser d'abord et il peut alors être tentant de faire les deux choses à la fois. Cependant, des erreurs peuvent être commises lors d'une refactorisation et une attention particulière est requise lors de leur révision. En plaçant ce code dans son propre commit clairement indiqué comme un refactor, le réviseur sait signaler tout écart par rapport au comportement logique existant comme une erreur possible.
Par exemple, à quelle vitesse pouvez-vous repérer l'erreur ici ?
Message de validation : "Mettre à jour la logique de l'astuce"

Que diriez-vous maintenant avec le refactor divisé?
Message de validation : "Extraire le taux de pourboire par défaut"

Message d'engagement : "Autoriser les taux de pourboire personnalisés"

3. Placer les corrections de bugs dans leurs propres commits
Parfois, au cours de la modification du code, vous remarquez un bogue dans le code existant que vous cherchez à modifier ou à développer. Dans l'intérêt d'aller de l'avant, vous pouvez simplement corriger ce bogue et l'inclure dans vos modifications autrement sans rapport dans le même commit. Lorsqu'il est mélangé de cette manière, il y a plusieurs complications:
- Les autres personnes qui consultent ce code peuvent ne pas savoir qu'un bogue est en cours de correction.
- Même lorsqu'on sait qu'un correctif de bogue est inclus, il peut être difficile de savoir quel code faisait partie de la correction du bogue et lequel faisait partie des autres modifications logiques.
4. Placer des changements logiques séparés dans leurs propres commits
Après avoir divisé les types de modifications ci-dessus, vous devriez maintenant vous retrouver avec un seul commit avec des modifications légitimes et logiques pour ajouter/mettre à jour/supprimer des fonctionnalités. Pour un petit changement concis, cela suffit souvent. Cependant, parfois, ce commit ajoute une fonctionnalité entièrement nouvelle (avec des tests) et s'affiche à plus de 1000 lignes (ou plus). Git ne présentera pas ces modifications de manière cohérente et pour bien comprendre ce code, il faudrait que le réviseur saute et garde une grande partie de ces lignes en mémoire à la fois pour suivre. En plus de la fatigue décisionnelle impliquée dans le traitement de chaque ligne, étirer votre mémoire de travail de cette manière est mentalement éprouvant et finalement inefficace.
Dans la mesure du possible, divisez les commits en fonction des domaines de sorte que chaque commit se compile indépendamment. Cela signifie que le code le plus indépendant peut être ajouté en premier, suivi du code qui en dépend, et ainsi de suite. Un code bien structuré devrait se diviser de cette manière assez naturellement, tandis que les difficultés rencontrées à ce stade pourraient suggérer des problèmes structurels plus importants tels que des dépendances circulaires. Cet exercice peut même conduire à des améliorations du code lui-même.
5. Fusionnez tous les changements de révision dans les commits auxquels ils appartiennent
Après avoir divisé votre travail en plusieurs commits propres, vous pouvez recevoir des commentaires de révision qui vous obligent à apporter des modifications au code qui apparaît dans un ou plusieurs d'entre eux. Certains développeurs réagiront à ces commentaires en ajoutant de nouveaux commits qui répondent à ces préoccupations. La liste des commits dans un PR donné peut commencer à ressembler à ceci :
- <Initial commits>
- Respond to review feedback
- Work
- More work
- Addressing more review feedback
Il en va de même lorsqu'une pull request est ouverte pour la première fois : chaque commit doit avoir un but et il ne doit pas être annulé par des modifications dans les commits ultérieurs pour les mêmes raisons que celles mentionnées ci-dessus.
Considérez ce changement initial suivi de plusieurs commits de « travail » :




Imaginez maintenant que vous voyez ces changements bien dans le processus (ou même des années plus tard). Ne voudriez-vous pas simplement voir ce qui suit ?

6. Rebase, rebase, rebase !
Si une branche de fonctionnalité existe depuis assez longtemps, soit en raison du temps nécessaire pour ajouter le code initial, soit en raison d'un long processus de révision du code, elle peut commencer à entrer en conflit avec les modifications apportées à la branche principale sur laquelle elle était initialement basée. Il existe maintenant deux façons de rendre la branche de fonctionnalité actuelle :
- Fusionnez la branche principale dans la branche de fonctionnalité. Cela générera un "commit de fusion" dans lequel toutes les modifications de code nécessaires pour résoudre les conflits sont incluses. Si la branche de fonctionnalité est particulièrement ancienne, ces types de commits peuvent être conséquents.
- Rebasez la branche de fonctionnalité par rapport à la branche principale. Le produit final ici est un nouvel ensemble de commits qui agissent comme s'ils venaient d'être créés sur la base de la branche principale mise à jour. Tout conflit devra être traité dans le cadre du processus de changement de base, mais toute preuve de la version originale du code aura disparu.
Si vous vous souciez de produire un historique propre (et vous devriez !), le rebasage est la meilleure option ici : tous les changements se complètent de manière ordonnée et linéaire. Vous n'avez pas besoin d'outils sophistiqués de visualisation de l'historique pour comprendre la relation entre les branches.
Considérez l'historique de projet suivant qui utilise la fusion entre les branches :

Comparez cela à un projet qui rebase toutes les modifications et interdit les commits de fusion même lors de la fusion de fonctionnalités dans la branche principale :

Dans le premier cas, les relations entre les changements doivent être tracées, réfléchies et déchiffrées ; dans ce dernier, vous avancez et reculez simplement dans le temps.
Certains diront que c'est en fait le changement de base qui détruit l'histoire ; que vous perdez l'historique des modifications apportées pour obtenir du code dans sa forme finale avant d'être fusionné. Mais ce type d'historique est rarement utile et dépend beaucoup du développeur : le parcours d'une personne peut différer de l'autre, mais ce qui compte, c'est de voir une série de commits dans l'historique qui reflètent les changements finaux qu'ils représentent… quel que soit le processus nécessaire pour obtenir là. Oui, il existe des cas particuliers où les commits de fusion sont inévitables, mais ils devraient être l'exception. Et souvent, les scénarios qui les provoquent (comme les branches de fonctionnalités de longue durée partagées par plusieurs membres de l'équipe) peuvent être évités en utilisant de meilleurs flux de travail (comme l'utilisation d'indicateurs de fonctionnalités au lieu de branches de fonctionnalités partagées ).
Contre-arguments
Il y a certainement des arguments qui peuvent être avancés contre cette approche et j'ai eu de nombreuses discussions avec des personnes qui ne sont pas d'accord avec cela. Ces points ne sont pas sans mérite et comme je l'ai mentionné au début de l'article, il n'y a pas une seule "bonne" façon de structurer les commits. Je veux souligner rapidement certains points que j'ai entendus et donner mon avis sur chacun.
"S'inquiéter de la structure des commits ralentit tellement le développement."
C'est l'un des points les plus courants que j'ai entendus contre cette approche. Il est certainement vrai qu'il faudra un peu plus de temps au développeur qui écrit le code pour examiner attentivement et diviser ses modifications. Cependant, cela est également vrai pour tout autre type de processus supplémentaires destinés à se prémunir contre les faiblesses inhérentes à la priorisation de la vitesse et, à long terme, cela pourrait ne pas faire gagner du temps à l'ensemble de l'équipe. Par exemple, l'argument selon lequel le développement sera ralenti est utilisé par les équipes qui n'écrivent pas de tests unitaires, mais ces mêmes équipes doivent alors passer plus de temps à réparer le code cassé et à tester manuellement les refactors. Et, une fois qu'une équipe a pris l'habitude de diviser ses modifications de cette manière, le temps supplémentaire ajouté est considérablement réduit car cela fait simplement partie du processus de développement normal.
"Mon projet utilise des outils qui ne permettent même pas des modifications de formatage triviales."
Je suis d'accord que c'est un excellent moyen de minimiser les dommages qui sont autrement causés par le barattage de code lié au formatage. En tant que développeur Android, je crois fermement à l'utilisation des formateurs automatiques à l'échelle de l'équipe et je ne jure que par des outils comme ktlint . Cependant, je sais aussi de première main, grâce à la configuration de tous ces outils, qu'ils ne sont pas parfaits et qu'il existe de nombreux changements de formatage possibles dont ils sont totalement agnostiques. Et, comme indiqué ci-dessus, certains changements triviaux ne sont pas simplement des changements de formatage, comme la réorganisation du code. Il y aura toujours des changements de code triviaux qui peuvent être apportés et il doit donc y avoir un plan pour savoir comment les gérer au mieux.
"Tous les sites d'hébergement git n'autorisent pas les demandes d'extraction avec plusieurs commits."
C'est très vrai! Mes recommandations sont principalement basées sur l'utilisation d'outils tels que GitHub et GitLab qui permettent à un PR d'avoir autant de commits que vous le souhaitez, mais il existe des outils tels que Gerrit qui ne le font pas. Dans ce cas, considérez simplement chaque commit comme étant son propre PR. Cela introduit encore plus de frais généraux pour l'auteur (et parfois pour les critiques), mais je pense qu'à long terme, cela en vaut la peine. Il peut même y avoir des moyens de rationaliser ce processus et de relier ces PR distincts les uns aux autres, comme l'utilisation de « changements dépendants » dans Gerrit.
"Un seul commit garantit que toutes les modifications compilent et réussissent les tests."
C'est aussi un très bon point. Les vérifications automatisées qui s'exécutent sur les sites d'hébergement git ne s'exécutent généralement que sur l'ensemble des modifications, et non sur chaque commit individuel. S'il y a un commit cassé en cours de route qui est corrigé par des modifications ultérieures, il n'y a aucun moyen de le détecter automatiquement. Vous voulez que chaque commit puisse être autonome au cas où vous auriez besoin un jour de revenir en arrière et de tester l'état du code à ce stade pour traquer les bogues, etc. En règle générale, il devrait être requis pour chaque commit dans un PR multi-commit pour compiler et réussir tous les tests pertinents, mais il n'y a aucun moyen de l'appliquer strictement (autre que de faire en sorte que chaque commit soit son propre PR). Cela nécessite de la vigilance, mais c'est juste quelque chose qui doit être mis en balance avec les avantages qui accompagnent la division du code.
"Un seul commit fournit le plus de contexte pour tous les changements."
Celui-ci est un point intéressant. Alors que les sites d'hébergement git comme GitHub permettent d'ajouter des commentaires en masse à un groupe de commits dans le cadre d'une description de relations publiques, rien de tel n'existe dans git lui-même. Cela signifie que le lien entre les commits ajoutés dans le même PR ne fait pas strictement partie de l'historique. Heureusement, des sites comme GitHub ont des fonctionnalités qui ajoutent un lien vers le PR qui a produit un commit lors de son affichage isolé :

Bien que ce ne soit pas aussi utile que d'avoir ce lien dans l'historique de git lui-même, pour de nombreux projets, c'est un moyen adéquat de garder une trace de la relation entre les commits.
Dernières pensées
J'espère vous avoir convaincu que diviser les changements de code en commits distincts de plusieurs types présente des avantages pour tout le monde dans le processus de développement :
- Cela peut aider le rédacteur à améliorer la structure du code et à mieux transmettre le contenu des modifications.
- Cela peut aider les réviseurs de code à réviser plus rapidement le code et à réduire la fatigue mentale en leur permettant de concentrer leur attention sur des modifications distinctes et significatives.
- Cela peut aider quiconque regarde l'historique du code à trouver plus rapidement les changements logiques et les bogues et à réduire de la même manière la charge mentale qui accompagne l'écrémage de grandes quantités d'historique.
Brian travaille chez Livefront , où il essaie toujours d'écrire un peu plus (git) l'histoire.