Annuler les derniers commits avec Git


(Amélioration de ce dont je parle ici)

Use case typique : on a merdé les derniers commits, et on veut oublier tout ce qu’on a fait et retourner à l’état d’il y a x commits précédents.

Par exemple, là je veux revenir à mon commit 85711ad... :

commit 093bab5aa9d41f580037d51421b7c5d0db73e2ce
Author: sam 
Date:   Sat Jun 28 09:37:55 2014 +0700

    Ok, c'est la merde internationale

commit 0b768f1c0e37a6141e6cd4c472eb3f369f4334d7
Author: sam 
Date:   Sat Jun 28 09:37:36 2014 +0700

    Je commence à merder

commit 85711ad1e8c54f3fd3048d405addef48921e90fd
Author: sam 
Date:   Sat Jun 28 09:37:20 2014 +0700

    J'adore ce commit

Il y a plein de manières de faire, et on voit sur la toile beaucoup de solutions à base de checkout et de reset. La plupart sont dangereuses ou ont un résultat inattendu, présuppose un état de votre repo ou va vous mettre dans une situation que vous ne maîtrisez pas.

Devinez quoi ? Il y a plus simple, et plus propre.

Etape 1: avoir une copie de travail propre

Avant d’inverser des commits, assurez vous que votre copie de travail est nette. Pas de fichiers modifiés en attente d’être commités. Le moins de fichiers non trackés par git possible (idéalement zéro, soit c’est commité, soit c’est dans le .gitignore).

Si vous avez des fichiers modifiés, vous pouvez soit les mettre de côté temporairement avec git stash, soit annuler toutes les modifications avec git reset --hard HEAD. Attention, cette dernière commande n’est pas réversible et va mettre à plat votre copie de travail pour qu’elle soit l’exacte copie du dernier commit de votre histo.

Etape 2

???

Etape 3: profit !

git revert --no-commit 85711ad1..HEAD

Ceci va modifier la copie de travail (donc les fichiers que vous avez sur le disque dur en direct, pas l’histo git) en appliquant des patchs qui contiennent les différences entre HEAD et le commit avec ce hash.

En clair : vos fichiers vont être dans l’état dans lequel ils étaient à ce commit. En prime, l’index est mis à jour.

Vous pouvez alors faire les derniers ajustements que vous le souhaitez. Il faut ensuite finaliser la procédure par un commit avec un message significatif :

git commit -m "Abort ! Abort ! Inversion des 2 derniers commits, retour à 85711a"

Si vous aviez fait un stash, c’est le moment de faire un stash apply derrière.

Maintenant, si vous matez l’histo, vous verrez qu’on n’a pas effacé les commits précédents, on a juste fait un commit qui inverse tout ce qu’ils avaient fait :

commit 03e55de36ad29a26a461874988d4066ebf6fe6be
Author: sam 
Date:   Sat Jun 28 09:43:32 2014 +0700

    Abort ! Abort ! Inversion des 2 derniers commits, retour à 85711a

commit 093bab5aa9d41f580037d51421b7c5d0db73e2ce
Author: sam 
Date:   Sat Jun 28 09:37:55 2014 +0700

    Ok, c'est la merde internationale

commit 0b768f1c0e37a6141e6cd4c472eb3f369f4334d7
Author: sam 
Date:   Sat Jun 28 09:37:36 2014 +0700

    Je commence à merder

commit 85711ad1e8c54f3fd3048d405addef48921e90fd
Author: sam 
Date:   Sat Jun 28 09:37:20 2014 +0700

    J'adore ce commit

Ce qui évite bien des problèmes : pas de réécriture de l’histo, possibilité de récupérer du code dans les commits inversés plus tard, claire indication de ce qui s’est passé…

N’oubliez pas que souvent, revenir à un commit précédent est overkill. Il est généralement beaucoup plus simple de juste récupérer un ou deux fichiers dans l’état de l’époque avec :

git checkout [hash] -- chemin/vers/fichier

