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.
| |
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:
| |
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:
| |
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:
| |
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:
| Flag | Working tree | Index | Histórico (HEAD) |
|---|---|---|---|
--soft | sem alteração | sem alteração | movido |
--mixed (padrão) | sem alteração | resetado para o alvo | movido |
--hard | resetado para o alvo | resetado para o alvo | movido |
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ó:
| |
--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:
| |
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:
| |
Adicione mais mudanças ao index antes e elas se juntam ao mesmo commit, sobre o mesmo pai:
| |
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.
| |
O Git abre um editor para a mensagem do commit de revert, e depois a registra:
| |
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:
| |
--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:
| |
| |
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ça | git restore <file> |
| No index, e você quer tirá-la dali mantendo a edição | git restore --staged <file> |
| Commitada, sem push, e é o último commit | git commit --amend |
| Commitada, sem push, e você quer voltar mais longe | git reset --soft / --mixed / --hard HEAD~n |
| Commitada e já recebeu push ou está compartilhada | git revert <commit> |
Perdida depois de um reset --hard do qual você se arrepende | git 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.