Como reverter vários commits git?

984

Eu tenho um repositório git que se parece com isso:

A -> B -> C -> D -> HEAD

Quero que o chefe do ramo aponte para A, ou seja, quero que B, C, D e HEAD desapareçam e quero que o cabeçote seja sinônimo de A.

Parece que eu posso tentar fazer uma nova reformulação (não se aplica, desde que eu coloquei alterações no meio) ou reverter. Mas como reverto várias confirmações? Eu reverto um de cada vez? A ordem é importante?

Conta
fonte
3
Se você quiser apenas redefinir o controle remoto, poderá vencê-lo com qualquer coisa! Mas vamos usar o quarto commit atrás: git push -f HEAD~4:master(assumindo que o ramo remoto seja mestre). Sim, você pode enviar qualquer commit assim.
U0b34a0f6ae 23/09/09
21
Se as pessoas puxaram, você deve fazer um commit que reverta as alterações usando git revert.
Jakub Narębski 23/09/09
1
Use git show de CABEÇA ~ 4 para garantir que você está empurrando a um direito de controle remoto
Matt Freeman
1
Possível duplicata de Como desfazer o último commit (s) no Git?
Jim caiu
5
"A ordem é importante?" Sim, se as confirmações afetarem as mesmas linhas nos mesmos arquivos. Então você deve começar a reverter a consolidação mais recente e voltar ao trabalho.
Avandeursen

Respostas:

1336

Expandindo o que escrevi em um comentário

A regra geral é que você não deve reescrever (alterar) o histórico que você publicou, porque alguém pode ter baseado seu trabalho nele. Se você reescrever (alterar) o histórico, terá problemas ao mesclar as alterações e atualizar para elas.

Portanto, a solução é criar um novo commit que reverta as alterações das quais você deseja se livrar. Você pode fazer isso usando o comando git revert .

Você tem a seguinte situação:

A <- B <- C <- D <- mestre <- CABEÇA

(as setas aqui se referem à direção do ponteiro: a referência "pai" no caso de confirmações, a confirmação superior no caso de cabeçalho de ramificação (ref de ramificação) e o nome da ramificação no caso de referência HEAD).

O que você precisa criar é o seguinte:

A <- B <- C <- D <- [(BCD) ^ - 1] <- mestre <- CABEÇA

onde "[(BCD) ^ - 1]" significa o commit que reverte as alterações nos commits B, C, D. A matemática nos diz que (BCD) ^ - 1 = D ^ -1 C ^ -1 B ^ -1, então você pode obter a situação necessária usando os seguintes comandos:

$ git revert --no-commit D
$ git revert --no-commit C
$ git revert --no-commit B
$ git commit -m "the commit message"

A solução alternativa seria verificar o conteúdo da confirmação A e confirmar este estado:

$ git checkout -f A -- .
$ git commit -a

Então você teria a seguinte situação:

A <- B <- C <- D <- A '<- mestre <- CABEÇA

A confirmação A 'tem o mesmo conteúdo que a confirmação A, mas é uma confirmação diferente (mensagem de confirmação, pais, data de confirmação).

A solução de Jeff Ferland, modificada por Charles Bailey, baseia-se na mesma idéia, mas usa git reset :

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit
Jakub Narębski
fonte
40
Se você adicionou arquivos em B, C ou D., git checkout -f A -- .não os excluirá, precisará fazê-lo manualmente. Eu apliquei essa estratégia agora, obrigado Jakub
oma 31/03
18
Essas soluções não são equivalentes. O primeiro não exclui arquivos recém-criados.
M33lky
10
@ Jerry: git checkout foopode significar ramo de checkout foo(alternar para branch) ou arquivo de checkout foo (do índice). --é usado para desambiguar, por exemplo, git checkout -- fooé sempre sobre arquivo.
Jakub Narębski 23/01
87
Além de ótima resposta. Este taquigrafia funciona para mimgit revert --no-commit D C B
welldan97
9
@ welldan97: Obrigado por um comentário. Ao escrever esta resposta git revertnão aceitou vários commits; é uma adição bastante nova.
Jakub Narębski
248

Maneira limpa que eu achei útil

git revert --no-commit HEAD~3..

