Débuter avec Git partie 7 : git rebase pour ré-écrire son historique

Aujourd’hui nous allons faire tomber un mythe : la commande git rebase. Non, elle n’est pas compliquée, c’est juste que vous y avez souvent été confrontée dans une situation de stress sans trop comprendre ce qui se passait, par exemple dans un git pull –rebase qui se passe mal. Nous allons étudier un cas simple et vous allez désormais l’utiliser quasiment au quotidien. Oui oui, au quotidien.

Fusion ? Git rebase ?

Si cette partie relativement théorique ne vous intéresse pas, sautez directement au chapitre suivant pour l’exemple pratique.

En prérequis nous allons faire un rappel rapide de ces deux notions. La fusion consiste à prendre les différents commits, à en étudier la possibilité des les assembler et à générer un nouveau commit. D’où le terme de fusion. Vos deux branches d’origine restent inchangées, modulo le nouveau commit créé.

Le rebase fonctionne différemment. Lorsque vous voulez effectuer un rebase, Git va rechercher le commit commun le plus récent entre les deux branches, puis va essayer de constituer une branche cohérente résultante de cette opération.

La grosse différence donc est que dans le cas de la fusion, on obtient le résultat dans un nouveau commit, alors que rebase va retravailler la branche sur laquelle vous êtes.

Cette explication est bien sûr clairement vulgarisée, imparfaite et sommaire, et je vous invite à lire la documentation officielle si vous souhaitez plus de détails.

Entrons dans le vif du sujet avec un cas d’usage que vous rencontrez tous les jours. Croyez-moi.

Le commit imparfait

Cas typique d’usage de rebase : la correction d’un commit imparfait. Vous êtes allé trop vite et avez oublié d’embarquer un fichier dans votre commit, alors vous faites un nouveau commit pour corriger, avec le plus souvent un message peu clair car vous êtes énervé. Ne niez pas, ça nous est tous arrivé et ça vous arrive peut-être encore souvent.

C’est parfait, on va corriger ça avec rebase.

Commençons par mettre l’environnement de test en place avec un nouveau dépôt Git. On passe rapidement sur ces opérations car on en a déjà parlé dans cet article.

$ git init .
Initialized empty Git repository in /home/user/debuter-avec-git/7/.git/
$ echo "foofoofoo" > file1
$ git add file1 
$ git commit file1 -m "initial commit"
[master (root-commit) 9a8a61f] initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 file1

On initie un nouveau dépôt, puis un créé un premier commit avec un fichier file1.

$ echo "barbarbar" > file2
$ git add file2
$ git commit file2 -m "add file2"
[master dcd1b75] add file2
 1 file changed, 1 insertion(+)
 create mode 100644 file2

On ajoute ensuite un deuxième commit, contenant un fichier file2.

La double boulette

Nous voulons maintenant ajouter deux fichiers en même temps au dépôt. Malheureusement pour vous, une réunion commence, vous êtes en télétravail et vous continuez à travailler pendant que votre chef soliloque dans votre casque (on en parle dans cet article). Et là…

$ echo "blablabla" > file3
$ echo "lalalala" > file4
$ git add file3
$ git commit file3 -m "add file3 and file4"
[master 122fa7d] add file3 and file4
 1 file changed, 1 insertion(+)
 create mode 100644 file3

La boulette ! Vous avez oublié d’ajouter le fichier file4 ! Tout ça à cause de la réunion !

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	file4

Vous tentez de corriger.

$ git add file4
$ git commit file4 -m "azlemjrlmzjaejrlj"
[master 23486d2] azlemjrlmzjaejrlj
 1 file changed, 1 insertion(+)
 create mode 100644 file4

Vous venez en fait d’aggraver la situation. Vous ajoutez à votre première erreur la lourdeur d’un nouveau commit sans réel intérêt et, de plus, un message de commit inutile, sans valeur pour vous et vos collègues. En effet jetons un œil à votre historique avec la commande git log.

