Configurer le pull en mode "rebase"

Par Maxime Bréhin • Publié le 17 février 2025 • 8 min

La configuration du mode de synchronisation est une opération simple qu’on effectue généralement une seule fois par poste, mais dont l’incidence sur le graphe d’historique peut être importante. Malheureusement, il s’agit d’un aspect assez mal documenté, ce que je trouve plutôt regrettable.

Voyons ensemble les différents modes qui s’offrent à nous et pourquoi je préfères le mode rebase = merges qui, lu comme ça, ressemble à une incohérence dans le système.

Configuration par défaut

Pendant longtemps (jusqu’à Git 2.2X), la configuration par défaut du mode de récupération était la fusion. Il a fallu attendre presque 15 ans à Git pour cesser ce mode par défaut (seulement en cas de divergence) et demander à l’utilisateur de faire un choix explicite. C’était mieux, mais pas pour autant très satisfaisant. Voyez plutôt le message qu’il affiche dans le terminal si on n’a pas renseigné cette configuration :

You have divergent branches and need to specify how to reconcile them.
You can do so by running one of the following commands sometime before
your next pull:

  git config pull.rebase false  # merge
  git config pull.rebase true   # rebase
  git config pull.ff only       # fast-forward only

You can replace "git config" with "git config --global" to set a default
preference for all repositories. You can also pass --rebase, --no-rebase,
or --ff-only on the command line to override the configured default per
invocation.

Je ne sais pas ce que vous en pensez, mais personnellement je trouve que ça manque de détail. On y comprend pas grand chose si ce n’est qu’il faut faire un choix parmi les 3 lignes proposées. Et vous savez quoi ? La liste proposée n’est même pas complète 🤦‍♀️, il manque justement le mode qui me semble le plus complet, ce qui aurait ajouté la ligne :

  git config pull.rebase merges   # rebase while preserving local merges

Comme je suis quelqu’un de sympa 😁, je vais vous expliquer à quoi correspondent ces modes. Vous pourrez ensuite choisir à votre convenance (ou me faire confiance aveuglément).

Avant d’aller plus loin…

Avant d’entrer dans le vif du sujet, je me dois de (re)préciser un aspect important dans la manière dont Git gère ses synchronisations entre un dépôt local et un dépôt distant.

Quand on travaille sur une branche dev localement et qu’on la partage sur un serveur Git distant, apparaît alors une nouvelle branche, généralement nommée origin/dev (origin étant le nom donné à notre dépôt distant). Git gère alors 2 branches séparées :

  • notre branche locale dev ;
  • la branche distante dev signalée localement par le nom origin/dev.

C’est ce procédé qui lui permet de faire des mises à jour incrémentales et qui nous permet d’obtenir des informations de “déphasage” entre local et distant.

La synchronisation locale (via la commande pull) se décompose alors en 2 étapes :

  1. la récupération des nouveautés du distant sur la branche origin/dev (via un fetch) ;
  2. l’application des nouveautés de la branche origin/dev sur la branche locale dev.

La configuration expliquée dans cet article concerne donc cette seconde étape. Reprenons maintenant le fil de nos explications…

Le pull en mode fusion

Pour utiliser ce mode, on doit renseigner la configuration pull.rebase = false :

git config --global pull.rebase false

En faisant ce choix, on demande à Git de considérer la branche distante comme étant une branche “différente” de la branche locale. L’intégration des nouveautés de la branche distante se fera alors à l’aide d’une fusion. Ça aura pour conséquence d’ajouter des décrochés réguliers dans notre historique.

Pour mieux comprendre cette incidence, utilisons un cas pratique et quelques schémas.

Cas d’exemple

Prenons un exemple simple de 2 collègues, Mélanie et Joseph qui travaillent sur une même branche dev depuis un commit initial c0. Chacun a travaillé sur des fichiers différents, Mélanie partage son travail la première (les commits m1 et m2), Joseph a réalisé de son côté les commits j1 et j2 sans les partager. Il récupère le travail de Mélanie et obtient l’historique suivant :

devorigin/devc0j1j2m1m2

Ce qui donnerait dans le terminal (avec la configuration qui va bien) :

*   fba897d - (HEAD -> dev) Merge branch 'dev' of … (Mélanie)
|\
| * 9a6c603 - (origin/dev) m2 (Mélanie)
| * de8aefc - m1 (Mélanie)
* | ac66030 - j2 (Joseph)
* | 4a44558 - j1 (Joseph)
|/
* 6422483 - c0 (Mélanie)

Jusque là, rien d’inquiétant, on obtient une bosse, mais ça va. Si maintenant Mélanie continue et partage un nouveau commit avant que Joseph ait pu faire son push, il devra encore se synchroniser et obtiendra ce qui suit :

devorigin/devc0j1j2m1m2m3

Dans son terminal, Joseph aurait ce qui suit :

*   f94a837 - (HEAD -> dev) Merge branch 'dev' of … (Joseph)
|\
| * 9ef225f - (origin/dev) m3 (Mélanie)
* | 95571f9 - Merge branch 'dev' of … (Joseph)
|\|
| * 9a6c603 - m2 (Mélanie)
| * de8aefc - m1 (Mélanie)
* | ac66030 - j2 (Joseph)
* | 4a44558 - j1 (Joseph)
|/
* 6422483 - c0 (Mélanie)

