Ameliorer les messages des commits avec CommitLint

Quel que soit le produit développé ou le langage utilisé, il y a toujours des développeurs et des commits quelque part..

Au travers de l’historique, les commits permettent de retracer toutes les modifications effectuées sur un projet.

Néanmoins, la qualité des messages peut beaucoup varier d’un développeur à l’autre, voire d’un jour à l’autre pour la même personne…

1. Qu’est-ce qu’un commit de qualité ?

Quand nous regardons l’historique d’un projet versionné sous git, nous constatons parfois une suite de commits peu expressifs..

e60930a9 (HEAD -> my_branch, origin/mybranch) Modif de la nav
49ff1d07 Fix describe bis
49ff1d07 Fix describe
3672f204 Update conf
03d00e2e Types
a5fdbc4b #261 suppress unecessary lines
886fd7af add logo and modify message for loading connections
a06a1272 support logo and redirection
1dd84e9c fix reconnect insert #291
a62f7880 No comment
26f99ee0 ...
d2be491c fix test
bfb2f370 commit final with all fix

Les développeurs arrivant sur le projet, ou pire ceux qui sont déjà sur le projet, peuvent difficilement exploiter un tel historique.

Pour citer quelques problématiques liés un tel historique :

  • difficile de distinguer les fonctionnalités des correctifs
  • messages trog génériques dont on ne comprend pas les impacts éventuels
  • structure des messages non homogène

2. Définir un format de message commun

Les développeurs ont tendance à écrire des messages qui ne sont pas assez expressifs. Ils ne décrivent pas toujours les changements opérés ni l’objectif du code ajouté.

Chaque commit devrait pouvoir répondre aux questions suivantes :

Quels types de changement suis-je en train de faire ?

Quel périmètre est affecté par mes changements ?

Comment décrire succinctement les modifications opérées ?

2.1. La spécification “Conventional commit”

Git ne définit pas de format de message. Cela reste du texte simple où chacun est libre de structurer son message comme il le souhaite.

La spécification “Conventional Commit” propose d’ajouter une couche de vérification des messages avant de les écrire dans git.

L’objectif est de créer une convention commune au sein du projet et faciliter ainsi le partage du code ainsi que les résolutions de bugs.

Cette spécification est largement inspirée des bonnes pratiques de contribution du projet Angular

2.2. Structure des messages

Les messages sont composés comme suit :

<type>[optional scope]: <subject>
<BLANK LINE>
[optional body]
<BLANK LINE>
[optional footer]

Type

décrit ce que fait votre commit.

Voici les valeurs couramment utilisées :

  • feat: une nouvelle fonctionnalité
  • fix: une correction de bug
  • docs: un changement uniquement dans la documentation
  • style: un changement qui n’affecte pas le sens du code (espace, reformattage, alignements dans le code…)
  • refactor: un changement qui n’est ni une correction de bug ni une évolution
  • perf: un changement qui améliore la performance
  • test: un ajout de tests manquants
  • chore: un changement sur le process de build ou des outils complémentaires

Scope

Décrit le périmètre de la modification (ex: nom du composant, module, packages, etc)

Subject

Description succincte des modifications

  • impératif et temps présent
  • pas de majuscule
  • pas de point

Body (optionnel)

Décrit plus en détail les objectifs et intentions qui ont motivé les changements apportés

Footer (optionnel)

Liens éventuels vers des outils de suivi de bugs

A noter :

En cas de “Breaking change”, il est possible de l’indiquer via un ‘!’ dans le type et dans le body en ajoutant “BREAKING CHANGE:” suivi de l’explication.

3. Exemples de messages attendus

Fonctionnalité implémentée: Possiblité de passer commande via un bouton

feat(order): add purchase order button

Mise à jour de la documentation pour expliquer les scripts d’initialisation du projet

docs(readme): document how to start project

Arrêt du support Node 6

refactor!: drop support for Node 6

BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.

4. Mise en place de CommitLint

commitlint est, comme son nom l’indique, un linter de (message de) commit, c’est-à-dire qu’il va analyser le texte qui lui sera transmis. L’idée est donc de déclencher cette analyse à chaque commit et d’interdire la finalisation du commit si le message ne satisfait pas nos contraintes.

Par défaut commitlint cherchera à ce que vous respectiez la structure suivante, issue de la spécification “Conventional Commit

4.1. Installation

Pour installer commitlint, il suffit d'executer la commande ci-dessous :

npm i -g @commitlint/cli @commitlint/config-conventional

pnpm add -D @commitlint/cli @commitlint/config-conventional

On crée ensuite le fichier de configuration local commitlint.config.js pour dire à commitlint quelle configuration charger :