$ git log
commit 23486d2a4052710025ea76b6a79ae4a481bef8af (HEAD -> master)
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:17:59 2021 +0100

    azlemjrlmzjaejrlj

commit 122fa7da505bfd924482dbeb393b1bd64796c874
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:17:12 2021 +0100

    add file3 and file4

commit dcd1b7500c56025b43172d05d2e071d38544d8e5
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:15:30 2021 +0100

    add file2

commit 9a8a61f662ea8397f03fdf1b19d45a053dbb83d7
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:14:33 2021 +0100

    initial commit

On le voit, le dernier commit est, disons-le, laid, sans valeur ajoutée pour personne. Il fait le travail certes mais doit être amélioré, surtout dans le cadre du travail collaboratif où vous n’êtes pas le seul à lire cet historique (voir plus bas). Vous allez faire perdre du temps à tous vos relecteurs.

Pas une mais deux erreurs en un seul commit
Pas une mais deux erreurs en un seul commit

Git rebase à la rescousse

Nous allons maintenant voir l’intérêt de rebase. Vous passez la commande suivante pour “retravailler” les deux derniers commits.

$ git rebase -i HEAD~2

Cette commande signifie que nous souhaitons appliquer la fonction rebase à partir de HEAD, notre position actuelle sur la branche courante (on en a parlé dans cet article) jusqu’au deuxième commit en arrière.

Un menu apparaît dans votre éditeur qui va vous demander des informations.

pick 122fa7d add file3 and file4
pick 23486d2 azlemjrlmzjaejrlj

# Rebase dcd1b75..23486d2 onto dcd1b75 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]

# Note that empty commits are commented out

Le haut de ce menu présente une liste des commits qui vont être retravaillés par rebase, les commits sont affichés selon leur ancienneté, de haut en bas (attention, contrairement à git log donc). Tout le reste du fichier est constitué de commentaires à caractère informatif.

Le premier mot précise quelle modification appliquer. Nous allons utiliser le mot-clé fixup pour fusionner le commit désigné avec le précédent, le message de commit du précédent sera également conservé et celui du commit designé sera supprimé. D’une pierre deux coups, donc.

pick 122fa7d add file3 and file4
fixup 23486d2 azlemjrlmzjaejrlj

Nous sauvons maintenant le contenu du fichier.

$ git rebase -i HEAD~2
Successfully rebased and updated refs/heads/master.

Le message annonce que l’opération rebase a été correctement effectuée. Un coup d’œil sur notre historique va nous le confirmer.

$ git log
commit 4fb9e75b8749453936d7cfa409127e228aefdc60 (HEAD -> master)
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:17:12 2021 +0100

    add file3 and file4

commit dcd1b7500c56025b43172d05d2e071d38544d8e5
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:15:30 2021 +0100

    add file2

commit 9a8a61f662ea8397f03fdf1b19d45a053dbb83d7
Author: Carl Chenet <chaica@ohmytux.com>
Date:   Tue Mar 2 13:14:33 2021 +0100

    initial commit

Nos deux erreurs ont bien été corrigées. Nous présentons maintenant un commit cohérent, avec un message de commit explicite. Une étude du commit nous le confirme.

$ git show --pretty=oneline 4fb9e75b8749453936d7cfa409127e228aefdc60
4fb9e75b8749453936d7cfa409127e228aefdc60 (HEAD -> master) add file3 and file4
diff --git a/file3 b/file3
new file mode 100644
index 0000000..6a4238f
--- /dev/null
+++ b/file3
@@ -0,0 +1 @@
+blablabla
diff --git a/file4 b/file4
new file mode 100644
index 0000000..854f93c
--- /dev/null
+++ b/file4
@@ -0,0 +1 @@
+lalalala

Les deux fichiers sont bien ajoutés par ce commit désormais unique.

L’utilisation de git rebase dans le cadre du travail collaboratif

