Desfazendo alterações e commits no Git


Sempre há algo que precisa ser desfeito. Você digitou uma edição que não pretendia, tentou um conserto que piorou as coisas, ou fez um commit de algo que preferiria que ninguém visse. O Git mantém um registro de quase todas as versões do seu projeto que já viu, então a maioria dos erros é mais reversível do que parecem no momento. Qual ferramenta o traz de volta depende de quão longe o erro viajou: ainda na sua editor, já preparado, já confirmado, ou já enviado para algum lugar onde um colega pode vê-lo. Este capítulo passa por cada estágio nessa ordem.
O que são push e pull?
Push envia seus commits para uma cópia compartilhada do repositório da qual os colegas também trabalham, e pull traz seus commits para você. Remotes e GitHub cobre ambos completamente; para este capítulo, "enviado" significa que o commit saiu da sua máquina.
Descartando uma alteração antes de fazer o commit
Digamos que você ainda está reformatando styles.css em weather-app de o loop de commit, e o novo layout se mostra uma má ideia. Você não o preparou, não fez o commit. Você quer o arquivo de volta do jeito que estava, nada mais. O comando para exatamente esse trabalho é git restore:
$ git status
On branch main
Changes not staged for commit:
modified: styles.css
$ git restore styles.css
$ git status
On branch main
nothing to commit, working tree cleangit restore <file> descarta uma edição não confirmada e coloca o arquivo de volta ao seu último estado salvo. Se você já tivesse preparado a alteração com git add antes de mudar de ideia, plain git restore não a tocará, já que o arquivo agora está na área de preparação ao invés de apenas no seu diretório de trabalho. Remova-a da preparação primeiro, depois restaure-a:
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.cssDois movimentos separados para duas áreas separadas: git restore --staged move um arquivo para fora da área de preparação, e git restore por si só descarta a edição no seu diretório de trabalho. git restore só alcança trabalho não confirmado, uma vez que uma alteração é confirmada, um comando diferente assume.
git restore <file> descarta uma edição que você não confirmou e coloca o arquivo de volta do jeito que estava. Se você já preparou a alteração com git add, remova-a da preparação primeiro com git restore --staged <file>, depois restaure o arquivo se quiser que a edição desapareça também. Isso só funciona em alterações que você ainda não confirmou, então não pode resgatar um erro que você já salvou. Guardando trabalho com git stash
Às vezes não há erro a corrigir, apenas timing ruim. Você está no meio da edição em styles.css e um colega precisa de você em outro branch agora mesmo, mas você não está pronto para fazer commit do estilo inacabado. git stash guarda suas alterações e lhe entrega um diretório de trabalho limpo para que você possa se afastar e voltar mais tarde.
$ git status
On branch main
Changes not staged for commit:
modified: styles.css
$ git stash
Saved working directory and index state WIP on main: 8f3c7d1 Cache forecast responses in localStorage
$ git status
On branch main
nothing to commit, working tree cleanA edição em styles.css não se foi, está estacionada. Traga-a de volta com git stash pop quando estiver pronto:
$ git stash pop
On branch main
Changes not staged for commit:
modified: styles.cssgit stash é para alterações que você não está pronto para fazer commit ainda, nunca cria um commit no seu branch.
git stash guarda alterações que você não está pronto para fazer commit, então seu diretório de trabalho fica limpo, e git stash pop as traz de volta mais tarde. Nada é confirmado e nada é perdido, a edição fica estacionada por um tempo. Eu recorro a isso constantemente sempre que um colega precisa de mim em outro branch agora mesmo. Desfazendo um commit com git revert
Digamos que o commit 8f3c7d1, "Cache forecast responses in localStorage", se mostra errado, e já foi enviado, então um colega já o baixou. Reescrever esse commit agora mudaria algo que alguém mais já tem, e a cópia deles e a sua desacordariam na próxima vez que eles fizerem pull. git revert resolve isso sem mudar nada que já existe: cria um commit novo que aplica o oposto da alteração.
$ git revert 8f3c7d1
[main 2b6f0d1] Revert "Cache forecast responses in localStorage"O histórico ainda mostra ambos os commits, o original e o que o desfaz, em ordem. A cópia de ninguém precisa ser reformulada, um colega puxa o novo commit de revert como qualquer outro.
git revert é sempre seguro em um commit que outros já têm, porque adiciona um novo commit ao invés de mudar um antigo.
git revert <commit> desfaz um commit adicionando um novo commit que o reverte, ao invés de apagar o original. Isso o torna a escolha segura uma vez que um commit já é compartilhado com qualquer outra pessoa, já que nada já salvo é mudado. O histórico mantém um registro tanto do erro quanto do conserto, o que eu gosto mais quanto mais tempo uso Git. Rebobinando com git reset: soft, mixed e hard
Há uma terceira forma de desfazer um commit, git reset, e funciona diferente de revert: ao invés de adicionar um novo commit que desfaz o antigo, reset move seu branch atual para apontar para um commit diferente completamente. Isso o torna rápido e limpo para commits que ninguém mais viu, e arriscado para commits que têm.
Reset é o primo mais amplo e fácil de usar indevidamente de restore e revert acima. Você não precisará dele para a maioria das correções do dia a dia, restore, revert e stash cobrem a vasta maioria de momentos "ajuda, desfaça isso". Você o verá em instruções de outras pessoas porém, então aqui está o resumo: reset move seu branch para trás no histórico, e um de seus modos, --hard, descarta trabalho não confirmado sem confirmação. Se um comando que você está copiando de algum lugar inclui git reset --hard, leia o que faz antes de executá-lo.
git reset na maioria das correções do dia a dia, restore, revert e stash já cobrem quase tudo. Ainda assim, conheça a forma dela: reset move seu branch de volta para um commit anterior, e seu modo --hard limpa seu diretório de trabalho para coincidir sem confirmação. Trate qualquer comando que você copie que inclua reset --hard com real cautela. Escolhendo revert ou reset
A regra que decide qual ferramenta procurar: alguém mais já fez pull desse commit? Se sim, revert. Se o commit só existe na sua máquina, em um branch que você não enviou, reset é bom e frequentemente o conserto mais arrumado. Quando você não tem certeza, revert é sempre a escolha segura, já que funciona em ambos os casos.
Recuperando um commit com git reflog
Um reset --hard, um rebase bagunçado, ou deletar um branch por engano pode parecer que um commit desapareceu completamente. Geralmente não desapareceu. Git mantém um log local de em todos os lugares que seu HEAD e branches apontaram, chamado reflog, e esse log é quase sempre seu jeito de voltar a um commit que parece perdido.
Você é improvável que precise disso frequentemente, mas aqui está o resumo: Git raramente joga qualquer coisa fora no momento que parece ter jogado. Se você já executar reset --hard e perceber depois que precisava daquele commit, muito provavelmente há um jeito de tê-lo de volta, coberto aqui para sempre que você estiver pronto para ir mais fundo.
reset --hard e perceber tarde demais que precisava daquele commit, é provavelmente ainda recuperável. Git mantém um registro local chamado o reflog de em todos os lugares que seu trabalho apontou, e isso é geralmente o suficiente para ter um commit de volta que parecia perdido. Isso é território de mergulho profundo, mas é reassegurador saber que está lá. 
