DevGuide

Git rebase vs merge : guide visuel des workflows

Sur cette page
  1. Le modèle mental en un paragraphe
  2. Scénario 1 : feature branch en solo, de courte durée
  3. Scénario 2 : feature branch de longue durée avec des commits en vrac
  4. Scénario 3 : récupérer les mises à jour de main dans ta branch
  5. Scénario 4 : faire le ménage avant une pull request
  6. Scénario 5 : branch collaborative à plusieurs auteurs
  7. Le rebase interactif et le squash, décodés
  8. La résolution de conflits dans chaque mode
  9. Une convention d'équipe qui marche vraiment
  10. Sources et pour aller plus loin

Rebase ou merge avec git, j'ai vu cette question tourner à la guerre de religion dans plus d'un standup, et la plupart du temps ça chauffe parce que les gens en choisissent un et l'utilisent pour absolument tout. Les deux ont raison, juste pour des boulots différents. Alors voici comment je tranche dans la vraie vie. Tout le modèle mental tient en un paragraphe, et ensuite je déroule cinq situations que je croise plus ou moins chaque semaine. Tu auras le branch diagram avant et après, les commandes exactes que je tape, et quoi faire quand un conflit explose en plein milieu. À la fin, tu attraperas le bon outil sans même y penser, et tu sauras lequel pousser auprès de ton équipe sur n'importe quelle branch touchée par plus d'une personne.

The short answer

Le merge relie deux branches avec un commit à deux parents et garde la bifurcation visible. Le rebase décolle tes commits et les rejoue sur une base fraîche pour une ligne droite. Le même code part en prod dans les deux cas. La seule règle qui ne plie jamais : ne jamais rebase une branch sur laquelle plus d'une personne pousse.

Mergepréserve l'historique
Rebaseréécrit l'historique
5 cascomment choisir
Carte réponse : le merge préserve l'historique avec un commit à deux parents, le rebase le réécrit en ligne droite.
Mêmes deux commits, deux logs très différents. Le merge garde la bifurcation visible, le rebase les rejoue par-dessus avec de nouveaux SHAs. PNG

Le modèle mental en un paragraphe

Le merge préserve l'historique. Le rebase réécrit l'historique. C'est tout, en vrai. Un merge relie deux branches avec un nouveau commit qui a deux parents, donc tu peux toujours voir exactement quand la branch a atterri et à quoi elle ressemblait à ce moment-là. Un rebase fait quelque chose de plus sournois. Il décolle tes commits, les rejoue par-dessus une base toute fraîche, et te rend une ligne droite qui fait comme si tu avais écrit la branch contre le main d'aujourd'hui depuis le début. Le merge dit la vérité sur ce qui s'est réellement passé. Le rebase préfère te raconter une histoire plus propre sur le point d'arrivée. Le même code fonctionnel part en prod dans les deux cas. La différence ne te mord que six mois plus tard, quand tu fais un bisect sur un bug bien vicieux à 23 h et que ton git log est soit une timeline lisible, soit une assiette de spaghettis.

Visual: merge vs rebase, same input

Before (both)        After merge              After rebase

main:    A---B---C    main: A---B---C---M      main: A---B---C---X'---Y'
              \           \         /
feature:       X---Y       X-------Y            feature: (gone, fast-forwarded)

   M    = merge commit with two parents (B,Y)
   X'Y' = rebased copies with new SHAs

Scénario 1 : feature branch en solo, de courte durée

L'un ou l'autre. C'est ta branch. Personne d'autre n'est dessus, tu as un ou deux commits, et la PR est relue et mergée avant le déjeuner.

Fais un rebase sur main d'abord si tu veux une ligne droite. Ou clique juste sur le bouton merge et garde le contexte de la branch. Sur une petite PR avec une petite équipe, sincèrement, je me fiche du choix, et je pense que toi aussi tu devrais t'en ficher. Quoi que ton repo fasse par défaut, prends ça. Ne gâche pas une code review à débattre là-dessus.

