Cet article est la première partie d’une série d’articles portant sur “l’introduction de hooks de pre-commit à un projet Python / Django.
- Partie I: Les Git hooks et la librairie pre-commit
- Partie II: Utilisation de librairies de formatage de code en tant que hooks de pre-commit
- Partie III: Utilisation de linters de code en tant que hooks de pre-commit
- Partie IV: Déclenchement de scripts personnalisés en tant que hooks de pre-commit.
Table des matières
À propos des Git hooks
Git dispose d’un mécanisme permettant le déclenchement de scripts lors de certaines actions, cette fonctionnalité est appelée les Git Hooks. En informatique, un hook (littéralement “crochet” ou “hameçon”) est un mécanisme qui permet à l’utilisateur d’un logiciel d’en personnaliser son comportement en lui faisant réaliser des actions supplémentaires à des moments déterminés.
A titre d’exemple, les hooks suivants sont déclenchés lors de l’exécution de la commande git commit:
pre-commit: le premier hook appelé au cours de l’exécution de la commande (avant même que l’utilisateur ne renseigne un message de commit). Il peut être utilisé pour réalisé des vérifications sur les fichiers modifiés, pour les reformater, etc …prepare-commit-msg: vous permet d’éditer un message de commit avant de l’afficher à l’auteur du commit.commit-msg: vous permet de réaliser une validation du message de commit (utile si vous avez défini des règles à suivre concernant la rédaction des messages de commit sur votre projet).
La liste complète des hooks mis à disposition par Git est assez longue! Certains hooks sont utilisés coté client (lorsque le développeur réalise une action) alors que d’autres sont utilisés coté serveur (lorsque un événement se produit sur le serveur distant).
L’un des hooks les plus populaire, et peut-être l’un des plus utile dans le contexte d’une équipe de développeurs, est le hook de pre-commit. Dans cet article, nous verrons comment mettre en place des hooks de pre-commit dans le contexte d’un projet Python (et Django) avec pour objectifs:
- De simplifier leur mise en place pour tout contributeur travaillant sur le projet.
- De prémunir les développeurs contre des erreurs courantes de programmation tout en harmonisant le format de leur code, et ce, avant même qu’ils ne le poussent sur le dépôt Git.
Les sources ayant servi à la rédaction de cet article sont disponibles sur Github. N’hésitez pas à pull le projet de test et à le modifier afin de tester le comportement des hooks de pre-commit.
Le hook de pre-commit
Comme nous l’avons vu plus haut, le hook de pre-commit est le premier hook déclenché pendant l’exécution de la commande git commit. Les cas d’usage les plus courants de ce hook sont de:
- Formater automatiquement le code modifié par le commit (par exemple en utilisant black). Les modifications apportées lors de l’exécution du hook de pre-commit devront alors être validées par le développeur et ajoutées à son commit.
- Procéder à des vérifications sur ce même code en utilisant des linters (par exemple flake8) afin de détecter les erreurs de code courantes. Si une erreur est trouvée par un linter lors de l’exécution du hook, alors le commit est annulé et le développeur doit résoudre l’erreur afin d’être en mesure de commit ses changements.
Comme nous le verrons plus tard dans un article suivant, il est aussi possible d’exécuter des vérifications sur l’ensemble du projet, indirectement liées au code modifié.
Bien entendu, il est tout à fait possible d’utiliser des linters et formateurs de code:
- Manuellement (en ligne de commande, depuis un terminal).
- Au travers d’un IDE (tel que PyCharm ou VisualStudio).
Mais l’utilisation de hooks de pre-commit vous permet de normaliser l’usage de linters et de formateurs de code au sein d’une équipe, vous assurant que les mêmes vérifications sont lancées pour chacun de vos collaborateurs, et ce, quel que soit leur environnement de travail ou l’éditeur de code qu’ils utilisent.
Il faut toutefois noter que l’utilisation de hooks de pre-commit ne remplace en aucun cas les vérifications qui pourraient être faites dans le cadre d’un processus d’intégration continu (couramment appelé CI). Les hooks doivent être considérés comme optionnels parce que les développeurs ne peuvent être forcés à les utiliser (l’option --no-verify de la commande git commit permet d’outrepasser leur exécution). Cependant, l’utilisation de hooks de pre-commit fera gagner un temps précieux à votre équipe lors des revues de code, les développeurs pouvant alors se concentrer sur la logique du code plutôt que d’avoir à signaler des erreurs de syntaxe, du code mal formaté ou autre.
L’utilisation de hooks de pre-commit doit être considérée comme une aide au développement.
Les hooks Git sont des scripts ordinaires qui sont placés dans le dossier .git/hooks à la racine de votre dépôt Git en local:
foo@bar:~$ ls -l .git/hooks
total 64
-rwxr-xr-x 1 pierre pierre 478 Mar 4 11:33 applypatch-msg.sample
-rwxr-xr-x 1 pierre pierre 896 Mar 4 11:33 commit-msg.sample
-rwxr-xr-x 1 pierre pierre 4726 Mar 4 11:33 fsmonitor-watchman.sample
-rwxr-xr-x 1 pierre pierre 189 Mar 4 11:33 post-update.sample
-rwxr-xr-x 1 pierre pierre 424 Mar 4 11:33 pre-applypatch.sample
-rwxr-xr-x 1 pierre pierre 641 Mar 4 11:40 pre-commit
-rwxr-xr-x 1 pierre pierre 1643 Mar 4 11:33 pre-commit.sample
-rwxr-xr-x 1 pierre pierre 416 Mar 4 11:33 pre-merge-commit.sample
-rwxr-xr-x 1 pierre pierre 1492 Mar 4 11:33 prepare-commit-msg.sample
-rwxr-xr-x 1 pierre pierre 1374 Mar 4 11:33 pre-push.sample
-rwxr-xr-x 1 pierre pierre 4898 Mar 4 11:33 pre-rebase.sample
-rwxr-xr-x 1 pierre pierre 544 Mar 4 11:33 pre-receive.sample
-rwxr-xr-x 1 pierre pierre 2783 Mar 4 11:33 push-to-checkout.sample
-rwxr-xr-x 1 pierre pierre 3650 Mar 4 11:33 update.sample
Tout fichier suffixé par .sample est généré automatiquement par Git, à titre d’exemple que vous pouvez réutiliser, mais n’est pas exécuté tant qu’il porte cette extension.
Voici un exemple de fichier .git/hooks/pre-commit valide:
#!/usr/bin/env bash
# If any command fails, exit immediately with that command's exit status
set -eo pipefail
flake8
Ce script va automatiquement lancer flake8 à chaque fois que vous utilisez la commande git commit. Pour le moment ce script est très simple mais, si vous désirez ajouter plus de vérifications, il risque rapidement de devenir très complexe. Certaines vérifications pourraient aussi se révéler assez difficile à écrire …
La librairie pre-commit
Heureusement, il est possible d’utiliser la librairie nommée pre-commit afin de maintenir plus facilement les actions à déclencher lors d’un commit. Cette librairie se décrit comme “A framework for managing and maintaining multi-languages pre-commit hooks”, ce qui peut être traduit par: un “cadre” permettant de gérer et de maintenir des hooks de pre-commit et ce quelque soit le langage de programmation utilisé. La libraire utilise Python pour fonctionner mais vous pouvez l’utiliser sur tout type de projet.
Elle peut être installée de la manière suivante:
$ pip install pre-commit
Les hooks doivent être listés dans un fichier de configuration nommé .pre-commit-config.yaml. Il est possible d’utiliser la ligne de commande pre-commit afin de générer un exemple:
$ pre-commit sample-config
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
Cet exemple étant une bonne base pour travailler, vous pouvez l’utiliser pour créer votre fichier .pre-commit-config.yaml:
$ pre-commit sample-config >> .pre-commit-config.yaml
Vous pouvez maintenant lancer la commande pre-commit install afin de mettre en place le script du hook de pre-commit:
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
Pour terminer, il est possible de lancer les hooks listés dans ce fichier, sur l’ensemble des fichiers du projet, en utilisant pre-commit run --all-files. C’est très utile lors de l’ajout de nouveaux hooks afin de tester leur comportement et bonne intégration à la base de code.
$ pre-commit run --all-files
Il est important de noter que dans le cadre d’un commit la plupart des hooks s’exécuteront en ne ciblant que les fichiers modifiés.
La syntaxe du fichier de configuration
Comme dit plus haut, la librairie pre-commit est conçue comme un “framework”. Cela apporte:
- Une unique manière de décrire une liste de hooks dans un fichier de configuration, écrit en
yaml. - Une manière générique d’importer et de lancer des scripts qui seront exécutés en tant que hooks de pre-commit (nommés “plugins” et décrits en utilisant le mot clé
repo).
Cette section ne décrira pas en profondeur les détails de la syntaxe du fichier .pre-commit-config.yam. Nous aurons l’opportunité de voir, plus tard, quelques mots clés au cours de l’intégration de nouveaux hooks. N’hésitez pas à lire la section “Ajouter des plugins pre-commit à votre projet” de la documentation de pre-commit, mais, pour le moment, vous avez simplement besoin de savoir que:
- Un fichier de configuration
.pre-commit-config.yamldécrit une liste derepos. - Chaque
reporeprésente un dépôt Git distant.- Il doit déclarer un attribut
revqui représente la révision (ou le tag) à utiliser. - Sa valeur peut être
local, vous permettant d’exécuter un script faisant partie de votre code source (dans cette situation, renseignerrevn’est pas nécessaire). - Chaque
repodoit déclarer une liste dehooks, décrivant la liste des hooks de pre-commit à exécuter (un même repository peut contenir plus d’un hook).- Chaque hook à exécuter est défini par un
id.
- Chaque hook à exécuter est défini par un
- Il doit déclarer un attribut
Les hooks mis à disposition par la librairie
Voici un exemple utilisant un des hooks disponible depuis le dépôt git de la librairie pre-commit:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3
hooks:
- id: trailing-whitespace
La librairie met à disposition son propre dépôt Git, contenant un certain nombre de hooks déjà prêts à l’emploi. Cette section va lister ceux que j’utilise le plus souvent, mais, n’hésitez pas à en ajouter plus à votre projet! Vous pouvez aussi jeter un oeuil à la liste complète des hooks disponibles sur le dépot Git de pre-commit.
check-yaml: s’assure que la syntaxe des fichiers.yamlsoit correct et valide leur format. À titre d’exemple, cela peut permettre d’éviter l’introduction d’erreurs de syntaxe dans un fichier.pre-commit-config.yaml. Des hooks similaires existent aussi pour d’autres types de fichiers:jsontomlxmlcheck-added-large-files: prévient le risque que des fichiers trop volumineux ne soient ajoutés à un commit.check-merge-conflict: vérifie que les fichiers modifiés ne contiennent pas de conflits de merge (qui auraient été oubliés par le développeur).debug-statements: vérifie que le code ne contient pas d’imports de debug ou d’instructionsbreakpoint().detect-private-key: détecte la présence de clés privées.- Si vous utilisez AWS, il peut être intéressant d’ajouter le hook
detect-aws-credentials. end-of-file-fixer: s’assure que tout fichier est vide ou bien se termine par un retour à la ligne.trailing-whitespace: retire les tabulations et espaces non nécessaires à la fin d’une ligne.
A titre d’exemple, vous pouvez consulter le commit du dépôt de test qui ajoute le fichier .pre-commit-config.yaml.
Prochaines étapes
Dans cet article nous avons:
- Vu en détail la mécanique de Git hooks et notamment le hook de pre-commit.
- Ajouté la librairie pre-commit à notre projet dans le but de pouvoir mettre en place une liste de pre-commit, de manière simple, sur l’environnement de travail de tous les contributeurs du projet.
- Ajouté nos premiers hooks en utilisant le dépôt Git de la librairie pre-commit.
N’hésitez pas à jeter un oeuil au dépôt Git lié à cet article. Les articles suivant vont détailler l’ajout des types de hooks suivants:
- formatters: des hooks qui vont automatiquement formater les fichiers modifiés par un commit.
- linters: des hooks qui vont effectuer des vérifications sur ces fichiers.
- local checks: des hooks qui vont lancer des scripts, provenant de votre code source, chaque fois que nous effectuons un commit.