Supprimer un commit spécifique

287

Je travaillais avec un ami sur un projet, et il a édité un tas de fichiers qui n'auraient pas dû être édités. D'une manière ou d'une autre, j'ai fusionné son travail dans le mien, soit lorsque je l'ai retiré, soit lorsque j'ai essayé de choisir les fichiers spécifiques que je voulais. Je cherche et joue depuis longtemps, essayant de comprendre comment supprimer les validations qui contiennent les modifications de ces fichiers, il semble que ce soit un basculement entre revenir et rebaser, et il n'y a pas d'exemples simples, et le les docs supposent que j'en sais plus que moi.

Voici donc une version simplifiée de la question:

Étant donné le scénario suivant, comment supprimer la validation 2?

$ mkdir git_revert_test && cd git_revert_test

$ git init
Initialized empty Git repository in /Users/josh/deleteme/git_revert_test/.git/

$ echo "line 1" > myfile

$ git add -A

$ git commit -m "commit 1"
[master (root-commit) 8230fa3] commit 1
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 myfile

$ echo "line 2" >> myfile

$ git commit -am "commit 2"
[master 342f9bb] commit 2
 1 files changed, 1 insertions(+), 0 deletions(-)

$ echo "line 3" >> myfile

$ git commit -am "commit 3"
[master 1bcb872] commit 3
 1 files changed, 1 insertions(+), 0 deletions(-)

Le résultat attendu est

$ cat myfile
line 1
line 3

Voici un exemple de la façon dont j'ai essayé de revenir

$ git revert 342f9bb
Automatic revert failed.  After resolving the conflicts,
mark the corrected paths with 'git add <paths>' or 'git rm <paths>'
and commit the result.
Joshua Cheek
la source
5
Si quelqu'un trouve cela en recherchant le même problème, voici ce que j'ai fini par faire: copier-coller. Sérieusement. J'ai passé plus de 6 heures à essayer de faire fonctionner les solutions suggérées, en vain. À la fin, je n'avais plus de temps, j'ai tiré l'original et j'ai simplement copié / collé environ 20 fichiers. Cela a pris moins de 5 minutes, et les choses vont bien depuis (même lorsque ces fichiers sont fusionnés avec des changements dans d'autres branches qui se sont produits avant ce fiasco). Je vous suggère également d'adopter cette approche. Non seulement c'est la plus simple, mais je soupçonne aussi que c'est la seule chose qui fonctionne.
Joshua Cheek
J'ai fait face à un problème similaire, mais peut-être plus complexe: j'avais une branche avec des centaines de commits que je voulais écraser. Malheureusement, les commits d'une autre branche ont été réintégrés dans la branche à un point intermédiaire, donc une "fusion" était nécessaire avant que je puisse écraser. J'ai emprunté un chemin similaire à celui suggéré par tk ci-dessous (sélection de cerises + utilisation de la notation de plage), mais cela a produit des conflits dans certains autres fichiers. Au final, copier-coller + quelques modifications manuelles étaient la voie à suivre la plus simple et la plus prévisible. Vaut vraiment la peine d'être pris en considération si vous vous trouvez à consacrer trop de temps à cela.
Federico

Réponses:

73

L'algorithme que Git utilise pour calculer les différences à inverser nécessite que

  1. les lignes en cours de restauration ne sont modifiées par aucun commit ultérieur.
  2. qu'il n'y ait aucun autre engagement "adjacent" plus tard dans l'histoire.

La définition de "adjacent" est basée sur le nombre de lignes par défaut d'un diff de contexte, qui est 3. Donc, si "monfichier" a été construit comme ceci:

$ cat >myfile <<EOF
line 1
junk
junk
junk
junk
line 2
junk
junk
junk
junk
line 3
EOF
$ git add myfile
$ git commit -m "initial check-in"
 1 files changed, 11 insertions(+), 0 deletions(-)
 create mode 100644 myfile

$ perl -p -i -e 's/line 2/this is the second line/;' myfile
$ git commit -am "changed line 2 to second line"
[master d6cbb19] changed line 2
 1 files changed, 1 insertions(+), 1 deletions(-)

