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

Mise à jour le 28 juin 2022, 21:48

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 ;
  • pre-push : dernière vérification avant partage (envoi au serveur).

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 (version 7) dont le but est d’enrober les hooks (en les créant si nécessaire) et d’intégrer leur gestion dans un sous-répertoire .husky à la racine du projet (husky fait appel implicitement à core.hooksPath).

Pour te faciliter la tâche on t’a gentiment créé un gabarit de projet GitHub 😘 : deliciousinsights/dev-automation.

Si tu préfères une mise en place « à la mano », voici comment procéder :

Dans ton terminal :

# Installation
npm install --save-dev husky
# Création du répertoire `.husky`
npx husky install
# Pour éviter que tes collègues oublient d’initialiser les hooks
npm set-script prepare "husky install"

Puis, toujours dans ton terminal, selon l’outillage que tu souhaites mettre en place, indique à husky quels scripts tu veux exécuter, pour quelles actions Git :

# Créera le fichier .husky/pre-commit avec les scripts demandés
husky add .husky/pre-commit "npx --no-install lint-staged && npx --no-install git-precommit-checks"
# Créera le fichier .husky/commit-msg avec les scripts demandés
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
# Créera le fichier .husky/pre-push avec les scripts demandés
npx husky add .husky/pre-push "npx --no-install validate-branch-name"

Un avantage non négligeable d’avoir les scripts husky intégrés au projet tient à ce que je peux désormais appeler des scripts propres au projet. Si j’ai créé des scripts dans un sous-répertoire git-hooks je peux les appeler depuis ma configuration husky. Voici un exemple pour le pre-commit :

#!/bin/sh
source "$(dirname "$0")/_/husky.sh"

npx --no-install lint-staged
npx --no-install git-precommit-checks
./git-hooks/check-stage.js

Note : 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 toi, 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é quelle que soit la personne qui contribue. Plusieurs modules npm existent à cet effet, mais un des rares qui fasse bien le travail (à savoir traiter juste les contenus stagés) est lint-staged (j'utilisais auparavant precise-commits, mais le projet n’est malheureusement plus maintenu).

Dans ton terminal :

# Installation
npm install --save-dev lint-staged
# Configuration : fichier `lint-staged.config.js` dans lequel
# on précise qu’on souhaite déclencher Prettier.
echo "module.exports = { '*.js': 'prettier --write' }" > lint-staged.config.js

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’errreur 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.

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

Il faut ensuite créer le fichier git-precommit-checks.config.js dans lequel tu définiras tes règles. Voici un exemple :

module.exports = {
  display: {
    notifications: true,
    offendingContent: true,
    rulesSummary: false,
    shortStats: true,
    verbose: false,
  },
  rules: [
    {
      message: 'Tu as des marqueurs de conflits qui traînent',
      regex: /^[<>|=]{4,}/m,
    },
    {
      message:
        'Arrêt du commit : tu as renseigné des choses qui ne doivent pas être commitées !',
      regex: /do not commit/i,
    },
    {
      message: 'Aurais-tu oublié de terminer certaines tâches ?',
      nonBlocking: true,
      regex: /(?:FIXME|TODO)/,
    },
    {
      message: 'On dirait bien que tu as laissé un "if (true)" quelque part',
      regex: /if\s+\(?(?:.*\|\|\s*)?true\)?/,
    },
    // JS specific
    {
      filter: /\.js$/,
      message:
        '😫 On dirait que tes auto-imports ont déconné avec les composants ou styles Material-UI',
      regex: /^import \{ .* \} from '@material-ui\//,
    },
    {
      filter: /\.js$/,
      message: '🤔 Hum ! N’as-tu pas oublié de retirer du "console.log(…)" ?',
      nonBlocking: true,
      regex: /^\s*console\.log/,
    },
    // Ruby/Rails specific
    {
      filter: /_spec\.rb$/,
      message: 'Tu as laissé traîner un "focus" dans tes tests RSpec',
      regex: /(?:focus: true|:focus => true)/,
    },
    {
      filter: /_spec\.rb$/,
      message:
        'Tu as laissé un appel à `save_and_open_page` dans tes tests Ruby',
      regex: /save_and_open_page/,
    },
    {
      filter: /\.rb$/,
      message:
        'Ça sent l’oubli après un debug manuel : regarde ce `binding.pry` qui traîne',
      regex: /^[^#]*\bbinding\.pry/,
    },
  ],
}

Une fois ceci en place, voici le type d’affichage qu’on peut obtenir dans le terminal en tentant un 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!

  === Aurais-tu oublié de terminer certaines tâches ? ===
  src/App.js
  src/utils/song.js

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

  === Tu as des marqueurs de conflits qui traînent ===
  src/components/Player.js

Garantir un message bien rédigé

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

Chez Delicious Insights on a choisi pour nos messages de commits de suivre la norme des commits conventionnels (héritée du conventional changelog).

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.

# Installation
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# Création du fichier de configuration commitlint.config.js
echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > commitlint.config.js

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

# Première tentative : message mal formaté
$ 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)


# Deuxième 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 tu as une mémoire de poisson rouge et tu ne te rappeles pas comment saisir ton message de commit, alors tu seras heureux de te faire assister dans ta 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 :

# Installation (en global nécessaire pour exposer la sous-commande Git "cz")
npm install --global commitizen
# Lancer l’assistant (au lieu de faire un `git commit`)
git cz

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

Des branches bien nommées

Tout comme pour nos messages de commits, on aime que nos branches soient bien nommées. Il n’existe hélas pas (encore) de nommage conventionnel des branches. Ce détail ne nous arrête pas car nous avons nos habitudes et conventions internes, alors faisons en sorte qu’elles soient respectées.

Une fois de plus on a trouvé un utilitaire à cet effet dans le chapeau magique de npm ✨ : validate branch name.

Sa mise en place est rapide :

# Installation
npm install --save-dev validate-branch-name

Et on configure le fichier .validate-branch-namerc.js en renseignant les motifs autorisés :

module.exports = {
  pattern: '^(main|staging|production)$|^(bump|feat|fix|rel(?:ease)?)/.+$',
  errorMsg:
    '🤨 Malheureux ! La branche que tu essaies de pusher ne respecte pas la norme.',
}

Voici ce que ça donne si on pushe une branche mal nommée :

$ git push -u origin tagada-pouet-pouet

  🤨 Malheureux ! La branche que tu essayes de pusher ne respecte pas la norme.
  Branch Name: "tagada-pouet-pouet"
  Pattern:"/^(main|staging|production)$|^(bump|feat|fix|rel(?:ease)?)\/.+$/g"

  husky - pre-push hook exited with code 1 (error)
  error: failed to push some refs to 'github.com:deliciousinsights/super-timor.git'

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

On voit à quel point l’automatisation s’améliore 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… À toi d’analyser tes contraintes et d’appliquer les solutions adaptées !

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

Découvrez nos cours vidéo ! 🖥

Nos cours vidéo sont un complément idéal et bon marché à nos articles techniques et formations présentielles. Autour de Git, de JavaScript et d’autres sujets, retrouvez des contenus de très grande qualité à des prix abordables, spécialement conçus pour lever vos plus gros points de blocage.