Rode git reset --hard num commit que seu time já deu pull nele, e seu histórico passa a discordar do deles. Rode git revert num typo que você nem chegou a dar push nele, e acaba de acrescentar um commit inútil ao log. O Git tem quatro comandos para desfazer mudanças, restore, reset, commit --amend e revert, e eles não são intercambiáveis. Escolher o errado ou não faz nada, ou faz muito mais do que você queria.

O que decide qual usar é onde a mudança está agora: na working tree, no index, num commit que só você tem, ou num commit que você já deu push.

Onde uma mudança fica no Git: working tree, index e histórico

Todo comando de desfazer atua em uma ou mais de três camadas:

  • Working tree: os arquivos no disco, enquanto você os edita.
  • Index: o snapshot que você monta com git add, esperando para se tornar o próximo commit.
  • Histórico: os commits já registrados, alcançáveis a partir do HEAD.

Uma mudança começa na working tree, passa para o index com git add, e vai para o histórico com git commit. Desfazê-la significa percorrer o caminho de volta pelas mesmas camadas. Quanto mais longe ela já foi, mais deliberado precisa ser o comando.

1
2
working tree  --add-->  index  --commit-->  histórico
   restore              restore --staged      reset / revert / amend

Como descartar mudanças fora do index com git restore

Você editou config.py, quebrou o arquivo, e quer a versão do Git de volta:

1
git restore config.py

Isso sobrescreve o arquivo na sua working tree com a versão no index, a mesma do HEAD a menos que você já tenha dado git add nele. A edição desaparece. Ela não vai para nenhum stash, e nenhum comando do Git a traz de volta. Para descartar todas as mudanças fora do index de uma vez:

1
git restore .

git restore substitui o antigo git checkout -- <file>. Os dois ainda funcionam, mas restore existe para que “me devolva este arquivo” deixe de compartilhar comando com “troque de branch”.

Como tirar um arquivo do index sem perder suas edições

Você rodou git add em três arquivos, e depois percebeu que um deles não pertence a este commit. A edição em si está correta:

1
git restore --staged migration.sql

migration.sql sai do index e volta ao estado “modificado, fora do index”. O arquivo no disco não é tocado. É o mesmo que o antigo git reset HEAD -- migration.sql, e é o único ponto em que reset e restore se sobrepõem: reset com um pathspec só afeta o index, que é exatamente o que restore --staged faz.

Como desfazer um commit com git reset: soft, mixed e hard

git restore não consegue tocar em um commit. Para isso você precisa do git reset, e a flag controla o quanto ele desfaz:

FlagWorking treeIndexHistórico (HEAD)
--softsem alteraçãosem alteraçãomovido
--mixed (padrão)sem alteraçãoresetado para o alvomovido
--hardresetado para o alvoresetado para o alvomovido

Os três movem o ponteiro do branch. A diferença é o quanto do seu trabalho vai junto com ele para trás.

--soft rebobina os commits, mas deixa as mudanças deles no index. Isso faz dele o jeito mais rápido de juntar os últimos três commits em um só:

1
2
git reset --soft HEAD~3
git commit -m "Add payment retry logic"

--mixed é o que você obtém com um git reset HEAD~1 simples. Ele rebobina o commit e devolve as mudanças dele para a working tree fora do index, para você poder dividi-las em commits menores ou descartar parte delas.

--hard descarta o diff por completo. A working tree termina como se o commit nunca tivesse existido:

1
git reset --hard HEAD~1

Não há nenhuma confirmação. Os commits que você passou por cima ainda podem ser encontrados pelo git reflog (veja abaixo), mas qualquer edição não commitada que estivesse na sua working tree naquele momento se perde para sempre, então confira o git status antes. E nunca rode isso em commits que outra pessoa já deu pull neles: mover o branch para trás num branch compartilhado deixa seu histórico e o dela discordando sobre o que aconteceu, o mesmo problema por trás da regra de ouro do rebase.