$ perl -p -i -e 's/line 3/this is the third line/;' myfile
$ git commit -am "changed line 3 to third line"
[master dd054fe] changed line 3
 1 files changed, 1 insertions(+), 1 deletions(-)

$ git revert d6cbb19
Finished one revert.
[master 2db5c47] Revert "changed line 2"
 1 files changed, 1 insertions(+), 1 deletions(-)

Ensuite, tout fonctionne comme prévu.

La deuxième réponse était très intéressante. Il existe une fonctionnalité qui n'a pas encore été officiellement publiée (bien qu'elle soit disponible dans Git v1.7.2-rc2) appelée Revert Strategy. Vous pouvez invoquer git comme ceci:

git revert --stratégie résoudre <commit>

et il devrait mieux comprendre ce que vous vouliez dire. Je ne connais pas la liste des stratégies disponibles ni la définition d'une stratégie.

Hal Eisen
la source
1
perl -pest utile pour écrire des programmes très courts (une ligne) qui bouclent et transmettent leur entrée à la sortie, semblable à sed. perl -iest utilisé pour éditer les fichiers en place . perl -eest de savoir comment soumettre le code à évaluer .
Hal Eisen
281

Il y a quatre façons de procéder:

  • Manière propre, en revenant mais gardez dans le journal le retour:

    git revert --strategy resolve <commit>
    
  • De manière dure, supprimez uniquement le dernier commit:

    git reset --soft "HEAD^"
    

Remarque: Évitez git reset --hardcar cela supprimera également toutes les modifications apportées aux fichiers depuis le dernier commit. Si --softcela ne fonctionne pas, essayez plutôt --mixedou --keep.

  • Rebase (affichez le journal des 5 dernières validations et supprimez les lignes que vous ne voulez pas, ou réorganisez, ou écrasez plusieurs validations en une seule, ou faites tout ce que vous voulez, c'est un outil très polyvalent):

    git rebase -i HEAD~5
    

Et si une erreur est commise:

git rebase --abort
  • Rebase rapide: supprimez uniquement un commit spécifique en utilisant son identifiant:

    git rebase --onto commit-id^ commit-id
    
  • Alternatives: vous pouvez également essayer:

    git cherry-pick commit-id
    
  • Encore une autre alternative:

    git revert --no-commit
    
  • En dernier recours, si vous avez besoin d'une totale liberté de modification de l'historique (par exemple, parce que git ne vous permet pas de modifier ce que vous voulez), vous pouvez utiliser cette application open source très rapide : reposurgeon .

Remarque: bien sûr, toutes ces modifications sont effectuées localement, vous devez git pushensuite appliquer les modifications à la télécommande. Et dans le cas où votre référentiel ne souhaite pas supprimer le commit ("pas d'avance rapide autorisée", ce qui se produit lorsque vous souhaitez supprimer un commit que vous avez déjà poussé), vous pouvez utiliser git push -fpour forcer le push des modifications.

Remarque 2: si vous travaillez sur une branche et que vous devez forcer le push, vous devez absolument l'éviter git push --forcecar cela peut écraser d'autres branches (si vous y avez apporté des modifications, même si votre paiement actuel est sur une autre branche). Vous préférez toujours spécifier la branche à distance lorsque vous forcez poussée : git push --force origin your_branch.

généreux
la source
Basé sur les suggestions de @gaborous: faites un "git rebase -i HEAD ~ 2". Vous avez maintenant plusieurs options. Dans le vim, vous pouvez voir quelques lignes commentées: l'une d'elles vous dit que vous pouvez simplement supprimer une ligne (qui devrait être le commit dont vous voulez vous débarrasser) et ce commit sera supprimé avec son journal dans votre historique.
Ilker Cat
git revert --strategy resolver <commit> .Cette commande a fonctionné pour moi. merci :)
Swathin
2
git rebase -i HEAD~5travaillé pour moi. Ensuite, je viens de supprimer le (s) commit (s) dont je n'avais pas besoin et j'ai pu résoudre le problème en moins de 30 secondes. Je vous remercie.
lv10
117

Voici une solution simple:

git rebase -i HEAD~x

(Remarque: xest le nombre de validations)

lors de l'exécution du fichier bloc-notes s'ouvrira. Entrez en dropplus de votre commit.
Si vous ne connaissez pas Vim, cliquez simplement sur chaque sélection de mots que vous souhaitez modifier, puis appuyez sur la touche "I" (pour le mode insertion). Une fois que vous avez fini de taper, appuyez sur la touche "esc" pour quitter le mode d'insertion.



entrez la description de l'image ici

et c'est tout, vous avez terminé ... Synchronisez simplement le tableau de bord git et les modifications seront poussées à distance.

Si le commit que vous supprimez était déjà sur la télécommande, vous devrez forcer le push. Puisque --force est considéré comme dangereux , utilisez git push --force-with-lease.

JD-V
la source
Existe-t-il un bon moyen de savoir ce que «x» devrait être si vous ne connaissez que le hachage du commit en question?
jlewkovich
1
Pour les personnes à mentalité cpp: x (nombre de commits) est inclusif. Par exemple, HEAD ~ 4 inclut les 4 derniers commits.
Herpes Free Engineer
suggestion parfaite. je veux juste ajouter, cela annule également les modifications de la validation supprimée.
celerno
a tenté de revenir sur 3 commits: git rebase -i HEAD-3 a obtenu une erreur fatale: besoin d'une seule révision non valide en amont 'HEAD-3'
Ustin
1
@Ustin c'est ~ 3 pas -3
JD-V
37

Vous avez le choix entre

  1. conserver l'erreur et introduire un correctif et
  2. suppression de l'erreur et modification de l'historique.

Vous devez choisir (1) si la modification erronée a été détectée par quelqu'un d'autre et (2) si l'erreur est limitée à une branche privée non poussée.

Git revert est un outil automatisé à faire (1), il crée un nouveau commit annulant un commit précédent. Vous verrez l'erreur et la suppression dans l'historique du projet, mais les personnes qui tirent de votre référentiel ne rencontreront pas de problèmes lors de la mise à jour. Cela ne fonctionne pas de manière automatisée dans votre exemple, vous devez donc modifier «monfichier» (pour supprimer la ligne 2), faire git add myfileet git commitgérer le conflit. Vous vous retrouverez alors avec quatre commits dans votre historique, avec le commit 4 revenant au commit 2.

Si personne ne se soucie que votre historique change, vous pouvez le réécrire et supprimer le commit 2 (choix 2). La manière la plus simple de le faire est d'utiliser git rebase -i 8230fa3. Cela vous déposera dans un éditeur et vous pouvez choisir de ne pas inclure la validation erronée en supprimant la validation (et en gardant "pick" à côté des autres messages de validation. Lisez les conséquences de cette opération .

Andrew Walker
la source
Le rebase peut être délicat, car il semble qu'il y ait eu une fusion.
Cascabel
3
git rebase -i 8230fa3, et supprimer la ligne de commit a très bien fonctionné pour moi avec mes modifications locales uniquement. Merci!
Samuel
26

Approch 1

Obtenez d'abord le hachage de validation (ex: 1406cd61) que vous devez rétablir. la solution simple sera sous la commande,

$ git revert 1406cd61

si vous avez validé plus de modifications liées aux fichiers 1406cd61 après la validation 1406cd61, la commande ci-dessus ne fonctionnera pas. Ensuite, vous devez faire les étapes ci-dessous, qui sont la cueillette des cerises.

Approch 2

Veuillez suivre ci-dessous la description des actions, puisque nous utilisons --force, vous devez avoir des droits d'administrateur sur le dépôt git pour ce faire.

Étape 1: recherchez le commit avant le commit que vous souhaitez supprimergit log

Étape 2: retirer cet engagementgit checkout <commit hash>

Étape 3: créer une nouvelle branche à l'aide de votre validation de paiement actuellegit checkout -b <new branch>

Étape 4: Maintenant, vous devez ajouter le commit après le commit supprimégit cherry-pick <commit hash>

Étape 5: Répétez maintenant l'étape 4 pour tous les autres commits que vous souhaitez conserver.

Étape 6: Une fois que tous les validations ont été ajoutées à votre nouvelle branche et validées. Vérifiez que tout est dans le bon état et fonctionne comme prévu. Vérifiez que tout a bien été commis:git status

