Desfazendo alterações e commits no Git


Sempre tem algo para desfazer. Você digitou uma edição que não devia, tentou uma correção que piorou as coisas, ou fez um commit de algo que preferia que ninguém visse. O Git guarda um registro de quase toda versão do seu projeto que já existiu, então a maioria dos erros é mais reversível do que parece no momento. A ferramenta certa para te trazer de volta depende de quão longe o erro já foi: ainda no seu editor, já no stage, já commitado, ou já enviado para algum lugar onde um colega de equipe pode ver. Este capítulo percorre cada etapa nessa ordem.
O que são push e pull?
Push envia seus commits para uma cópia compartilhada do repositório na qual seus colegas também trabalham, e pull traz os commits deles para você. Remotes and GitHub cobre os dois em detalhes; para este capítulo, "enviado" (pushed) significa que o commit saiu da sua máquina.
Descartando uma alteração antes de commitá-la
Digamos que você ainda esteja reestilizando styles.css em weather-app, do ciclo de commit, e o novo layout acaba se revelando uma má ideia. Você não colocou no stage, não commitou. Você só quer o arquivo de volta como ele estava por último, nada mais. O comando certo 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 commitada e devolve o arquivo ao seu último estado salvo. Se você já tinha colocado a alteração no stage com git add antes de mudar de ideia, o git restore sozinho não vai afetá-la, já que o arquivo agora está na área de stage, e não apenas no seu diretório de trabalho. Tire-o do stage primeiro, depois restaure-o:
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.cssDuas ações separadas para duas áreas separadas: git restore --staged move um arquivo de volta para fora da área de stage, e git restore por si só descarta a edição no seu diretório de trabalho. git restore só alcança trabalho não commitado; depois que uma alteração é commitada, outro comando entra em ação.
git restore <file> descarta uma edição que você ainda não commitou e devolve o arquivo do jeito que estava por último. Se você já colocou a alteração no stage com git add, tire-a do stage primeiro com git restore --staged <file>, depois restaure o arquivo se quiser que a edição também desapareça. Isso só funciona em alterações que você ainda não commitou, então não é capaz de resgatar um erro que você já salvou. Guardando trabalho de lado com git stash
Às vezes não há erro nenhum para corrigir, só uma questão de timing. Você está no meio de uma edição em styles.css e um colega precisa de você em outro branch agora mesmo, mas você não está pronto para commitar um estilo pela metade. git stash guarda suas alterações de lado e te devolve um diretório de trabalho limpo, para você poder se afastar e voltar depois.
$ 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 desapareceu, só 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 serve para alterações que você ainda não está pronto para commitar; ele nunca cria um commit no seu branch.
git stash guarda de lado alterações que você ainda não está pronto para commitar, deixando seu diretório de trabalho limpo, e git stash pop as traz de volta depois. Nada é commitado e nada se perde, a edição fica só estacionada por um tempo. Eu uso isso constantemente sempre que um colega precisa de mim em outro branch imediatamente. Desfazendo um commit com git revert
Digamos que o commit 8f3c7d1, "Cache forecast responses in localStorage", esteja errado, e ele já foi enviado, então um colega já fez pull dele também. Reescrever esse commit agora mudaria algo que outra pessoa já tem, e a cópia dela e a sua ficariam divergentes na próxima vez que ela fizesse pull. git revert resolve isso sem alterar nada que já existe: ele cria um commit totalmente novo que aplica o oposto da alteração.
$ git revert 8f3c7d1
[main 2b6f0d1] Revert "Cache forecast responses in localStorage"O histórico continua mostrando os dois commits, o original e o que o desfaz, em ordem. Nenhuma cópia precisa ser remodelada; um colega faz pull do novo commit de revert como faria com qualquer outro.
git revert é sempre seguro em um commit que outras pessoas já têm, porque ele adiciona um commit novo em vez de alterar um antigo.
git revert <commit> desfaz um commit adicionando um commit totalmente novo que o reverte, em vez de apagar o original. Isso faz dele a escolha segura quando um commit já foi compartilhado com qualquer outra pessoa, já que nada que já foi salvo é alterado. O histórico guarda um registro tanto do erro quanto da correção, algo que eu gosto cada vez mais quanto mais uso o Git. Voltando no tempo com git reset: soft, mixed e hard
Existe uma terceira forma de desfazer um commit, git reset, e ela funciona de modo diferente do revert: em vez de adicionar um novo commit que desfaz o antigo, o reset move seu branch atual para apontar para um commit completamente diferente. Isso o torna rápido e limpo para commits que ninguém mais viu ainda, e arriscado para commits que outras pessoas já têm.
O reset é o primo mais amplo, e mais fácil de usar errado, do restore e do revert vistos acima. Você não vai precisar dele na maioria das correções do dia a dia; restore, revert e stash já cobrem a grande maioria dos momentos de "socorro, desfaz isso". Mesmo assim, você vai encontrá-lo em instruções de outras pessoas, então aqui vai o resumo: o reset move seu branch para trás no histórico, e um dos seus modos, --hard, descarta trabalho não commitado sem nenhuma confirmação. Se um comando que você está copiando de algum lugar incluir git reset --hard, leia o que ele faz antes de executá-lo.
git reset na maioria das correções do dia a dia; restore, revert e stash já cobrem quase tudo. Mesmo assim, saiba como ele funciona: o reset move seu branch de volta para um commit anterior, e seu modo --hard apaga seu diretório de trabalho para corresponder, sem nenhuma confirmação. Trate com bastante cautela qualquer comando que você copiar e que inclua reset --hard. Escolhendo entre revert e reset
A regra que decide qual ferramenta usar: alguém mais já fez pull deste commit? Se sim, revert. Se o commit existe só na sua máquina, em um branch que você não enviou, o reset serve bem e costuma ser a correção mais limpa. Quando você não tiver certeza, o revert é sempre a escolha segura, já que funciona nos dois casos.
Recuperando um commit com git reflog
Um reset --hard, um rebase confuso, ou apagar um branch por engano pode parecer que um commit desapareceu completamente. Geralmente não é o caso. O Git mantém um registro local de todos os lugares para onde seu HEAD e seus branches já apontaram, chamado de reflog, e esse registro é quase sempre seu caminho de volta a um commit que parece perdido.
É improvável que você precise disso com frequência, mas aqui vai o resumo: o Git raramente descarta algo de fato no momento em que parece que descartou. Se algum dia você rodar reset --hard e depois perceber que precisava daquele commit, muito provavelmente existe uma forma de recuperá-lo, coberta aqui para quando você estiver pronto para se aprofundar.
reset --hard e perceber tarde demais que precisava daquele commit, provavelmente ele ainda é recuperável. O Git mantém um registro local chamado reflog de todos os lugares para onde seu trabalho já apontou, e isso costuma ser suficiente para trazer de volta um commit que parecia ter ido embora. Isso é território de Deep Dive, mas é reconfortante saber que existe. 
