Code Publié le 13 novembre 2023

Une convention pour des commits plus clairs

Parler de l'élégance d'un arbre de commits Git (git-tree), c'est souvent évoquer l'utilisation de Git Flow, mais un aspect tout aussi crucial est souvent négligé : le message des commits. Imaginez un arbre avec un tronc droit et de belles branches, mais dont les feuilles font peine à voir. Ce n’est plus si élégant, n'est-ce pas ? De même, un git-tree bien structuré peut perdre tout son charme sans convention claire des commits.

Une convention pour des commits plus clairs

Dans le monde souvent complexe de la programmation, le développeur se trouve fréquemment face à des projets versionnés avec git, où les conventions de commit semblent être plus une suggestion qu'une règle. Que chacun applique sa propre méthodologie, ou que les normes établies au début s'érodent avec le temps et l'arrivée de nouveaux collaborateurs, le résultat est un historique de commits difficile à suivre. Ajoutez à cela les commits génériques tels que "misc fixes" ou le peu recommandable "wip", et vous avez une recette pour la confusion. Heureusement, il existe une solution à ce dilemme, une voie pour apporter clarté et cohérence à votre gestion de version : les conventions de commit.

Conventional Commits pour améliorer les messages de commit

Qu'est-ce que c'est ?

Conventional Commits est une spécification standardisée permettant d'ajouter de la clarté et de la cohérence aux messages de commit. Elle n'est pas liée à un langage de programmation ni à un outil de versionning en particulier, mais définit simplement un ensemble de règles pour avoir des commits bien formatés. Elle est basée sur les Angular Commit Guidelines dans sa logique, mais est bien plus permissive : type et scope sont totalement libres et peuvent varier selon les projets. À noter, les différents type proposés par Angular (build, ci, docs, feat, fix, perf, refactor, style, test) sont recommandés dans un premier temps, car ils sont généralement suffisants.

Comme défini dans leur documentation, voici le format standard de commit proposé :

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Et pour illustrer cela, voici différents exemples :

  • feat(settings)!: allow users to disable notifications
  • fix: prevent opening forbidden resources
  • docs: update delivery instructions in README.md
  • test(ui): add unit tests on dates formatting

La convention est étroitement liée à la spécification SemVer qui propose une nomenclature unifiée des numéros de version des logiciels et librairies publiés. Étant donné un numéro de version formaté MAJOR.MINOR.PATCH (par exemple pour la version nommée “7.22.3”, MAJOR=7, MINOR=22, PATCH=3):

  • s'il y a des changements non rétrocompatibles, donc des commits avec BREAKING CHANGE dans le footer du commit (et/ou un ! après le type/scope), il faut incrémenter MAJOR pour la nouvelle version.
  • sinon, s'il y a des ajouts de fonctionnalités rétrocompatibles, donc des commits de type feat, il faut incrémenter MINOR.
  • autrement, s'il y a des corrections d’anomalies rétrocompatibles, donc des commits de type fix, il faut incrémenter PATCH.

La mise en œuvre de Conventional Commits est généralement simple. Il existe plusieurs outils qui facilitent l'adoption de cette spécification dans votre projet, dont certains sont cités ci-dessous. Vous pouvez néanmoins retrouver toutes les informations nécessaires sur son site officiel.

Les conventions alternatives de nommage de messages de commits

De nombreuses autres conventions de nommage existent. Pour un projet, une plateforme, ou même une équipe, il est possible de définir sa propre convention. Voici quelques exemples :

  • gitmoji : Utilise des emojis pour décrire le type du commit (✨ pour une nouvelle fonctionnalité, 🐛 pour une correction de bug, 📝 pour la documentation, etc.). Plus fun mais pas sans désavantages ; l'emoji n'est pas forcément supporté sur tous les terminaux, et l'alternative en shortcode (:sparkles:, :bug:, ...) manque de convention unique pour un rendu uniforme sur les plateformes.
  • Jira Smart Commits : Pratique pour lier le travail réalisé sur un commit avec une tâche Jira. Très lié à l'outil, il permet de réaliser des commandes directement sur la tâche (ajouter du temps, changer le statut, etc.). Néanmoins, le commit seul est moins lisible et manque de contexte.
  • jQuery Commit Guidelines : Convention spécifique, utilisée pour le projet jQuery, elle est très proche de Conventional Commits mais avec un format différent. Plus restrictive, elle permet néanmoins de référencer des issues et pull-requests depuis GitHub.

On peut retenir de cette exploration que Conventional Commits est une spécification simple et flexible, facile à mettre en place et à adopter. Elle est donc recommandée tant que vous n'avez pas de besoin spécifique.

Rien ne vous empêche d'ailleurs dans le body d'un commit de lier une issue GitHub ou d'ajouter un emoji, par exemple.

Écosystème

De nombreux outils sont développés autour de Conventional Commits pour faciliter son adoption et son utilisation. En voici quelques-uns que nous utilisons :

#1 Génération de messages commit

Pour ceux qui veulent être guidés, il existe des outils qui permettent de générer des messages de commit conformes à la convention choisie. Un des plus utilisés est commitizen, et peut s'utiliser en ligne de commande et être utilisé à la place de git commit :

➜  splash.iosapp git:(feature/universal-links) ✗ git cz
cz-cli@4.3.0, cz-conventional-changelog@3.3.0

? Select the type of change that you're committing:
  chore:    Other changes that don't modify src or test files
  revert:   Reverts a previous commit
❯ feat:     A new feature
  fix:      A bug fix
  docs:     Documentation only changes
  style:    Changes that do not affect the meaning of the code (white-space,
formatting, missing semi-colons, etc)
[...]