Comme nous avons vu, nous aurions pu conserver cette historique et continuer à travailler, mais le problème survient lorsqu’un collègue va vouloir relire l’historique. Si nous n’avions pas corrigé, votre collègue aurait lu azlemjrlmzjaejrlj comme message de commit et aurait donc dû lire lui-même le détail du commit, une opération que l’on peut donc facilement éviter avec un message de commit explicite.

Voyons maintenant comment corriger l’erreur au niveau du dépôt partagé avec les collègues. Nous avions poussé notre double erreur vers le dépôt Gitlab.

$ git push origin master
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 273 bytes | 273.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://gitlab.com/articles/debuter-git-7.git
   9b95c2c..95fe5b2  master -> master

Après notre utilisation du rebase, nous retentons de pousser notre modification.

$ git push origin master
To https://gitlab.com/articles/debuter-git-7.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://gitlab.com/chaica/debuter-git-7.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Une erreur apparaît. Git nous informe qu’il ne peut pas effectuer l’opération fast-forward, assurant la bonne conduite de la mise-à-jour distante. Il va falloir effectuer une opération un peu plus complexe avec l’option –force-with-lease.

$ git push --force-with-lease origin master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 341 bytes | 341.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To https://gitlab.com/chaica/debuter-git-7.git
 + 95fe5b2...831f45c master -> master (forced update)

Attention. Je ne détaille pas cette option ici mais pour information N’UTILISEZ JAMAIS L’OPTION –force sans être absolument 100% sûr de ce que vous faites et/ou avant d’avoir consulté vos collègues. C’est très souvent une très mauvaise idée. Et presque 100% du temps une bêtise si vous travaillez sur la branche master en collaboration avec d’autres personnes. À éviter donc.

L’option –force-with-lease, contrairement à l’option –-force, va vérifier que le pointeur distant est encore bien positionné sur votre dernier commit précédant le rebase et que tous les pointeurs distants utilisent bien ce commit et donc que quelqu’un d’autre n’a pas retravaillé votre historique, auquel cas vous écraseriez son travail avec –force.

En gros, retenez que vous êtes sûr à 100% de n’impacter personne d’autre avec l’option –force-with-lease de git push quand vous souhaitez pousser votre historique modifié.

Maintenant le retravail de votre historique a bien été poussé sur le dépôt distant, votre commit est bien plus lisible et votre message de commit explicite, prêt à être relu par un collègue.

Le retravail des messages de commits

Il s’agissait ici d’un exemple très simple pour vous initier à la puissance de rebase. Nous n’avons travaillé que sur 2 commits. Vous pourriez en embarquer bien davantage dans votre opération. Vous pouvez même envisager des opérations concernant l’intégralité des commits sur une branche. Boowaa!

Une autre utilisation très classique est le retravail des messages de commits. Nous avons vu – lorsque que rebase vous demande quelle(s) opération(s) vous souhaitez effectuer – que nous avions à notre disposition l’option suivante :

# r, reword <commit> = use commit, but edit the commit message

Vous pouvez donc revenir sur un ou plusieurs messages de commits voire même tous les messages de commits de votre branche, par exemple pour y appliquer un formalisme nécessaire à votre gestionnaire de tickets ou à votre CI/CD. Vous ne pourrez bientôt plus vous en passer, les petites typos que vous avez laissées partout traîner dans vos messages parce que vous ne saviez pas comment les corriger ? Rebase commence à vous plaire, je le sens.

La puissance de git rebase

Notons que nous avons utilisé l’option fixup pour fusionner deux commits pendant l’opération et supprimer le message de celui sur lequel on avait déclaré vouloir effectuer le fixup.

Si vous aviez voulu conserver les deux messages de commits pour les retravailler, vous auriez pu utiliser l’option squash, très couramment utilisée également dans les opérations de rebase.

# s, squash <commit> = use commit, but meld into previous commit

D’autres options plus avancées comme edit ou exec permettent des opérations puissantes et complexes, mais on sort un peu du cadre de l’article. À découvrir donc le jour où vous en aurez besoin 😉

