Como você corrige uma mesclagem incorreta e reproduz suas confirmações boas em uma mesclagem fixa?

407

Confirmei acidentalmente um arquivo indesejado ( filename.origenquanto resolvia uma mesclagem) no meu repositório há vários commits atrás, sem que eu o notasse até agora. Eu quero excluir completamente o arquivo do histórico do repositório.

É possível reescrever o histórico de alterações, que filename.orignunca foi adicionado ao repositório em primeiro lugar?

Grant Limberg
fonte

Respostas:

297

Por favor, não use esta receita se a sua situação não for a descrita na pergunta. Esta receita é para corrigir uma mesclagem incorreta e reproduzir suas confirmações boas em uma mesclagem fixa.

Embora filter-branchfaça o que você deseja, é um comando bastante complexo e eu provavelmente escolheria fazer isso git rebase. Provavelmente é uma preferência pessoal. filter-branchpode fazê-lo em um único comando um pouco mais complexo, enquanto a rebasesolução está executando as operações lógicas equivalentes uma etapa de cada vez.

Experimente a seguinte receita:

# 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

(Observe que na verdade você não precisa de uma ramificação temporária, você pode fazer isso com um 'HEAD desanexado', mas é necessário anotar o ID de confirmação gerado pela git commit --amendetapa para fornecer ao git rebasecomando em vez de usar a ramificação temporária nome.)

CB Bailey
fonte
6
Não git rebase -iseria mais rápido e fácil? $ git rebase -i <sh1-of-merge> Marque o correto como "edit" $ git rm somefile.orig vez que eu fiz isso. Provavelmente falta alguma coisa.
Wernight
12
git rebase -ié muito útil, especialmente quando você tem várias operações de rebase-y para executar, mas é uma dor certa descrever com precisão quando você não está realmente apontando por cima do ombro de alguém e pode ver o que ele está fazendo com o editor. Eu uso o vim, mas nem todo mundo ficaria feliz com: "ggjcesquash <Esc> jddjp: wq" e instruções como "Mova a linha superior para depois da segunda linha atual e altere a primeira palavra na linha quatro para 'editar' agora salve e sair rapidamente parece mais complexo do que as etapas reais. Você normalmente acaba com algumas ações --amende --continuetambém.
CB Bailey
3
Fiz isso, mas um novo commit foi reaplicado em cima do emendado, com a mesma mensagem. Aparentemente, o git fez uma fusão de três maneiras entre o commit antigo e não corrigido que contém o arquivo indesejado e o commit fixo do outro ramo, e assim criou um novo commit sobre o antigo, para reaplicar o arquivo.
6
@UncleCJ: Seu arquivo foi adicionado em uma consolidação de mesclagem? Isso é importante. Esta receita foi projetada para lidar com uma confirmação de mesclagem incorreta. Não funcionará se o seu arquivo indesejado foi adicionado em uma confirmação normal no histórico.
CB Bailey
1
Estou surpreso como eu poderia fazer tudo isso usando smartgit e nenhum terminal! Obrigado pela receita!
Cregox 14/09/11
209

Introdução: Você tem 5 soluções disponíveis

O pôster original declara:

Confirmei acidentalmente um arquivo indesejado ... para o meu repositório há vários commits atrás ... Quero excluir completamente o arquivo do histórico do repositório.

É possível reescrever o histórico de alterações, que filename.orignunca foi adicionado ao repositório em primeiro lugar?

Existem várias maneiras de remover completamente o histórico de um arquivo do git:

  1. Alterações confirmadas.
  2. Redefinições rígidas (possivelmente mais uma rebase).
  3. Rebase não interativo.
  4. Reestruturações interativas.
  5. Filtrar filiais.

No caso do pôster original, alterar o commit não é realmente uma opção por si só, já que ele fez vários commit adicionais depois, mas por uma questão de completude, também explicarei como fazê-lo, para qualquer pessoa que apenas deseje alterar seu commit anterior.

Observe que todas essas soluções envolvem alterar / reescrever o histórico / confirmações de uma maneira ou outra, portanto, qualquer pessoa com cópias antigas das confirmações precisará fazer um trabalho extra para sincronizar novamente sua história com a nova história.


Solução 1: Alterando confirmações

Se você acidentalmente fez uma alteração (como adicionar um arquivo) no commit anterior e não deseja mais que o histórico dessa alteração exista, basta alterar o commit anterior para remover o arquivo:

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

