Supprimer un commit
Il arrive occasionnellement qu’on doive annuler un commit et abandonner le travail qu’il contient, et ça serait vraiment contre-productif de défaire ça à la main (ou alors on aime perdre du temps et risquer d’introduire des erreurs).
Quand on vient juste de créer ce commit et qu’on est encore sur la même branche, l’opération est triviale. Par contre, quand il s’agit d’un commit plus ancien, ça demande un peu plus de savoir faire. Voyons comment régler le problème dans ces deux cas de figure.
Cet article fait partie de notre série pour apprendre à annuler, défaire et corriger.
Retirer mon dernier commit
Reprécisons bien l’objectif visé : on ne veut plus du commit ni du travail qu’il contient. Pour annuler un commit et garder le travail qu’il contient, c’est une autre procédure.
Petit rappel : Git, c’est un peu la block-chain avant l’heure. L’identité de chaque commit est construite sur l’identité de son ou ses parents. Notre emplacement dans cette chaîne est défini par un pointeur nommé HEAD.
Quand on parle d’annuler le dernier commit, on parle alors de le déréférencer pour qu’il n’apparaisse plus dans la chaîne. Pour le dire autrement : on prend HEAD et on le ramène un cran en arrière.
On n’effectue pas d’opération “destructrice” des objets Git, c’est un système de nettoyage (garbage collector) qui s’en occupera plus tard (compte environ 1 mois). Le gros avantage à ça, c’est qu’en cas d’erreur, on peut annuler l’annulation 🙂 !
Quelle commande ?
On utilise la commande reset
et le mode --keep
pour cette opération (certains vous recommandent l’option --hard
, ce qui est très discutable). Si tu veux creuser plus la commande, on a un article complet qui l’explique.
La commande reset
nous permet de changer notre position actuelle (HEAD). L’option définit ce qu’on souhaite faire du travail “résiduel”. Pour la faire courte, l’option --keep
nous prémunit de toute perte de travail qui n’aurait pas encore été commité. On annule ainsi des commits et leurs contenus sans faire la bourde d’annuler tout notre travail en cours dans le working directory et le stage.
Dernier point important : il faut passer une référence en paramètre à la commande pour lui indiquer où refaire pointer HEAD. Puisqu’on veut ici défaire notre dernier commit, on va donc lui demander de revenir un cran en arrière de notre position actuelle, soit HEAD~1
(il s’agit ici de ce qu’on appelle une “syntaxe de révision”, ça fonctionne tout aussi bien avec l’identifiant d’un commit, 0ec4749
dans l’exemple ci-dessous).
Avec un exemple, c’est mieux !
Dans l’historique suivant, on veut supprimer le dernier commit, celui désigné par la croix. La chronologie va de gauche à droite. Le commit précédent est donc le 0ec4749
.
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%% gitGraph commit id: "45097b7" commit id: "213545b" commit id: "1b99b2c" commit id: "0ec4749" commit id: "(HEAD) d97e0a7" type: REVERSE
On lance alors la commande
git reset --keep HEAD~1 # Ça marche aussi avec "git reset --keep 0ec4749"
Et on obtient le résultat suivant :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%% gitGraph commit id: "45097b7" commit id: "213545b" commit id: "1b99b2c" commit id: "(HEAD) 0ec4749"
Le commit n’apparaît plus dans l’historique. Il est bien “supprimé” !
Et si on s’est trompé ?
J’insiste encore une fois : il est réellement déréférencé, ce qui veut dire qu’on a encore la possibilité de le récupérer si on s’était trompé. Dans ce cas, il faudrait user d’une autre syntaxe de révision, toujours avec la commande reset :
git reset --keep HEAD@{1}
J’ai même tendance à utiliser plutôt la référence de la branche courante plutôt que HEAD, le résultat est strictement le même, mais ça facilite d’autres situation d’annulation :
git reset --keep (nom-de-ta-branche)@{1}
# En exemple concret avec la branche "dev" :
# git reset --keep dev@{1}
Retirer un commit plus ancien
Comme on vient de le voir, la commande reset agit sur le bout de chaîne. Ça veut donc dire qu’elle ne nous sera d’aucune utilité pour retirer un commit plus ancien. On utilisera alors la commande rebase.
J’entends déjà hurler les gens qui s’y sont frotté sans jamais l’apprendre 😱. Dédramatisons un coup, la procédure n’est pas bien difficile quand on suit les étapes :
- On lance le rebase en mode interactif en demandant à Git de partir du commit précédent celui qu’on souhaite retirer ;
- Dans l’éditeur (ouvert par le mode interactif), on demande à supprimer le commit qui nous intéresse ;
- On enregistre, on ferme l’onglet de l’éditeur ;
- On vérifie après que Git ait fait sa tambouille !
Techniquement, la seule chose “difficile” est de savoir lancer la commande :
git rebase -r -i (commit-à-supprimer)~1
Par chance, pas mal d’interfaces graphiques proposent le rebase, tu n’auras alors pas à taper à la commande 😮💨. Petit bémol : elles ne permettent pas toujours d’accéder au potentiel complet de la commande.
Démonstrature
On reprend l’exemple vu précédemment, sauf qu’on veut cette fois supprimer de l’historique le second commit (celui avec la croix) : 213545b
. J’ai ajouté des messages de commits pour faciliter la lecture.
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%% gitGraph commit id: "45097b7 - 1er commit" commit id: "213545b - 2nd commit" type: REVERSE commit id: "1b99b2c - 3e commit" commit id: "0ec4749 - 4e commit" commit id: "(HEAD) d97e0a7 - 5e commit"
On lance la commande
git rebase -r -i 213545b~1 # ou directement "git rebase -r -i 45097b7"
Parce que j’ai demandé le mode interactif, Git ouvre mon éditeur (ou une autre interface selon l’outil utilisé) et me propose la liste des commits sur le format suivant (ou équivalent) :
…
pick 45097b7 1er commit
pick 213545b 2nd commit # (C’est celui-ci qu’on veut supprimer)
pick 1b99b2c 3e commit
pick 0ec4749 4e commit
pick d97e0a7 5e commit
# Rebase 45097b7..d97e0a7 onto 45097b7 (7 commands)
#
# Commands:
#
# … (tout un tas d’actions possibles, dont :)
#
# d, drop <commit> = remove commit
#
# … (et encore d’autres actions et infos utiles, dont :)
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
Les commits les plus anciens sont en haut, les plus récent en bas. Il s’agit de la liste des opérations (commits) à effectuer sur la nouvelle “base”. Cette base, c’est le commit qu’on a passé à la commande rebase (le 45097b7
), le point de départ sur lequel appliquer les actions.
Le mot pick
qui préfixe les 5 premières lignes (nos commits) désigne l’action à mener pour chacune. Puisqu’on veut supprimer, on utilise le préfixe d
ou drop
. On peut aussi simplement retirer la ligne ou la commenter, l’effet sera le même.
Personnellement, j’ai tendance à supprimer la ligne, je trouve ça plus clair de n’avoir que la liste des commits à appliquer ou modifier. On va tout de même continuer ici avec la méthode “conventionnelle” :
…
pick 45097b7 1er commit
drop 213545b 2nd commit # (j’ai mis "drop" pour dire que je veux le supprimer)
pick 1b99b2c 3e commit
pick 0ec4749 4e commit
pick d97e0a7 5e commit
Il ne me reste plus qu’à enregistrer et fermer l’onglet ou l’éditeur. Git déroule les actions une à une et me redonne la main (sauf en cas de problème qui nécessiterait un arbitrage comme lors d’une fusion).
Évidemment, je vérifie mon historique pour voir si tout est conforme à mes attentes :
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': { 'commitLabelFontSize': '16px' }, 'gitGraph': {'showBranches': false} } }%% gitGraph commit id: "45097b7 - 1er commit" commit id: "7b45b93 - 3e commit" commit id: "067214a - 4e commit" commit id: "(HEAD) 60b3490 - 5e commit"
Les commits ont bien été rejoués, mon second commit a été retiré de l’historique.
Je me suis trompé, puis-je annuler le rebase ?
C’est bien connu, en foirant ton rebase, tu es damné, tu dois traverser les neufs cercles de l’Enfer 😈 …
Mais non, je plaisante. Défaire un rebase, ça implique de ramener ta tête de branche où elle se situait avant.
Bon, j’admets que si tu ne connais pas rebase et son fonctionnement, ça dois être très flou 🥸. Simplifions donc : le rebase a fait tout un tas d’opérations, mais il n’a déplacé qu’une seule fois l’étiquette de la branche courante. Donc annuler le rebase, c’est remettre cette étiquette où elle était avant, en faisant éventuellement pointer HEAD dessus.
Pour reprendre nos syntaxes de révisions, il s’agit de la syntaxe (nom-de-ta-branche)@{1}
. Ce qui signifie qu’on utilise la même solution que dans la première partie de cet article :
git reset --keep (nom-de-ta-branche)@{1}
# En exemple concret avec la branche "dev" :
# git reset --keep dev@{1}
Finalement, tout ça ne demande pas beaucoup de commandes, juste un peu de savoir faire !
Tu peux aussi regarder le programme de notre formation "Comprendre Git" ou nous poser tes questions sur notre forum discord.