Étape 7: passez à votre branche casséegit checkout <broken branch>

Étape 8: Maintenant, effectuez une réinitialisation matérielle sur la branche cassée du commit avant celle que vous souhaitez supprimergit reset --hard <commit hash>

Étape 9: fusionnez votre branche fixe dans cette branchegit merge <branch name>

Étape 10: Remettez les modifications fusionnées à l'origine. AVERTISSEMENT: cela écrasera le dépôt à distance!git push --force origin <branch name>

Vous pouvez effectuer le processus sans créer de nouvelle branche en remplaçant les étapes 2 et 3 par l'étape 8, puis ne pas exécuter les étapes 7 et 9.

tk_
la source
1
La première approche a fonctionné comme un charme. Il vous inclut un commit spécifiant que votre commit souhaité a été annulé, ce qui est vraiment sympa à des fins de suivi.
suarsenegger
18

Vous pouvez supprimer les commits indésirables avec git rebase. Supposons que vous ayez inclus des validations d'une branche de rubrique d'un collègue dans votre branche de rubrique, mais décidez plus tard que vous ne souhaitez pas ces validations.

git checkout -b tmp-branch my-topic-branch  # Use a temporary branch to be safe.
git rebase -i master  # Interactively rebase against master branch.

À ce stade, votre éditeur de texte ouvrira la vue de rebase interactive. Par exemple

git-rebase-todo

  1. Supprimez les commits dont vous ne voulez pas en supprimant leurs lignes
  2. Sauvegarder et quitter

Si le rebase n'a pas réussi, supprimez la branche temporaire et essayez une autre stratégie. Sinon, continuez avec les instructions suivantes.

git checkout my-topic-branch
git reset --hard tmp-branch  # Overwrite your topic branch with the temp branch.
git branch -d tmp-branch  # Delete the temporary branch.

Si vous poussez votre branche de sujet vers une télécommande, vous devrez peut-être forcer la poussée car l'historique de validation a changé. Si d'autres travaillent sur la même branche, prévenez-les.

Dennis
la source
Pourriez-vous s'il vous plaît donner un exemple de «essayer une autre stratégie»?
pfabri
@pfabri, vous pouvez par exemple sélectionner deux plages de validations où vous omettez la mauvaise validation. Vous pouvez annuler le mauvais commit. Vous pouvez éviter d'utiliser une solution git en annulant même manuellement les modifications, ou en commençant à partir d'une nouvelle branche sans le mauvais commit et en refaisant manuellement les bonnes modifications. Si le mauvais commit contient des données sensibles, vous aurez besoin d'une stratégie plus prudente: help.github.com/en/articles/…
Dennis
8

D'après les autres réponses ici, j'étais un peu confus avec la façon dont git rebase -ipourrait être utilisé pour supprimer un commit, donc j'espère que c'est OK de noter mon cas de test ici (très similaire à l'OP).

Voici un bashscript que vous pouvez coller pour créer un référentiel de test dans le /tmpdossier:

set -x

rm -rf /tmp/myrepo*
cd /tmp

mkdir myrepo_git
cd myrepo_git
git init
git config user.name me
git config user.email me@myself.com

mkdir folder
echo aaaa >> folder/file.txt
git add folder/file.txt
git commit -m "1st git commit"

echo bbbb >> folder/file.txt
git add folder/file.txt
git commit -m "2nd git commit"

echo cccc >> folder/file.txt
git add folder/file.txt
git commit -m "3rd git commit"

echo dddd >> folder/file.txt
git add folder/file.txt
git commit -m "4th git commit"

echo eeee >> folder/file.txt
git add folder/file.txt
git commit -m "5th git commit"

À ce stade, nous avons un file.txtavec ces contenus:

aaaa
bbbb
cccc
dddd
eeee

À ce stade, HEAD est au 5ème commit, HEAD ~ 1 serait le 4ème - et HEAD ~ 4 serait le 1er commit (donc HEAD ~ 5 n'existerait pas). Disons que nous voulons supprimer le 3e commit - nous pouvons émettre cette commande dans le myrepo_gitrépertoire:

git rebase -i HEAD~4

( Notez que les git rebase -i HEAD~5résultats avec "fatal: Besoin d'une seule révision; invalide en amont HEAD ~ 5". ) Un éditeur de texte (voir capture d'écran dans la réponse de @Dennis ) s'ouvrira avec ces contenus:

pick 5978582 2nd git commit
pick 448c212 3rd git commit
pick b50213c 4th git commit
pick a9c8fa1 5th git commit

# Rebase b916e7f..a9c8fa1 onto b916e7f
# ...

Nous obtenons donc tous les commits depuis (mais sans inclure ) notre HEAD demandé ~ 4. Supprimez la ligne pick 448c212 3rd git commitet enregistrez le fichier; vous obtiendrez cette réponse de git rebase:

error: could not apply b50213c... 4th git commit

When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To check out the original branch and stop rebasing run "git rebase --abort".
Could not apply b50213c... 4th git commit

À ce stade, ouvrez myrepo_git / folder/file.txtdans un éditeur de texte; vous verrez qu'il a été modifié:

aaaa
bbbb
<<<<<<< HEAD
=======
cccc
dddd
>>>>>>> b50213c... 4th git commit

Fondamentalement, gitvoit que lorsque HEAD est arrivé au 2e commit, il y avait du contenu de aaaa+ bbbb; puis il a un patch de cccc+ ajouté ddddqu'il ne sait pas comment ajouter au contenu existant.

Donc, ici, gitvous ne pouvez pas décider pour vous - c'est vous qui devez prendre une décision: en supprimant le 3e commit, soit vous gardez les changements introduits par lui (ici, la ligne cccc) - soit vous ne le faites pas. Si vous ne le faites pas, supprimez simplement les lignes supplémentaires - y compris le cccc- en folder/file.txtutilisant un éditeur de texte, pour qu'il ressemble à ceci:

aaaa
bbbb
dddd

... puis enregistrez folder/file.txt. Vous pouvez maintenant exécuter les commandes suivantes dans le myrepo_gitrépertoire:

$ nano folder/file.txt  # text editor - edit, save
$ git rebase --continue
folder/file.txt: needs merge
You must edit all merge conflicts and then
mark them as resolved using git add

Ah - donc pour marquer que nous avons résolu le conflit, nous devons git add le folder/file.txt, avant de faire git rebase --continue:

$ git add folder/file.txt
$ git rebase --continue

Ici, un éditeur de texte s'ouvre à nouveau, montrant la ligne 4th git commit- ici, nous avons une chance de changer le message de validation (qui dans ce cas pourrait être significativement changé en 4th (and removed 3rd) commitou similaire). Disons que vous ne voulez pas - alors quittez simplement l'éditeur de texte sans enregistrer; une fois que vous faites cela, vous obtiendrez:

$ git rebase --continue
[detached HEAD b8275fc] 4th git commit
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/master.

À ce stade, vous avez maintenant un historique comme celui-ci (que vous pouvez également inspecter avec disons gitk .ou d'autres outils) du contenu de folder/file.txt(avec, apparemment, des horodatages inchangés des commits originaux):

1st git commit  |  +aaaa
----------------------------------------------
2nd git commit  |   aaaa
                |  +bbbb
----------------------------------------------
4th git commit  |   aaaa
                |   bbbb
                |  +dddd
----------------------------------------------
5th git commit  |   aaaa
                |   bbbb
                |   dddd
                |  +eeee

Et si précédemment, nous avions décidé de conserver la ligne cccc(le contenu du 3ème commit git que nous avions supprimé), nous aurions eu:

1st git commit  |  +aaaa
----------------------------------------------
2nd git commit  |   aaaa
                |  +bbbb
----------------------------------------------
4th git commit  |   aaaa
                |   bbbb
                |  +cccc
                |  +dddd
----------------------------------------------
5th git commit  |   aaaa
                |   bbbb
                |   cccc
                |   dddd
                |  +eeee

Eh bien, c'était le genre de lecture que j'espérais avoir trouvé, pour commencer à chercher comment git rebasefonctionne en termes de suppression de commits / révisions; alors j'espère que ça pourrait aider les autres aussi ...

sdaau
la source
Quelle visite inestimable - le cas d'utilisation exact avec lequel je me débattais, cela m'a énormément aidé.
pfabri
2

Il semble donc que le mauvais commit ait été incorporé dans un commit de fusion à un moment donné. Votre commit de fusion a-t-il été retiré? Si oui, alors vous voudrez utiliser git revert; vous devrez serrer les dents et surmonter les conflits. Si non, vous pouvez éventuellement soit rebaser, soit revenir en arrière, mais vous pouvez le faire avant la validation de la fusion, puis refaire la fusion.

Il n'y a pas vraiment d'aide que nous puissions vous apporter pour le premier cas, vraiment. Après avoir essayé le rétablissement et constaté que l'échec automatique, vous devez examiner les conflits et les corriger de manière appropriée. C'est exactement le même processus que la résolution des conflits de fusion; vous pouvez utiliser git statuspour voir où se trouvent les conflits, modifier les fichiers non fusionnés, trouver les mecs en conflit, découvrir comment les résoudre, ajouter les fichiers en conflit et enfin valider. Si vous utilisez git commitseul (non -m <message>), le message qui apparaît dans votre éditeur doit être le message modèle créé par git revert; vous pouvez ajouter une note sur la façon dont vous avez résolu les conflits, puis enregistrer et quitter pour valider.

Pour le deuxième cas, qui résout le problème avant votre fusion, il existe deux sous-cas, selon que vous avez effectué plus de travail depuis la fusion. Si ce n'est pas le cas, vous pouvez simplement git reset --hard HEAD^annuler la fusion, revenir en arrière, puis refaire la fusion. Mais je suppose que oui. Vous finirez donc par faire quelque chose comme ceci:

  • créer une branche temporaire juste avant la fusion et vérifier
  • faire le retour (ou utiliser git rebase -i <something before the bad commit> <temporary branch>pour supprimer le mauvais commit)
  • refaire la fusion
  • rebaser votre travail suivant sur: git rebase --onto <temporary branch> <old merge commit> <real branch>
  • supprimer la branche temporaire
Cascabel
la source
1

Donc, vous avez fait un peu de travail et poussé, appelons-les commits A et B. Votre collègue a également fait du travail, valide C et D. aussi (commit F), et a découvert que votre collègue a changé certaines choses qu'il ne devrait pas avoir.

Donc, votre historique de commit ressemble à ceci:

A -- B -- C -- D -- D' -- E -- F

Vous voulez vraiment vous débarrasser de C, D et D '. Puisque vous dites que vous avez fusionné vos collègues de travail avec les vôtres, ces validations sont déjà "là-bas", donc supprimer les validations en utilisant par exemple git rebase est un non-non. Croyez-moi, j'ai essayé.

Maintenant, je vois deux solutions:

  • si vous n'avez pas encore envoyé E et F à votre collègue ou à quelqu'un d'autre (généralement votre serveur "d'origine"), vous pouvez toujours les supprimer de l'historique pour le moment. C'est votre travail que vous souhaitez sauvegarder. Cela peut être fait avec un

    git reset D'
    

    (remplacez D 'par le hachage de validation réel que vous pouvez obtenir git log

    À ce stade, les validations E et F ont disparu et les modifications sont à nouveau des modifications non validées dans votre espace de travail local. À ce stade, je les déplacerais vers une branche ou les transformerais en patch et les enregistrer pour plus tard. Maintenant, inversez le travail de votre collègue, soit automatiquement avec un, git revertsoit manuellement. Lorsque vous avez fait cela, rejouez votre travail en plus. Vous pouvez avoir des conflits de fusion, mais au moins ils seront dans le code que vous avez écrit, au lieu du code de votre collègue.

  • Si vous avez déjà poussé le travail que vous avez fait après les validations de votre collègue, vous pouvez toujours essayer d'obtenir un «patch inversé» manuellement ou en utilisant git revert, mais puisque votre travail est «dans la voie», pour ainsi dire, vous obtiendrez probablement plus de conflits de fusion et plus de confusions. On dirait que c'est dans ça que vous vous êtes retrouvé ...

Frans
la source
0

git revert --strategy resolver si commit est une fusion: utilisez git revert --strategy resolver -m 1

ShaDow RiDeR
la source