Solução 2: reinicialização total (possivelmente mais uma rebase)

Como a solução 1, se você apenas deseja se livrar do seu commit anterior, também tem a opção de fazer uma redefinição definitiva do pai:

git reset --hard HEAD^

Esse comando reinicializará sua ramificação para o compromisso pai anterior .

No entanto , se, como o pôster original, você fez várias confirmações após a confirmação em que deseja desfazer a alteração, ainda é possível usar redefinições rígidas para modificá-la, mas isso também envolve o uso de uma nova base. Aqui estão as etapas que você pode usar para alterar um commit mais adiante no histórico:

# 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}

Solução 3: Rebase não interativo

Isso funcionará se você quiser remover completamente uma confirmação do histórico:

# 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}

Solução 4: rebotes interativos

Esta solução permitirá que você realize o mesmo que as soluções nº 2 e nº 3, ou seja, modifique ou remova confirmações mais antigas do que a confirmação imediatamente anterior, de modo que a solução que você escolher usar depende de você. Os rebotes interativos não são adequados para refazer centenas de confirmações, por motivos de desempenho, portanto, eu usaria os rebotes não interativos ou a solução de ramificação do filtro (veja abaixo) nesses tipos de situações.

Para iniciar o rebase interativo, use o seguinte:

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

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

Isso fará com que o git rebobine o histórico de consolidação novamente para o pai da consolidação que você deseja modificar ou remover. Em seguida, ele apresentará uma lista dos commit rebobinados na ordem inversa, independentemente do editor que o git estiver configurado para usar (este é o Vim por padrão):

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)

A confirmação que você deseja modificar ou remover estará no topo desta lista. Para removê-lo, basta excluir sua linha na lista. Caso contrário, substitua "pegar" com "editar" no 1 st linha, assim:

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

Em seguida, insira git rebase --continue. Se você optou por remover o commit por completo, então tudo o que você precisa fazer (além da verificação, consulte a etapa final desta solução). Se, por outro lado, você quiser modificar o commit, o git reaplicará o commit e pausará o 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

Neste ponto, você pode remover o arquivo e alterar a confirmação e, em seguida, continue a nova atualização:

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

É isso aí. Como etapa final, se você modificou a confirmação ou a removeu completamente, é sempre uma boa ideia verificar se nenhuma outra alteração inesperada foi feita em sua ramificação, diferenciando-a do estado anterior à rebase:

git diff master@{1}

Solução 5: filtrando ramificações

Por fim, essa solução é melhor se você deseja eliminar completamente todos os vestígios da existência de um arquivo do histórico, e nenhuma das outras soluções é adequada à tarefa.

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

Isso removerá <file>de todas as confirmações, iniciando na confirmação raiz. Se, em vez disso, você deseja apenas reescrever o intervalo de confirmação HEAD~5..HEAD, pode passar isso como um argumento adicional para filter-branch, como apontado nesta resposta :

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

Novamente, após a filter-branchconclusão, geralmente é uma boa idéia verificar se não há outras alterações inesperadas diferenciando sua ramificação com seu estado anterior antes da operação de filtragem:

git diff master@{1}

Alternativa para ramificação de filtro: BFG Repo Cleaner

Ouvi dizer que a ferramenta BFG Repo Cleaner é executada mais rapidamente do que git filter-branch, portanto, convém verificar isso como uma opção. É até mencionado oficialmente na documentação da ramificação do filtro como uma alternativa viável:

O git-filter-branch permite que você faça reescritas complexas com scripts do shell do seu histórico do Git, mas você provavelmente não precisará dessa flexibilidade se estiver simplesmente removendo dados indesejados, como arquivos grandes ou senhas. Para essas operações, você pode considerar o BFG Repo-Cleaner , uma alternativa baseada em JVM para git-filter-branch, geralmente pelo menos 10 a 50x mais rápido para esses casos de uso e com características bastante diferentes:

  • Qualquer versão específica de um arquivo é limpa exatamente uma vez . O BFG, diferentemente do git-filter-branch, não oferece a oportunidade de manipular um arquivo de maneira diferente, com base em onde ou quando ele foi confirmado no seu histórico. Essa restrição oferece os principais benefícios de desempenho do The BFG e é adequada para a tarefa de limpeza de dados inválidos - você não se importa onde estão os dados incorretos, apenas deseja que eles sejam removidos .

  • Por padrão, o BFG aproveita ao máximo as máquinas com vários núcleos, limpando as árvores de arquivos de confirmação em paralelo. O git-filter-branch limpa as confirmações sequencialmente (isto é, de uma única thread), embora seja possível escrever filtros que incluam seu próprio paralelismo, nos scripts executados em cada confirmação.

  • As opções de comando são muito mais restritiva do que git branch-filtro, e dedicou apenas às tarefas de remoção indesejada data- por exemplo: --strip-blobs-bigger-than 1M.

