Écrire de bons messages de commit dans Git

1. Vue d’ensemble

Le message de commit est la chose que tu relis le plus souvent lors du debug, de la review de code ou de l’écriture du changelog — plus que le code que tu viens d’écrire. Cet article t’aide à maîtriser la structure standard des messages de commit, appliquer Conventional Commits, et vérifier automatiquement les messages avec commitlint + husky. À la fin, tu écriras des commits clairs et cohérents, et ton équipe pourra générer automatiquement le changelog à partir de l’historique git.

2. Pourquoi le message de commit est important

Un bon message de commit répond à 3 questions: qu’est-ce qui change, pourquoi ça change, et quel est l’impact. Sans cela, tu dois faire git show sur chaque commit pour deviner l’intention de l’auteur — ça te fait perdre beaucoup de temps comparé à écrire un message correct dès le départ.

Comparons deux messages pour le même changement:

# Mauvais
fix bug

# Bon
fix(payment): gérer le null pointer quand PaymentService.charge() reçoit une Order sans Customer

Le mauvais message ne dit pas quel bug, où. Le bon message indique le module (payment), l’action (fix), et la cause spécifique — on comprend en 5 secondes de lecture.

3. Structure standard d’un message de commit

Git recommande une structure en 3 parties: subject (titre), body (contenu), footer (pied de page).

<type>(<scope>): <subject>

<body>

<footer>
  • subject: résumé court, ≤ 50 caractères, à l’impératif (“add” pas “added”).
  • body: explique pourquoi on change, pas quoi (le diff dit déjà quoi).
  • footer: métadonnées — breaking change, issue associée, co-author.

Exemple complet:

fix(order): éviter la double-charge lors de la retry de paiement

PaymentService.charge() n'a pas de clé d'idempotence donc quand il y a un
timeout réseau et que le client réessaie, le système appelle la passerelle API
deux fois pour la même Commande.

Ajoute idempotencyKey = order.getId() + "-" + attemptCount à l'en-tête de la
requête pour que la passerelle déduplique automatiquement.

Closes #482

3.1. Ligne subject

La ligne subject est la plus importante — elle s’affiche dans git log --oneline, dans le titre de la PR, dans le changelog. Règles:

# Mauvais - majuscule initiale, point final, temps passé
Fixed the bug in order service.

# Bon - minuscule, pas de point, impératif
fix: order n'enlève pas le stock quand le paiement passe par portefeuille électronique

Note: La limite de 50 caractères n’est pas une loi immuable, mais dépasser 72 caractères sera tronqué sur plusieurs terminaux et sur l’interface GitHub. Essaie d’être concis.

3.2. Partie body

Le body sert à expliquer le contexte — ce que le diff de code ne peut pas dire: pourquoi tu as choisi cette solution, pourquoi les autres ont été rejetées, y a-t-il des effets de bord.

refactor(cart): passer CartService du mode synchrone à asynchrone

Avant, CartService.checkout() appelait InventoryService et ShippingService
séquentiellement, latence totale ~800ms. Passe à CompletableFuture pour
exécuter ces deux appels en parallèle, réduis la latence à ~450ms.

L'API publique ne change pas, seule l'implémentation interne est modifiée.

Sépare le subject et le body par une ligne vide — git et tous les outils qui parsent les commits se basent sur cette convention.

4. Conventional Commits

Conventional Commits est le standard le plus courant actuellement, il définit des type fixes pour que les outils puissent classifier automatiquement et générer le changelog.

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Les type couramment utilisés:

Type Signification
feat ajouter une nouvelle fonctionnalité
fix corriger un bug
docs uniquement des changements de documentation
style formatage de code, pas de changement logique
refactor restructurer le code, pas de changement comportement
test ajouter/modifier des tests
chore tâches diverses (build, dépendances, config)
perf améliorer les performances

Exemple avec scope comme nom du module:

feat(auth): supporter la connexion via OAuth2 Google

Ajoute GoogleOAuth2Provider implémentant OAuth2LoginProvider.
Configure client-id, client-secret dans application.properties.

4.1. Breaking change

Quand tu fais un changement qui casse la rétrocompatibilité, marque-le avec ! après type/scope ou dans le footer BREAKING CHANGE:.

feat(api)!: changer la réponse de GET /orders/{id} en OrderDto

BREAKING CHANGE: le champ "customerName" devient un objet "customer"
contenant id, name, email. Le client doit mettre à jour le parser.

Semantic-release et les outils CI/CD se basent sur la ligne BREAKING CHANGE: pour augmenter automatiquement la version majeure.

5. Vérifier automatiquement les messages de commit

Une bonne convention n’est utile que si l’équipe la suit de manière cohérente. Utilise commitlint pour bloquer les commits au mauvais format localement, avant le push.

