Comment corriger une mauvaise fusion et rejouer vos bons engagements sur une fusion fixe?

407

J'ai accidentellement validé un fichier indésirable ( filename.origlors de la résolution d'une fusion) dans mon référentiel il y a plusieurs validations, sans que je le remarque jusqu'à présent. Je souhaite supprimer complètement le fichier de l'historique du référentiel.

Est-il possible de réécrire l'historique des modifications tel qu'il filename.orign'a jamais été ajouté au référentiel en premier lieu?

Grant Limberg
la source

Réponses:

297

Veuillez ne pas utiliser cette recette si votre situation n'est pas celle décrite dans la question. Cette recette sert à corriger une fusion incorrecte et à rejouer vos bonnes validations sur une fusion fixe.

Bien filter-branchque vous fassiez ce que vous voulez, c'est une commande assez complexe et je choisirais probablement de le faire avec git rebase. C'est probablement une préférence personnelle. filter-branchpeut le faire en une seule commande, légèrement plus complexe, tandis que la rebasesolution effectue les opérations logiques équivalentes une étape à la fois.

Essayez la recette suivante:

# create and check out a temporary branch at the location of the bad merge
git checkout -b tmpfix <sha1-of-merge>

# remove the incorrectly added file
git rm somefile.orig

# commit the amended merge
git commit --amend

# go back to the master branch
git checkout master

# replant the master branch onto the corrected merge
git rebase tmpfix

# delete the temporary branch
git branch -d tmpfix

