Git interactive rebase, squash, amend et autres façons de réécrire l'histoire

Tute Costa
Traduit par George Kosmopoulos

“Tu peux rebaser sur master et nous fusionnerons ta demande de pull”.

“Est-ce que tu peux s'il te plaît écraser tes commits ensemble pour que nous ayons un historique git propre et réversible ?”.

“Peux-tu réécrire le message de ton commit pour mieux décrire le problème qu'il résout et la manière dont il le résout ?”.

Les questions de ce type sont souvent posées dans les pull requests. Voyons pourquoi elles existent, comment les poser et les problèmes qu'elles peuvent amener.

Reformuler le message du dernier commit

L'une des réécritures d'historique les plus simples que l'on puisse faire avec git est de changer le dernier message de commit. Disons que juste après avoir effectué un commit, vous trouvez une erreur de frappe ou que vous trouviez une meilleure façon de décrire la modification. Pour faire la correction, vous exécutez :

git commit --amend

Git ouvrira un éditeur avec le message du dernier commit, afin que vous puissiez le modifier. Après avoir sauvegardé, un nouveau commit sera créé avec les mêmes modifications et le nouveau message, remplaçant le commit avec le message précédent.

Cela peut être utile pour inclure des fichiers que vous avez oublié de suivre, ou pour inclure des modifications aux fichiers de votre dernier commit. Pour ce faire, vous pouvez ajouter les modifications, puis effectuer la modification :

git add README.md config/routes.rb
git rm notes.txt
git commit --amend

En dehors de l'édition du message de commit, le nouveau commit contiendra les changements spécifiés avec git add et git rm. Vous pouvez aussi éditer l'auteur. Par exemple :

git commit --amend --author="Tute Costa and Dan Croak <tute+dan@thoughtbot.com>"

Succès Déverrouillé ! Vous pouvez maintenant modifier le dernier commit de votre dépot pour inclure des changements plus récents dans les fichiers, et/ou pour améliorer le message de commit. Mais ne commencez pas à modifier toutes ces choses avant d'avoir compris la dernière section de ce billet intitulée “DANGER”.

Reformuler d'autres messages de commit

J'aimerais en parler maintenant, mais nous avons besoin de comprendre un outil plus général avant. Restez à l'écoute ! Tout le reste sera plus facile une fois que nous aurons lu…

Le Rebase Interactif

git rebase réapplique les commits, un par un, dans l'ordre, de votre branche actuelle vers une autre. Il accepte plusieurs options et paramètres, il s'agit donc d'une explication de la partie émergée de l'iceberg, suffisante pour combler le fossé entre les commentaires de StackOverflow ou de GitHub et les pages de manuel de git.

Une option intéressante qu'il accepte est --interactive (-i en abrégé), qui ouvrira un éditeur avec une liste des commits qui sont sur le point d'être modifiés. Cette liste accepte les commandes, permettant à l'utilisateur d'éditer la liste avant d'initier l'action de rebase.

Voyons un exemple.

Reformuler d'autres messages de commit, essai numéro 2

Imaginons que je veuille reformuler l'un des 4 derniers commits de ce blog. Je lance alors git rebase -i HEAD~4, et voici ce que je vois :

pick 07c5abd Introduire OpenPGP et enseigner l'utilisation de base
pick de9b1eb Fixer PostChecker::Post#urls
pick 3e7ee36 Les enfants, arrêtez le surlignage
pick fa20af3 git interactive rebase, squash, amend

# Rebase 8db7e8b..fa20af3 onto 8db7e8b
#
# 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
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Nous voyons les quatre dernières modifications, de la plus ancienne à la plus récente. Vous voyez le commentaire sous la liste des commits ? Bon travail d'explication, git ! pick - qui veut dire choisir (p en abrégé) est l'action par défaut. Dans ce cas, elle réappliquerait le commit tel quel, sans modification de son contenu ou de son message. Sauvegarder (et exécuter) ce fichier n'apportera aucun changement au dépot.

Si je dis “reword” - qui veut dire reformulé (r en abrégé) dans un commit que je veux éditer :

pick 07c5abd Introduire OpenPGP et enseigner l'utilisation de base
pick de9b1eb Fixer PostChecker::Post#urls
pick 3e7ee36 Les enfants, arrêtez le surlignage
pick fa20af3 git interactive rebase, squash, amend

Lorsque je sauvegarde et quitte l'éditeur, git va suivre les commandes décrites, me faisant revenir dans l'éditeur, comme si j'avais modifié le commit 3e7ee36. J'édite ce message de commit, je sauvegarde et quitte l'éditeur, et voici le résultat :