16 thoughts on “Annuler les derniers commits avec Git

  • ZZelle

    Pour inverser les derniers commits, il vaut mieux utiliser git rebase -i.
    Pour inverser les 2 derniers commits:

    #$ git log --oneline -2 # Affiche 2 derniers commits
    d3e5d39 Use patch ports to interconnect integration/physical bridges
    8df1e96 Increase default metadata_workers, backlog to 4096

    #$ git rebase -i HEAD^^ # on arrive dans votre éditeur favori avec les 2 derniers commits
    pick d3e5d39 Use patch ports to interconnect integration/physical bridges
    pick 8df1e96 Increase default metadata_workers, backlog to 4096

    # On échange les 2 lignes
    pick 8df1e96 Increase default metadata_workers, backlog to 4096
    pick d3e5d39 Use patch ports to interconnect integration/physical bridges

    # On sauve et on quitte l'éditeur

    #$ git log --oneline -2 # Affiche 2 derniers commits
    xxxxxxx Use patch ports to interconnect integration/physical bridges
    yyyyyyy Increase default metadata_workers, backlog to 4096
    #Les sha1 changent

  • Sam Post author

    Félicitation, tu viens juste de réécrire ton historique, t’assurant que ton prochain push ou pulll ne marchera pas si les commits sont publiques, boussillant ta syncro avec les historiques des autres commiters, et rendant difficile la récupération de toute modif des commits qut tu as annulé.

    C’est ce que je disais par:

    La plupart sont dangereuses ou ont un résultat inattendu, présuppose un état de votre repo ou va vous mettre dans une situation que vous ne maîtrisez pas.

    La méthode que tu propose ne marche que si tu n’as jamais pushé ou mergé les dits commits. Si quelqu’un sur le web lis ta méthode et l’utilise sur des commits pushés ou mergés, tu vas le mettre dans une merde pas croyable, et il aura aucune idée de pourquoi.

  • Blef

    @ZZelle je pense que ce que Sam soulève comme problème c’est si on a déjà push la branche par exemple. Donc à part si on veut se faire arracher par ses collègues en forçant un push on est obligé de faire ce genre d’opération

  • Blef

    Ouais voilà hein. Sinon astuce, plutôt que de faire un git reset –hard HEAD j’aime bien faire un git checkout . ça me fait économiser de la frappe !

  • Sam Post author

    @Blef: ouai, mais ça fait que switcher les fichiers changés, il me semble ça del pas ceux qui ne sont pas dans les autres commits.

  • Sanao

    Le terme plus exacte est annuler les derniers commits.

    Je trouve que inverser est “confusant” au sens où l’on a l’impression que tu veux réécrire ton historique en changeant l’ordre de tes commits.

  • G-rom

    Idem, tu devrais mettre “annuler” au lieu de “inverser”. D’ailleurs @ZZelle t’as pris au mot, sa méthode inverse bien les 2 derniers commit

  • Sam Post author

    Inverser: Mettre dans le sens contraire.

    C’est ici exactement ce que l’on fait.

    On avait les commit :

    C
    B
    A

    Et on fait un commit de plus, qui fait :

    A
    B
    C
    C
    B
    A

    Le fait que le BCA soit fait en un seul commit ne doit pas vous tromper. On appliquer bien exactement l’inverse de la série de commit précédents.

    Effectivement, on annule bien les dernier commits, dans le sens supprime leurs effets. On les annule en plaçant une série de modifications parfaitement symétriques : l’inverse des commits précédents.

    La raison pour laquelle j’ai choisi inverser et non annuler comme mot, c’est que l’on peut annuler (supprimer les effets de) de plusieurs manières. Des manières qu’il vaut mieux éviter à moins de savoir exactement ce que l’on fait.

    La méthode de ZZelle, annule les commits, mais pas par inversion. Par suppression. C’est ce qui rend la méthode dangereuse : en supprimant une partie de l’historique, on se maintenant s’assurer que tous les autres clones fassent la même modification (et pas d’autres) dans le même temps. Une situation très difficile à gérer.

  • kontre

    Inverser a un autre sens de celui que ce que tu prends, Sam. Ça veut aussi dire prendre un truc, le mettre à la place d’un machin, et mettre le machin là où était le truc. C’est ce que j’avais compris de ton titre. Tu devrais changer ton titre, là c’est trop ambigu. Je préfère annuler: on a fait un truc, et on veut le défaire. Y’a plusieurs manières d’annuler, mais aussi plusieurs manières d’inverser !

    ZZelle ne supprime rien et n’annule, elle chamboulle l’historique. en échangeant 2 commits. C’est dangereux, mais pas mal de giteux trouvent ça élégant, notamment pour les pull requests. Numpy demande à ce qu’un pull request soit compacté en un unique commit avant de merger, par exemple, pour garder l’hisorique global du projet plus clean.

  • Sam Post author

    Ok pour le changement de titre.

    Par contre, je fais pas de tuteux pour les giteux. Je le fais pour ceux qui pigent pas git. Et j’en vois tous les jours qui suivent les tutos à base de reset –hard et de rebase et autres joyeuseté et qui bousillent leur environnement de travail à cause de ça. Ils perdent des jours là dessus. Parfois ils abandonnent Git juste à cause de ça. Franchement ça m’énerve quand on donne ces techniques dangereuses, c’est irresponsable. Au moins mettre un label énorme en rouge, et proposer AVANT l’alternative safe. Les gens se rendent pas compte…

    Par ailleurs, le fait de compacter ses commits pour un pull request est une problématique différente. Ici on parle d’annuler un commit, et je pense que c’est beaucoup plus clair d’avoir une trace de l’annulation dans l’historique. De plus, compacter, c’est nécessaire sur un gros projet pour la lisibilité de l’histo. Mais franchement, pour une équipe qui a 3 dev, ce n’est pas du tout nécessaire. Qu’on recommande ça partout, juste pour “avoir un bel historique” ça me fout les nerfs. Est-ce que vous savez combien de personnes se sont fait niquez en faisant un rebase ? C’est une technique super advance, à recommander uniquement à ceux qui savent ce qu’ils font.

  • Sam Post author

    Merci d’avoir insisté pour changer le titre. C’est le genre de chose qu’on a tendance à ne pas voir (et même à ne pas vouloir voir) quand on a écrit l’article. Si il y avait juste une personne qui m’avait dit ça, je ne l’aurais pas fait. Mais comme vous avez tous, gentiment et patiemment, expliqué qu’il fallait le changer, ça a finit par rentrer.

    Je ne le répète pas assez, mais vous êtes un super lectorat et je suis très content de vous avoir en comment.

  • chaiyachaiya

    Juste en passant, “…git reset –hard HEAD. Attention, cette dernière commande n’est pas réversible…” n’est pas tout à fait vrai.
    La commande reflog de git permet de revenir à une position donnée du HEAD par lequel il est passé. Mais c’est un peu le coté obscur de git.

    Sinon pour être sûr d’avoir bien compris. Si je faisais un revert pour chaque commit et qu’ensuite je squash l’ensemble des commits de revert en un seul commit, on obtient le même résultat ?

  • Sam Post author

    Cette command va aussi remettre à plat la copie de travail, et toute modif de celle-ci n’est pas dans l’histo git, donc on ne peut pas récupérer ça via git reflog.

    Avec le squash, on obtient le même résultat, si tu fais el squash avant la publication des commits.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.