Faire une sauvegarde pour éviter le stress avec git rebase
Faire une sauvegarde pour éviter le stress avec git rebase

Ceinture et bretelles

N’oubliez jamais que mieux vaut prévenir que guérir.

Avant de tenter une manipulation qui vous semble complexe, n’hésitez pas à copier l’intégralité du répertoire racine qui contient vos sources (celui qui contient le répertoire .git) vers un autre endroit avant d’effectuer votre opération. C’est moche et ça fait rigoler les puristes certes (je vous vois rigoler là au fond) mais ça peut vous éviter bien des ennuis si vous êtes coincé et que personne n’est là pour vous aider. Il faut bien s’entraîner avant de maîtriser, c’est vrai pour tous les outils. Avec une sauvegarde à côté, on ne pourra jamais vous reprocher d’avoir perdu des données.

De plus, bien souvent dans les opérations de rebase qui tournent mal, un simple git rebase –abort vous permet de revenir à l’état antérieur à l’opération.

Conclusion

Nous allons nous arrêter ici pour l’article d’aujourd’hui. Nous avons montré un cas d’utilisation très commun de rebase et comment pousser un historique modifié par rapport à celui qui avait déjà été poussé vers un dépôt partagé.

Je vous encourage à tester rapidement le retravail des messages de commits, c’est également une option dont je me sers assez souvent. Nul doute qu’en maîtrisant ces bases, vous aborderez sereinement les cas d’utilisations plus complexes si un jour vous y êtes confronté.

Finalement et pour rappel, le rebase est une opération puissante et donc comme beaucoup de choses dans Git potentiellement dangereuse, n’hésitez pas à faire une sauvegarde de vos données avant de vous lancer.

Me suivre sur les réseaux sociaux

N’hésitez pas à me suivre directement sur les différents sociaux pour suivre au jour le jour mes différentes projets dans le Logiciel Libre :

Suivre l’actualité du Logiciel Libre et Open Source francophone

Abonnez-vous au Courrier du hacker, une newsletter hebdomadaire résumant le meilleur de l’actualité francophone du Logiciel Libre et Open Source. Déjà plus de 160 numéros et 3500 abonnés.

3 thoughts on “Débuter avec Git partie 7 : git rebase pour ré-écrire son historique

  1. Hello, il me semble que git commit –amend est plus adapté que le rebase ici. Et je serais un peu plus prudent concernant git push –force-with-lease : ça permet effectivement de ne pas écraser le travail d’un collègue mais ça n’empêche pas qu’un collègue ait déjà récupéré la branche avant le force push, ce qui peut causer des problèmes lorsqu’il voudra pousser les modifs qu’il aura faites en partant de ce code.

    • Tout a fait, pour la première partie. Par contre –amend ne marchera pas sur n commits ou avec que ça implique (reword, squash sur plusieurs commits). L’idée n’est pas vraiment de proposer la meilleure solution mais plutôt de mettre le pied à l’étrier aux débutants pour commencer à utiliser rebase et rapidement dépasser le premier cas d’usage comme vu en fin d’article.

  2. Bonjour Carl, je me permets une petite précision concernant le fait que n’importe quel rebase peut être défait avec un reset, grâce au reflog (https://delicious-insights.com/fr/articles/git-reset/). Je ne suis pas fan de l’approche “faites une sauvegarde” du répertoire avant le rebase. Je trouve que ça tend à rendre les gens craintifs de la commande sans leur exposer le fait qu’avec Git tout ce qui a fait l’objet d’un commit peut être retrouvé (avant le passage du GC).

    Sinon, j’en profite pour te partager des petites astuces complémentaires autour de rebase grâce à aux commits de fixup :

    https://delicious-insights.com/fr/articles/git-protip-autofixup/
    https://delicious-insights.com/fr/articles/git-protip-autoreword/

    Merci pour ton travail de vulgarisation. Espérons que ça aide les gens à utiliser Git au meilleur de ses capacités.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *