Stack Labs Blog moves to Dev.to | Le Blog Stack Labs déménage sur Dev.to 🚀

24 avril 2019 | Git | Olivier Revial

Git : passage en force oui, mais en --force-with-lease

Temps de lecture estimé : 8 minutes

Les adeptes du rebase Git le savent, il est parfois nécessaire d’utiliser un force push pour écraser l’historique d’une branche distante. A travers cet article nous verrons quels sont les risques d’une telle pratique, et comment nous pouvons rajouter un peu de sécurité lors de ce type d’opérations.

⚠️ Disclaimer : cet article n’a pas pour but du choix du rebase contre le merge. ⚠️

Le push --force c’est pas interdit ? 😈

Certains d’entre vous vont se dire, en lisant le titre de l’article, qu’il ne faut jamais faire de push --force. Si c’est votre cas, c’est que vous faites probablement des commits de merge lorsque vous voulez merger des branches. Si en revanche vous utilisez la méthode de rebase (par exemple avec un workflow basé sur le feature branching) pour merger vos branches, vous êtes forcément confronté à un problème : vous avez besoin d’écraser votre feature branche avec les derniers commits de la branche master/dev.

Prenons un exemple simple : sur notre projet nous avons une branche master qui est la branche de production. Paul et Marie créent tous les deux une feature branch à partir de master. Vu que Paul avait une fonctionnalité très courte à implémenter, il merge rapidement sa feature branch sur la branche master, le pointeur sur master a donc avancé d’un commit. Lorsque Marie termine sa fonctionnalité, elle voit que sa feature branch n’est pas à jour par rapport à master puisque Paul y a rajouté un commit entre temps. La situation est la suivante :

A partir de ce constat Marie a donc 2 possiblités :

  1. Faire un commit de merge :
  2. Intégrer les commits manquants de master sur sa feature branch locale à l’aide d’un rebase :

En faisant un rebase Marie réécrit l’historique de sa branche locale ce qui signifie que sa branche locale ne partage plus le même historique que sa branche distante. Elle va donc devoir écraser l’historique de sa branche distance avant de pouvoir faire un merge en fast-forward de sa feature branche dans master, pour arriver à un état similaire à celui-ci :

Pour écraser la branche distance avec sa branche locale, Marie va donc lancer la commande suivante :

> git push --force origin feature-2 

Les risques du push --force

Imaginons maintenant que nous avons affaire à une grosse fonctionnalité et que Thomas avait lui aussi travaillé sur la même feature branch.

Marie est la première à commencer à travailler sur la fonctionnalité, elle commit une première partie, puis la pousse sur la feature branch jaune, qui ressemble alors à ça :

Lorsque Thomas veut à son tour travailler sur la même fonctionnalité, il récupère localement la feature branch initiée par Marie, code sa partie, la commit (ici en rouge) et la pousse sur la feature branche distante. Jusqu’ici tout va bien, la branche distante ressemble désormais à ceci :

Il se trouve que Marie a entretemps continué à travailler sur sa partie et a rajouté un commit sur sa branche locale. Sa partie est maintenant prête mais elle sait que la feature 1 a été mergée sur master, elle va donc faire un rebase de la branche master sur sa branche, ce qui va lui donner la branche locale suivante :

Tout est prêt pour Marie, elle va donc pousser son code sur la branche distante :

> git push --force origin feature-2 

En faisant cela, Marie vient de réécrire l’historique de la branche distante… et d’écraser le commit de Thomas ! En effet, la branche distante avait changé mais Marie l’a simplement ignoré en appliquant le flag --force à son commit.

A l’inverse, le flag --force-with-lease empêche de pousser une branche locale si la branche distante n’a pas le même ancêtre. En tapant la commande suivante :

> git push --force-with-lease origin feature-2 
To /tmp/repo
 ! [rejected]        feature-2 -> feature-2 (stale info)
error: failed to push some refs to '/tmp/repo'

Marie n’aurait pas pu pousser sur la branche distante. Elle aurait donc d’abord du faire un merge ou un rebase sur sa branche locale, ce qui lui aurait permis de récupérer le commit de Thomas avant de faire son push, pour arriver enfin à l’état final voulu pour la branche distante :

Travailler seul pour éviter les problèmes ? 🤷

