Git Rebase, Merge ou Pull ?

Git Rebase, Merge ou Pull ?

A quoi servent git merge, git rebase et git pull ?

On va commencer par le plus simple : qu’est que git pull ? Et bien, c’est très simple, git pull c’est l’enchaînement de git fetch et de git merge. Oui bon, si vous ne savez pas ce qu’est git fetch et git merge, ça n’avance pas à grand chose…

Note

La commande git pull peut être configurée pour appliquer un git rebase à la place d'un git merge à la suite du git fetch. Pour que ce soit configuré de façon permanente, taper la commande suivante :

git config --global pull.rebase true

Sinon, si vous voulez appliquer un rebase de façon occasionnelle, vous pouvez aussi ajouter l'option --rebase à la commande git pull :

git pull --rebase

La commande git fetch permet de récupérer les modifications depuis le remote, c’est-à-dire depuis le serveur git distant dans la plupart des cas. Les modifications sont récupérées et sont stoquées dans le dossier .git mais elles ne sont pas appliquées. Vous pouvez par exemple voir ce qui a été poussé par vos collègues avant de les appliquer à votre espace de travail à l’aide des commandes git log et git diff ou bien en créant une nouvelle branche locale à partir de leurs commits.

Le rôle de git merge ou git rebase est alors d’appliquer les modifications récupérées avec la commande git fetch à votre espace de travail.

Comme son nom l’indique, git merge permet de merger deux branches en créant un nouveau commit. Supposons que nous ayons les deux branches devet master suivantes et que nous voulions merger la branche dev dans la branche master.

* 6f36e0b (dev) Thrid modification on branch dev
* ffa8732 Second modification on branch dev
* 95a09fc First modification on branch dev
| * 8d8885d (HEAD -> master, origin/master) Modify the third file
| * 3a152d8 Modify the second file
| * d101b26 Add a third file
| * 025fc2c Add a second file
|/
* c8324c9 Modify file1.txt
* 46db71c add file1.txt

Après avoir taper la commande git merge dev depuis la branche master, on obtient le graphe suivant montrant la création d’un nouveau commit 83b1c98 sur la branche master :

*   83b1c98 (HEAD -> master) Merge branch 'dev'
|\
| * 6f36e0b (dev) Thrid modification on branch dev
| * ffa8732 Second modification on branch dev
| * 95a09fc First modification on branch dev
* | 8d8885d (origin/master) Modify the third file
* | 3a152d8 Modify the second file
* | d101b26 Add a third file
* | 025fc2c Add a second file
|/
* c8324c9 Modify file1.txt
* 46db71c add file1.txt	

Note

Par défaut, lors d'un merge, si git le peut, il fera un fast-forward ce qui est assimilable à un rebase. Ce comportement est désactivable avec l'option --no-ff de la commande git merge. Nous en rediscuterons plus bas mais nous considérons ici que c'est cas pour simplifier la compréhension.

Maintenant faisons la même chose avec un rebase :

* 1fcff4e (HEAD -> master) Modify the third file
* d52a178 Modify the second file
* a373ff3 Add a third file
* ea6a6ef Add a second file
* 6f36e0b (dev) Thrid modification on branch dev
* ffa8732 Second modification on branch dev
* 95a09fc First modification on branch dev
* c8324c9 Modify file1.txt
* 46db71c add file1.txt

Lors d’un rebase, git commence par repérer le dernier commit commun aux deux branches (ici le commit c8324c9). Ensuite il applique les commits de la branche sur laquelle on veut se “rebaser” (ici la branch dev). Ainsi, les commits 95a09fc, ffa8732 et 6f36e0b sont appliqués. Enfin, les commits de la branche sur laquelle on est (ici master) sont appliqués.

Note