(Notez que vous n'avez pas réellement besoin d'une branche temporaire, vous pouvez le faire avec un 'HEAD détaché', mais vous devez prendre note de l'ID de validation généré par l' git commit --amendétape à fournir à la git rebasecommande plutôt que d'utiliser la branche temporaire Nom.)

CB Bailey
la source
6
Ne serait-ce pas git rebase -iplus rapide et toujours aussi simple? $ git rebase -i <sh1-of-merge> Marquer la bonne comme "modifier" $ git rm somefile.orig $ git commit --amend $ git rebase --continue Cependant pour une raison quelconque, j'ai toujours ce fichier quelque part le dernier fois que je l'ai fait. Manque probablement quelque chose.
Wernight
12
git rebase -iest très utile, surtout lorsque vous avez plusieurs opérations de rebase à effectuer, mais il est difficile de décrire avec précision lorsque vous ne pointez pas réellement sur l'épaule de quelqu'un et que vous pouvez voir ce qu'il fait avec son éditeur. J'utilise vim, mais tout le monde ne serait pas satisfait de: "ggjcesquash <Esc> jddjp: wq" et des instructions comme "Déplacer la ligne du haut après la deuxième ligne actuelle et changer le premier mot de la ligne quatre pour" éditer "maintenant enregistrer et quitter "semblent rapidement plus complexes que les étapes réelles. Vous finissez normalement avec quelques - uns --amendet des --continueactions, ainsi.
CB Bailey
3
Je l'ai fait mais un nouveau commit a été réappliqué en plus de celui modifié, avec le même message. Apparemment, git a fait une fusion à 3 voies entre l'ancien commit non modifié contenant le fichier indésirable et le commit fixe de l'autre branche, et il a donc créé un nouveau commit au-dessus de l'ancien, pour réappliquer le fichier.
6
@UncleCJ: Votre fichier a-t-il été ajouté dans un commit de fusion? C'est important. Cette recette est conçue pour faire face à un mauvais commit de fusion. Cela ne fonctionnera pas si votre fichier indésirable a été ajouté dans un commit normal dans l'historique.
CB Bailey
1
Je suis étonné de voir comment j'ai pu faire tout cela en utilisant smartgit et pas de terminal du tout! Merci pour la recette!
cregox
209

Intro: vous avez 5 solutions disponibles

L'affiche originale indique:

J'ai accidentellement validé un fichier indésirable ... dans mon référentiel il y a plusieurs validations ... Je souhaite supprimer complètement le fichier de l'historique du référentiel.

Est-il possible de réécrire l'historique des modifications tel qu'il filename.orign'a jamais été ajouté au référentiel en premier lieu?

Il existe de nombreuses façons de supprimer complètement l'historique d'un fichier de git:

  1. Modifier les commits.
  2. Réinitialisations matérielles (éventuellement plus un rebase).
  3. Rebase non interactive.
  4. Rebases interactives.
  5. Filtrage des branches.

Dans le cas de l'affiche originale, la modification du commit n'est pas vraiment une option en soi, car il a fait plusieurs commits supplémentaires par la suite, mais par souci d'exhaustivité, j'expliquerai également comment le faire, pour toute autre personne qui veut de modifier leur engagement précédent.

Notez que toutes ces solutions impliquent de modifier / réécrire l' historique / les commits d'une manière une autre, donc toute personne ayant d'anciennes copies des commits devra faire un travail supplémentaire pour resynchroniser leur historique avec le nouvel historique.


Solution 1: modification des validations

Si vous avez accidentellement apporté une modification (comme l'ajout d'un fichier) dans votre validation précédente, et que vous ne souhaitez plus que l'historique de cette modification existe, vous pouvez simplement modifier la validation précédente pour en supprimer le fichier:

git rm <file>
git commit --amend --no-edit

Solution 2: réinitialisation matérielle (éventuellement plus une rebase)

Comme la solution n ° 1, si vous voulez simplement vous débarrasser de votre commit précédent, vous avez également la possibilité de simplement réinitialiser matériellement son parent:

git reset --hard HEAD^

Cette commande réinitialisera durablement votre branche au précédent 1 er commit parent.

Cependant , si, comme l'affiche originale, vous avez effectué plusieurs validations après la validation à laquelle vous souhaitez annuler la modification, vous pouvez toujours utiliser des réinitialisations matérielles pour la modifier, mais cela implique également l'utilisation d'une rebase. Voici les étapes que vous pouvez utiliser pour modifier un commit plus loin dans l'historique:

# Create a new branch at the commit you want to amend
git checkout -b temp <commit>

# Amend the commit
git rm <file>
git commit --amend --no-edit

# Rebase your previous branch onto this new commit, starting from the old-commit
git rebase --preserve-merges --onto temp <old-commit> master

# Verify your changes
git diff master@{1}

Solution 3: rebase non interactif

Cela fonctionnera si vous souhaitez simplement supprimer un commit de l'historique:

# Create a new branch at the parent-commit of the commit that you want to remove
git branch temp <parent-commit>

# Rebase onto the parent-commit, starting from the commit-to-remove
git rebase --preserve-merges --onto temp <commit-to-remove> master

# Or use `-p` insteda of the longer `--preserve-merges`
git rebase -p --onto temp <commit-to-remove> master

# Verify your changes
git diff master@{1}

Solution 4: rebases interactives

Cette solution vous permettra d'accomplir les mêmes choses que les solutions n ° 2 et n ° 3, c'est-à-dire de modifier ou de supprimer les validations plus loin dans l'historique que votre validation précédente, de sorte que la solution que vous choisissez d'utiliser vous appartient en quelque sorte. Les rebases interactifs ne sont pas bien adaptés pour rebaser des centaines de validations, pour des raisons de performances, donc j'utiliserais des rebases non interactifs ou la solution de branche de filtre (voir ci-dessous) dans ce genre de situations.

Pour commencer le rebase interactif, utilisez ce qui suit:

git rebase --interactive <commit-to-amend-or-remove>~

# Or `-i` instead of the longer `--interactive`
git rebase -i <commit-to-amend-or-remove>~

Cela amènera git à rembobiner l'historique des validations vers le parent de la validation que vous souhaitez modifier ou supprimer. Il vous présentera ensuite une liste des validations de rembobinage dans l'ordre inverse dans l'éditeur que git est configuré pour utiliser (c'est Vim par défaut):

pick 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`
pick 7668f34 Modify Bash config to use Homebrew recommended PATH
pick 475593a Add global .gitignore file for OS X
pick 1b7f496 Add alias for Dr Java to Bash config (OS X)

Le commit que vous souhaitez modifier ou supprimer sera en haut de cette liste. Pour le supprimer, supprimez simplement sa ligne dans la liste. Sinon, remplacez "choisir" par "modifier" sur la 1ère ligne, comme ceci:

edit 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`

Ensuite, entrez git rebase --continue. Si vous avez choisi de supprimer entièrement le commit, alors c'est tout ce que vous devez faire (autre que la vérification, voir l'étape finale pour cette solution). Si, d'autre part, vous vouliez modifier le commit, alors git réappliquera le commit puis suspendra le rebase.

Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

À ce stade, vous pouvez supprimer le fichier et modifier la validation, puis poursuivre la rebase:

git rm <file>
git commit --amend --no-edit
git rebase --continue

C'est ça. Comme dernière étape, que vous ayez modifié ou supprimé complètement la validation, c'est toujours une bonne idée de vérifier qu'aucune autre modification inattendue n'a été apportée à votre branche en la différenciant avec son état avant le rebase:

git diff master@{1}

Solution 5: Filtrage des branches

Enfin, cette solution est préférable si vous souhaitez effacer complètement toutes les traces de l'existence d'un fichier de l'historique, et aucune des autres solutions n'est tout à fait à la hauteur.

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>'

Cela supprimera <file>toutes les validations, à partir de la validation racine. Si, à la place, vous souhaitez simplement réécrire la plage de validation HEAD~5..HEAD, vous pouvez passer cela comme argument supplémentaire à filter-branch, comme indiqué dans cette réponse :

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD

Encore une fois, une fois l'opération filter-branchterminée, il est généralement judicieux de vérifier qu'il n'y a pas d'autres modifications inattendues en différenciant votre branche de son état précédent avant l'opération de filtrage:

git diff master@{1}

Alternative à la branche de filtre: BFG Repo Cleaner

J'ai entendu dire que l' outil BFG Repo Cleaner s'exécute plus rapidement que git filter-branch, alors vous voudrez peut-être également vérifier cela en option. Il est même mentionné officiellement dans la documentation de la branche filtre comme une alternative viable:

git-filter-branch vous permet de faire des réécritures complexes par script shell de votre historique Git, mais vous n'avez probablement pas besoin de cette flexibilité si vous supprimez simplement les données indésirables comme les gros fichiers ou les mots de passe. Pour ces opérations, vous pouvez envisager le BFG Repo-Cleaner , une alternative basée sur JVM à git-filter-branch, généralement au moins 10 à 50 fois plus rapide pour ces cas d'utilisation, et avec des caractéristiques assez différentes:

  • Toute version particulière d'un fichier est nettoyée exactement une fois . Contrairement à git-filter-branch, le BFG ne vous donne pas la possibilité de gérer un fichier différemment en fonction de l'endroit ou du moment où il a été validé dans votre historique. Cette contrainte offre l'avantage de performance de base du BFG et est bien adaptée à la tâche de nettoyage des données incorrectes - vous ne vous souciez pas de savoir où se trouvent les données incorrectes, vous voulez juste qu'elles disparaissent .

  • Par défaut, le BFG tire pleinement parti des machines multicœurs, nettoyant les arbres de fichiers de validation en parallèle. git-filter-branch nettoie les validations séquentiellement (c'est-à-dire de manière monothread), bien qu'il soit possible d'écrire des filtres qui incluent leur propre parallélisme, dans les scripts exécutés par rapport à chaque commit.

  • Les options de commande sont beaucoup plus restrictives que la branche filtre git, et exclusivement dédié aux tâches de suppression Data- par exemple indésirables: --strip-blobs-bigger-than 1M.

Ressources additionnelles

  1. Pro Git § 6.4 Git Tools - Historique de réécriture .
  2. Page de manuel de git-filter-branch (1) .
  3. Page de manuel de git-commit (1) .
  4. Page de manuel de git-reset (1) .
  5. Page de manuel de git-rebase (1) .
  6. Le BFG Repo Cleaner (voir aussi cette réponse du créateur lui-même ).
Communauté
la source
Provoque-t-il filter-branchun recalcul des hachages? Si une équipe travaille avec un référentiel où un gros fichier doit être filtré, comment font-ils pour que tout le monde se retrouve avec le même état du référentiel?
YakovL
@YakovL. Tout recalcule les hachages. En fait, les commits sont immuables. Il crée une histoire entièrement nouvelle et y déplace le pointeur de votre branche. La seule façon de s'assurer que tout le monde a la même histoire est une réinitialisation matérielle.
Mad Physicist
118

Si vous n'avez rien engagé depuis, juste git rmle fichier et git commit --amend.

Si tu as

git filter-branch \
--index-filter 'git rm --cached --ignore-unmatch path/to/file/filename.orig' merge-point..HEAD

passera par chaque modification de merge-pointà HEAD, supprimera filename.orig et réécrira la modification. L'utilisation --ignore-unmatchsignifie que la commande n'échouera pas si, pour une raison quelconque, filename.orig est absent d'une modification. C'est la manière recommandée dans la section Exemples de la page de manuel git-filter-branch .

Remarque pour les utilisateurs de Windows: le chemin du fichier doit utiliser des barres obliques

Schwern
la source
3
Merci! git filter-branch a fonctionné pour moi où l'exemple de rebase donné comme réponse n'a pas fonctionné: les étapes semblaient fonctionner, mais la poussée a échoué. A fait un pull, puis a poussé avec succès, mais le fichier était toujours là. J'ai essayé de refaire les étapes de rebase, puis tout s'est compliqué avec les conflits de fusion. J'ai cependant utilisé une commande de branche de filtre légèrement différente, la "Une méthode améliorée" donnée ici: github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index- filtre 'git update-index --remove filename' <introduction-revision-sha1>
..HEAD
1
Je ne sais pas laquelle est la méthode améliorée . La documentation officielle de Git git-filter-branchsemble donner la première.
Wernight
5
Consultez zyxware.com/articles/4027/… Je trouve que c'est la solution la plus complète et la plus simple qui impliquefilter-branch
leontalbot
2
@atomicules, si vous essayez de pousser le dépôt local vers le dépôt distant, git insistera pour tirer d'abord de la télécommande, car elle a des changements que vous n'avez pas localement. Vous pouvez utiliser --force flag pour pousser vers la télécommande - il en supprimera entièrement les fichiers. Mais attention, assurez-vous de ne pas forcer l'écrasement d'autre chose que les fichiers.
sol0mka
1
N'oubliez "pas d' 'utiliser Windows et non lorsque vous utilisez Windows, ou vous obtiendrez une erreur de «mauvaise révision» formulée de manière inutile.
cz
49

C'est le meilleur moyen:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Assurez-vous simplement de sauvegarder les copies des fichiers en premier.

ÉDITER

L'édition de Neon a malheureusement été rejetée lors de l'examen.
Voir l'article Neons ci-dessous, il pourrait contenir des informations utiles!


Par exemple, pour supprimer tous les *.gzfichiers accidentellement validés dans le référentiel git:

$ du -sh .git ==> e.g. 100M
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
$ git push origin master --force
$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now
$ git gc --aggressive --prune=now

Cela ne fonctionnait toujours pas pour moi? (Je suis actuellement à git version 1.7.6.1)

$ du -sh .git ==> e.g. 100M

Je ne sais pas pourquoi, car je n'avais qu'une seule branche principale. Quoi qu'il en soit, j'ai finalement fait nettoyer mon dépôt git en poussant dans un nouveau dépôt git vide et nu, par exemple

$ git init --bare /path/to/newcleanrepo.git
$ git push /path/to/newcleanrepo.git master
$ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 

(Oui!)

Ensuite, je le clone dans un nouveau répertoire et je déplace le dossier .git dans celui-ci. par exemple

$ mv .git ../large_dot_git
$ git clone /path/to/newcleanrepo.git ../tmpdir
$ mv ../tmpdir/.git .
$ du -sh .git ==> e.g. 5M 

(ouais! enfin nettoyé!)

Après avoir vérifié que tout va bien, vous pouvez supprimer les répertoires ../large_dot_gitet ../tmpdir(peut-être dans quelques semaines ou mois à partir de maintenant, juste au cas où ...)

Darren
la source
1
Cela a fonctionné pour moi avant le "Cela ne fonctionnait toujours pas pour moi?" comment
shadi
Excellente réponse, mais suggère d'ajouter --prune-emptyà la commande filter-branch.
ideasman42
27

La réécriture de l'historique de Git nécessite de modifier tous les ID de validation affectés.Par conséquent, tous ceux qui travaillent sur le projet devront supprimer leurs anciennes copies du référentiel et faire un nouveau clone après avoir nettoyé l'historique. Plus il dérange de personnes, plus vous avez besoin d'une bonne raison de le faire - votre fichier superflu ne pose pas vraiment de problème, mais si seulement vous travaillez sur le projet, vous pouvez aussi bien nettoyer l'historique Git si vous le souhaitez à!

Pour le rendre aussi simple que possible, je recommanderais d'utiliser le BFG Repo-Cleaner , une alternative plus simple et plus rapide à git-filter-branchspécifiquement conçue pour supprimer des fichiers de l'historique Git. Une façon de vous faciliter la vie ici est qu'il gère en fait toutes les références par défaut (toutes les balises, les branches, etc.) mais il est également 10 à 50 fois plus rapide.

Vous devez suivre attentivement les étapes ici: http://rtyley.github.com/bfg-repo-cleaner/#usage - mais le bit principal est juste celui-ci: téléchargez le pot BFG (nécessite Java 6 ou supérieur) et exécutez cette commande :

$ java -jar bfg.jar --delete-files filename.orig my-repo.git

L'intégralité de votre historique de référentiel sera analysé et tout fichier nommé filename.orig(qui ne fait pas partie de votre dernier commit ) sera supprimé. C'est beaucoup plus facile que d'utiliser git-filter-branchpour faire la même chose!

Divulgation complète: je suis l'auteur du BFG Repo-Cleaner.

Roberto Tyley
la source
4
C'est un excellent outil: une seule commande, elle produit une sortie très claire et fournit un fichier journal qui correspond à chaque ancien commit au nouveau . Je n'aime pas installer Java mais ça vaut le coup.
mikemaccana
C'est la seule chose qui a fonctionné pour moi, mais c'est parce que je ne travaillais pas correctement avec la branche filtre git. :-)
Kevin LaBranche
14
You should probably clone your repository first.