Como corrigir o último commit com git commit –amend

Se o erro está no commit mais recente, um reset é exagero. --amend substitui o commit da ponta em vez de rebobinar além dele:

1
git commit --amend -m "Fix payment retry logic"

Adicione mais mudanças ao index antes e elas se juntam ao mesmo commit, sobre o mesmo pai:

1
2
git add retry.py
git commit --amend --no-edit

O commit emendado é um objeto novo, com um hash novo, igual a um commit rebaseado. A regra é a mesma do rebase: só dê amend no que você ainda não deu push. Dê amend num commit que já está num branch compartilhado e todo mundo que deu pull nele vai ter que reconciliar dois commits que afirmam ser a mesma mudança.

Como desfazer um commit que já recebeu push com git revert

Uma vez que um commit está num branch compartilhado, reset e --amend saem de cena. Reescrever um histórico que outras pessoas já buscaram é a receita para branches divergentes e commits duplicados. git revert toma o caminho contrário: deixa o commit problemático onde está e acrescenta um novo que faz o oposto.

1
git revert a1b2c3d

O Git abre um editor para a mensagem do commit de revert, e depois a registra:

1
2
3
Revert "Add aggressive cache invalidation"

This reverts commit a1b2c3d4e5f6789a1b2c3d4e5f6789a1b2c3d4.

O histórico agora guarda tanto o commit original quanto o seu inverso. Nada foi reescrito, então um git pull simples em qualquer outro clone já traz a correção. Para reverter um intervalo como um único commit:

1
2
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert the last three deploy changes"

--no-commit (-n) aplica o inverso de cada commit na working tree e no index sem commitar, de forma que três reverts virem um commit só em vez de três. Se um revert entra em conflito com mudanças feitas depois, o Git para do mesmo jeito que no meio de um merge: resolva os marcadores de conflito, dê git add no arquivo, e rode git revert --continue.

Então “desfazer um push” quer dizer: revert o commit, depois dê push no revert. Nunca force um push de histórico reescrito para um branch do qual outras pessoas dão pull.

Como se recuperar de um reset malfeito com git reflog

Depois de um reset --hard, os commits desaparecem do git log. Eles continuam existindo. O git reflog lista cada posição que o HEAD já ocupou na sua máquina, incluindo a de logo antes do reset:

1
git reflog
1
2
f3a9c21 HEAD@{0}: reset: moving to HEAD~1
7d4e1b8 HEAD@{1}: commit: Add payment retry logic

git reset --hard HEAD@{1} te devolve ao 7d4e1b8. Por quanto tempo as entradas do reflog ficam disponíveis, e por que o reflog é local, está na seção de recuperação de Git Rebase vs Merge. Funciona do mesmo jeito, qualquer que tenha sido o comando que moveu o HEAD.

O reflog te devolve a onde você estava. Ele não diz qual commit mais antigo foi o último bom. Para isso, o git bisect reduz centenas de commits ao único que introduziu o bug.

Git reset vs revert vs restore: o que usar em cada caso

A mudança está…Comando
Editada, fora do index, e você quer que ela desapareçagit restore <file>
No index, e você quer tirá-la dali mantendo a ediçãogit restore --staged <file>
Commitada, sem push, e é o último commitgit commit --amend
Commitada, sem push, e você quer voltar mais longegit reset --soft / --mixed / --hard HEAD~n
Commitada e já recebeu push ou está compartilhadagit revert <commit>
Perdida depois de um reset --hard do qual você se arrependegit reflog + git reset --hard HEAD@{n}

Para qualquer coisa já commitada, uma pergunta decide a linha: mais alguém já viu esse commit? Se não, reset ou amend, e siga em frente. Se já viu, revert. Custa um commit extra no log, e nenhum colega precisa desembaraçar um histórico que não bate mais com o dele.

Artigos relacionados