Avez-vous remarqué quelque chose de particulier concernant les commits de la branche master qui ont été appliqués ? Et si je vous donne en plus du log précédent, le log de la branch remote origin/master ?

	* 1fcff4e (HEAD -> master) Modify the third file
	* d52a178 Modify the second file
	* a373ff3 Add a third file
	* ea6a6ef Add a second file
	* 6f36e0b (dev) Thrid modification on branch dev
	* ffa8732 Second modification on branch dev
	* 95a09fc First modification on branch dev
	| * 8d8885d (origin/master) Modify the third file
	| * 3a152d8 Modify the second file
	| * d101b26 Add a third file
	| * 025fc2c Add a second file
	|/
	* c8324c9 Modify file1.txt
	* 46db71c add file1.txt

Et oui, git a créé des nouveaux commits !

OK mais alors, faut-il utiliser git merge ou git rebase ?

C’est là que je lance un troll et que je vais déclencher les foudres des pro-merges et des pro-rebases. Plus sérieusement, dans cette section je vais vous donner ma vision des choses dans 3 cas usuels : la récupération des commits du remote, le rapratriement des modifications d’une feature dans la branche principale de dev et inversement la récupération des commits de la branche de principale de dev pour le développement de votre feature. Cependant les choix peuvent différer suivant le workflow utilisé, les règles imposés par l’équipe ou encore la difficulté du merge à effectuer.

Bon tout d’abord, regardez le dernier log ci-dessus. Que se passerait-il si on décidait de pousser le rebase que l’on a fait ?

Si vous décidiez de pousser le rebase précédent git vous rejètera car vous voulez changer l’historique. C’est néanmoins possible avec l’option -f de la command git push mais c’est fortement déconseillé.

Ainsi, la première règle à respecter est de ne pas faire de rebase si cela modifie l’historique de repository remote. Contrairement au merge, un rebase change l’historique et ne peut donc pas être appliquer dans tous les cas. C’est notament pour cette raison que certains refusent de faire des rebase pour ne pas modifier l’historique.

Cas de la récupération des modifications du remote

Prenons maintenant le cas le plus courant, celui de la récupération des modifications du remote. Supposons que tous les développeurs fassent des merges (ou des pull avec la configuration par défaut comme c’est souvent le cas) pour récupérer les commits du remote.

Partons d’une branche de développement avec 3 commits :

git merge 1

Supposons que 2 développeurs partent du même commit n°3 pour commencer leur développement. Le premier développeur va commiter deux modifications en local :

git merge 2

Avant de pouvoir pousser ses modifications sur le remote, il va devoir récupérer les modifications du remote. Si ce développeur utilise un merge ou un pull (configuration par défaut) et supposant qu’il y ait eu des modifications sur le remote entretemps (ou bien que le fast-forward soit désactivé), il aura un historique semblable à celui-là :

git merge 3

Maintenant, prenons notre second développeur. Ce dernier veut également pousser 2 commits :

git merge 4

Si lui aussi récupère les modifications du remote avec un merge ou un pull, il va se retrouver avec l’historique suivant :

git merge 5

Et si on ajoute un troisième développeur ?

git merge 7

Bon vous avez compris, l’historique peut vite devenir incompréhensible. Et j’ai considéré un cas relativement simple avec des développements qui commencent depuis le même commit. Mais si par exemple un merge avait été fait par un quatrième développeur depuis le commit n°6, je vous laisse imaginer le bazard…

Et si tous ces développeurs avaient fait des rebases plutôt, que ce serait-il passer ? Avant de pousser leurs modifications, chacun des développeurs auraient récupérés les commits du remote et auraient ensuite appliqué les leurs. Cela nous aurait donné l’historique suivant :

git rebase

Je ne sais pas vous mais je trouve ça plus clair !

Note

Il y a quand même un détail à savoir et qui peut avoir son importance. Lorsque vous faites un merge, vous allez peut-être être amené à résoudre des conflits sur un ou plusieurs fichiers mais pour un fichier donné, vous n'aurez à résoudre des conflits qu'une seule fois. Ce n'est pas le cas lorque vous faites un rebase. Comme vos commits sont appliqués un à un, vous serez amené peut-être à résoudre des conflits plusieurs fois de suite sur un même fichier.

