Hook’il est mon beau commit ?
Par Delicious Insights • Publié le 20 déc. 2018

Cet article est également disponible en anglais.

Lorsque je développe et que je partage mon historique sur des projets, j’aime me rassurer en me disant que ce que je produis est clair, utile et aussi optimal que possible.

Auparavant, quand je regardais un historique, je constatais parfois qu'il était difficile à lire, à analyser, donc pas ou peu exploitable. Désormais, je vise des commits de qualité, pas un ramassis de fix stuff en mode fourre-tout !

Par « commit de qualité », j’entends 2 choses :

  • je veux un contenu et un code optimaux ;
  • l’historique résultant doit être précis et exploitable.

Bien entendu, en bon fainéant, je ne souhaite pas devoir me poser ces questions pour chaque commit, donc je vais automatiser un max.

Ça tombe bien, pour ça j’ai Git et ses hooks :

  • pre-commit : pour analyser, éventuellement retravailler mon code ;
  • commit-msg : pour vérifier mon message de commit ;

Mettre en place tout ça facilement et partager…

En revanche, Git peine à nous proposer l’automatisation de la mise en place au sein d’un projet ou d’une équipe (malgré la 2.9 et son git config core.hooksPath …).

Pour palier à ça on va utiliser npm et un super module nommé husky dont le but est d’enrober les hooks (en les créant si nécessaire) et d’intégrer leur gestion dans le fichier de configuration package.json du projet.

Dans votre terminal :

npm install --save-dev husky

Dans votre package.json :

"husky": {
  "hooks": {
    "commit-msg": "…",
    "pre-commit": "…"
  }
},

Un avantage non négligeable est qu’on peut désormais appeler des scripts intégrés au projet. Par exemple imaginons que nous ayons créé des scripts dans un sous répertoire git-hooks. Je pourrais alors appeler mon script depuis ma configuration husky :

"pre-commit": "./git-hooks/check-stage.js"

Notez qu’on peut également chaîner les appels :

"pre-commit": "./git-hooks/check-stage.js && ./git-hooks/another-script"

NB: Je tiens à préciser qu’il n’est pas nécessaire de travailler avec du JavaScript pour mettre en place husky. L’avantage de cette approche est qu’elle fonctionne partout 😁.

Analyser le code avant le commit

Je ne sais pas pour vous, mais j’ai beau essayer d’être un super-héros-développeur, parfois je fais un peu n’importe quoi… J’oublie des choses dans mon code, j’oublie certaines conventions qu’on est censés utiliser au sein des projets (typiquement le formatage du code).

Peu m’importe les raisons, le résultat est le même : mon code ne respecte pas les conventions, avec des trucs qui traînent dedans qui ne devraient pas y être. Et en regardant un peu autour de moi je m’aperçois que ne suis pas le seul à faire ce genre d’erreur (ça rassure de savoir qu’on est tous capables de faire des erreurs 😅).

Reste donc à trouver des outils pour nous guider et nous corriger.

Mon deuxième cerveau (alias Christophe, mon boss) m’avait déjà trouvé un outil formidable capable d’auto-formater mon code dans plein de langages (JS, CSS, HTML, SCSS, Markdown, JSX…) : Prettier. On l’utilisait déjà dans notre éditeur VSCode (à la sauvegarde, on préfère). Mais au cas où il déconnerait, ou si un contributeur à mes projets décide d’utiliser un autre éditeur sans Prettier, alors je veux garantir que son code soit quand même reformaté.

L’idée est simple : il faut exécuter Prettier sur le code qui va être commité quel que soit la personne qui contribue. Plusieurs modules npm existent à cet effet, mais le seul qui fasse bien le travail (à savoir traiter juste les contenus stagés) est precise-commits.

Dans votre terminal :

npm install --save-dev precise-commits

Dans votre package.json :

"husky": {
  "hooks": {
    "commit-msg": "…",
    "pre-commit": "precise-commit"
  }
},

Il me reste encore à éviter de laisser traîner des trucs dans mon code. Pour ça je n'ai pas trop trouvé d’outil me permettant de configurer et d’étendre le comportement/l’analyse au sein de mon projet, alors j’ai créé le mien 🤘, et pour faire original je l’ai appelé git-precommit-checks.

Cet outil me permet d’empêcher le commit de se terminer (donc d’être créé) dès lors que dans les ajouts/modifications on trouve des marqueurs de conflits, des console.log… dans mes fichiers JS, etc. Des messages d’errreurs sont affichés sur la sortie standard pour décrire l’origine des problèmes et les fichiers concernés.

Je regarde également si j’ai laissé des FIXME ou TODO, auquel cas j’affiche juste un avertissement sans bloquer le commit.

Dans votre terminal :

npm install --save-dev git-precommit-checks

Dans votre package.json :

"husky": {
  "hooks": {
    "commit-msg": "…",
    "pre-commit": "git-precommit-checks && precise-commit"
  }
},
"hooks": {
  "pre-commit": [
    {
      "message": "You’ve got leftover conflict markers",
      "regex": "/^[<>|=]{4,}/m"
    },
    {
      "filter": "\\.js$",
      "message": "You’ve got leftover `console.log`",
      "regex": "console\\.log"
    },
    {
      "message": "You have unfinished devs",
      "nonBlocking": "true",
      "regex": "(?:FIXME|TODO)"
    }
  ]
},

