Git Flow et Conventional Commits : structurer son workflow sans se compliquer la vie

Abdoul Kadri ASSOUMANE MAMANE
Abdoul Kadri ASSOUMANE MAMANE Auteur
13 min de lecture
1,362 vues

Écouter l'article

Sans convention de branches sur un dépôt partagé, chacun pousse sur main quand il veut. Imaginons un scénario illustratif : deux fonctionnalités incompatibles sont mergées en production le même jour, et le site tombe. Un workflow Git défini avant de démarrer un projet limite ce genre de situation. L'une des stratégies les plus connues s'appelle Git Flow.

Cet article présente comment structurer ses branches, rédiger ses commits et organiser ses pull requests. Ce ne sont pas des règles absolues, mais des pratiques qui limitent les situations chaotiques. Les commandes ont été exécutées sur un dépôt de test avec Git 2.43 (branches, fusions --no-ff, tags annotés, git stash push -m, --force-with-lease).

Deux branches qui ne meurent jamais

Git Flow repose sur deux branches permanentes. main contient le code en production : ce qui tourne sur le serveur, en ce moment. develop contient les fonctionnalités validées qui attendent la prochaine release.

La branche main est protégée. Personne ne pousse directement dessus. Elle ne reçoit du code que par des merges depuis release/* ou hotfix/*. Si main est cassée, la production est cassée. Cette séparation garantit que le code en production a traversé un processus de validation.

La branche develop est la base de travail quotidienne. Elle contient les dernières fonctionnalités intégrées. Quand un développeur commence quelque chose de nouveau, il part de develop, pas de main. main reflète ce que les utilisateurs voient, develop reflète ce que l'équipe a terminé.

Les feature branches : une fonctionnalité, une branche

Chaque fonctionnalité est développée dans sa propre branche. Le nommage feature/nom-explicite n'est pas cosmétique : quand il y a 15 branches actives, la différence entre feature/user-authentication et branch3 est la différence entre comprendre et deviner.

# Partir de develop, toujours
git checkout develop
git pull origin develop
git checkout -b feature/password-reset

# Travailler, committer régulièrement
# On ajoute les fichiers explicitement (ou git add -p pour choisir les morceaux)
git add app/Http/Controllers/Auth/PasswordResetController.php resources/views/auth/reset.blade.php
git commit -m "feat(auth): add password reset form"
git add app/Services/PasswordResetTokenService.php
git commit -m "feat(auth): implement token generation and email sending"
git add tests/Feature/PasswordResetTest.php
git commit -m "test(auth): add password reset validation tests"

# La fonctionnalité est terminée : merger dans develop
git checkout develop
git pull origin develop
git merge --no-ff feature/password-reset
git push origin develop
git branch -d feature/password-reset

On évite git add . : il ajoute tout ce qui traîne dans le répertoire de travail (fichiers de debug, .env local, dumps), et le commit perd son sens. Nommer les fichiers, ou utiliser git add -p, force à relire ce qu'on s'apprête à committer.

Le --no-ff force un commit de merge même si Git pourrait faire un fast-forward. Sans lui, l'historique perd la trace de l'intégration de la feature. Avec --no-ff, un git log --graph montre clairement les branches et leurs points de fusion.

Le git pull origin develop avant le merge est une habitude défensive. Si un collègue a poussé quelque chose sur develop pendant le développement de la feature, on préfère résoudre les conflits localement plutôt que sur le serveur après un push.

Les release branches : préparer la mise en production

Quand develop contient assez de fonctionnalités pour une nouvelle version, on crée une branche de release. C'est un sas de décompression entre le développement et la production.

git checkout develop
git checkout -b release/2.1.0

# Derniers ajustements : mise à jour de la version, corrections mineures
# (les fichiers concernés sont modifiés puis ajoutés avant chaque commit)
git commit -m "chore: bump version to 2.1.0"
git commit -m "fix: correct typo in welcome email"

# Merger dans main ET dans develop
git checkout main
git merge --no-ff release/2.1.0
git tag -a v2.1.0 -m "Version 2.1.0: password reset, newsletter, blog redesign"
git push origin main --tags

git checkout develop
git merge --no-ff release/2.1.0
git push origin develop
git branch -d release/2.1.0

Si main est protégée côté plateforme (GitHub, GitLab), le merge vers main passe en pratique par une pull request plutôt que par un git push direct. La logique reste identique.

La branche de release ne reçoit que des corrections mineures : typos, numéros de version, petits bugs découverts en dernière minute. Pas de nouvelles fonctionnalités. Si quelqu'un veut ajouter "juste un petit truc" dans la release, la réponse est non : ça ira dans la prochaine.

Le tag v2.1.0 avec un message descriptif est précieux six mois plus tard quand on se demande ce qui a changé entre deux versions. Sans tags, il faut reconstituer l'historique à partir des commits. Avec, un git tag -l donne la chronologie complète.

Le merge dans develop est facile à oublier, mais il est vital. Sans lui, les corrections faites dans la release (la typo de l'e-mail, par exemple) existeraient en production mais pas dans develop. La prochaine release réintroduirait le bug.

Les hotfix branches : quand la production brûle

Imaginons un bug critique découvert en production un vendredi soir. La règle du hotfix : partir de main, corriger, merger dans main ET dans develop, sans passer par une release branch pour livrer le correctif.

git checkout main
git checkout -b hotfix/sql-injection-fix

# Corriger le problème (fichiers modifiés puis ajoutés avant le commit)
git commit -m "fix(security): use bound parameters in search query"

# Merger en urgence dans main
git checkout main
git merge --no-ff hotfix/sql-injection-fix
git tag -a v2.1.1 -m "Hotfix: SQL injection in search"
git push origin main --tags

# Ne pas oublier develop
git checkout develop
git merge --no-ff hotfix/sql-injection-fix
git push origin develop
git branch -d hotfix/sql-injection-fix

Cas particulier : si une branche release/* est en cours au moment du hotfix, le correctif doit être mergé dans cette branche de release à la place de develop. La release sera ensuite mergée dans develop et le correctif y arrivera avec elle. Mergé uniquement dans develop, il serait absent de la release en préparation, et donc de la prochaine version livrée.

Le numéro de version passe de 2.1.0 à 2.1.1 : seul le numéro de patch change, ce qui indique une correction et non une fonctionnalité. Le semantic versioning (SemVer, MAJEUR.MINEUR.PATCH) sert à communiquer ce type d'information sans lire le changelog.

Certaines équipes font les hotfix directement sur main sans branche dédiée. Ça marche techniquement, mais on perd la traçabilité. Des mois plus tard, quand quelqu'un se demande pourquoi un commit est arrivé directement sur main, la branche hotfix/sql-injection-fix raconte l'histoire mieux qu'un commit isolé.

Conventional Commits : écrire des messages qui servent à quelque chose

"fix stuff", "updates", "wip" : ces messages de commit ne servent à personne. Quand on cherche quand un bug a été introduit avec git bisect, chaque commit devrait expliquer ce qu'il fait et pourquoi.

Les Conventional Commits proposent un format structuré :

type(scope): description courte

Explication détaillée si nécessaire.
Le corps du message explique le "pourquoi", pas le "quoi" : le diff montre déjà le quoi.

Refs: #142

Les types utilisés au quotidien :

  • feat : une nouvelle fonctionnalité visible par l'utilisateur. feat(blog): add article search by tag
  • fix : une correction de bug. fix(auth): prevent session fixation on login
  • refactor : du code réorganisé sans changer le comportement. refactor(models): extract slug generation into HasSlug trait
  • test : des tests ajoutés ou modifiés. test(blog): cover article pagination edge cases
  • docs : de la documentation. docs: update deployment checklist with shared hosting steps
  • chore : de la maintenance technique. chore: update composer dependencies

Le scope entre parenthèses est optionnel mais utile. Sur un projet avec un blog, un admin et une API, le scope dit tout de suite quelle partie est touchée. Dans un git log --oneline, fix(blog): resolve N+1 on article index est bien plus exploitable que fix: resolve error.

L'impératif présent dans la description ("add", "fix", "remove") suit la convention de Git lui-même : les messages auto-générés par git merge et git revert sont à l'impératif. Autant rester cohérent.

Lien avec le semantic versioning

Les types de commit se traduisent directement en numéro de version SemVer : un feat incrémente le numéro MINEUR (2.1.0 vers 2.2.0), un fix incrémente le PATCH (2.1.0 vers 2.1.1), et un ! après le type (feat(api)!: ...) ou un pied de message BREAKING CHANGE: incrémente le MAJEUR (2.1.0 vers 3.0.0). Des outils de release automatique s'appuient sur cette correspondance pour calculer la prochaine version et générer le changelog.

Pull Requests : le filet de sécurité avant le merge

Le code review via pull request est le dernier moment où quelqu'un d'autre que l'auteur regarde le code avant qu'il atteigne develop ou main.

Un template de PR bien conçu guide la revue :

## Contexte
Quel problème cette PR résout ? Lien vers l'issue si applicable.

## Changements
- Ajout du formulaire de réinitialisation de mot de passe
- Génération de token avec expiration de 60 minutes
- E-mail de reset avec lien sécurisé
- Tests couvrant les cas: token valide, token expiré, e-mail inexistant

## Type
- [x] Nouvelle fonctionnalité
- [ ] Correction de bug
- [ ] Refactoring
- [ ] Documentation

## Vérifications
- [x] Tests ajoutés et passent
- [x] Aucune régression sur les tests existants
- [x] Code conforme aux conventions du projet
- [ ] Documentation mise à jour si nécessaire

Le "Contexte" est la section la plus importante. Un reviewer qui ouvre une PR de 400 lignes sans comprendre le problème résolu ne peut pas faire une revue pertinente. Une phrase claire ("Les utilisateurs ne peuvent pas réinitialiser leur mot de passe si leur session a expiré") vaut mieux qu'un titre vague comme "auth improvements".

La checklist oblige l'auteur à vérifier ces points avant de demander la revue. Un "Tests ajoutés et passent" non coché signale au reviewer que la PR n'est pas prête.

Automatiser la vérification des conventions

Les conventions de commits ne valent rien si personne ne les vérifie. Un outil comme commitlint, intégré dans un hook pre-commit ou un workflow CI, rejette automatiquement les commits qui ne respectent pas le format :

# .github/workflows/pr-checks.yml
name: PR Checks
on: [pull_request]

jobs:
  lint-commits:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Validate commit messages
        uses: wagoid/commitlint-github-action@v6

Le fetch-depth: 0 récupère l'historique complet. Par défaut, actions/checkout ne fait qu'un checkout superficiel (un seul commit), ce qui ne permet pas à commitlint de lire tous les commits de la PR.

Cette automatisation évite les discussions répétitives en code review : plus besoin de commenter « merci de reformater le message de commit », la CI s'en charge.

Deux commandes à connaître

Au-delà du workflow, deux commandes servent régulièrement :

# Visualiser l'historique avec les branches
git log --oneline --graph --all --decorate

# Rebase interactif pour nettoyer ses commits locaux avant de merger
# (fusionne ou réordonne les 3 derniers commits)
git rebase -i HEAD~3

Pendant le développement, on commite souvent ("wip", "fix test", "oops typo"). Avant de proposer la PR, le rebase interactif permet de regrouper ces commits en un ou deux commits propres avec des messages explicites : l'historique de develop reste lisible, même si l'historique local était chaotique.

Une règle s'impose : ne jamais réécrire l'historique (rebase, commit --amend, push --force) d'une branche que d'autres personnes ont déjà récupérée. Le rebase interactif ne s'utilise que sur ses propres commits locaux, ou sur une feature branch dont on est le seul à dépendre. Si la branche a déjà été poussée, --force-with-lease reste préférable à un --force brut. Pour mettre un travail inachevé de côté en changeant de branche, git stash push -m "message" suffit : le message évite d'avoir une liste de stashes anonymes.

Adapter Git Flow à la réalité du projet

Git Flow dans sa version complète (avec les branches feature, release, hotfix, develop et main) peut être trop lourd pour un développeur solo ou une petite équipe. La complexité doit être proportionnelle à la taille de l'équipe et à la fréquence des déploiements.

Sur un projet solo, on simplifie : main pour la production, des feature branches pour les développements en cours, et des tags pour marquer les versions. Pas de branche develop séparée : main joue les deux rôles et on déploie après chaque merge.

Sur un projet avec 3 à 5 développeurs et des releases planifiées, le Git Flow complet prend tout son sens. La branche develop sert de tampon, les releases sont préparées proprement, et les hotfix ont un chemin clair.

Pour un produit déployé en continu, Git Flow est mal adapté. Le cycle develop → release → main introduit des étapes et des branches longues qui vont à l'encontre de livraisons fréquentes et petites. Vincent Driessen, l'auteur du modèle, l'a d'ailleurs précisé dans une note de réflexion datée du 5 mars 2020, ajoutée à son article d'origine : pour les applications web livrées en continu, un workflow plus simple, comme GitHub Flow (des feature branches mergées directement dans main, avec déploiement automatique), est préférable. Git Flow garde son intérêt pour les logiciels versionnés avec plusieurs versions maintenues en parallèle.

Choisir un workflow parce qu'il est populaire plutôt que parce qu'il résout un vrai problème conduit à de la cérémonie inutile. Git Flow répond au besoin de coordination entre développeurs et de stabilité des versions livrées. Seul sur un side-project, ce besoin est limité.

Le workflow de ce portfolio

Le portfolio dont parle ce blog est un projet solo, et il applique la version allégée décrite ci-dessus : pas de branche develop, des branches feat/… ou fix/… fusionnées dans main via des pull requests (parfois regroupées dans une branche d'intégration pour un gros chantier), et des commits conventionnels rédigés le plus souvent en français, par exemple feat(seeders): ..., fix(contenu): ... ou chore: .... Une GitHub Action exécute d'abord les contrôles (Pint, PHPStan, audit des dépendances, tests PHPUnit), puis le job de déploiement ne démarre que si ces vérifications passent. Le Git Flow complet décrit plus haut n'y est pas utilisé : il est présenté ici comme référence pour un travail en équipe.

Partager cet article

Envie de ne rien rater ?

Recevez les nouveaux articles directement dans votre boîte mail. Pas de spam, uniquement du contenu technique.

Commentaires (0)

Laisser un commentaire

0 / 2000

Aucun commentaire pour le moment. Soyez le premier à réagir !

Lecture

Articles similaires

Envie d'en discuter ?

Une opportunité d'alternance, un projet ou une question technique : écrivez-moi, je réponds généralement sous 24 à 48 heures.