# Either option, both fine
git checkout feature
git rebase main      # or just push and let GitHub merge

git checkout main
git merge feature    # or "git merge --no-ff feature" if you want the commit graph

Scénario 2 : feature branch de longue durée avec des commits en vrac

Le rebase gagne. Ça fait deux semaines que tu vis sur cette branch. Il y a 14 commits, les messages disent « wip », « fix typo » et « actually fix it », et maintenant il faut livrer un truc qu'un humain peut réellement relire.

Le rebase interactif justifie son salaire pile ici. Squashe tout ce bruit en un commit logique par sujet et réécris les messages pour qu'ils disent quelque chose. Puis merge. Maintenant ton relecteur lit trois commits qui veulent dire quelque chose au lieu de quatorze « wip ». Et la prochaine personne qui fera un bisect sur ce code dans six mois tombera sur un vrai changement plutôt que sur un fix de typo. Je fais ça sur quasiment chaque longue branch avant qu'elle ne monte, sans exception.

git checkout feature
git rebase -i main
# In the editor:
#   pick   a1b2c3d  Implement core logic
#   squash e4f5g6h  wip
#   squash 1a2b3c4  fix typo
#   pick   5d6e7f8  Add tests
#   squash 9g0h1i2  actually fix it
# Save and write the cleaned commit messages.
Visual: 14 messy commits to 2 clean ones

Before                              After

A---B---C main                      A---B---C main
         \                                   \
          X1---X2---X3---X4 ...               X1'---X2' feature (clean)
          X14 feature                         "Core logic"  "Tests"

Scénario 3 : récupérer les mises à jour de main dans ta branch

Le rebase gagne. Main a continué d'avancer pendant que tu avais le nez dans le guidon. Maintenant tu veux que ta branch repose sur la dernière version.

Un rebase ici et ta branch reste une ligne propre. Aucun de ces commits bouche-trou « Merge branch 'main' into feature » qui encombrent tout. Merge plutôt main dans ta feature et tu collectionnes un merge commit à chaque rattrapage. Ils s'empilent dans le diff de la PR et enterrent les trois lignes que tu as vraiment changées sous un tas de paperasse Git. Sur une branch qui n'est qu'à moi, je rebase pour rattraper. À chaque fois.

git checkout feature
git fetch origin
git rebase origin/main
# Resolve any conflicts (see the conflicts section)
git push --force-with-lease

Ne saute pas le --force-with-lease ici. Il vérifie d'abord le remote et abandonne si quelqu'un d'autre a poussé sur ta branch depuis ton dernier fetch, au lieu d'écraser bêtement son travail comme le fait --force tout court. J'ai dressé mes doigts à ne jamais taper --force nu sur quoi que ce soit de partagé. Cette seule habitude a sauvé tout un après-midi de commits à un collègue une fois, et je ne suis jamais revenu en arrière depuis.

Terminal exécutant git rebase origin/main sur une feature branch, puis git push --force-with-lease pour mettre à jour le remote sans risque.
Rattraper une branch perso : fetch, rebase sur origin/main, puis push with lease pour ne jamais écraser le commit d'un collègue. PNG

Scénario 4 : faire le ménage avant une pull request

Rebase et squash. Le code est terminé. Il marche. Mais l'historique est un bazar, et avant d'ouvrir la PR tu veux un commit bien rangé par changement logique.

git checkout feature
git rebase -i origin/main
# Reorder, squash, edit messages to tell the story:
#   pick   <sha>  Refactor auth middleware
#   pick   <sha>  Add login rate limiting
#   pick   <sha>  Update auth tests
git push --force-with-lease

Maintenant ton relecteur ouvre trois commits, chacun un changement autonome avec un message qui lui dit réellement ce qu'il fait. Cinq minutes de rebase t'achètent un historique que tu pourras bisect proprement plus tard. Et les relecteurs ont tendance à approuver ces PR plus vite, d'après mon expérience en tout cas.