Par la suite, l'interface nous demande notamment le scope, le message, la description optionnelle, les breaking changes. Le résultat est un commit correctement formaté suivant Conventional Commits. Il est bien sûr possible de configurer commitizen pour utiliser une autre convention de nommage.

Il existe d'autres générateurs, par exemple le plugin VSCode Conventional Commits pour ceux qui préfèrent ne pas sortir de l'IDE.

#2 Linting pour vérifier les messages de commit

Avoir une convention, c'est bien. La respecter, c'est mieux. Dans l'écosystème, des outils permettent de vérifier que les messages de commit respectent bien la convention. Nous avons choisi ici commitlint, populaire au sein de la communauté.

Par défaut, commitlint propose une configuration qui correspond à Conventional Commits. Une fois installé, il suffit de lancer la commande commitlint pour vérifier que les commits respectent bien la convention :

commitlint --from=HEAD~1

Il est aussi possible de l'intégrer à votre CI/CD :

  • En tant qu'action lancée à chaque pull-request, pour vérifier tous les nouveaux commits qui vont être mergés :
    commitlint --from=origin/main --to=HEAD
  • En tant que git-hook, à chaque commit, pour le vérifier localement :
    npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'

Voici un exemple de commitlint en action :

commitlint --from=origin/develop --to=HEAD --verbose
⧗   input: docs: add setup steps in Readme.md
✔   found 0 problems, 0 warnings
⧗   input: FIX(api): change api headers to authenticate
✖   type must be lower-case [type-case]
✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]

✖   found 2 problems, 0 warnings
ⓘ   Get help: <https://github.com/conventional-changelog/commitlint/#what-is-commitlint>

À noter dans cet exemple : bien que Conventional Commits précise que la casse n'a pas d'importance, la configuration par défaut de commitlint est plus stricte et impose une casse en minuscule pour le type. Il est néanmoins possible de modifier cette règle ou de changer la convention utilisée, pour adapter l'outil à vos besoins.

Commitlint n'est pas le seul linter git existant, on pourrait mentionner gitlint ou encore Conventional Commits Linter qui fonctionnent de manière similaire.

Outils de génération automatique de Changelogs

Un point central à l'adoption d'une convention de nommage est la génération automatique de changelogs. De même ici, plusieurs outils sont à disposition en ligne. Nous pouvons citer :

  • Release Please, développé par Google, permet de le faire au travers d'une GitHub action.
  • Le plugin fastlane semantic_release qui peut s'intégrer dans d'autres CI/CD.

Ces outils parsent le git-tree pour ranger les commits et préparer une release note pour chaque version calculée grâce à la syntaxe liée à SemVer.

Pour notre besoin d'un changelog simple et clair, nous avons nous-mêmes écrit un script en Python, qui prend en paramètre un tag ou un commit hash et crée une release note avec les commits suivant ce point de départ. Il ne prend pas en compte le body et le footer des commits mais c'est quelque chose que vous pouvez rajouter à votre convenance.

import re
import subprocess
import sys

# Run git command
git_command = f"git log --pretty=format:%s {sys.argv[1]}..HEAD"
result = subprocess.run(git_command, shell=True, stdout=subprocess.PIPE).stdout.decode('utf-8')

# Define regex for commits
regex = re.compile(r'(?P<type>\w+?)(\((?P<scope>.+?)\))?(?P<breaking>!)?:\s(?P<message>.+)')
types = ["feat", "fix", "build", "ci", "chore", "docs", "perf", "refactor", "revert", "style", "test"]

# Process commits
commits = []
for line in result.split('\n'):
    match = regex.match(line)
    if match and match.group('type') in types:
        commits.append({
            'type': match.group('type'),
            'scope': match.group('scope'),
            'isBreakingChange': match.group('breaking') is not None,
            'message': match.group('message')
        })

# Print changelog
hasBreakingChanges = any(commit['isBreakingChange'] for commit in commits)
print(f"Release note{' WITH BREAKING CHANGES:' if hasBreakingChanges else ':'}")

for type in types:
    filtered_commits = [commit for commit in commits if commit['type'] == type]
    if not filtered_commits:
        continue
    print(f"\n{type}")
    for commit in sorted(filtered_commits, key=lambda x: x['scope'] or ""):
        print(f"- {commit['scope']} {commit['message']}" if commit['scope'] else f"- {commit['message']}")

On exécute la commande python3 changelog.py 2.3.1 et voici le rendu :

Release note WITH BREAKING CHANGES:

feat
- handle universal links
- (settings) allow users to disable notifications

fix
- change api headers to authenticate

docs
- add setup steps in Readme.md

Conclusion

En adoptant Conventional Commits, vous créez une structure dans vos messages de commit qui rendra votre travail plus lisible et plus compréhensible pour tous ceux qui interagissent avec votre code. Cela aide non seulement votre équipe actuelle, mais également ceux qui rejoindront votre projet à l'avenir. De nombreux outils existent pour vous aider à adopter cette convention, et il est possible de la personnaliser pour l'adapter à vos besoins. Ajouter cette étape à votre workflow peut sembler fastidieux au début, mais les bénéfices à long terme sont évidents.

Et si vous n'avez pas le temps d'ajouter les vérifications automatiques, vous pouvez toujours commencer par adopter la convention et laisser les outils pour plus tard. Dans le pire des cas, si un commit atterrit sans respecter les spécifications de Conventional Commits, ce n'est pas grave, cela signifie simplement que le commit sera ignoré par des outils basés sur les spécifications.

Bien sûr, il est toujours possible de contribuer si vous voulez étoffer l'écosystème autour de Conventional Commits ou améliorer le projet lui-même.

Rémi

Ingénieur Logiciel · Mobile

Lire sa présentation

Ne rate aucune nouveauté !

Chaque mois, reçois ton lot d'informations nous concernant. Avec par exemple la sortie de nouveaux articles ou de projets clients.