420 votes

Comment réparer une mauvaise fusion, et rejouer vos bons commits sur une fusion corrigée ?

J'ai accidentellement commis un fichier indésirable ( filename.orig lors de la résolution d'une fusion) à mon dépôt il y a plusieurs commits, sans que je le remarque jusqu'à maintenant. Je veux supprimer complètement le fichier de l'historique du dépôt.

Est-il possible de réécrire l'historique des modifications de telle sorte que filename.orig n'a jamais été ajouté au référentiel en premier lieu ?

302voto

Charles Bailey Points 244082

Veuillez ne pas utiliser cette recette si votre situation n'est pas celle décrite dans la question. Cette recette est destinée à corriger une mauvaise fusion et à rejouer vos bons commits sur une fusion corrigée.

Bien que filter-branch fera ce que vous voulez, c'est une commande assez complexe et je choisirais probablement de faire cela avec git rebase . C'est probablement une préférence personnelle. filter-branch peut le faire en une seule commande, un peu plus complexe, alors que l'application rebase La solution consiste à effectuer 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 besoin d'une branche temporaire, vous pouvez le faire avec un 'detached HEAD', mais vous devez prendre note de l'identifiant de commit généré par la commande git commit --amend étape à fournir à la git rebase plutôt que d'utiliser le nom de la branche temporaire).

6 votes

Est-ce qu'un git rebase -i être plus rapide et toujours aussi facile ? $ git rebase -i <sh1-of-merge> Marquer le bon comme "edit" $ git rm somefile.orig $ git commit --amend $ git rebase --continue Cependant, pour une raison quelconque, j'ai toujours ce fichier quelque part la dernière fois que j'ai fait ça. Il me manque probablement quelque chose.

12 votes

git rebase -i est très utile, surtout lorsque vous avez plusieurs opérations de rebasement à effectuer, mais il est très difficile de le décrire avec précision lorsque vous ne pointez pas par-dessus 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 heureux avec : "ggjcesquash<Esc>jddjp:wq" et des instructions comme "Déplacez la ligne supérieure après la deuxième ligne actuelle et changez le premier mot de la ligne quatre en 'edit', puis enregistrez et quittez" semblent rapidement plus complexes que les étapes réelles. Vous vous retrouvez normalement avec des --amend et --continue des actions, également.

3 votes

J'ai fait cela mais un nouveau commit a été réappliqué par-dessus le commit 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 corrigé de l'autre branche, et a donc créé un nouveau commit par dessus l'ancien, pour ré-appliquer le fichier.

219voto

Cupcake Points 22154

Intro : Vous disposez de 5 solutions

L'affiche originale déclare :

J'ai accidentellement commis un fichier non désiré ... à mon référentiel il y a plusieurs commits il y a plusieurs commits... Je veux supprimer complètement le fichier de l'historique du dépôt.

Est-il possible de réécrire l'historique des changements de telle sorte que filename.orig n'a jamais été ajouté au référentiel en premier lieu ?

Il existe de nombreuses manières différentes de supprimer complètement l'historique d'un fichier de git :

  1. Modifier les engagements.
  2. Remise à zéro (plus éventuellement un rebasement).
  3. Rebasement non interactif.
  4. Rebases interactives.
  5. Filtrage des branches.

Dans le cas de l'affiche originale, modifier le commit n'est pas vraiment une option en soi, puisqu'il a fait plusieurs commits supplémentaires par la suite, mais dans l'intérêt de la d'être complet, je vais aussi expliquer comment le faire, pour tous ceux qui veulent juste veut modifier son commit précédent.

Notez que toutes ces solutions impliquent modification/réécriture histoire/commits d'une manière ou d'une autre, donc toute personne ayant des anciennes copies des commits devra faire travail supplémentaire pour re-synchroniser leur historique avec le nouvel historique.


Solution 1 : Modifier les engagements

Si vous avez accidentellement fait une modification (comme l'ajout d'un fichier) dans votre précédent précédente, et que vous ne voulez plus que l'historique de cette modification existe, alors vous pouvez simplement modifier le commit précédent pour en retirer le fichier :

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

