Git Rebase ou Merge : quand utiliser lequel
git merge et git rebase amènent tous deux les changements de votre branche de fonctionnalité sur main, et cette ressemblance superficielle est précisément la raison pour laquelle on les confond et on finit avec un historique emmêlé ou réécrit sans l'avoir voulu. Ils résolvent le même problème de manières fondamentalement différentes, et choisir le mauvais pour la situation mène à des catastrophes de force-push ou à des graphes de commits illisibles.
Ce que fait réellement chacun
Supposons que main ait avancé depuis que vous avez créé votre branche, et que votre branche de fonctionnalité ait ses propres commits :
A---B---C feature
/
D---E---F---G main
Merge crée un nouveau commit qui relie les deux historiques, avec deux parents :
git checkout feature
git merge main
A---B---C-------H feature
/ /
D---E---F---G-------- main
Rien ne change pour A, B ou C. H est un nouveau commit de fusion qui enregistre « ces deux lignes d'historique sont maintenant jointes ». L'historique complet, y compris le fait que vous avez bifurqué avant l'arrivée de F et G, reste intact.
Rebase prend vos commits et les rejoue, un par un, au-dessus de la nouvelle base :
git checkout feature
git rebase main
D---E---F---G---A'---B'---C' feature
A', B', C' sont de nouveaux commits : mêmes changements, mêmes messages, mais un parent différent, donc des hachages de commit différents. Le point de bifurcation a disparu ; on dirait désormais que vous avez créé votre branche à partir de G et écrit vos trois commits à partir de là, en ligne droite.
Pourquoi cette distinction compte
Le compromis fondamental : merge préserve ce qui s'est réellement passé, rebase produit un historique d'apparence plus propre qui ne s'est pas littéralement déroulé ainsi. Les deux sont utiles, pour des publics différents.
- Merge est honnête sur le travail parallèle. Si deux personnes construisaient toutes deux sur un ancien
mainet que leurs branches ont divergé puis convergé, un commit de fusion l'enregistre fidèlement. Il est non destructif : aucun commit existant n'est modifié, donc il est toujours sûr sur des branches partagées. - Rebase fait en sorte que l'historique se lise comme s'il s'était déroulé en série, ce qui est plus facile à suivre ensuite (
git logmontre une ligne propre au lieu d'un treillis de commits de fusion), mais il réécrit les hachages de commit, et c'est là que réside le danger.
La règle qui compte vraiment : ne jamais rebaser un historique partagé
git checkout feature
git rebase main
git push --force-with-lease
Si feature est une branche sur laquelle vous seul travaillez, c'est parfait : vous réécrivez des commits sur lesquels personne d'autre n'a basé de travail. Si feature (ou pire, main) est une branche que d'autres ont déjà récupérée et sur laquelle ils ont construit, la rebaser réécrit l'historique sous leurs pieds : l'historique de leur branche locale et celui du distant divergent, et leur prochain pull produit un enchevêtrement confus de commits en double, ou leurs commits locaux semblent disparaître (ils n'ont pas disparu, ils ne sont simplement plus accessibles depuis la branche qu'ils croyaient suivre).
La règle pratique : rebasez librement sur les branches où vous seul poussez, fusionnez (ou utilisez un rebase suivi d'un fast-forward via un outil de PR) pour tout ce qui est déjà partagé.
Rebase interactif : nettoyer avant de partager
L'autre usage courant du rebase ne concerne pas du tout une autre base : il s'agit de nettoyer vos propres commits non poussés avant d'ouvrir une PR.
git rebase -i HEAD~4
Cela ouvre un éditeur listant vos quatre derniers commits, et vous permet de les réordonner, de fusionner (squash) "fix typo" et "actually fix typo" dans le commit auquel ils appartiennent, ou de reformuler les messages, le tout avant que quiconque les ait vus. C'est l'usage le plus sûr et le plus courant du rebase en pratique : transformer un historique de travail en cours désordonné en un petit nombre de commits cohérents, avant qu'il ne devienne historique partagé.
Les conflits de fusion se vivent différemment
Avec un merge, vous résolvez les conflits une seule fois, dans l'unique commit de fusion. Avec un rebase, si plusieurs commits touchent les mêmes lignes, vous pouvez rencontrer le même conflit à répétition (une fois par commit rejoué), ce qui est plus fastidieux mais aussi plus précis : vous résolvez chaque changement historique dans le contexte où il a réellement été fait, et non un seul diff géant à la fin.
git rebase main
# CONFLICT in commit B
# ...resolve...
git add .
git rebase --continue
# CONFLICT in commit C, same lines
# ...resolve again...
git add .
git rebase --continue
Si un rebase tourne au calvaire de conflits répétés, git rebase --abort vous ramène exactement là où vous étiez au départ : à retenir avant d'être trois conflits plus loin sans savoir si vous aggravez la situation.
Un choix par défaut pratique
- Vous travaillez sur votre propre branche de fonctionnalité et voulez rattraper
mainavant de continuer :rebase. - Nettoyer des commits avant d'ouvrir une PR :
rebase -i. - Intégrer à
mainune branche de fonctionnalité terminée et relue : selon la convention de votre équipe ; les deux sont défendables, et la cohérence compte plus que le choix. - Tout ce qui a déjà été poussé et peut-être récupéré par quelqu'un d'autre :
merge, ou rebase uniquement avec l'accord explicite de l'équipe et un avertissement préalable.
À retenir
Merge dit la vérité sur l'historique parallèle au prix d'un graphe plus emmêlé ; rebase produit un graphe linéaire propre au prix de la réécriture de l'identité des commits. La ligne qui compte vraiment n'est pas « lequel est meilleur », mais de savoir si les commits que vous allez réécrire sont encore uniquement les vôtres.