web-dev-qa-db-fra.com

Comment corrigez-vous une mauvaise fusion et répétez vos bons commits sur une fusion fixe?

J'ai accidentellement commis un fichier indésirable (filename.orig en résolvant une fusion) dans mon référentiel il y a plusieurs validations, sans que je m'en rende compte jusqu'à présent. Je veux supprimer complètement le fichier de l'historique du référentiel.

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

396
Grant Limberg

S'il vous plaît, n'utilisez pas cette recette si votre situation n'est pas celle décrite dans la question. Cette recette est destinée à réparer une mauvaise fusion et à rejouer vos bons commits sur une fusion fixe.

Bien que filter-branch fasse ce que vous voulez, la commande est assez complexe et je choisirais probablement de le faire avec git rebase. C'est probablement une préférence personnelle. filter-branch peut le faire en une seule commande légèrement plus complexe, alors que la solution rebase exécute 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 noter l'ID de validation généré par l'étape git commit --amend pour fournir à la git rebase commande plutôt que d'utiliser le nom de branche temporaire.)

293
CB Bailey

Intro: Vous avez 5 solutions disponibles

L'affiche originale déclare:

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

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

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

  1. Modifier les commits.
  2. Réinitialisations matérielles (éventuellement plus une base).
  3. Base 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, puisqu’il a fait plusieurs commits supplémentaires par la suite, mais par souci d’exhaustivité, j’expliquerai également comment le faire, pour ceux qui le souhaitent. pour modifier leur précédent engagement.

Notez que toutes ces solutions impliquent la modification/réécriture de l'historique/des commits d'une manière ou d'une autre, de sorte que toute personne possédant d'anciennes copies des commits doivent 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 (telle que l'ajout d'un fichier) dans votre commit précédent et que vous ne voulez plus que l'historique de cette modification existe, vous pouvez simplement modifier le commit précédent pour en supprimer le fichier:

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

Solution 2: Réinitialisation matérielle (éventuellement plus une base)

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

git reset --hard HEAD^

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

Cependant , si, comme pour l'affiche originale, vous avez effectué plusieurs validations après la validation souhaitée. pour annuler la modification, vous pouvez toujours utiliser des réinitialisations dures pour la modifier, mais cela implique également l’utilisation d’une base. Voici les étapes que vous pouvez utiliser pour modifier un commit plus en arrière 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 interactive

Cela fonctionnera si vous voulez 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 tâches que les solutions n ° 2 et n ° 3, c’est-à-dire modifier ou supprimer les validations plus loin dans l’historique que votre validation immédiatement précédente. La solution que vous choisissez d’utiliser dépend donc de vous. Les rebases interactives ne conviennent pas à des centaines de commits pour des raisons de performances. J'utiliserais donc des rebases non interactives ou la solution de branche de filtre (voir ci-dessous) dans de telles situations.

Pour commencer la base interactive, 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 fera en sorte que git rembobine l’historique de la validation au parent de la validation que vous souhaitez modifier ou supprimer. Il vous présentera ensuite une liste des commits rembobinés dans l'ordre inverse de 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 "pick" par "edit" sur le 1st ligne, comme si:

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, il ne vous reste plus qu'à le vérifier (voir la dernière étape de cette solution). Si, par contre, vous voulez modifier le commit, alors git réappliquera le commit, puis mettra en pause la base.

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 continuer la base:

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

C'est ça. Enfin, que vous ayez modifié le commit ou le supprimé complètement, il est toujours judicieux de vérifier qu'aucune autre modification inattendue n'a été apportée à votre branche en la différenciant par son état avant la base de rebase:

git diff master@{1}

Solution 5: Filtrage des branches

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

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

Cela supprimera <file> de toutes les validations, à partir de la validation racine. Si vous souhaitez simplement réécrire la plage de validation HEAD~5..HEAD, vous pouvez alors le transmettre 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 le filter-branch terminé, il est généralement judicieux de vérifier qu'il n'y a pas d'autres modifications inattendues en différenciant votre branche avec son état précédent avant l'opération de filtrage:

git diff master@{1}

Solution de remplacement de filtre: BFG Repo Cleaner

J'ai entendu dire que l'outil BFG Repo Cleaner est plus rapide que git filter-branch, vous devriez donc peut-être vérifier cette option. Il est même mentionné officiellement dans la documentation de filtre-branche comme alternative viable:

