Configurer le pull en mode "rebase"
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. Vois plutôt le message qu’il t’affiche dans le terminal si tu n’as 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 tu en penses, 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 tu sais 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 t’expliquer à quoi correspondent ces modes. Tu pourras ensuite choisir à ta 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 tu travailles sur une branche dev
localement et que tu la partages sur un serveur Git distant, apparaît alors une nouvelle branche, généralement nommée origin/dev
(origin
étant le nom donné à ton dépôt distant). Git gère alors 2 branches séparées :
- ta branche locale
dev
; - la branche distante
dev
signalée localement par le nomorigin/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 :
- la récupération des nouveautés du distant sur la branche
origin/dev
(via un fetch) ; - l’application des nouveautés de la branche
origin/dev
sur la branche localedev
.
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, tu dois renseigner la configuration pull.rebase = false
:
git config --global pull.rebase false
En faisant ce choix, tu demandes à 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 ton 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, Michelle et Jacky qui travaillent sur une même branche dev
depuis un commit initial c0
. Chacun a travaillé sur des fichiers différents, Michelle partage son travail la première (les commits m1
et m2
), Jacky a réalisé de son côté les commits j1
et j2
sans les partager. Il récupère le travail de Michelle et obtient l’historique suivant :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "j1" commit id: "j2" branch "origin/dev" commit id: "m1" commit id: "m2" checkout dev merge "origin/dev"
Jusque là, rien d’inquiétant, on obtient une bosse, mais ça va. Si maintenant Michelle continue et partage un nouveau commit avant que Jacky ait pu faire son push, il devra encore se synchroniser et obtiendra ce qui suit :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "j1" commit id: "j2" branch "origin/dev" commit id: "m1" commit id: "m2" checkout dev merge "origin/dev" checkout "origin/dev" commit id: "m3" checkout dev merge "origin/dev"
Si ensuite Jacky pousse pendant que Michelle crée un nouveau commit localement, si cette dernière récupère le travail de Jacky, voilà la tête qu’aura l’historique :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" branch "origin/dev" commit id: "m1" branch "origin/dev (1er pull)'" commit id: "m2" branch "origin/dev (2e pull)" commit id: "m3" checkout "origin/dev" commit id: "j1" commit id: "j2" merge "origin/dev (1er pull)'" merge "origin/dev (2e pull)" checkout dev merge "origin/dev"
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 donnerait ce qui suit :
* 6a84d61 - Merge branch 'dev' of … (Michelle)
|\
| * f94a837 - (origin/dev) Merge branch 'dev' of … (Jacky)
| |\
| * \ 95571f9 - Merge branch 'dev' of … (Jacky)
| |\ \
| * | | ac66030 - j2 (Jacky)
| * | | 4a44558 - j1 (Jacky)
* | | | 9ef225f - m3 (Michelle)
| |_|/
|/| |
* | | 9a6c603 - m2(Michelle)
| |/
|/|
* | de8aefc - m1 (Michelle)
|/
* 6422483 - c0 (Michelle)
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.
Tu l’auras 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 tu comprennes 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
:
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0"
Michelle a créé 2 commit, m1
et m2
qu’elle partage avant Jacky.
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "m1" commit id: "m2"
Jacky a produit les commits j1
et j2
. Localement, avant récupération du travail de Michelle, son historique est le suivant :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "j1" commit id: "j2"
Après récupération, voici ce qu’il obtient :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "m1" commit id: "m2" commit id: "j1’" commit id: "j2’"
Les commits locaux de Jacky 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 Jacky. Il décide de créer une sous-branche qu’il fusionne localement avant de partager l’historique à jour avec la bosse issue de sa fusion locale :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "m1" commit id: "m2" commit id: "j1’" commit id: "j2’" branch feature checkout feature commit id: "j3" commit id: "j4" checkout dev merge feature
Pendant ce temps, Michelle à ajouté deux nouveaux commits à son historique local, m3
et m4
:
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "m1" commit id: "m2" commit id: "j1’" commit id: "j2’" commit id: "m3" commit id: "m4"
Elle doit se synchroniser avant de pouvoir partager son travail et obtient l’historique suivant après avoir fait un pull :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "m1" commit id: "m2" commit id: "j1’" commit id: "j2’" commit id: "j3" commit id: "j4" commit id: "m3’" commit id: "m4’"
Si elle partage son travail, Jacky risque d’être colère 😡 : « Tu as cassé ma belle bosse qui marquait explicitement mon travail dans l’historique ».
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 Michelle aurait été le suivant :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'dev'} } }%% gitGraph commit id: "c0" commit id: "m1" commit id: "m2" commit id: "j1’" commit id: "j2’" branch "(feature)" checkout "(feature)" commit id: "j3" commit id: "j4" checkout dev merge "(feature)" commit id: "m3’" commit id: "m4’"
Les commits m3
et m4
sont rejoués par dessus la version partagée par Jacky, donc celle avec la bosse de fusion. Note au passage qu’elle récupère la bosse mais l’étiquette de branche (sauf si Jacky l’avait poussé), d’où la notation entre parenthèses (feature)
.
À toi désormais de faire ton choix. Attention cependant à ce que toutes les personnes travaillant sur un projet aient la même configuration, sinon je te garantis un historique bien moisi !
Une dernière astuce
En cas de problème, tu peux très bien annuler le pull que tu viens de réaliser, changer ta configuration et refaire ton pull comme il faut. Tu trouveras tout le détail de cette procédure dans cet autre article : « Annuler un pull ».
Vous pouvez aussi regarder le programme de notre formation "Comprendre Git" ou nous poser vos questions sur notre forum discord.