Vous faites un pull du dernier main et Git s’arrête avec CONFLICT (content): Merge conflict in checkout.js. Vous résolvez le conflit, et le log affiche maintenant un commit nommé Merge branch 'main' into feature/checkout, posé au-dessus des trois commits que vous aviez réellement écrits. Quatre commits, un seul changement réel. Un collègue qui lit cet historique le mois prochain ne peut pas savoir lequel est lequel.

git merge et git rebase répondent à la même question : comment mettre ma branche à jour par rapport aux changements faits ailleurs. Ils laissent des traces très différentes, et choisir le mauvais sur une branche partagée cause de vrais dégâts.

Ce que merge et rebase font différemment au graphe de commits

Supposons que main a reçu les commits D, E, F pendant que vous travailliez sur feature, qui a vos propres commits A, B, C partis de l’ancienne pointe de main :

1
2
3
      A---B---C  feature
     /
D---E---F  main

git merge main, lancé depuis feature, crée un nouveau commit avec deux parents : la pointe de feature et la pointe de main. Rien ne change dans A, B ou C. Mêmes hashes, mêmes parents, même place dans le graphe :

1
2
3
      A---B---C
     /         \
D---E---F-------M  feature (commit de merge)

git rebase main, lancé lui aussi depuis feature, fait l’inverse. Il laisse main intact et rejoue A, B et C un par un par-dessus F, en générant un nouveau commit pour chacun : A’, B’, C’, avec de nouveaux hashes et de nouveaux parents :

1
2
3
D---E---F  main
         \
          A'---B'---C'  feature

Le résultat ressemble exactement à ce que vous auriez obtenu en créant la branche depuis F dès le départ, sans jamais diverger. C’est tout l’intérêt : un historique linéaire, sans commit de merge dans le log. Le coût, c’est que A, B et C n’existent plus pour Git. A’, B’ et C’ les remplacent, et quiconque avait déjà récupéré A, B ou C se retrouve avec des commits qui ne correspondent plus aux vôtres.

Ce qu’est un fast-forward merge et quand Git s’en sert à la place d’un commit de merge

Un merge n’a besoin d’un commit de merge que si les deux branches ont bougé. Si feature est partie de F et que main n’a pas bougé depuis, fusionner feature dans main n’a rien à concilier : Git avance simplement le pointeur de main jusqu’à C et s’arrête. C’est un fast-forward, et cela laisse le même historique plat qu’un rebase :

1
2
3
4
5
6
Avant :  D---E---F  main
                  \
                   A---B---C  feature

Après (fast-forward merge de feature dans main) :
D---E---F---A---B---C  main

Git ne crée un commit de merge que lorsque les deux côtés ont ajouté des commits que l’autre n’a pas. Certaines équipes en forcent un quand même, pour que chaque branche de feature laisse une marque visible dans main à l’endroit où elle a été intégrée :

1
git merge --no-ff feature/checkout

Comment nettoyer vos commits avec le rebase interactif

Le rebase peut aussi modifier votre propre historique, sans qu’aucune autre branche n’intervienne. git rebase -i ouvre vos N derniers commits dans un éditeur, une ligne par commit :

1
git rebase -i HEAD~3
1
2
3
pick a1b2c3d Add checkout validation
pick e4f5a6b fix typo
pick c7d8e9f WIP: still debugging

Changez le mot en début de ligne et Git agit en conséquence quand le rebase démarre :

CommandeEffet
pickConserve le commit tel quel
rewordConserve le commit, modifie son message
squashLe fusionne avec le commit précédent, en combinant les deux messages
fixupLe fusionne avec le commit précédent, en abandonnant ce message
dropSupprime le commit
editMet le rebase en pause ici pour modifier le commit à la main

Transformez cette liste en squash e4f5a6b fix typo et fixup c7d8e9f WIP: still debugging et vous obtenez un seul commit, Add checkout validation, au lieu de trois qui n’ont de sens que dans l’ordre où vous les avez écrits. Faites-le avant d’ouvrir la pull request, pas après que quelqu’un ait récupéré votre branche — le squash réécrit le hash de chaque commit qui suit, comme n’importe quel autre rebase.

Si vous corrigez des commits antérieurs au fur et à mesure, git commit --fixup <hash> suivi de git rebase -i --autosquash <base> déplace chaque fixup à côté de sa cible et le marque pour vous, si bien que vous ne réordonnez jamais la liste à la main.

Comment git pull --rebase garde votre branche plate

Le même choix revient à chaque pull. Un git pull classique, sur une branche où vous avez des commits locaux et où le remote a avancé, fait un fetch suivi d’un merge. C’est comme ça que vous vous retrouvez avec un commit Merge branch 'main' into feature/checkout pour aucune autre raison qu’un mauvais timing. git pull --rebase récupère les mêmes commits et rejoue les vôtres par-dessus :

1
git pull --rebase origin main

Des changements non commités font échouer le démarrage du rebase. Ajoutez --autostash et Git les met de côté, fait le rebase, puis les réapplique :

1
git pull --rebase --autostash origin main