robots.thoughtbot.com tc-git-rebase % git rebase -i HEAD~4
[detached HEAD dd62a66] Arrêtez le surlignage
 Author: Caleb Hearth
 Date: Fri Oct 31 10:52:26 2014 -0500
 2 files changed, 39 insertions(+), 42 deletions(-)
Successfully rebased and updated refs/heads/tc-git-rebase.

Dans son message d'engagement, Caleb dit : “Arrêtez le surlignage”, que vous soyez un enfant ou non.

Succès Déverrouillé ! Vous pouvez maintenant modifier le message de n'importe quel commit que vous souhaitez. Vous pouvez le faire, mais assurez-vous de bien comprendre la section “DANGER”.

Réassembler des commits ensemble

Deux autres commandes de rebase interactif nous sont proposées :

  • squash - qui veut dire écraser (s en abrégé), qui fusionne le commit avec le précédent (celui de la ligne précédente).
  • fixup - qui veut dire réparer (f en abrégé), qui agit comme “squash”, mais rejette le message

Nous allons continuer à travailler sur l'exemple de rebase précédent. Nous avons eu quatre commits, le mien pour ce billet de blog, et trois autres de Caleb, qui étaient liés à son article précédent sur PGP :

pick 07c5abd Introduire OpenPGP et enseigner l'utilisation de base
pick de9b1eb Fixer PostChecker::Post#urls
pick 3e7ee36 Les enfants, arrêtez le surlignage
pick fa20af3 git interactive rebase, squash, amend

Disons que je veux fusionner les commits de Caleb, parce qu'ils appartiennent au même modifications logiques, et ainsi nous pouvons utiliser git revert facilement si nous constatons que nous préférons ne pas avoir ces modifications dans ce référentiel. Nous voudrons garder le premier message de validation, et écraser les deux validations suivantes dans la précédente. Je change pick en squash là où c'est approprié :

pick 07c5abd Introduire OpenPGP et enseigner l'utilisation de base
s de9b1eb Fixer PostChecker::Post#urls
s 3e7ee36 Les enfants, arrêtez le surlignage
pick fa20af3 git interactive rebase, squash, amend

Sauvegarder, et j'atterris dans l'éditeur pour décider du message de commit des trois commits fusionnés (notez comme ils sont concaténés l'un après l'autre) :

# This is a combination of 3 commits.
# The first commit's message is:
Introduire OpenPGP et enseigner l'utilisation de base

Outre la démystification d'un outil, d'un protocole et d'une étiquette relativement complexes, cet article vise à résoudre des problèmes tels que celui décrit dans ce tweet :

> J'ai envoyé des informations sensibles à quelqu'un avec PGP. Iel m'a répondu, avec mon
> original, en texte clair. Iel ne s'en est pas rendu·e compte.

# This is the 2nd commit message:

Fixer PostChecker::Post#urls

# This is the 3rd commit message:

Les enfants, arrêtez le surlignage

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    Caleb Hearth
# Date:      Tue Sep 2 09:39:07 2014 -0500
#
# rebase in progress; onto 71d4789
# You are currently editing a commit while rebasing branch 'tc-git-rebase' on '71d4789'.

Je décide de supprimer le troisième message, et d'ajouter une note plus pertinente au deuxième message de validation. Je sauvegarde l'éditeur, et les quatre commits sont transformés en deux : celui de Caleb, et le mien après. C'est bien !

Nous aurions pu utiliser la commande fixup, si nous avions vu plus tôt que nous voulions les changements, mais pas le message de commit, du troisième commit. Dans ce cas, les commandes auraient été les suivantes :

pick 07c5abd Introduire OpenPGP et enseigner l'utilisation de base
s de9b1eb Fixer PostChecker::Post#urls
f 3e7ee36 Les enfants, arrêtez le surlignage
pick fa20af3 git interactive rebase, squash, amend

Lors de l'enregistrement, l'éditeur aurait inclus le troisième message de livraison déjà commenté pour nous :

# This is a combination of 3 commits.
# The first commit's message is:
Introduire OpenPGP et enseigner l'utilisation de base

Outre la démystification d'un outil, d'un protocole et d'une étiquette relativement complexes, cet article vise à résoudre des problèmes tels que celui décrit dans ce tweet :

> J'ai envoyé des informations sensibles à quelqu'un avec PGP. Iel m'a répondu, avec mon
> original, en texte clair. Iel ne s'en est pas rendu·e compte.

- https://twitter.com/csoghoian/status/505366816685060096