Si ensuite Joseph pousse pendant que Mélanie crée un nouveau commit localement (m4), si cette dernière récupère le travail de Joseph, voilà la tête qu’aura l’historique sur le poste de Mélanie :

Schéma des branches après plusieurs allers/retours

On voit tout à coup apparaître un entremêlement de branches qui en réalité ne sont que les branches dev et origin/dev mais dont les intégrations successives ont “saccagé” le graphe.

La même version dans le terminal de Mélanie donnerait ce qui suit :

*   e9c9c3a - (HEAD -> dev) Merge branch 'dev' of … (Mélanie)
|\
| *   f94a837 - (origin/dev) Merge branch 'dev' of … (Joseph)
| |\
| * \   95571f9 - Merge branch 'dev' of … (Joseph)
| |\ \
| * | | ac66030 - j2 (Joseph)
| * | | 4a44558 - j1 (Joseph)
* | | | 7efc16d - m4 (Mélanie)
| |_|/
|/| |
* | | 9ef225f - m3 (Mélanie)
| |/
|/|
* | 9a6c603 - m2 (Mélanie)
* | de8aefc - m1 (Mélanie)
|/
* 6422483 - c0 (Mélanie)

En définitive, peu importe l’interface choisie, le résultat est un sacré foutoir. Là où les 2 collègues ont produit 4 commits au total (m1, m2, j1 et j2), on se retrouve avec 8 commits et un imbroglio d’historique.

Vous l’aurez compris, je n’aime pas et n’encourage pas cette option.

Le pull en mode rebase

Ce mode considère que la branche distante et la branche locale sont un même ensemble, une seule et même branche, on va donc mettre à jour nos nouveautés locales par dessus les évolutions distantes pour garantir cette forme d’unicité (le rebase à la place du merge).

Attention cependant, car sous ce mode rebase se cachent plusieurs possibilités, même si elles ne sont pas toutes énoncées comme on l’a vu au début de cet article. On distingue donc les modes suivants :

  • rebase = true
  • rebase = merges

Personnellement, je ne vois pas d’intérêt particulier au premier car il applanit nos fusions locales au moment du pull. Pour que vous comprenniez mieux de quoi je parle ici, on va reprendre l’exemple précédent avec exactement les mêmes opérations par chacun des 2 participants à la branche. On ajoutera cependant un dernier aller/retour avec une bosse produite dans l’historique par l’un des 2 participants.

On repart du début, avec un historique ne contenant que le commit c0 :

devc0

Mélanie a créé 2 commit, m1 et m2 qu’elle partage avant Joseph.

devc0m1m2

Joseph a produit les commits j1 et j2. Localement, avant récupération du travail de Mélanie, son historique est le suivant :

devc0j1j2

Après récupération, voici ce qu’il obtient :

devc0m1m2j1’j2’

Les commits locaux de Joseph ont été « rejoués » par dessus l’historique mis à jour. On a donc des copies de j1 et j2 qui sont j1’ et j2’. Surtout, on a un historique de branche “à plat”, une branche unique dev intégrant l’ensemble des commits sans ajout superflu.

Exemple avec rebase = true

On continue depuis notre historique avec Joseph. Il décide de créer une sous-branche qu’il fusionne localement en préservant la bosse :

devfeaturec0m1m2j1’j2’j3j4

Pendant ce temps, Mélanie à ajouté deux nouveaux commits à son historique local, m3 et m4 :

devc0m1m2j1’j2’m3m4

Elle partage ses modifications avant Joseph. Alponse doit alors se synchroniser et, malheur, sa bosse à disparu :

devc0m1m2j1’j2’m3m4j3’j4’

Joseph, qui voulait préserver ce décroché visuel dans son historique est bien embêté. Tout ça par la faute de cette option de configuration pas tout à fait adaptée.

Exemple avec rebase = merges

C’est là qu’intervient le rebase = merges, le Zoro sauveur des pull !

Avec cette option, le résultat de la synchronisation de Joseph aurait été le suivant :

devfeaturec0m1m2j1’j2’m3m4j3’j4’

Les commits j3 et j4 sont rejoués par dessus la version partagée par Mélanie tout en reproduisant la bosse de fusion. Notez au passage que l’étiquette de branche peut-être déplacée automatiquement si une autre configuration est renseignée (git config --global pull.updateRefs true présente depuis Git 2.38).

À vous désormais de faire votre choix. Attention cependant à ce que toutes les personnes travaillant sur un projet aient la même configuration, sinon je vous garantis un historique bien moisi !

Une dernière astuce

En cas de problème, vous pouvez très bien annuler le pull que vous venez de réaliser, changer votre configuration et refaire votre pull comme il faut. Vous trouverez tout le détail de cette procédure dans cet autre article : « Annuler un pull ».

Vous voulez aller plus loin et maîtriser pleinement les fondamentaux de Git ou être accompagné pour garantir la qualité de vos projets grâce à une bonne mise en place de Git ? On peut vous aider ou vous former, il suffit de nous décrire votre besoin !
Vous pouvez aussi regarder le programme de notre formation "Comprendre Git" ou nous poser vos questions sur notre forum discord.