Remove your file from all branches history:
git filter-branch --tree-filter 'rm -f filename.orig' -- --all

Remove your file just from the current branch:
git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    

Lastly you should run to remove empty commits:
git filter-branch -f --prune-empty -- --all
paulalexandru
la source
1
Bien que toutes les réponses semblent être sur la piste des branches de filtre, celle-ci montre comment nettoyer TOUTES les branches de votre historique.
Cameron Lowell Palmer
4

Juste pour ajouter cela à la solution de Charles Bailey, j'ai juste utilisé un git rebase -i pour supprimer les fichiers indésirables d'un commit précédent et cela a fonctionné comme un charme. Les marches:

# Pick your commit with 'e'
$ git rebase -i

# Perform as many removes as necessary
$ git rm project/code/file.txt

# amend the commit
$ git commit --amend

# continue with rebase
$ git rebase --continue
Sverrir Sigmundarson
la source
4

La manière la plus simple que j'ai trouvée a été suggérée par leontalbot(comme commentaire), qui est un article publié par Anoopjohn . Je pense que ça vaut son propre espace comme réponse:

(Je l'ai converti en script bash)

#!/bin/bash
if [[ $1 == "" ]]; then
    echo "Usage: $0 FILE_OR_DIR [remote]";
    echo "FILE_OR_DIR: the file or directory you want to remove from history"
    echo "if 'remote' argument is set, it will also push to remote repository."
    exit;
fi
FOLDERNAME_OR_FILENAME=$1;

#The important part starts here: ------------------------

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

if [[ $2 == "remote" ]]; then
    git push --all --force
fi
echo "Done."

Tous les crédits vont à Annopjohn, et leontalbotpour le signaler.

REMARQUE

Sachez que le script n'inclut pas de validations, alors assurez-vous de ne pas faire d'erreurs et d'avoir une sauvegarde en cas de problème. Cela a fonctionné pour moi, mais cela peut ne pas fonctionner dans votre situation. UTILISEZ-LE AVEC ATTENTION (suivez le lien si vous voulez savoir ce qui se passe).

lepe
la source
3

Certainement, git filter-branchc'est la voie à suivre.

Malheureusement, cela ne suffira pas à supprimer complètement filename.origde votre référentiel, car il peut toujours être référencé par des balises, des entrées de reflog, des télécommandes, etc.

Je recommande également de supprimer toutes ces références, puis d'appeler le garbage collector. Vous pouvez utiliser le git forget-blobscript de ce site Web pour faire tout cela en une seule étape.

git forget-blob filename.orig

nachoparker
la source
1

Si c'est le dernier commit que vous voulez nettoyer, j'ai essayé avec la version 2.14.3 de git (Apple Git-98):

touch empty
git init
git add empty
git commit -m init

# 92K   .git
du -hs .git

dd if=/dev/random of=./random bs=1m count=5
git add random
git commit -m mistake

# 5.1M  .git
du -hs .git

git reset --hard HEAD^
git reflog expire --expire=now --all
git gc --prune=now

# 92K   .git
du -hs .git
clarkttfu
la source
git reflog expire --expire=now --all; git gc --prune=nowest une très mauvaise chose à faire. Sauf si vous manquez d'espace disque, laissez git garbage collecter ces commits après quelques semaines
avmohan
Merci d'avoir fait remarquer cela. Mon dépôt a été soumis avec de nombreux fichiers binaires volumineux et le dépôt est entièrement sauvegardé chaque nuit. Donc, je voulais juste tout en sortir;)
clarkttfu
-1

Vous pouvez aussi utiliser:

git reset HEAD file/path

paolo granada lim
la source
3
Si le fichier a été ajouté à une validation, cela ne supprime même pas le fichier de l'index, il réinitialise simplement l'index à la version HEAD du fichier.
CB Bailey