module.exports = {
  extends: ['@commitlint/config-conventional'],
}

Et pour que tout ça s’exécute automatiquement à chaque message de commit renseigné, on demande à husky de le charger via le hook commit-msg :

npx husky add .husky/commit-msg 'pnpm commitlint --edit $1'

Une fois le hook configurer, nous pouvons faire un test avec un message de commit qui ne respecte pas la convention :

$ git add .

$ git commit -m "Ajout de commitlint"

✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...
✔  contents checks: everything went fine! Good job! 👏
⧗   input: Ajout de commitlint
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

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

husky - commit-msg hook exited with code 1 (error)

Comme nous pouvons le voir, le message de commit ne passe pas la validation.

Essayons maintenant avec un message conforme :

$ git status

On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   .husky/commit-msg
        new file:   commitlint.config.js
        modified:   package.json
        modified:   pnpm-lock.yaml
$ git commit -m "chore: add commitlint"

✔ Preparing lint-staged...
✔ Hiding unstaged changes to partially staged files...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Restoring unstaged changes to partially staged files...
✔ Cleaning up temporary files...
✔  contents checks: everything went fine! Good job! 👏
[main 69061ef] chore: add commitlint
 4 files changed, 777 insertions(+)
 create mode 100755 .husky/commit-msg
 create mode 100644 commitlint.config.js

Cette fois, nous voyons notre message passer la validation!

4.2. La personnalisation des règles de validation

Il est par exemple possible de forcer l’usage des scopes et d’en restreindre le choix à une liste prédéfinie

//commitlint.config.js
module.exports = {
    extends: ['@commitlint/config-conventional'],
    rules: {
        'scope-empty': [2, 'never'],
        'scope-enum': [2, 'always', [
            'logging',
            'services',
            'docs',
            'dependencies',
            'profiles',
            'users',
            'api',
            'segments',
            'configuration'
        ]]
    }
};

Pour aller plus loin, consultez la liste des règles de commitlint

4.3. Exemple d’historique git avec des commits structurés et expressifs

86f80528 (HEAD -> master, origin/master, origin/HEAD) fix(changelog): display scope in breaking change messages
6b578392 fix(CLI): show symlinked themes in `omz theme list`
64cb1530 chore: add Konfekt as universalarchive maintainer
492f712d feat(plugins): add `universalarchive` plugin to conveniently compress files (#6846)
2118d35e fix(vi-mode)!: add back edit-command-line key binding as 'vv' (#9573)
79980b00 fix(vi-mode): hide cursor-change logic behind `VI_MODE_SET_CURSOR` setting
94ce46d4 docs(vi-mode): revamp README and document settings
66e0438d fix(archlinux): update URL and key server in `pacmanallkeys` (#9569)
9e5f280f feat(CLI): add `plugin info` subcommand (#9452)

5. Commitizen pour aider à ecrire les messages

commitlint propose l’intégration avec un assistant appelé commitizen qui viens faciliter l'ecriture des messages respectant la convention Conventional Commit

**5.1. Installation

Pour installer commitizen, il suffit d'executer la commande ci-dessous :

npm i -g @commitlint/cz-commitlint commitizen

pnpm add -D @commitlint/cz-commitlint commitizen

Pour l’associer à commitlint on doit configurer la passerelle dans le fichier package.json comme decrit ci-dessous :

{
  "config": {
    "commitizen": {
      "path": "@commitlint/cz-commitlint"
      }
  },
  "devDependencies": ...
}

On devra ensuite utiliser cz à la place de notre habituel git commit :

$ git add .

$ cz
cz-cli@4.2.5, @commitlint/cz-commitlint@17.1.2

? Select the type of change that you're committing: feat
? What is the scope of this change (e.g. component or file name) (press enter to skip): (max 96 chars)
 (0) 
? Write a short, imperative tense description of the change: (max 96 chars)
 (14) add commitizen
? Provide a longer description of the change (press enter to skip):
 
? Are there any breaking changes?: No
? Does this change affect any open issues?: No
✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...
✔  contents checks: everything went fine! Good job! 👏
[main e0bf97b] feat: add commitizen
 2 files changed, 366 insertions(+)
$ git log
commit e0bf97b4624667777741fd4a5fb06a83e59de4b0 (HEAD -> main, origin/main)
Author: Rajiv Mounguengue <rajiv.mounguengue@hotmail.fr>
Date:   

    feat: add commitizen

Pour terminer, Commitlint, associé à husky et commitizen, nous permet de qualifier et normer au mieux nos messages de commits. On gagne alors en confort grâce à l’automatisation, on uniformise et gagne en qualité grâce au respect de la convention, on favorise la lisibilité et le suivi du projet avec un historique désormais clair et efficace.