* Use examples that reasonably approximates `gpg2` output.
* Reject backslash from <abbr title="Uniform Resource Locator">URL</abbr>s
  before checking them

  Markdown allows backslashes in <abbr title="Uniform Resource
  Locator">URL</abbr>s to escape characters, which are passed directly to the
  <abbr title="Uniform Resource Locator">URL</abbr>, so `http://some\_url.com`
  becomes `http://some_url.com`.

  This mitigates issues with markdown syntax highlighting thinking that
  emphasized text has started, such as `_this text_`.

# This is the 2nd commit message:

Fixer PostChecker::Post#urls

# The 3rd commit message will be skipped:

#     Les enfants, arrêtez le surlignage

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    Caleb Hearth
# Date:      Tue Sep 2 09:39:07 2014 -0500
#
# rebase in progress; onto 71d4789
# You are currently editing a commit while rebasing branch 'tc-git-rebase' on
'71d4789'.

Sauvegarde et sorties :

[detached HEAD 809241b] Introduire OpenPGP et enseigner l'utilisation de base
 Author: Caleb Hearth
 Date: Tue Sep 2 09:39:07 2014 -0500
 2 files changed, 1429 insertions(+), 1 deletion(-)
 create mode 100644 source/posts/2014/10-31-pgp-and-you.md
Successfully rebased and updated refs/heads/tc-git-rebase.

Le résultat est le même : 2 commits au lieu de 4, chacun avec un article de blog unique et différent.

Succès Déverrouillé ! Vous pouvez maintenant fusionner les commits. Comme toujours, faites attention à la section DANGER.

Rebaser sur la branche master

Nous créons une bibliothèque open source, commençons à travailler sur une branche de fonctionnalités, et la branche master en amont avance. Notre historique ressemble à ceci :

       A---B---C feature
     /
D---E---F---G upstream/master

La responsable de la bibliothèque nous demande de “rebaser sur master”, afin de corriger les conflits de fusion qui pourraient survenir entre les deux branches, et de conserver notre jeu de modifications ensemble. La responsable aimerait voir un historique comme :

               A'--B'--C' feature
             /
D---E---F---G upstream/master

Nous voulons réappliquer nos commits, un par un, dans l'ordre, sur le master d'upstream ( la branche en amont). Cela ressemble à la description de la commande rebase !

Voyons quelles commandes nous permettraient d'obtenir le scénario souhaité :

# Pointer notre remote `upstream` vers la fourche original
git remote add upstream https://github.com/thoughtbot/factory_bot.git

# Récupérer les derniers commits de `upstream` (la fourche original)
git fetch upstream

# Checkout (bouger vers) notre branche de fonctionnalités
git checkout feature

# Réappliquer sur la branche master en amont
git rebase upstream/master

# Corriger les conflits, puis `git rebase --continue`,
# répéter jusqu'à ce que ce soit fait
# Pousser sur notre fourche
git push --force origin feature

Succès déverrouillé ! Votre branche de fonctionnalités sera appliquée sur le dernier master de la fourche (fork) originale.

Et c'est ainsi que nous arrivons à…

DANGER : Vous réécrivez l'histoire

Vous voyez le --force dans la dernière commande git push ? Cela signifie que nous écrasons l'historique du dépot. Il est toujours prudent de le faire dans les commits que nous ne partageons pas avec les autres membres de l'équipe, ou dans les branches qui nous appartiennent (voir mes initiales dans l'exemple de ce billet de blog).

Mais si vous forcez à pousser des éditions qui ont déjà été partagées avec l'équipe (des commits qui existent en dehors de mon dépôt, comme les modifications que j'ai apportées aux commits PGP qui ont déjà été partagées), alors la branche de tout le monde sera désynchronisée.

Réécrire l'histoire signifie abandonner des commits existants et en créer de nouveaux, qui peuvent être très similaires mais qui sont différents. Si d'autres personnes basent leur travail sur vos commits précédents, et que vous réécrivez et forcez vos commits, les membres de votre équipe devront fusionner à nouveau leur travail (s'ils remarquent la perte potentielle).

Chez thoughtbot, nous préfixons nos branches avec nos initiales, signalant que ces commits peuvent être réécrits et que d'autres ne devraient pas ajouter de commits à la branche. Lorsque ces commits atterrissent dans master ou une branche partagée, nous ne les réécrivons plus jamais.

Il est donc possible de réécrire l'historique git, à condition que les commits réécrits n'existent que dans votre dépôt, ou que vous et votre équipe sachiez que personne d'autre ne doit baser son travail sur eux.

Succès déverrouillé !! Vous savez maintenant comment rebaser tout en étant un bon citoyen.