Article mis à jour le : 05-05-2022
Un petit tour d'horizon sur les principales options git: merge, fast-forward et Rebase, avec des explicationsNote : article actualisé le 08/10/2021 suite à certaines remarques de lecteurs.
Git jouit d'une popularité croissante et devient souvent une condition sine-qua-none pour pouvoir candidater à certaines offres d'emploi. Il est donc intéressant de voir parfois certaines notions avancées, qu'elles soient critiquables ou pas. Dans ce billet, nous allons aborder le fast-forward et le rebase.
Au sommaire de cette article:
La plupart des utilisateurs de Git se contentent de réaliser les opération suivantes:
La troisième est celle qui nous intéresse ici: elle consiste à faire, en une seule commande, un git fetch (recupère les modifs distantes mais sans les merger avec mon travail) + un git merge (fusionner mon travail avec celui qui est sur le dépôt distant).
La commande merge est souvent donc réalisée automatiquement. Pourant il y a une donnée important à connaître: il s'agit de la notion de fast-forward. Il s'agit d'une manière particulière de gérer l'historique des branches. Sachez qu'elle est utilisée par défaut, que vous fassiez un merge ou un pull directement.
Imaginons que vous créiez une branche feature/toto à partir de la branche develop. Vous réalisez vos travaux sur votre branche, plusieurs commits, puis enfin, en local ou directement sur le dépôt distant via un serveur comme GitLab, un merge. Si entre le moment où vous avez créé votre branche et celui ou vous voulez répercuter vos modifications sur la branche develop cette dernière n'a pas eu de nouvelles modifications - que vous n'auriez donc pas eu - Git va opter pour un merge avec fast-forward.
Concrètement, il s'agit d'intégrer l'historique de votre branche dans celui de la branche develop. Si vous aviez opté pour un merge sans fast-forward, vous auriez un historique distinct. Explications avec les petits schémas ci-dessous.
Vous remarquerez qu'en l'absence de fast-forward, il y a un commit de merge (vert).
Avis perso : j'aime bien cette approche, qui permet de conserver un historique complet et d'annuler le merge en seul coup en faisant un revert sur le commit de merge (certains outils comme GitHub permettent de le faire depuis leur interface).
Comme expliqué plus haut, le fast-forward est réalisé par défaut. Si vous souhaitez que cela ne soit pas le cas, vous avez deux possibilités:
1- au cas par cas
Qu'il s'agisse d'un merge ou d'un pull, vous avez juste à rajouter le paramètre -no-ff pour le désactiver juste pour cette commande, ou à l'inverse, si vous l'avez désactivé par défaut, utiliser le paramètre -ff pour l'activer juste pour cette commande.
ou
2- globalement
Dans votre fichier de configuration git ( ~/.gitconfig sous Linux) vous pouvez rajouter le bloc suivant:
ou le faire avec la commande git config:
Note: Désactiver le fast forward a quand même un effet gênant: il créé un commit de merge à chaque fois que vous faites un pull origin, ce qui va polluer votre historique local. Plutôt que de faire un rebase (nous verrons cela plus loin), vous pouvez autoriser le fast-forward juste pour les pull:
Avis perso : en local, j'applique les deux principes suivants :
Le rebase est lié, par le principe, au fast-forward dans la mesure où lui aussi sert à réécrire l'Histoire, mais de manière beaucoup plus radicale.
Imaginons le cas suivant: je crée ma propre branche depuis disons, la branche develop. Pendant que je fais mes développements, mes commits dans ma branche, d'autre personnes vont inclure leur développement (via des merges ou des fixes directement sur develop) dans la branche develop. Quand j'ai fini mon travail, je souhaite donc merger celui-ci dans develop. Que je sois pour ou contre le fast-forward ne compte pas puisqu'ici il n'est pas possible dans la mesure ou des modifications ont eu lieu dans develop. Si je me contente de merger, voici ce que nous aurons dans l'historique:
Certains lead-developpeurs ou encore release-managers n'apprécient pas tout cela, de voir des boucles dans l'historique du projet. Attention: c'est toujours une bonne pratique de créer une branche pour chaque nouveau développement, cependant il arrive donc parfois que certains responsables de projets n'apprécient pas de voir dans l'historique final toutes sortes de boucles. La commande rebase peut alors avoir son intérêt, elle permet de merger les historiques. Ainsi, au lieu d'avoir ce que nous voyons dans l'image ci-dessus, voici ce que nous aurions :
Que s'est-il passé ? Reprenons tout dans l'ordre chonologique :
En local, voici ce que cela a donné au niveau Git:
Squash: action de ressembler des commits en un seul.
Reprenons notre cas d'avant la fusion:
Je me rends compte que mes deux premiers commits servent à corriger le même bug, et qu'ils auraient pu être faits en un seul. Alors évidemment, par sécurité, on commite plusieurs fois quand on développe, on garde ainsi notre historique, cela fait une sauvegarde, mais à la fin, quand tout fonctionne, on peut aussi avoir envie de tout nettoyer et factoriser. Avant de merger (avec ou sans rebase) mes modifications dans develop, je vais donc réécrire l'histoire de mes commits. Nous allons encore utiliser la commande rebase:
Elle va lancer git en mode interactif pour éditer les 5 derniers commits (j'ai pris 5 au hasard, ici nous ne voulons modifier que les 2 premiers).
Leur détail va apparaître dans les premières ligne de l'éditeur de texte, un commit par ligne, avec en dessous plein de commentaires (précédés du caractères #) vous indiquant la marche à suivre:
Si supprimer la ligne d'un commit supprime tout simplement ce dernier, changer le premier mot par un de ceux proposés vous permettra de faire des modifications. Dans notre cas, nous allons donc:
Conformément à ce qui est écrit dans les commentaires, je vais donc :
SI vous fermez l'éditeur, Git vous invite à saisir votre message pour le premier commit. Ensuite, il fusionnera vous deux commits, vous pourrez consulter le résultat avec la commande git log.
Note : l'interface de certains outils comme GitHub permet aussi de faire un merge squash. Cela permet ainsi de merger tous vos commits en un seul tout en permettant de changer le message qui décrira ce commit. Bien utile !
Les éléments mentionnées ci-dessus sont à prendre avec du recul. En effet, il y a des pour et des contre, il y a des gens qui disent amen et d'autres qui hurlent en voyant cela. En fait tout dépend donc du projet et surtout par qui il est géré, chacun ayant son opinion. En tant que développeur, on s'adapte. Je vais quand même vous donner mon avis.
Donc à mon humble opinion, le fast-forward et surtout le rebase me font penser à des petits héritages de SVN où tout était linéaire (SVN ne gérait pas les branches). L'intérêt de Git, c'est notamment les branches, et de conserver leur historique qui permet ainsi de tout pouvoir tracer, même si cela devient parfois illisible. Cependant, j'utilise le rebase presque au quotidien pour des raisons de nettoyage : un commit par branche pour faciliter la lecture et les cherry-pick, ou alors pour rebase depuis master quand ma branche de travail devient un peu vieille. En résumé, je rebase ma branche régulièrement et essaye de pousser des choses "propres" avec un seul commit par fonctionnalité en utilisant la fonction squash.