Configurez-le une bonne fois pour toutes plutôt que de taper le flag à chaque fois, par branche ou globalement :

1
2
git config branch.feature/checkout.rebase true
git config --global pull.rebase true

C’est sûr, car cela ne réécrit que des commits qui n’existent que sur votre branche locale. Dès que ce n’est plus le cas, le rebase cesse d’être sûr.

La règle d’or : ne jamais rebaser des commits que quelqu’un d’autre a déjà récupérés

Le rebase réécrit les hashes des commits. Si vous rebasez une branche dont les anciens commits sont déjà entre les mains de quelqu’un d’autre — il a récupéré votre branche, ou c’est main, ou c’est n’importe quoi avec plus d’un contributeur — son historique et le vôtre ne sont plus d’accord sur ce qu’est cette branche. Git n’a aucun moyen de savoir que votre A et son A’ sont le même changement logique. Il voit deux commits sans lien, et le prochain merge entre les deux copies duplique tout ce qui a divergé.

La règle qui évite ça : ne rebasez que les branches sur lesquelles personne d’autre n’a encore commencé à travailler. Votre propre branche de feature, avant d’avoir donné à quelqu’un un hash sur lequel s’appuyer, c’est du terrain libre. Une branche déjà pushée que des collègues utilisent, main, ou n’importe quelle branche partagée de longue durée : celles-là se fusionnent, jamais on ne les rebase.

Rebaser une branche déjà pushée exige un push forcé pour que le remote accepte l’historique réécrit :

1
git push --force-with-lease origin feature/checkout

--force-with-lease refuse le push si le remote a des commits que vous n’avez pas encore récupérés — exactement la situation où quelqu’un a construit sur l’ancien historique et où un --force brut écraserait silencieusement son travail. Jamais de --force brut, et seulement sur une branche dont vous êtes sûr qu’elle n’appartient qu’à vous.

Comment récupérer d’un mauvais rebase avec git reflog

Un rebase sur la mauvaise base, ou un rebase interactif où vous avez supprimé le mauvais commit, semble irrécupérable : les commits que vous aviez une minute plus tôt n’apparaissent plus dans git log. Ils n’ont pas disparu. git reflog enregistre chaque position vers laquelle HEAD a pointé sur votre machine, rebases compris :

1
git reflog
1
2
3
4
a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feature/checkout
9f0e1d2 HEAD@{1}: rebase (pick): Add checkout validation
7c8b9a0 HEAD@{2}: rebase (start): checkout main
c4d5e6f HEAD@{3}: commit: WIP: still debugging

HEAD@{3} est l’endroit où pointait votre branche juste avant le début du rebase. Faites un reset vers ce point et vous retrouvez exactement l’état d’avant, commits supprimés compris :

1
git reset --hard HEAD@{3}

Les entrées du reflog expirent après 90 jours par défaut, ou 30 jours pour les commits vers lesquels aucune autre référence ne pointe. C’est un filet de sécurité pour la dernière heure ratée, pas un stockage permanent. C’est aussi purement local : ça ne se pushe jamais, et ça ne peut pas défaire une réécriture que quelqu’un d’autre a déjà récupérée.

Le même réflexe — revenir à un état connu et fonctionnel après un mauvais changement — s’applique aussi en dehors de l’historique Git. Un Deployment Kubernetes garde son propre historique de rollouts pour la même raison, et kubectl rollout undo fait pour un déploiement raté ce que git reset --hard sur une entrée du reflog fait pour un rebase raté.

Git rebase vs merge : lequel utiliser et quand

SituationUtilisez
Mettre à jour votre branche locale, pas encore pushée, par rapport à mainrebase (ou pull --rebase)
Nettoyer vos propres commits avant d’ouvrir une pull requestrebase -i
Intégrer une branche de feature terminée dans mainmerge (avec ou sans --no-ff, selon la convention de votre équipe)
La branche est partagée, pushée, ou c’est main elle-mêmemerge — ne la rebasez jamais
Vous devez savoir exactement quand une feature est arrivée dans mainmerge --no-ff — le commit de merge le marque
Vous voulez un historique linéaire sans commit de merge dans le logrebase de votre côté, avant d’ouvrir la pull request

La décision porte en réalité sur qui d’autre a déjà vu ces commits. Vous seul ? Rebase, et gardez un historique propre. Au moins une autre personne ? Merge. Un log un peu plus désordonné ne vous coûte rien ; un historique partagé réécrit coûte un après-midi à tous ceux qui l’ont récupéré.

Si votre équipe n’a pas encore de convention, commencez ici : pull.rebase = true en local, rebase -i pour ranger vos commits avant d’ouvrir la pull request, puis un simple merge pour les intégrer dans main. Vous obtenez un historique propre par branche sans jamais réécrire un commit dont dépend quelqu’un d’autre, CI comprise. Un workflow comme Construire et Publier une Image Docker avec GitHub Actions tague chaque build avec le SHA du commit, et ces SHA doivent continuer d’exister après que le pipeline les a utilisés. Définissez git config --global pull.rebase true dès aujourd’hui : à lui seul, il élimine la plupart des commits de merge accidentels de votre log.

Articles associés