DevOps, CI/CD et conteneurs

Branches et workflows Git : merge, rebase, Git Flow et trunk-based development

Intermédiaire10 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Dans Git, une branche ne coûte rien : les vraies décisions portent sur sa durée de vie et sur la manière de l'intégrer. Ce guide explique les merges fast-forward et à trois voies, le rebase et ses règles, la résolution des conflits, puis compare les workflows réellement utilisés par les équipes — branches de fonctionnalité avec pull requests, Git Flow, GitHub Flow et trunk-based development — en indiquant lequel convient à quel modèle de livraison.

Ce que vous allez apprendre

  • Expliquer ce qu'enregistre un commit de merge et dans quels cas Git fait plutôt un fast-forward
  • Utiliser le rebase pour garder un historique linéaire et savoir précisément quand ne pas l'utiliser
  • Résoudre les conflits de merge avec méthode et éviter les façons classiques de les bâcler
  • Comparer Git Flow, GitHub Flow et trunk-based development, et choisir le modèle adapté à une équipe
  • Appliquer cherry-pick, rebase interactif et branches protégées à des situations de livraison réelles

Dans Git, créer une branche ne coûte rien, ce qui déplace les questions intéressantes de « faut-il créer une branche ? » vers « combien de temps vit-elle, comment revient-elle dans la ligne principale, et à quoi ressemble l'historique ensuite ? ». Des réponses différentes donnent un quotidien très différent : une équipe qui rebase plusieurs fois par jour des branches de courte durée sur main ne vit pas dans le même monde qu'une équipe qui entretient des branches develop, release/* et hotfix/* avec un cérémonial de merge pour chacune. Ce guide explique la mécanique — merge, rebase, conflits — avec assez de précision pour que vous puissiez prévoir l'effet d'une commande, puis compare les workflows qui reposent dessus.

Branches, merge base et fast-forward

Une branche est un pointeur vers un commit. Quand deux branches partagent un ancêtre commun et que l'une a avancé, Git appelle cet ancêtre la merge base. Un merge apporte dans la branche courante les modifications faites sur l'autre branche depuis la merge base.

Si la branche courante n'a pas bougé depuis la merge base — main n'a aucun nouveau commit et feature en a trois —, il n'y a rien à combiner. Git se contente d'avancer le pointeur main jusqu'à la pointe de feature. C'est un merge fast-forward : pas de commit de merge, un historique linéaire, et aucune trace qu'une branche ait jamais existé.

Si les deux branches ont avancé, Git effectue un merge à trois voies : il compare la merge base avec chacune des deux pointes, applique les deux ensembles de modifications et enregistre le résultat dans un commit de merge à deux parents. C'est ce commit de merge qui rend la branche visible dans l'historique par la suite.

git switch main
git merge feature            # fast-forward if possible, merge commit otherwise
git merge --no-ff feature    # always create a merge commit, even when a fast-forward was possible
git merge --ff-only feature  # refuse to merge unless a fast-forward is possible

--no-ff est utilisé par les équipes qui veulent que chaque fonctionnalité apparaisse comme un unique commit de merge dans git log --first-parent, ce qui permet d'annuler une fonctionnalité entière avec git revert -m 1 <merge-commit>. --ff-only est utilisé par les équipes qui exigent un historique linéaire et rebasent les branches avant de les merger.

Rebase

git rebase main, lancé sur feature, reprend chaque commit de feature depuis la merge base et le réapplique au-dessus de la pointe actuelle de main, en créant de nouveaux commits avec de nouveaux hashes. Tout se passe comme si vous aviez créé la branche aujourd'hui : vos modifications se trouvent au-dessus de celles des autres, l'historique est linéaire et un merge ultérieur dans main se fait en fast-forward.

Le rebase est le bon outil pour tenir à jour une branche privée. C'est le mauvais outil pour tout ce qui est déjà partagé, car les commits réécrits n'ont plus les mêmes hashes que ceux que vos collègues ont récupérés, et leur prochain pull produit des commits en double et des conflits déroutants. La règle tient en une phrase : ne rebasez jamais des commits qui existent en dehors de votre dépôt, sauf si toutes les personnes concernées sont au courant et d'accord (une branche personnelle de pull request sur laquelle vous êtes seul à pousser est l'exception courante).

Le rebase interactif (git rebase -i main) ouvre la liste des commits sur le point d'être rejoués et vous permet de les réordonner, de les fusionner (squash), de les modifier, d'en réécrire le message ou de les supprimer. C'est ainsi que l'on transforme huit commits « wip » en deux commits cohérents avant d'ouvrir une pull request.

pick 3f2a1c9 Add DiscountCode entity
squash 8b7d0e2 fix typo
squash c41e77a wip
pick 92ad5b1 Apply discount codes at checkout
reword d05f3a6 tests

Après l'enregistrement, Git rejoue les commits selon ces instructions et demande les messages quand c'est nécessaire. Associé à git add -p, le rebase interactif permet de remettre aux relecteurs un petit nombre de commits qui font chacun une seule chose.

Résoudre les conflits

Un conflit survient quand les deux côtés ont modifié les mêmes lignes (ou quand l'un a supprimé un fichier que l'autre a modifié) et que Git ne peut pas trancher. Il s'arrête, marque le fichier et attend.

<<<<<<< HEAD
    BigDecimal total = subtotal.add(shipping);
=======
    BigDecimal total = subtotal.add(shippingCost).add(tax);
>>>>>>> feature/tax

La partie au-dessus de ======= correspond à la branche courante (HEAD), celle du dessous à la branche en cours de merge. La marche à suivre :

  1. Voir ce qui est en conflit

    git status liste les chemins sous « Unmerged paths ». git diff affiche les conflits avec les deux versions.
  2. Amener chaque fichier au résultat voulu

    Supprimez les marqueurs et écrivez le code qui doit exister — souvent une combinaison des deux côtés, parfois ni l'un ni l'autre. Compilez et lancez les tests avant de continuer.
  3. Marquer le conflit comme résolu

    git add <file> indique à Git que le conflit est réglé.
  4. Terminer l'opération

    git merge --continue (ou git commit) pour un merge ; git rebase --continue pour un rebase, qui peut alors présenter le commit suivant en conflit. --abort, dans les deux cas, revient à l'état d'avant.

Les outils de merge (git mergetool, ou la vue à trois panneaux d'IntelliJ IDEA et de VS Code) affichent côte à côte la base, « ours » et « theirs », ce qui est bien plus simple que de lire des marqueurs dès que le conflit dépasse une ligne ou deux. Activer rerere (« reuse recorded resolution ») permet à Git de mémoriser la façon dont vous avez résolu un conflit et d'appliquer automatiquement la même résolution la prochaine fois qu'il le rencontre — précieux quand on rebase plusieurs fois une branche de longue durée.

Deux habitudes réduisent les conflits plus qu'aucun outil : intégrer main chaque jour pour que la divergence reste faible, et éviter de mélanger reformatage massif ou déplacement de fichiers et changements de logique dans un même commit.

Les workflows comparés

Une fois la mécanique maîtrisée, les workflows ne sont que des recettes qui disent quand créer une branche et comment la merger.

Branches de fonctionnalité avec pull requests

Chaque changement a sa branche, créée depuis main, sur laquelle se fait le travail ; une pull request (merge request dans GitLab) rassemble les commentaires de revue et les résultats de CI avant que la branche soit mergée puis supprimée. C'est le fonctionnement de base sur GitHub, GitLab et Bitbucket. Les variables sont la durée de vie des branches et le mode de merge : des branches limitées à un jour ou deux et mergées par squash ou rebase donnent une main propre et linéaire ; des branches qui vivent des semaines recréent les problèmes d'intégration que l'intégration continue a précisément pour but d'éviter.

GitHub Flow

Un modèle volontairement minimal : main est toujours déployable ; chaque changement passe par une branche et une pull request ; merger dans main déclenche le déploiement. Il n'y a pas de branche de release, puisqu'une release correspond simplement au contenu de main. Ce modèle convient aux services déployés en continu et aux équipes dotées de tests automatisés solides.

Git Flow

Le modèle élaboré, avec deux branches permanentes et trois types de branches temporaires :

BrancheCréée depuisMergée dansRôle
main——Uniquement les versions de production, chacune taguée avec son numéro de version
developmainmain via releaseBranche d'intégration de la prochaine version
feature/*developdevelopFonctionnalités individuelles
release/*developmain et developStabiliser une version : montée de version, corrections de bugs uniquement
hotfix/*mainmain et developCorrectif urgent en production

Git Flow répond à de vraies questions pour les logiciels livrés en versions numérotées, avec plusieurs versions maintenues en production chez les clients : où vont les correctifs de la 2.3 pendant que la 2.4 est en stabilisation et que la 2.5 est en développement ? Son coût, c'est le cérémonial et les branches de longue durée — develop peut s'écarter de main pendant des semaines —, et pour un service web déployé plusieurs fois par jour, ce n'est que de la surcharge. L'auteur du modèle a lui-même souligné qu'il avait été conçu avant que la livraison continue ne devienne une pratique courante.

Trunk-based development

Tout le monde commite sur main (le tronc) au moins une fois par jour, soit directement, soit via des pull requests qui ne vivent que quelques heures. Les fonctionnalités inachevées restent invisibles grâce à des feature flags, ou en construisant le nouveau chemin à côté de l'ancien (« branch by abstraction ») plutôt que sur une branche. Les branches de release, quand elles existent, sont créées depuis le tronc et ne reçoivent que des correctifs cherry-pickés, jamais de nouveau travail. Les recherches de DORA associent ce modèle à de meilleures performances de livraison, et c'est le partenaire naturel de l'intégration continue. Il suppose une confiance réelle dans la suite de tests, de petits changements et un investissement dans les feature flags ; les équipes qui n'ont pas tout cela s'y sentent mal à l'aise.

Faire son choix

SituationChoix raisonnable
Service web déployé en continu, une seule version en productionGitHub Flow ou trunk-based
Produit à versions numérotées, une seule version maintenueBranches de fonctionnalité plus une branche de release de courte durée par version
Bibliothèque ou produit on-premise avec plusieurs versions maintenuesGit Flow ou un modèle « branches de release avec cherry-picks »
Grande équipe, monorepo, automatisation pousséeTrunk-based avec feature flags
Validation réglementaire des mises en productionN'importe lequel des modèles ci-dessus, avec la validation portant sur le déploiement, pas sur le merge

La leçon qui revient toujours : le workflow doit suivre le modèle de livraison, et non l'inverse.

Branches de release, cherry-picks et tags

Même les équipes trunk-based ont parfois besoin d'une branche de release : une version a été livrée aux clients et nécessite un correctif alors que main a avancé. La démarche consiste à créer release/2.4 depuis le commit tagué, à corriger le bug d'abord sur main (pour qu'il ne soit jamais perdu), puis à faire git cherry-pick <hash> sur la branche de release, à y poser le tag v2.4.1 et à builder depuis ce tag. Corriger d'abord sur main évite l'échec classique où un correctif ne va que sur la branche de release et réapparaît sous forme de régression dans la version suivante.

Protégez les branches importantes sur la plateforme d'hébergement : exigez des pull requests, exigez que la vérification de CI passe, interdisez les force-push et les suppressions sur main et release/*. Les règles de protection ne coûtent rien et évitent les accidents les plus destructeurs.

Conventions de commit

Un modèle de branches fonctionne mieux avec des commits cohérents. Conventional Commits (feat:, fix:, refactor:, avec un scope facultatif et un ! pour les changements incompatibles) est un format très répandu que les outils savent analyser pour générer les changelogs et calculer automatiquement la prochaine version sémantique. Que vous adoptiez ou non ces préfixes, gardez des commits qui ont un sens après le rebase, nommez les branches d'après leur intention (fix/null-shipping-cost, feat/discount-codes) et faites référence au gestionnaire de tickets dans la pull request plutôt que dans chaque commit.

Erreurs fréquentes

  • Rebaser une branche partagée. Vos collègues récupèrent des commits en double et des conflits à leur prochain pull. Ne rebasez que ce qui vous appartient ; mergez tout le reste.
  • Des branches de fonctionnalité qui durent. Trois semaines de travail intégrées d'un coup produisent un marathon de conflits et masquent les bugs d'intégration. Mergez de petits morceaux protégés par des flags.
  • Résoudre un conflit en gardant « ours » partout. Choisir un côté à l'aveugle écarte la modification de l'autre personne et réintroduit le bug qu'elle avait corrigé. Lisez les deux côtés.
  • Merger main dans une branche de fonctionnalité à répétition, puis merger en retour. L'historique devient un treillis de commits de merge qui masque ce que la fonctionnalité a changé. Rebasez plutôt les branches privées, ou mergez depuis main avec parcimonie.
  • Un hotfix uniquement sur la branche de release. Le correctif n'arrive jamais sur main et le bug revient à la version suivante. Corrigez d'abord sur main, puis reportez le correctif par cherry-pick sur la branche de release.
  • Supprimer la branche avant que la pull request soit mergée. Sur la plupart des plateformes, la pull request est alors fermée avec elle. Laissez la plateforme supprimer la branche au moment du merge.
  • Faire un force-push sans --force-with-lease. Vous écrasez le commit qu'un collègue a poussé une minute plus tôt.

Points clés à retenir

  • Un merge se fait en fast-forward quand la cible n'a pas bougé ; sinon, il crée un commit de merge à deux parents qui enregistre la combinaison.
  • Le rebase recrée les commits au-dessus d'une autre branche et produit un historique linéaire ; utilisez-le sur les branches privées, jamais sur des commits que d'autres ont pu récupérer.
  • Résolvez les conflits en lisant les deux côtés, en éditant jusqu'au résultat voulu et en testant, puis add et --continue ; rerere et une intégration quotidienne rendent les conflits rares et petits.
  • GitHub Flow et le trunk-based development conviennent au déploiement continu ; Git Flow convient aux produits versionnés avec plusieurs versions maintenues ; alignez le workflow sur le modèle de livraison.
  • Corrigez d'abord sur main et faites un cherry-pick vers les branches de release, taguez les versions avec des tags annotés et protégez main contre les force-push et les merges non relus.
  • Des commits et des branches petits et bien nommés, remis en ordre par un rebase interactif avant la revue, rendent n'importe quel workflow plus agréable à vivre.

Sources et lectures complémentaires

Questions fréquentes

Faut-il faire un merge ou un rebase de ma branche de fonctionnalité sur main ?
Faites un rebase tant que la branche n'appartient qu'à vous : l'historique reste linéaire et vos commits passent au-dessus du dernier état de main, si bien que le merge final devient trivial. Dès que la branche est poussée et que d'autres ont pu s'appuyer dessus, arrêtez de rebaser et mergez. Beaucoup d'équipes combinent les deux : rebase en local pour se mettre à jour, puis squash-merge ou merge avec --no-ff via une pull request.
Git Flow est-il encore recommandé ?
Son propre auteur a indiqué qu'il convient aux logiciels livrés sous forme de versions explicites, avec plusieurs versions maintenues — applications de bureau, bibliothèques, produits on-premise — et que les services web déployés en continu gagnent à adopter un modèle plus simple. Si vous déployez depuis main plusieurs fois par jour, les branches develop et release de Git Flow ajoutent du cérémonial sans rien apporter.
Comment reporter un seul commit de correction de main vers une ancienne branche de release ?
Placez-vous sur la branche de release et lancez git cherry-pick avec le hash du commit. Git applique la même modification sous la forme d'un nouveau commit sur la branche de release. Un cherry-pick fonctionne d'autant mieux que le commit d'origine est petit et autonome ; s'il dépend d'autres changements, vous aurez des conflits, et il est souvent plus propre de corriger d'abord sur la plus ancienne branche maintenue puis de merger vers les plus récentes.
Qu'est-ce qu'un squash merge, et quand est-ce une mauvaise idée ?
Un squash merge regroupe tous les commits d'une branche en un seul commit sur la branche cible, ce qui donne un historique propre à raison d'un commit par fonctionnalité. C'est un bon choix par défaut pour les pull requests encombrées de commits de travail en cours. C'est une mauvaise idée quand les commits de la branche ont chacun un sens — un refactoring suivi d'un changement de comportement — ou quand vous mergerez à nouveau la même branche plus tard, car Git ne peut pas savoir que le commit squashé provient de cette branche et rejouera les conflits.