Scénario 5 : branch collaborative à plusieurs auteurs

Le merge gagne. Vous êtes deux ou trois à pousser sur la même feature branch en même temps.

C'est la seule règle que je ne plierai pas. Ne rebase jamais une branch partagée. Voici pourquoi. Le rebase donne à chaque commit un SHA tout neuf, donc à la seconde où tu force-push, le clone de tout le monde se retrouve d'un coup désynchronisé avec un historique qui n'existe plus. La prochaine personne qui pull tombe sur une bifurcation et un tas de commits en double, et le merge qui démêle tout ça lui résiste du début à la fin. Donc sur une branch partagée, je merge main dedans et je vis simplement avec les merge commits :

git checkout shared-feature
git fetch origin
git merge origin/main
# Resolve conflicts, commit, push (regular push, no force)
git push

Oui, ces merge commits sont du bruit. Je vois ça comme le prix à payer pour que personne ne perde de travail, et c'est un prix que je paierai à chaque fois. Et ce n'est même pas du bruit qui reste. Quand la branch est enfin prête, celui qui possède le merge la squashe en un seul commit propre sur main, et tout le milieu en bazar n'atteint jamais main de toute façon.

Le rebase interactif et le squash, décodés

Lance git rebase -i et Git te dépose dans un éditeur. Une liste de tes commits, le plus vieux en haut, un verbe posé devant chacun. Il y a une poignée de verbes disponibles. Dans la vraie vie je n'attrape que ceux-là :

  • pick, laisse celui-ci tranquille. Garde-le tel quel.
  • squash (ou s), replie ce commit dans le pick au-dessus, écrase les diffs ensemble, puis te laisse écrire un message combiné.
  • fixup (ou f), comme squash, sauf qu'il jette le message de ce commit et garde celui du dessus. Mon réflexe pour chaque ligne « wip » et « fix typo ».
  • reword (ou r), garde le commit et ses changements. Ouvre juste un éditeur pour que tu corriges le message.
  • drop (ou d), explose le commit entièrement. Parfait pour ce console.log que tu jurais d'enlever et que tu as commit quand même.

Réorganiser ne coûte rien. Monte ou descends une ligne avant de sauvegarder et Git les rejoue dans cet ordre. Le piège apparaît quand deux commits que tu as déplacés touchent les mêmes lignes. Là, le replay s'arrête sur un conflit, tu corriges, tu continues avec git rebase --continue. Honnêtement ça me mord plus quand je réorganise que quand je squashe simplement, donc je réorganise avec un peu de prudence.

La résolution de conflits dans chaque mode

Conflits de merge

Quand Git n'arrive pas à combiner deux changements automatiquement, il s'arrête et balance des conflict markers dans les fichiers sur lesquels il a buté. Tu corriges les fichiers, tu fais git add dessus, puis git commit pour sceller le merge. Git pré-remplit le message du merge commit pour toi. Tu es libre de le réécrire, mais la plupart des gens le sauvegardent tel quel, et c'est très bien comme ça.

Conflits de rebase

C'est là que le rebase fait trébucher les gens. Parce qu'il rejoue tes commits un par un, tu peux tomber sur le même conflit encore et encore, une fois par commit qui touche ce bout de code. Le rythme reste le même à chaque fois. Corrige les fichiers, git add, puis git rebase --continue. Le détail qui piège tout le monde : c'est --continue, pas commit. Si ça vire au calvaire et que tu veux juste sortir, git rebase --abort te ramène exactement là où tu avais commencé, comme si rien ne s'était passé.

# During a rebase conflict:
# 1) Look at what is conflicting
git status

# 2) Edit the files, remove the conflict markers
# 3) Mark as resolved
git add path/to/file

# 4) Continue with the next commit
git rebase --continue

# Or bail out if it gets too messy
git rebase --abort

Une convention d'équipe qui marche vraiment