git-filter-branch vous permet de réécrire des scripts complexes de votre historique Git avec un script Shell, mais vous n'avez probablement pas besoin de cette souplesse si vous supprimez simplement les données non souhaitées fichiers ou mots de passe. Pour ces opérations, vous voudrez peut-être envisager The BFG Repo-Cleaner , une alternative à la JVM basée sur git-filter-branch, généralement au moins 10 à 50 fois plus rapide pour ces cas d'utilisation, et avec des les caractéristiques:

  • Toute version d'un fichier est nettoyée exactement une fois . Contrairement à git-filter-branch, BFG ne vous permet pas de gérer un fichier différemment en fonction du lieu ou du moment où il a été validé dans votre historique. Cette contrainte confère l’avantage principal de BFG en termes de performances et convient parfaitement à la tâche de nettoyage des données incorrectes - vous ne vous en souciez pas se trouvent les données incorrectes, vous voulez juste il est parti .

  • Par défaut, BFG tire pleinement parti des machines multicœurs en nettoyant les arborescences de fichiers de validation en parallèle. git-filter-branch nettoie les commits séquentiellement (c’est-à-dire d’une manière mono-thread), bien qu’il soit possible d’écrire des filtres incluant leur propre parallellisme, dans les scripts exécutés pour chaque commit .

  • Les options de commande sont beaucoup plus restrictifs que la branche git-filter et sont dédiés uniquement à la suppression des données indésirables, par exemple: --strip-blobs-bigger-than 1M.

Ressources additionnelles

  1. Pro Git § 6.4 Outils Git - Historique de la réécriture .
  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. git-rebase (1) Page de manuel .
  6. The BFG Repo Cleaner (voir aussi cette réponse du créateur lui-même ).
203
user456814

Si vous n'avez rien commis depuis, il suffit de git rm le fichier et de 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 de chaque changement de merge-point à HEAD, supprimera filename.orig et réécrira le changement. Utiliser --ignore-unmatch signifie que la commande n'échouera pas si, pour une raison quelconque, nomfichier.orig est absent d'une modification. C'est la méthode recommandée dans la section Exemples de la page de manuel de git-filter-branch .

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

118
Schwern

C'est la meilleur façon:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Veillez simplement à sauvegarder les copies des fichiers en premier.

EDIT

Le montage de Neon a malheureusement été rejeté lors de la révision.
Voir l'article de Neons ci-dessous, il pourrait contenir des informations utiles!


Par exemple. supprimer tous les fichiers *.gz bloqués accidentellement 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 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, car je n'avais qu'une branche maîtresse. Quoi qu’il en soit, mon dépôt git a finalement été véritablement nettoyé en intégrant un nouveau référentiel 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 clone cela dans un nouveau répertoire et je déplace 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 les répertoires ../large_dot_git et ../tmpdir (peut-être dans quelques semaines ou un mois à partir de maintenant, au cas où ...)

48
Darren

La réécriture de l'historique Git nécessite la modification de tous les identifiants de commit affectés. Ainsi, tous ceux qui travaillent sur le projet devront supprimer leurs anciennes copies du référentiel et créer un nouveau clone une fois que vous aurez nettoyé l'historique. Plus le nombre de personnes qui gênent, plus vous avez besoin d’une bonne raison - votre fichier superflu ne pose pas vraiment de problème, mais si seulement vous travaillez sur le projet, vous pourriez aussi bien nettoyer l’historique de Git si vous le souhaitez!

Pour simplifier au maximum les choses, nous vous recommandons d'utiliser BFG Repo-Cleaner , une alternative plus simple et plus rapide à git-filter-branch, spécialement conçue pour supprimer les fichiers de l'historique Git. Une façon de vous faciliter la vie ici est qu’il gère réellement toutes les références par défaut (toutes les balises, branches, etc.), mais c’est aussi - 10 - 50x plus rapide.

Vous devez suivre attentivement les étapes ici: http://rtyley.github.com/bfg-repo-cleaner/#usage - mais le bit de base est juste ceci: téléchargez le jarre BFG) (requiert Java 6 ou supérieur) et exécutez la commande suivante:

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

L’ensemble de votre historique de référentiel sera analysé, ainsi que tout fichier nommé filename.orig (qui ne figure pas dans votre latest commit ) sera supprimé. C'est beaucoup plus facile que d'utiliser git-filter-branch pour faire la même chose!

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

27
Roberto Tyley
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
13
paulalexandru

Juste pour ajouter que la solution de Charles Bailey, je viens d'utiliser 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
4

Le moyen le plus simple que j'ai trouvé a été suggéré par leontalbot (en tant que commentaire), qui est un message publié par Anoopjohn . Je pense que sa valeur son propre espace comme une 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 à leontalbot pour l'avoir signalé.

NOTE

Sachez que le script n'inclut pas de validations. Veillez donc à ne pas commettre d'erreurs et à disposer d'une copie de sauvegarde en cas de problème. Cela a fonctionné pour moi, mais cela peut ne pas fonctionner dans votre situation. UTILISEZ-LE AVEC PRÉCAUTION (suivez le lien si vous voulez savoir ce qui se passe).

4
lepe

Décidément, git filter-branch est la voie à suivre.

Malheureusement, cela ne suffira pas pour supprimer complètement filename.orig de votre rapport, 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 ramasse-miettes. Vous pouvez utiliser le script git forget-blob de this website pour faire tout cela en une seule étape.

git forget-blob filename.orig

3
nachoparker

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
1
clarkttfu

C’est pour cela que git filter-branch a été conçu.

0
CesarB