Solution 2 : Réinitialisation matérielle (éventuellement accompagnée d'un rebasement)

Comme pour la solution n°1, si vous voulez simplement vous débarrasser de votre précédent commit, alors vous avez également la possibilité de faire une réinitialisation matérielle de son pare-feu. avez aussi la possibilité de faire une réinitialisation de son parent :

git reset --hard HEAD^

Cette commande va réinitialiser votre branche à la valeur 1 précédente. st parent commettre.

Cependant si, comme l'auteur de l'article original, vous avez fait plusieurs commits après le commit sur lequel vous voulez annuler la modification, vous pouvez toujours utiliser les hard resets. le commit sur lequel vous voulez annuler le changement, vous pouvez toujours utiliser les hard resets pour le 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'histoire :

# 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 : Rebasement non interactif

Cela fonctionnera si vous voulez juste supprimer entièrement 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 : rebasements interactifs

Cette solution vous permettra d'accomplir les mêmes choses que les solutions #2 et #3, c'est-à-dire modifier ou supprimer des commits plus anciens que le commit précédent. La solution que vous choisissez d'utiliser dépend donc de vous. Les rebasements interactifs ne sont pas bien adaptés pour rebaser des centaines de commits, pour des raisons de performance. pour des raisons de performance, j'utiliserais donc des rebasements non interactifs ou la solution (voir ci-dessous) dans ce genre de situation.

Pour commencer le rebasement 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>~

Ceci fera que git remontera l'historique des livraisons jusqu'au parent de la livraison que vous voulez modifier ou supprimer. que vous voulez modifier ou supprimer. Il vous présentera alors une liste des des commits remontés 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 voulez modifier ou supprimer sera en haut de cette liste. Pour le supprimer, il suffit de supprimer sa ligne dans la liste. Sinon, remplacez "pick" par "modifier" sur la ligne 1 st 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 avez à faire (autre que la vérification, voir l'étape finale pour cette solution). Si, d'un autre côté, vous voulez modifier le commit, alors git réappliquera le commit et mettra ensuite en pause le rebasement.

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 le commit, puis poursuivre la procédure de rebasement :

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

C'est ça. Comme étape finale, que vous ayez modifié le commit ou que vous l'ayez supprimé complètement, c'est toujours une bonne idée de vérifier qu'aucun autre changement inattendu n'a été faite à votre branche en la comparant avec son état avant le rebasement :

git diff master@{1}

Solution 5 : filtrer les branches

Enfin, cette solution est la meilleure si vous voulez effacer complètement toute trace de l'existence d'un fichier dans l'historique, et qu'aucune autre solution n'est à la hauteur. d'existence d'un fichier dans l'historique, et qu'aucune des autres solutions n'est à la hauteur de la à la hauteur de la tâche.

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

Cela supprimera <file> de tous les commits, en commençant par le commit racine. Si au lieu de cela, vous voulez juste réécrire la plage de commit HEAD~5..HEAD alors vous pouvez le passer comme un 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, après le filter-branch est terminée, c'est généralement une bonne idée de vérifier qu'il n'y a pas d'autres changements inattendus en comparant votre branche avec son état précédent avant l'opération de filtrage :

git diff master@{1}

Alternative filtre-branche : Nettoyeur BFG Repo

J'ai entendu dire que le Nettoyeur BFG Repo fonctionne plus rapidement que l'outil git filter-branch Vous pouvez donc envisager cette option. Il est même mentionné officiellement dans le documentation sur les filtres et les branches comme une alternative viable :

git-filter-branch vous permet de faire des réécritures complexes de votre historique Git. de votre historique Git, mais vous n'avez probablement pas besoin de cette flexibilité si vous êtes simplement suppression des données indésirables comme les fichiers volumineux ou les mots de passe. Pour ces opérations, vous pouvez envisager Le BFG Repo-Cleaner une alternative à git-filter-branch, basée sur la JVM alternative à git-filter-branch, typiquement au moins 10-50x 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 . Le BFG, contrairement à git-filter-branch, ne vous donne pas la possibilité de traiter un fichier différemment en fonction de l'endroit ou du moment où il a été livré au sein de votre organisation. un fichier différemment en fonction de l'endroit ou du moment où il a été livré dans votre historique. Cette contrainte donne l'avantage principal de performance de The BFG, et est bien adaptée à la tâche de nettoyage des mauvaises données. soins les mauvaises données sont, vous voulez juste ça disparu .

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

  • Le site options de la commande sont beaucoup plus restrictives que git-filter branch, et dédiées uniquement à la tâches de suppression des données non désirées - par ex : --strip-blobs-bigger-than 1M .

Ressources supplémentaires

  1. Pro Git § 6.4 Les outils Git - Réécrire l'histoire .
  2. Page de manuel de git-filter-branch(1) .
  3. Page de manuel de git-commit(1) .
  4. git-reset(1) Page de manuel .
  5. Page de manuel de git-rebase(1) .
  6. Le nettoyeur BFG Repo (voir aussi cette réponse du créateur lui-même ).

0 votes

Fait filter-branch provoquer le recalcul des hashs ? Si une équipe travaille sur un repo où un gros fichier doit être filtré, comment faire pour que tout le monde se retrouve avec le même état du repo ?

0 votes

@YakovL. Tout recalcule les hashs. En fait, les commits sont immuables. Il crée un historique entièrement nouveau, et déplace votre pointeur de branche vers celui-ci. La seule façon de s'assurer que tout le monde a le même historique est une remise à zéro.

0 votes

Vous êtes un sauveur de vies. La solution 5 l'a fait pour moi !

121voto

Schwern Points 33677

Si vous n'avez rien fait depuis, il suffit de git rm le dossier et git commit --amend .

Si vous avez

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

va passer par chaque changement de merge-point à HEAD supprimez filename.orig et réécrivez la modification. Utilisation de --ignore-unmatch signifie que la commande n'échouera pas si, pour une raison quelconque, filename.orig est absent d'une modification. C'est la méthode recommandée dans la section Exemples du manuel de l'utilisateur de Page de manuel de git-filter-branch .

Note pour les utilisateurs de Windows : Le chemin d'accès au fichier doit utiliser des barres obliques

3 votes

Merci ! git filter-branch a fonctionné pour moi alors que l'exemple rebase donné en réponse ne l'a pas fait : Les étapes semblaient fonctionner, mais le push a échoué. J'ai fait un pull, puis un push avec succès, mais le fichier était toujours là. J'ai essayé de refaire les étapes de rebasement et tout s'est gâté avec les conflits de fusion. J'ai utilisé une commande filter-branch légèrement différente, la commande "An Improved Method" donnée ici : github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index-filter 'git update-index --remove filename' <introduction-revision-sha1>..HEAD

1 votes

Je ne suis pas sûr de savoir lequel est le amélioré méthode. La documentation officielle de Git git-filter-branch semble donner le premier.

5 votes

Vérifiez zyxware.com/articles/4027/ Je trouve que c'est la solution la plus complète et la plus directe qui implique filter-branch

50voto

Darren Points 181

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

Veillez simplement à sauvegarder les copies des fichiers au préalable.

EDIT

Le montage de Néon a malheureusement été rejeté lors de la révision.
Voir le post de Neons ci-dessous, il pourrait contenir des informations utiles !


Par exemple, pour supprimer tous les *.gz les fichiers accidentellement commis dans le dépôt 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 n'a toujours pas fonctionné pour moi ? (Je suis actuellement à la version 1.7.6.1 de git)

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

Je ne sais pas pourquoi, puisque je n'avais qu'une seule branche principale. Quoi qu'il en soit, j'ai finalement réussi à nettoyer mon dépôt git en le poussant dans un nouveau dépôt git vide et dépouillé, par ex.

$ 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, j'ai cloné ce dossier dans un nouveau répertoire et j'ai déplacé son 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 alors supprimer le fichier ../large_dot_git et ../tmpdir annuaires (peut-être dans quelques semaines ou mois, juste au cas où...)

1 votes

Cela a marché pour moi avant le commentaire "Ça n'a toujours pas marché pour moi ?".

0 votes

Excellente réponse, mais je suggère d'ajouter --prune-empty à la commande filter-branch.

27voto

Roberto Tyley Points 4352

Réécrire l'historique de Git nécessite de modifier tous les identifiants de commit concernés, et donc tous ceux qui travaillent sur le projet devront supprimer leurs anciennes copies du dépôt, et faire un nouveau clone après avoir nettoyé l'historique. Plus il y a de personnes que cela dérange, plus vous avez besoin d'une bonne raison pour le faire - votre fichier superflu ne cause pas vraiment de problème, mais si seulement vous travaillent sur le projet, vous pouvez aussi bien nettoyer l'historique Git si vous le souhaitez !

Pour faciliter les choses, je vous recommande d'utiliser la fonction BFG Repo-Cleaner une alternative plus simple et plus rapide à git-filter-branch spécifiquement conçu pour supprimer les fichiers de l'historique Git. L'une des façons de vous faciliter la vie est qu'il gère en fait tous par défaut (tous les tags, branches, etc.) mais il est également 10 - 50x plus rapide.

Vous devez suivre attentivement les étapes décrites ici : http://rtyley.github.com/bfg-repo-cleaner/#usage - mais l'essentiel est le suivant : téléchargez l'application Bocal BFG (nécessite Java 6 ou plus) et lancez cette commande :

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

L'historique complet de votre dépôt sera analysé, et tout fichier nommé filename.orig (ce n'est pas dans votre dernier site commettre ) seront supprimés. C'est beaucoup plus facile que d'utiliser git-filter-branch pour faire la même chose !

Révélation complète : je suis l'auteur du BFG Repo-Cleaner.

4 votes

Il s'agit d'un excellent outil : une seule commande, il produit une sortie très claire et fournit une fichier journal qui fait correspondre chaque ancien commit au nouveau . Je n'aime pas installer Java mais cela en vaut la peine.

0 votes

C'est la seule chose qui a fonctionné pour moi mais c'est comme si je ne travaillais pas correctement avec git filter-branch :-)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X