Afin d'éviter ça, il est recommandé de faire des rebases fréquemment.

Cas du développement d’une feature

Lors du développement d’une feature, il est d’usage de créer une nouvelle branche pour faire ses développements avant de les pousser sur la branche de développement commune. Considérons l’historique suivant :

git merge 8

Lorsque le développement de la feature est terminé, pour mettre ses modifications dans la branche de développement, le développeur peut donc, depuis la branche dev, faire soit un rebase, soit un merge.

Supposons qu’il fasse un rebase, on aurait donc l’historique suivant :

git rebase 2

Il y a un problème mineure et un problème majeur avec cette méthode.

Tout d’abord, en faisant comme ça, tous les commits de la branche feature feront partie de la branche principale de développement dev ce qui n’est pas forcément ce que l’on souhaite. Il est préférable à mon sens d’avoir sur la branche dev un seul commit correspondant à l’implémentation de la feature, quitte à garder le détail des commits de la feature dans une branche séparée (en la poussant sur le remote).

Mais le point le plus problématique est le point que j’ai évoqué plus haut, c’est-à-dire que si le commit “6” ait été poussé sur le remote, avec cette méthode, on ne pourrait pas pousser la branche de dev sur le remote à moins que l’on force le push.

Vous avez compris, dans ce cas précis, je préconise plutôt de faire un merge ce qui nous donnera l’historique suivant :

git merge 9

Ainsi, la branche dev n’aura qu’un seul commit correspondant à votre feature et ne sera donc pas poluée par tous les petits commits que vous auriez pu faire du développement. Et si toute fois, vous souhaitez garder l’historique du développement de votre feature, rien ne vous empêche de pousser votre branche sur le remote.

Cas de la récupération des commits de la branche dev pour le développement de votre feature

Considérons à présent le cas inverse, vous avez commencé le développement de votre feature mais l’un de vos collègues a commité une modification sur la branche de dev que vous avez besoin de récupérer pour le développement de votre feature.

git merge 8

Supposons que vous ayez l’historique précédent, cela revient donc à récupérer le commit “6” sur la branche feature.

Il y a deux cas de figure : le cas où votre branche feature est poussée sur le remote et celui où votre branche feature n’est qu’en local dans votre espace de travail.

Dans le premier cas, si vous avez bien suivi ce que j’ai dit plus haut vous ne pouvez pas faire de rebase.

git rebase 3

Vous voyez donc dans ce cas d’un rebase modifierait l’historique et il sera impossible de pousser sur le remote à moins de forcer le push. Dans ce cas, il faudra plutôt utiliser un merge.

git merge 10

Dans le second cas, c’est-à-dire le cas où votre branche feature n’est qu’en local, je recommande plutôt de faire un rebase. L’historique sera plus clair puisque ce sera comme si vous aviez commencé votre branche feature un (ou plusieurs) commit plus tard.

git rebase 4

Conclusion

J’ai présenté dans cet article trois cas de figure très courants dans un processus de développement et donné pour chacun d’eux ce que je préconiserais. Cependant, comme je l’ai évoqué plus haut, il y a souvent des cas un peu plus complexes qui ne sont pas aussi simples à régler. Le plus important à mon sens est d’abord de bien comprendre la différence entre un rebase et un merge et savoir comment sera votre historique après avoir appliqué l’une ou l’autre méthode. Il est aussi important de définir et respecter le même process au sein d’une même équipe. Enfin, certains préfèrent ne jamais modifier l’historique pour par exemple garder la cohérence chronologique des commits. Je comprends cette logique mais pour ma part je préfère avoir un historique clair et le plus linéaire possible.

Je finirai cette article par rappeler qu’avec git vous pouvez toujours essayer un merge ou un rebase et revenir en arrière si ça ne vous convient pas. Tant que vous n’avez pas poussé, vous pourrez annuler vos modifications et recommencer.