Recursos adicionais

  1. Pro Git § 6.4 Git Tools - Reescrevendo o Histórico .
  2. Manual do git-filter-branch (1) .
  3. Manual do git-commit (1) .
  4. Manual do git-reset (1) .
  5. Manual do git-rebase (1) .
  6. O BFG Repo Cleaner (veja também esta resposta do próprio criador ).
Comunidade
fonte
Será que filter-branchcausa recalcular de hashes? Se uma equipe trabalha com um repositório em que um arquivo grande deve ser filtrado, como eles fazem isso para que todos terminem com o mesmo estado do repositório?
precisa saber é o seguinte
@YakovL. Tudo recalcula os hashes. Na verdade, os commits são imutáveis. Ele cria uma história totalmente nova e move o ponteiro da ramificação para ela. A única maneira de garantir que todos tenham o mesmo histórico é uma reinicialização total.
Físico louco
118

Se você não cometeu nada desde então, apenas git rmo arquivo e git commit --amend.

Se você tem

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

passará por cada alteração de merge-pointpara HEAD, excluir filename.orig e reescrever a alteração. Usar --ignore-unmatchsignifica que o comando não falhará se, por algum motivo, filename.orig estiver ausente de uma alteração. Essa é a maneira recomendada na seção Exemplos na página do manual git-filter-branch .

Nota para usuários do Windows: O caminho do arquivo deve usar barras

Schwern
fonte
3
Obrigado! O git filter-branch funcionou para mim, onde o exemplo de rebase fornecido como resposta não funcionou: as etapas pareciam funcionar, mas o envio falhou. Deu um puxão e depois empurrou com sucesso, mas o arquivo ainda estava por aí. Tentei refazer as etapas de rebase e depois tudo ficou confuso com conflitos de mesclagem. No entanto, usei um comando filter-branch um pouco diferente, o "Um método aprimorado" fornecido aqui: github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index- filtro 'git update-index --remove filename' <introduction-revision-sha1>
..HEAD
1
Não tenho certeza de qual é o método aprimorado . A documentação oficial do Git git-filter-branchparece dar a primeira.
Wernight
5
Confira zyxware.com/articles/4027/... acho que é a solução mais completa e direta que envolvefilter-branch
leontalbot
2
@atomicules, se você tentar enviar o repositório local para o remoto, o git insistirá em retirá-lo primeiro, porque há alterações que você não possui localmente. Você pode usar o --force flag para enviar para o controle remoto - ele removerá completamente os arquivos de lá. Mas tome cuidado, certifique-se de não forçar a substituição de algo que não seja apenas os arquivos.
Sol0mka
1
Lembre-se de usar "e não 'ao usar o Windows, ou você receberá um erro de "revisão ruim", sem ajuda.
CZ
49

Esta é a melhor maneira:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Apenas certifique-se de fazer backup das cópias dos arquivos primeiro.

EDITAR

Infelizmente, a edição de Neon foi rejeitada durante a revisão.
Veja a publicação de Neons abaixo, pode conter informações úteis!


Por exemplo, para remover todos os *.gzarquivos acidentalmente confirmados no repositório 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

Isso ainda não funcionou para mim? (Atualmente, estou na versão 1.7.6.1 do git)

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

Não sei por que, já que eu só tinha UM ramo principal. Enfim, finalmente consegui meu repositório git realmente limpo, inserindo um novo repositório vazio e vazio, por exemplo,

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

(sim!)

Então eu clonei isso em um novo diretório e movi a pasta .git para este. por exemplo

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

(sim! finalmente limpo!)

Depois de verificar se está tudo bem, você pode excluir os diretórios ../large_dot_gite ../tmpdir(talvez daqui a algumas semanas ou meses, apenas no caso ...)

