Dynamic-Mess.com


"The world is a dynamic mess of jiggling things..."

Merge, Fast-Forward et rebase: un peu de culture git

Article posté le 27-02-2017 dans la catégorie Développement

Article mis à jour le : 05-05-2022

Un petit tour d'horizon sur les principales options git: merge, fast-forward et Rebase, avec des explications

Note : 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:

1- Fast-Forward

La plupart des utilisateurs de Git se contentent de réaliser les opération suivantes:

  1. fetch ;
  2. checkout ;
  3. pull ;
  4. commit ;
  5. push.

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.

A- Explications

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.

Git merge

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).

B- Paramétrage

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.

git merge --no-ff

ou

git pull --no-ff

2- globalement

Dans votre fichier de configuration git ( ~/.gitconfig sous Linux) vous pouvez rajouter le bloc suivant:

[merge]
    ff = no
    commit = no

ou le faire avec la commande git config:

git config --global merge.commit no
git config --global merge.ff no

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:

[pull]
        ff = yes

Avis perso : en local, j'applique les deux principes suivants :

2- Rebase

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.

A- Explications

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:

Rebase

 

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 :

Rebase 2

Que s'est-il passé ? Reprenons tout dans l'ordre chonologique :

  1. Je pars de (5), sur la branche develop ;
  2. je fais mon travail ;
  3. mais dans le même temps, une autre personne travaille aussi et merge son travail sur develop ;
  4. quand j'ai fini, pour me mettre à jour de develop, notamment pour résoudre d'éventuels conflits, en local, je fais un rebase depuis develop ;
  5. Avec cette commande, Git place mon travail après celui des autres, en dernier, même si historiquement il a peut-être été fait avant.

En local, voici ce que cela a donné au niveau Git:

git checkout develop // Je me mets sur develop
git pull // Je met ma copie de develop à jour
git checkout -b maBranche // Je crée ma branche
// Ensuite, je fais des commits sur ma branche, et d'autres personnes vont merger leur travail sur develop
// Quand j'ai fini, je veux me mettre à jour:

git pull --rebase origin develop // j'aurai aussi pu le faire depuis ma branche develop locale

 

B- Squash : fusionner et reprendre ses historiques avec l'option interactive

Squash: action de ressembler des commits en un seul.

Reprenons notre cas d'avant la fusion:

Rebase

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:

git rebase -i HEAD~5

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:

# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.

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 ! 

 

3- Avis perso

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.

 


Cet article vous a plu? Découvrez d'autres articles :


comments powered by Disqus