Choisis-en une parmi ces deux. Inscris-la dans ton fichier CONTRIBUTING et arrête de la rejuger à chaque sprint. Les deux te mènent à un historique propre au final. Le choix relève du goût, pas du bien-contre-le-mal, et je suis assez sûr que l'équipe qui se met simplement d'accord sur une bat l'équipe encore en train de chasser la « meilleure ».

Convention A : rebase pour le perso, merge pour le partagé

  • Ta propre branch : rebase sur main avant que la PR ne monte. Force-push (with lease) autant que tu veux, c'est la tienne.
  • Merger la PR : squash-merge ou rebase-merge depuis le bouton GitHub/GitLab, pour que main reste une ligne droite.
  • Branch partagée : merge main dedans, laisse les merge commits tranquilles, puis squash-merge vers main une fois fini.

Convention B : toujours merge, jamais rebase

  • Ta propre branch : ne touche pas du tout à l'historique. Besoin de rattraper main ? Merge-le dedans. Ne rebase pas.
  • Merger la PR : merge classique avec --no-ff, pour que chaque branch apparaisse comme sa propre bulle dans le graphe.
  • Ce que tu y gagnes : personne ne perd jamais un commit à cause d'un rebase raté. Ce que ça te coûte : le bisect devient plus brouillon par la suite.

Sources et pour aller plus loin

Questions fréquentes

Est-ce que je peux rebase une branch déjà poussée sur un remote ?

Oui, du moment qu'elle n'est qu'à toi. Renvoie-la avec git push --force-with-lease plutôt que --force nu, pour que Git refuse si quelqu'un d'autre a glissé un commit sur la branch dans ton dos. Mais à la seconde où d'autres poussent aussi sur cette branch ? La réponse bascule en non catégorique. Ne la rebase pas, merge à la place. J'ai nettoyé derrière une branch partagée rebasée une fois, et ce n'est pas comme ça que tu veux passer ton vendredi.

Mon rebase part en vrille et je veux m'en sortir. Comment ?

Respire. Celui-là est indolore. En plein rebase, que tu sois dans l'éditeur interactif ou jusqu'au cou dans les conflits, lance simplement git rebase --abort. Ça rembobine la branch exactement là où elle était avant que tu commences. Tes commits d'origine ne sont allés nulle part, donc rien n'est perdu. Je sors de mes rebases tout le temps et ça ne m'a sincèrement jamais coûté quoi que ce soit.

Quelle est la différence entre squash-merge et rebase-merge sur GitHub ?

Le squash-merge aplatit toute la PR en un seul commit sur main avec un message généré, donc une PR de dix commits atterrit en une seule ligne dans ton log. Le rebase-merge rejoue chacun de ces commits sur main séparément et garde l'historique fin. Ma règle du pouce ? Squash les petites PR de fix dont personne n'a besoin de voir les entrailles. Rebase les grosses features où chaque commit est une vraie étape que tu voudrais réellement relire plus tard.

Quand est-ce que j'utilise git pull plutôt que git fetch puis merge ?

Même destination, en vrai. git pull n'est que git fetch collé à git merge FETCH_HEAD sous le capot. J'ai tendance à les séparer exprès. Je lance git fetch, je jette un œil à ce qui a atterri, puis git rebase origin/main quand je suis bien prêt, pour que rien ne s'intègre dans mon dos. Si tu préfères que git pull rebase plutôt que merge à chaque fois, fais git config --global pull.rebase true une bonne fois et oublie ça.

Est-ce que le rebase va casser mes tests ?

Ça peut, de façon sournoise. Tes commits A, B et C peuvent tous être au vert pris isolément. Mais une fois que A est rejoué par-dessus un main qui a bougé sous lui, le A' rebasé tourne soudain contre du code qu'il n'a jamais vu, et ça suffit à le faire virer au rouge. Donc je relance la suite après chaque rebase. Sans exception. La bonne nouvelle, c'est que la CI le fait généralement pour toi à la seconde où tu force-push, donc tu seras fixé vite de toute façon.