Darren
fonte
1
Isso funcionou para mim antes do "Isso ainda não funcionou para mim?" comentário
shadi
Ótima resposta, mas sugiro adicionar --prune-emptyao comando filter-branch.
ideasman42
27

Reescrever o histórico do Git exige a alteração de todos os IDs de confirmação afetados, para que todos que estão trabalhando no projeto precisem excluir suas cópias antigas do repositório e fazer um novo clone depois de limpar o histórico. Quanto mais pessoas incomodar, mais você precisará de um bom motivo para fazê-lo - seu arquivo supérfluo não está realmente causando um problema, mas se você estiver trabalhando no projeto, poderá limpar o histórico do Git, se quiser para!

Para torná-lo o mais fácil possível, recomendo o uso do BFG Repo-Cleaner , uma alternativa mais simples e rápida para o git-filter-branchprojetado especificamente para remover arquivos do histórico do Git. Uma maneira de facilitar a sua vida aqui é que ele realmente lida com todos os árbitros por padrão (todas as tags, ramificações etc.), mas também é 10 - 50x mais rápido.

Você deve seguir cuidadosamente as etapas aqui: http://rtyley.github.com/bfg-repo-cleaner/#usage - mas o principal é exatamente isso: faça o download do jar BFG (requer Java 6 ou superior) e execute este comando :

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

Todo o histórico do repositório será verificado e qualquer arquivo nomeado filename.orig(que não esteja na sua confirmação mais recente ) será removido. Isso é consideravelmente mais fácil do que usar git-filter-branchpara fazer a mesma coisa!

Divulgação completa: sou o autor do BFG Repo-Cleaner.

Roberto Tyley
fonte
4
Essa é uma excelente ferramenta: um único comando, produz uma saída muito clara e fornece um arquivo de log que corresponde a cada confirmação antiga da nova . Não gosto de instalar Java, mas vale a pena.
Mikemaccana
Essa é a única coisa que funcionou para mim, mas é assim porque eu não estava trabalhando com o git filter-branch corretamente. :-)
Kevin LaBranche 16/03
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
fonte
1
Embora todas as respostas pareçam estar na trilha de ramificação de filtro, esta destaca como limpar TODAS as ramificações em seu histórico.
Cameron Lowell Palmer
4

Apenas para adicionar isso à solução de Charles Bailey, eu apenas usei um git rebase -i para remover arquivos indesejados de um commit anterior e funcionou como um encanto. Os passos:

# 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
fonte
4

A maneira mais simples que encontrei foi sugerida por leontalbot(como comentário), que é um post publicado pela Anoopjohn . Eu acho que vale o seu próprio espaço como resposta:

(Eu converti para um 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."

Todos os créditos vão para Annopjohne leontalbotpara apontar.

NOTA

Esteja ciente de que o script não inclui validações; verifique se você não comete erros e se possui um backup caso algo dê errado. Funcionou para mim, mas pode não funcionar na sua situação. USE-O COM CUIDADO (siga o link se quiser saber o que está acontecendo).

lepe
fonte
3

Definitivamente, git filter-branché o caminho a percorrer.

Infelizmente, isso não será suficiente para remover completamente filename.origdo seu repositório, pois ainda pode ser referenciado por tags, entradas de reflog, controles remotos e assim por diante.

Eu recomendo remover todas essas referências também e chamar o coletor de lixo. Você pode usar o git forget-blobscript deste site para fazer tudo isso em uma única etapa.

git forget-blob filename.orig

nachoparker
fonte
1

Se é o último commit que você deseja limpar, tentei com a versão 2.14.3 do 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
fonte
git reflog expire --expire=now --all; git gc --prune=nowé uma coisa muito ruim de se fazer. A menos que você está ficando sem espaço em disco, deixe lixo git recolher esses commits depois de algumas semanas
avmohan
Obrigado por apontar isso. Meu repositório foi enviado com muitos arquivos binários grandes e o backup é feito inteiramente todas as noites. Então, eu só queria que cada bit de fora;)
clarkttfu
-1

Você também pode usar:

git reset HEAD file/path

Paolo Granada Lim
fonte
3
Se o arquivo foi adicionado a uma confirmação, isso nem remove o arquivo do índice, apenas redefine o índice para a versão HEAD do arquivo.
CB Bailey