Este comando reverte os últimos 3 commit com apenas um commit.

Também não reescreve o histórico.

mergulho profundo
fonte
16
Esta é a resposta mais simples e melhor #
Julien Deniau
3
@JohnLittle encena as mudanças. git commita partir daí realmente fará o commit.
x1a4
14
Isso não funcionará se algumas confirmações forem confirmadas por mesclagem.
MegaManX
5
O que os dois pontos no final fazem?
cardamomo
5
@cardamom Especifica um intervalo. HEAD~3..é o mesmo queHEAD~3..HEAD
Toine H
238

Para fazer isso, basta usar o comando revert , especificando o intervalo de confirmações que você deseja que seja revertido.

Levando em conta o seu exemplo, você teria que fazer isso (supondo que você esteja no ramo 'master'):

git revert master~3..master

Isso criará um novo commit no seu local com o commit inverso de B, C e D (o que significa que ele desfará as alterações introduzidas por esses commit):

A <- B <- C <- D <- BCD' <- HEAD
Vencedor
fonte
129
git revert --no-commit HEAD~2..é uma maneira um pouco mais idiomática de fazer isso. Se você estiver no ramo principal, não será necessário especificar o mestre novamente. A --no-commitopção permite ao git tentar reverter todas as confirmações de uma só vez, em vez de encher o histórico com várias revert commit ...mensagens (assumindo que é isso que você deseja).
Kubi
6
@ Victor Consertei seu alcance de confirmação. O início do intervalo é exclusivo, o que significa que não está incluído. Portanto, se você deseja reverter os últimos 3 commit, você precisa iniciar o intervalo a partir do pai do terceiro commit, ie master~3.
2
@kubi não há como incluir os SHAs em uma mensagem de confirmação, usando uma única confirmação (seu método, mas sem ter que inserir manualmente as confirmações que foram revertidas)?
Chris S
@ ChrisisS Meu primeiro pensamento seria não usar --no-commit(para que você obtenha um commit separado para cada reversão) e, em seguida, junte todos eles juntos em uma rebase interativa. A mensagem de confirmação combinada conterá todos os SHAs e você poderá organizá-los da maneira que desejar, usando seu editor de mensagens de confirmação favorito.
Radon Rosborough
71

Semelhante à resposta de Jakub, isso permite que você selecione facilmente confirmações consecutivas para reverter.

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'
konyak
fonte
9
Sua solução funcionou bem para mim, mas com uma ligeira modificação. Se tivermos esse caso Z -> A -> B -> C -> D -> HEAD e se eu quiser retornar ao estado A, então, estranhamente, eu teria que executar git revert - no-commit Z .. HEAD
Bogdan
3
Concordo com @Bogdan, a gama revert é assim: SHA_TO_REVERT_TO..HEAD
Vadym Tyemirov
11
O intervalo está errado. Deveria ser B^..HEAD, caso contrário, B é excluído.
Tessus # 28/18
3
Concorde com @tessus, então a coisa certa a fazer seria: git revert --no-commit B^..HEADougit revert --no-commit A..HEAD
Yoho
64
git reset --hard a
git reset --mixed d
git commit

Isso funcionará como uma reversão para todos eles de uma vez. Dê uma boa mensagem de confirmação.

Jeff Ferland
fonte
4
Se ele quiser HEADse parecer A, provavelmente deseja que o índice corresponda, portanto git reset --soft Dé provavelmente mais apropriado.
CB Bailey
2
--soft resetting não move o índice; portanto, quando ele confirma, parece que o commit veio diretamente de um e não de D. Isso faria com que o ramo se dividisse. --mixed deixa as alterações, mas move o ponteiro do índice, para que D se torne o commit pai.
9119 Jeff Ferland
5
Sim, acho que o git reset --keep é exatamente o que tenho acima. Foi lançada na versão 1.7.1, lançada em abril de 2010, então a resposta não estava disponível naquele momento.
quer
git checkout Aentão git commitacima não funcionou para mim, mas esta resposta funcionou.
SimplGy 29/08
Por que é git reset --mixed Dnecessário? Especificamente porque reset? É porque, sem redefinir para D, que HEAD apontaria para A, fazendo com que B, C e D fossem "pendurados" e coletados de lixo - o que não é o que ele deseja? Mas então porque --mixed? Você já respondeu " --softredefinir não move o índice ..." Então, movendo o índice, isso significa que o índice conterá as alterações de D, enquanto o Working Directory conterá as alterações de A - desta maneira a git statusou git diff(que compara Index [D] a Diretório de Trabalho [A]) mostrará a substância; esse usuário está indo de D de volta para A?
The Red Pea
39