Lorsqu’on aborde le sujet du --force-with-lease, il n’est pas rare de s’entendre dire : “ok mais moi je travaille tout seul sur ma branche donc il n’y a pas de risque”. Cependant, même si vous travaillez tout seul sur votre branche (ou sur votre projet), il peut y avoir des risques d’écrasement de branches. Cela peut arriver par exemple si vous poussez du code depuis plusieurs postes (votre PC fixe et votre PC portable par exemple).

Dans le doute, pensez à toujours utiliser le flag --force-with-lease lorsque vous poussez votre code sur des branches distantes, ça ne coûte rien et ça permet de sécuriser un peu le push. Le jour où vous serez plusieurs à contribuer à une branche ou à votre projet, vous serez bien content d’avoir le réflexe d’utiliser le --force-with-lease.

Une solution 0 défaut ?

Comme toutes les fonctionnalités de Git, il vaut mieux savoir comment fonctionne la commande qu’on utilise au risque de faire des erreurs. Le flag --force-with-lease n’échappe pas à cette règle.

Si le --force-with-lease permet de se prémunir d’un certain nombre de risques auxquels on s’expose en utilisant le simple --force, il y a toutefois un risque à l’utiliser. Lors d’un --force-with-lease, Git va vérifier que la référence de la branche distante correspond à la référence de la branche locale, sans quoi il va tout simplement refuser le push.

Dans le cas où vous faites un git fetch, vous allez mettre à jour la référence de la branche distante sur votre copie locale, ce qui veut dire que Git va ensuite croire que vous êtes à jour de la branche distante, alors que vous n’avez pas récupéré le code (vous avez seulement récupéré la référence). Le blog d’Atlassian met en évidence ce “défaut” :

> git push --force-with-lease
To /tmp/repo
! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'
  
> git fetch
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /tmp/repo
 1a3a03f..d7cda55  dev        -> origin/dev

> git push --force-with-lease
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 845 bytes | 0 bytes/s, done.
Total 9 (delta 0), reused 0 (delta 0)
To /tmp/repo
 d7cda55..b57fc84  dev -> dev

Pour éviter cette situation, mieux vaut toujours récupérer le code distant à l’aide d’un pull (ou d’un pull --rebase) plutôt que de faire un simple fetch.

Quelques bonnes pratiques

Mettre --force-with-lease par défaut

L’avantage du flag push --force-with-lease, vu qu’il apporte une sécurité supplémentaire par rapport au simple push --force, c’est que vous pouvez l’utiliser de manière systématique pour remplacer à la fois votre push standard et votre push --force.

Je recommande donc de toujours utiliser cette commande. Pour cela, vous pouvez par exemple créer un alias Git :

> git config --global alias.pfl 'push --force-with-lease'

Ici j’ai créé un alias pfl pour “Push –Force-with-Lease”, mais vous pouvez utiliser l’alias que vous voulez (un de mes collègues utilise l’alias yolo 🤘).

Désormais pour pousser votre code il vous suffira de taper :

> git pfl

⚠️ Il est bien sûr possible de créer un alias qui remplace le push --force par le push --force-with-lease mais je ne recommande pas cette option. En effet, en faisant ça vous risquez d’oublier que vous utilisez le force-with-lease, et vous risquez donc un jour d’utiliser un push --force depuis une autre machine qui n’aura pas été configurée de la même manière, et donc vous ferez potentiellement un push destructeur !

Protéger la branche de développement

Si je parle de réécrire l’historique de vos branches distantes en forçant les push, je rappelle que cela s’applique uniquement sur les feature branches lorsque vous utilisez le rebase comme méthode de merge des commits.

En aucun cas vous ne devez réécrire l’historique de la branche commune de développement (par exemple master ou develop), cette branche devrait d’ailleurs être protégée contre le force push.

Cette pratique est d’ailleurs tellement importante qu’elle est le comportement par défaut sur des outils d’hébergement Git comme Gitlab :

Ainsi, seules des merge requests sur l’outil permettent de pousser du code sur une branche protégée.

Le mot de la fin

Comme nous l’avons vu, l’utilisation du flag --force-with-lease permet de se prémunir d’un certain nombre de risques que présente le flag --force. S’il permet de rajouter un peu de sécurité lors de la publication de branches distantes, il doit tout de même être utilisé en connaissance de cause pour éviter d’écraser des changements distants en pensant bien faire.

Ainsi, une fois ces risques connus et maîtrisés, il devient très pratique d’intégrer le --force-with-lease à son workflow Git.


Références :

Git