Une fois ceci en place, voici le type d'affichage qu’on peut obtenir dans le terminal lors de la tentative de commit avec des textes filtrés :

$ git commit -m 'feat(demo): display pre-commit checks'

  husky > pre-commit (node v10.14.1)
  ✔  contents checks: there may be something to improve or fix!

  === You have unfinished devs ===
  src/App.js
  src/utils/song.js

  ✖  contents checks: oops, something’s wrong!  😱

  === You’ve got leftover conflict markers ===
  src/components/Player.js

Garantir un message bien rédigé

Bon là c’est plus difficile… ou pas !

Chez Delicious Insights on a choisi de respecter le conventional changelog pour nos messages de commits.

On a ajouté une petite astuce pour la lecture du log en utilisant une ellipse à la fin de nos messages dès lors qu’il y a plus d’une ligne de description (hors référence de ticket/issue).

Ça veut dire qu’on s’attend à avoir une structure de message bien précise (au moins pour la première ligne). Comme là aussi on est vite confrontés à des erreurs humaines, l’idée est d’être assistés.

Une fois de plus on a un module tout beau pour ça sur npm : commitlint. Cet outil vient analyser le texte saisi, et bloque le commit si les règles attendues ne sont pas respectées.

npm install --save-dev @commitlint/cli @commitlint/config-conventional

On doit également préciser dans le package.json qu'on utilise le conventional-changelog, et dire à husky qu’on veut l’utiliser :

"commitlint": {
  "extends": [
    "@commitlint/config-conventional"
  ]
},
"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
    "pre-commit": "git-precommit-checks && precise-commit"
  }
},

À l’usage, voici ce que ça peut donner :

# Première tentative : message mal formatté
$ git commit -m 'Bad message format'

  husky > commit-msg (node v10.14.1)
  ⧗   input:
  Bad message format

  ✖   message may not be empty [subject-empty]type may not be empty [type-empty]
  ✖   found 2 problems, 0 warnings
  husky > commit-msg hook failed (add --no-verify to bypass)


# Seconde tentative : structure correcte, clé "type" inconnue
$ git commit -m 'type(context): message type is unknow'

  husky > commit-msg (node v10.14.1)
  ⧗   input:
  type(context): message type is unknow

  ✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]
  ✖   found 1 problems, 0 warnings
  husky > commit-msg hook failed (add --no-verify to bypass)


# Dernière tentative : bien formaté, avec un type valable
$  git commit -m 'feat(context): this one is well formatted'

  husky > commit-msg (node v10.14.1)
  ⧗   input: feat(context): this one is well formatted
  ✔   found 0 problems, 0 warnings

Analyser le message après c’est bien, mais avant, c’est mieux !

Si comme moi vous avez une mémoire de poisson rouge et ne vous rappelez pas comment saisir votre message de commit, alors vous serez heureux de vous faire assister dans votre saisie.

L’outil commitlint déjà installé propose un assistant de saisie (@commitlint/prompt-cli), mais j’ai une préférence pour un autre, git commitizen qui nous permettra de faire git cz au lieu de git commit pour afficher l’assistant :

npm install --save-dev commitizen cz-conventional-changelog

On précise dans la configuration (package.json) qu'on veut utiliser le conventional-changelog :

  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }

Voici le résultat obtenu en utilisant commitizen :

$ git cz
  cz-cli@2.9.6, cz-conventional-changelog@1.2.0


  Line 1 will be cropped at 100 characters. All other lines will be wrapped after 100 characters.

  ? Select the type of change that you're committing:
    docs:     Documentation only changes
    style:    Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
    refactor: A code change that neither fixes a bug nor adds a feature
  ❯ perf:     A code change that improves performance
    test:     Adding missing tests or correcting existing tests
    build:    Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
    ci:       Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
  (Move up and down to reveal more choices)

  ? Denote the scope of this change ($location, $browser, $compile, etc.):
  context

  ? Write a short, imperative tense description of the change:
  this is a good description

  ? Provide a longer description of the change:

  ? List any breaking changes or issues closed by this change:
  Close #42

  husky > commit-msg (node v10.14.1)
  ⧗   input: feat(context): this is a good description
  ✔   found 0 problems, 0 warnings

  [master 72015cc] feat(context): this is a good description

Et voilà, ceinture et bretelles pour tout le monde !

On voit à quel point l’automatisation se simplifie avec les années. J’apprécie particulièrement le partage et la configuration au sein du projet !

Bien évidemment on peut aller encore plus loin et passer un correcteur orthographique, notifier le problème via une notif système ou une synthèse vocale… À vous d’analyser vos contraintes et d’appliquer les solutions adaptées !

Pour une vision plus large sur le fonctionnement des hooks vous pouvez consulter notre article précédent sur le sujet. J’ai également parlé de tout ça lors de mon talk à Paris Web 2018.