Primeiro, verifique se a sua cópia de trabalho não está modificada. Então:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

e depois apenas confirmar. Não se esqueça de documentar qual é o motivo da reversão.

mateusz.fiolka
fonte
1
Trabalhou para mim! Ocorreu um problema com as alterações obsoletas do ramo de recurso feitas no ramo de desenvolvimento (algumas correções), de modo que o ramo do recurso teve que sobrescrever todas as alterações feitas no desenvolvimento (incluindo a exclusão de alguns arquivos).
silentser
1
Não funciona com arquivos binários:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux
2
Essa é uma solução muito mais flexível que a resposta aceita. Obrigado!
Brian Kung
2
Re: uso de arquivos bináriosgit diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
weinerk
2
Isso funcionará mesmo se você desejar reverter um intervalo de confirmações que contém confirmações de mesclagem. Ao usar git revert A..Zvocê obteriaerror: commit X is a merge but no -m option was given.
Juliusz Gonera
35

Estou tão frustrado que esta pergunta não pode ser respondida. Todas as outras questões estão relacionadas a como reverter corretamente e preservar a história. Esta pergunta diz "Eu quero que o chefe do ramo aponte para A, ou seja, quero que B, C, D e HEAD desapareçam e quero que o cabeçote seja sinônimo de A."

git checkout <branch_name>
git reset --hard <commit Hash for A>
git push -f

Eu aprendi muito lendo o post de Jakub, mas um cara da empresa (com acesso para enviar para o nosso ramo de "testes" sem solicitação de solicitação) fez 5 commits ruins, tentando consertar e consertar um erro que ele cometeu há 5 commits atrás. Não apenas isso, mas uma ou duas solicitações pull foram aceitas, que agora eram ruins. Então esqueça, eu encontrei o último bom commit (abc1234) e executei o script básico:

git checkout testing
git reset --hard abc1234
git push -f

Eu disse aos outros 5 funcionários que trabalhavam neste repositório que é melhor anotarem suas alterações nas últimas horas e limpar / re-ramificar nos testes mais recentes. Fim da história.

Suamere
fonte
2
Eu não tinha publicado os commits, então essa era a resposta que eu precisava. Obrigado, @Suamere.
Tom Barron
A melhor maneira de fazer isso é git push --force-with-lease, que reescreverá o histórico apenas se ninguém mais se comprometer com a ramificação depois ou dentro do intervalo das confirmações para vaporizar. Se outras pessoas usaram o ramo, seu histórico nunca deve ser reescrito e o commit deve ser visivelmente revertido.
Frandroid
1
@frandroid "a história nunca deve ser reescrita", apenas os acordos sith em absolutos. A questão desse tópico e o ponto da minha resposta é exatamente que, para um cenário específico, toda a história deve ser apagada.
Suamere
@ Suamere Claro, essa é a questão. Mas, como sua resposta menciona o que você tinha a dizer aos outros, há potencial para problemas. Por experiência pessoal, o push -f pode atrapalhar sua base de código se outras pessoas se comprometerem após o que você está tentando apagar. --force-with-lease alcança o mesmo resultado, exceto que ele salva sua bunda se você estava prestes a atrapalhar seu repo. Por que arriscar? Se --force-with-lease falhar, você poderá ver qual confirmação interfere, avaliar adequadamente, ajustar e tentar novamente.
Frandroid
1
@Suamere Thank you! Concordo que a pergunta afirma claramente que deseja reescrever a história. Estou na mesma situação que você e estou adivinhando o OP, em que alguém fez dezenas de reverências feias e confirmações e reviravoltas estranhas por acidente (enquanto eu estava de férias) e o estado precisa ser revertido. De qualquer forma, junto com um bom aviso de saúde, essa deve ser a resposta aceita.
Lee Richardson
9