<!-- Ne s'applique pas aux projets Node, juste un exemple si tu utilises Maven wrapper pour husky-git-hooks -->

En réalité commitlint s’exécute sur Node, installe-le via npm:

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

Crée le fichier de configuration:

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

Attache le hook commit-msg via husky:

npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg

Essaie maintenant de faire un commit au mauvais format:

git commit -m "fixed bug"
⧗   input: fixed bug
✖   le subject ne peut pas être vide [subject-empty]
✖   le type doit être l'un des suivants [feat, fix, docs, style, refactor, test, chore, perf]
✖   trouvé 2 problèmes, 0 avertissements
husky - le script commit-msg a échoué (code 1)

Le commit est bloqué immédiatement, tu es obligé de corriger le format avant de créer le commit.

5.1. Utiliser avec un projet Java/Spring Boot

Si le projet utilise Maven, tu peux toujours utiliser husky + commitlint en parallèle (ils nécessitent seulement Node runtime, ils ne touchent pas au build Java). Ou si tu veux rester en pur Java, écris un script simple de vérification et appelle-le via core.hooksPath:

#!/bin/bash
# .githooks/commit-msg
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf)(\([a-z-]+\))?: .{1,50}"
MSG=$(cat "$1")

if ! [[ "$MSG" =~ $PATTERN ]]; then
  echo "Le message de commit ne respecte pas le format Conventional Commits" >&2
  exit 1
fi
git config core.hooksPath .githooks
chmod +x .githooks/commit-msg

5.2. Écrire des tests pour le script de vérification du format

Si tu maintiens ton propre script de vérification (sans commitlint), tu devrais avoir des tests pour la regex. Supposons que tu encapsules la logique de parsing dans une classe Java pour la réutiliser dans la CI:

public class CommitMessageValidator {

    private static final Pattern PATTERN =
        Pattern.compile("^(feat|fix|docs|style|refactor|test|chore|perf)" +
                         "(\\([a-z0-9-]+\\))?: .{1,50}");

    public boolean isValid(String message) {
        String subject = message.lines().findFirst().orElse("");
        return PATTERN.matcher(subject).find();
    }
}
class CommitMessageValidatorTest {

    private final CommitMessageValidator validator = new CommitMessageValidator();

    @Test
    void shouldAcceptConventionalFormat() {
        boolean result = validator.isValid("fix(order): éviter la double-charge lors de la retry");

        assertThat(result).isTrue();
    }

    @Test
    void shouldRejectMessageWithoutType() {
        boolean result = validator.isValid("fixed the bug");

        assertThat(result).isFalse();
    }

    @Test
    void shouldRejectUnknownType() {
        boolean result = validator.isValid("update: ajouter quelque chose");

        assertThat(result).isFalse();
    }
}

Ce test assure que la logique de validation n’a pas de régression quand tu modifies la regex plus tard — par exemple, ajouter un nouveau type mais oublier de mettre à jour le test.

6. Erreurs courantes

Commit trop gros, message générique. “update code” pour un commit qui modifie 15 fichiers différents est un signe que tu devrais le diviser en plusieurs commits plus petits, chacun avec un seul objectif clair (atomic commit).

Mélanger plusieurs types de changements dans un commit. Faire un fix et un refactor non lié dans le même commit rend git revert et git bisect difficiles à utiliser — revert un fix pourrait entraîner la perte de toute la partie refactor.

Écrire un body qui répète le subject. Le body doit ajouter du contexte, pas réitérer la première ligne avec des phrases plus longues.

# Répétitif, sans valeur
fix(order): corriger le bug du calcul du montant total

Corriger le bug du calcul du montant total dans OrderService.

Utiliser des emoji/caractères de décoration au lieu du contenu. Les emoji ne sont pas mauvais, mais si tu les utilises pour remplacer une vraie description, le changelog généré automatiquement sera vide d’informations.

Note: Conventional Commits convient aux équipes ayant une CI/CD qui génère automatiquement le changelog ou utilisant le versioning sémantique. Si l’équipe est petite avec peu de commits, appliquer la règle trop strictement peut créer des frottements inutiles — privilégie la clarté à la conformité mécanique du format.

7. Conclusion

Un bon message de commit n’a pas besoin d’être long, il suffit d’avoir la bonne structure: subject concis à l’impératif, body expliquant pourquoi, footer notant les métadonnées nécessaires. Appliquer Conventional Commits aide l’équipe à lire l’historique git plus rapidement et permet aux outils d’automatiser le changelog et la versioning. Configure commitlint ou des git hooks personnalisés pour transformer la convention en contrainte automatique, en évitant de dépendre de la conscience de chacun dans l’équipe.

gitconventional-commitsbest-practicescommitlintdevops