Esta é uma expansão de uma das soluções fornecidas na resposta de Jakub

Fui confrontado com uma situação em que as confirmações necessárias para reverter eram um pouco complexas, com várias confirmações sendo confirmadas por mesclagem e eu precisava evitar reescrever o histórico. Não pude usar uma série de git revertcomandos porque acabei encontrando conflitos entre as alterações de reversão sendo adicionadas. Acabei seguindo os seguintes passos.

Primeiro, verifique o conteúdo da confirmação do destino, deixando HEAD na ponta do ramo:

$ git checkout -f <target-commit> -- .

(O - garante que <target-commit>seja interpretado como uma confirmação em vez de um arquivo; o. Refere-se ao diretório atual.)

Em seguida, determine quais arquivos foram adicionados nas confirmações sendo revertidas e, portanto, precisam ser excluídos:

$ git diff --name-status --cached <target-commit>

Os arquivos adicionados devem aparecer com um "A" no início da linha e não deve haver outras diferenças. Agora, se algum arquivo precisar ser removido, prepare esses arquivos para remoção:

$ git rm <filespec>[ <filespec> ...]

Por fim, confirme a reversão:

$ git commit -m 'revert to <target-commit>'

Se desejar, verifique se estamos de volta ao estado desejado:

$git diff <target-commit> <current-commit>

Não deve haver diferenças.

Warren Dew
fonte
Tem certeza de que pode gitHEAD apenas com a ponta do galho?
Suamere 8/11
2
Essa foi uma solução muito melhor para mim, pois na minha eu tinha consolidação de fusão.
sovemp
3

A maneira mais fácil de reverter um grupo de confirmações no repositório compartilhado (que as pessoas usam e você deseja preservar o histórico) é usar git revertem conjunto com o git rev-list. O último fornecerá uma lista de confirmações, o primeiro fará a reversão em si.

Existem duas maneiras de fazer isso. Se você deseja reverter várias confirmações em uma única confirmação, use:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert -n $i; done

isso reverterá um grupo de confirmações necessárias, mas deixe todas as alterações na sua árvore de trabalho; você deve confirmar todas elas normalmente.

Outra opção é ter uma única confirmação por alteração revertida:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-edit -s $i; done

Por exemplo, se você tiver uma árvore de confirmação como

 o---o---o---o---o---o--->    
fff eee ddd ccc bbb aaa

para reverter as alterações de eee para bbb , execute

for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done
Ruslan Kabalin
fonte
apenas usei isso. Obrigado!
Ran Biron
2

Nenhum deles funcionou para mim, então eu tinha três confirmações para reverter (as últimas três confirmações), então fiz:

git revert HEAD
git revert HEAD~2
git revert HEAD~4
git rebase -i HEAD~3 # pick, squash, squash

Trabalhou como um encanto :)

Dorian
fonte
2
Essa é uma opção viável apenas se suas alterações ainda não forem enviadas.
Kboom 18/0318
0

Na minha opinião, uma maneira muito fácil e limpa pode ser:

volte para A

git checkout -f A

aponte a cabeça do mestre para o estado atual

git symbolic-ref HEAD refs/heads/master

Salve 

git commit
nulll
fonte
1
Você pode explicar o motivo da votação baixa?
nulll 13/02/19
Isso funciona bem. Qual é o sentido de dar voto negativo a essa resposta útil ou alguém pode explicar qual é a melhor prática?
Levent Divilioglu 01/07/19
É o mesmo que git checkout master; git reset --hard A? Ou, se não, você poderia explicar um pouco mais sobre o que isso faz?
MM
simplesmente funciona, mas o simbólico-ref HEAD parece não ser um comando "seguro"
Sérgio
err Eu não quero mestre correção, quero corrigir um ramo
Sérgio
-6

Se você quiser reverter temporariamente as confirmações de um recurso, poderá usar a série de comandos a seguir.

Aqui está como funciona

git log --pretty = on-line | grep 'feature_name' | cut -d '' -f1 | xargs -n1 git revert --no-edit

Ajit Singh
fonte