Skip to content
This page has been auto-translated and may contain errors.View in English

Desfazendo alterações e commits no Git

docs.scrimba.com

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:

bash
$ 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 clean

git 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:

bash
$ git add styles.css
$ git restore --staged styles.css
$ git restore styles.css

Dois 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.

Tutoriais e scripts mais antigos usam git checkout -- <file> para o mesmo trabalho que git restore <file> faz hoje. checkout costumava funcionar como "mudar de branch" e "restaurar um arquivo", o que era confuso o suficiente para o Git dividir os dois em switch e restore. Você ainda encontrará checkout em material mais antigo, reconheça, mas procure restore em qualquer coisa que você escrever agora. Você também pode descartar cada alteração não preparada na pasta atual de uma só vez com git restore ., o que vale a pena pausar antes de executar, já que limpa todos os arquivos de uma vez.

O desfazimento de restore para firmemente no diretório de trabalho. Um commit é um objeto salvo que fica por perto até que a coleta de lixo do Git o remova, é por isso que um commit ruim é recuperável através do reflog mesmo após um reset difícil. Uma edição não confirmada nunca foi transformada em um objeto salvo: restore verifica a última versão salva de um arquivo sobre sua edição, e não há nenhum estado anterior armazenado em qualquer lugar para cavar. Essa é a razão prática para fazer commit cedo e frequentemente. Um commit bagunçado é quase sempre corrigível; uma edição que você nunca salvou geralmente não é.

JunoDescartando uma alteração não confirmadagit 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.
JunoDescartando uma alteração não confirmadagit restore <file> é o substituto moderno para git checkout -- <file>, e git restore . limpa toda alteração não preparada na pasta de uma só vez, então execute git status primeiro para ver o que isso tocaria. Você ainda verá checkout fazendo esse trabalho em respostas e scripts mais antigos, é a mesma ideia sob um nome mais antigo e sobrecarregado.
JunoDescartando uma alteração não confirmadarestore nunca cria ou toca um commit, só sobrescreve seu diretório de trabalho com a última versão salva de um arquivo. Isso significa que não deixa rastro para recuperar se você estiver errado, ao contrário de quase tudo mais neste capítulo. Faça commit pequeno e frequentemente, e "oops" vira um problema que reset ou o reflog podem corrigir ao invés de uma alteração que nunca teve um ponto de salvamento para retornar.

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.

bash
$ 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 clean

A edição em styles.css não se foi, está estacionada. Traga-a de volta com git stash pop quando estiver pronto:

bash
$ git stash pop
On branch main
Changes not staged for commit:
        modified:   styles.css

git stash é para alterações que você não está pronto para fazer commit ainda, nunca cria um commit no seu branch.

Um único stash raramente cobre uma semana real de trabalho, então conheça o resto do kit de ferramentas. git stash list mostra tudo que você guardou, mais recente primeiro. git stash pop reaaplica a entrada superior e a remove da lista; git stash apply a reaaplica mas a deixa na lista, útil se você quer a mesma alteração guardada em dois branches. Nomeie um stash no caminho, então list não é uma parede de entradas sem rótulo mais tarde:

bash
$ git stash push -m "wip: forecast card layout"
$ git stash list
stash@{0}: On main: wip: forecast card layout

Um stash é construído da mesma maquinaria de commit que tudo mais no Git. git stash salva seu estado preparado e não preparado como um par de objetos de commit e aponta uma referência especial, refs/stash, para eles, é por isso que git stash list lê como uma pequena história por conta própria. O reflog depende desse mesmo truque, uma referência apontando para commits que nenhum branch alcança.

JunoGuardando trabalhogit 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.
JunoGuardando trabalhogit stash list mostra cada alteração guardada, pop reaaplica e remove a mais recente, apply reaaplica sem remover. Nomeie stashes no caminho com push -m, uma lista stash cheia de entradas sem rótulo uma semana depois ajuda ninguém, incluindo você.
JunoGuardando trabalho Um stash é um par de objetos de commit que refs/stash aponta, sentado fora de qualquer branch, é por isso que se comporta como sua própria pequena história. Uma vez que você vê assim, stash para de parecer mágica, é o mesmo modelo de objeto fazendo o mesmo truque novamente.

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.

bash
$ 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.

Reverter uma alteração que se sobrepõe com commits posteriores pode produzir um conflito, o mesmo tipo que mesclagem e conflitos cobre: Git marca o arquivo com <<<<<<<, =======, e >>>>>>> e espera por você para resolvê-lo. Corrija os marcadores, git add o arquivo, depois termine com git revert --continue.

bash
$ git revert 8f3c7d1
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js

$ git add src/index.js
$ git revert --continue

A segurança de revert em histórico compartilhado repousa na mesma ideia por trás de nunca fazer rebase em um branch público: reescrever um commit que outros já baixaram força a cópia deles e a sua a sair de sincronia, e alguém tem que forçar seu caminho passado a incompatibilidade depois. Revert deixa todo commit existente exatamente como era. Só adiciona um novo, então a cópia de ninguém precisa ser reconciliada com a sua.

JunoDesfazendo um commit com revertgit 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.
JunoDesfazendo um commit com revert Revert pode conflitar do mesmo jeito que um merge faz quando commits posteriores tocam as mesmas linhas, resolva do mesmo jeito: corrija os marcadores, add o arquivo, depois git revert --continue. Procure por ele qualquer vez que o commit que você está desfazendo já foi enviado ou compartilhado.
JunoDesfazendo um commit com revert Revert nunca toca um commit existente, então é a ferramenta de desfazimento que funciona bem com um branch do qual outras pessoas já estão fazendo pull. Mantenha como seu padrão no momento que um commit ruim deixou sua própria máquina.

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 <commit> move seu branch para apontar para esse commit, e --soft, --mixed, e --hard decidem quanto mais se move com ele. Imagine três camadas: seu histórico de commit, onde o ponteiro do branch senta, a área de preparação, o que está alinhado para fazer commit, e seu diretório de trabalho, os arquivos no disco. Os hashes curtos abaixo lêem do mesmo jeito que Histórico cobre com git log --oneline, e o alvo HEAD~1 significa "um commit antes de HEAD", o jeito padrão de dizer "desfaça o novo commit".

bash
$ git log --oneline
8f3c7d1 Cache forecast responses in localStorage
4f2a1c9 Fix temperature conversion in Celsius-to-Fahrenheit formula
5f3d8b2 Add five-day forecast to the dashboard
1a2b3c4 Set up project structure

$ git reset --soft HEAD~1

--soft move o ponteiro do branch para trás um commit e para por aí. A área de preparação e diretório de trabalho não são tocados, então tudo do commit desfeito senta preparado, pronto para ser reconfirmado como você quiser.

bash
$ git reset --mixed HEAD~1

--mixed é o que executa se você deixar a flag de fora. Move o ponteiro do branch para trás e também limpa a área de preparação, então as alterações do commit desfeito voltam como edições não preparadas no seu diretório de trabalho. Nada é perdido, execute git add novamente antes de fazer commit.

bash
$ git reset --hard HEAD~1

git reset --hard move o ponteiro do branch para trás e sobrescreve seu diretório de trabalho para coincidir, então todo trabalho não confirmado e o commit desfeito ambos desaparecem da vista em um passo, sem nenhum prompt pedindo se você tem certeza. Este é o modo para pausar antes de executar.

Isso também explica o que git commit --amend estava fazendo em o loop de commit: amend é próximo a um reset --soft HEAD~1 seguido imediatamente por um novo commit, é por isso que substitui seu último commit ao invés de empilhar em cima dele.

Reset só move ponteiros e, no modo --hard, o diretório de trabalho para coincidir. Nunca deleta um objeto de commit completamente. O commit que um reset --hard deixou para trás ainda está sentado no banco de dados de objetos do Git, inacessível de qualquer branch, o que importa porque há um jeito de alcançá-lo novamente: git reflog, próximo. Em um branch compartilhado porém, mover seu branch para trás e fazer push significa força-push sobre commits que seus colegas já têm, então seu próximo pull desacorda com o histórico reformulado. Essa é a razão prática que reset permanece uma ferramenta de limpeza local enquanto revert cobre qualquer coisa que já deixou sua máquina.

JunoRebobinando com git reset Você não procurará por 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.
JunoRebobinando com git resetgit reset --soft mantém as alterações do commit desfeito preparadas, --mixed (o padrão) as desagrega mas mantém as edições, --hard descarta as edições completamente. commit --amend é basicamente um soft reset imediatamente seguido por um novo commit, é por isso que substitui o último commit ao invés de empilhar em cima dele.
JunoRebobinando com git reset Reset só move ponteiros, e com --hard, seu diretório de trabalho para coincidir, nunca deleta o objeto de commit em si. Esse objeto fica no banco de dados até que o reflog e coleta de lixo o alcancem. Faça push de um branch reescrito e o próximo pull de seus colegas colide com histórico que não coincide mais com o deles.

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.

"Alguém fez pull dele" realmente significa "este commit é alcançável de uma referência que alguém mais tem", o que na prática significa se senta em um branch que foi enviado. Um commit em um branch local que você inventou cinco minutos atrás e nunca enviou é território aberto para reset. No momento que você o envia, trate como compartilhado: fazer reset localmente e força-push significa coordenar com quem mais toca esse branch primeiro.

JunoEscolhendo revert ou reset Faça uma pergunta antes de desfazer um commit: alguém mais já fez pull dele? Se sim, procure por revert, é sempre seguro. Se o commit só existe em sua máquina, reset funciona bem também. Quando você não tem certeza qual se aplica, revert é o padrão mais seguro.
JunoEscolhendo revert ou reset Uma vez que um commit é enviado e qualquer um poderia tê-lo, procure por revert. Enquanto é puramente local para você, reset é bom e frequentemente o conserto mais arrumado, já que não deixa uma entrada "revert de um revert" sentada no log.
JunoEscolhendo revert ou reset O teste real é alcançabilidade: este commit está sentado em uma referência que alguém mais fez pull? Local e não enviado, reset é território aberto. Enviado e compartilhado, trate fazer reset dele como algo para coordenar primeiro, já que um força-push posterior colide com qualquer coisa que seus colegas já têm.

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.

A versão curta: git reflog lista posições recentes de HEAD, cada uma com um hash curto, e você pode git reset --hard qualquer um daqueles hashes para voltar a exatamente esse estado. Lê como um segundo histórico: cada lugar onde HEAD esteve, incluindo commits que seus branches não apontam mais.

Um commit sem nenhum branch, tag, ou HEAD apontando para ele mais é inacessível: à deriva no repositório, ainda salvo, só faltando um rótulo que leve de volta para ele. O reflog é uma lista cronológica de em todos os lugares que HEAD se moveu em sua máquina, essa troca de branch, aquele reset, aquele rebase, cada entrada codificada por uma referência curta como HEAD@{2}. Encontre a entrada de justo antes do erro e aponte um branch de volta para ele:

bash
$ git reflog
2b6f0d1 HEAD@{0}: reset: moving to HEAD~1
8f3c7d1 HEAD@{1}: commit: Cache forecast responses in localStorage
4f2a1c9 HEAD@{2}: commit: Fix temperature conversion in Celsius-to-Fahrenheit formula

$ git reset --hard 8f3c7d1

Esse é o commit que reset --hard parecia apagar, restaurado. Dois limites vale a pena conhecer. Primeiro, o reflog vive só em sua própria máquina, fazer push e pull nunca o tocam, então não pode recuperar um commit que um colega perdeu na deles. Segundo, não dura para sempre: Git eventualmente executa coleta de lixo e limpa commits inacessíveis uma vez que envelheceram passado um padrão padrão de algumas semanas, então a rede de segurança cobre erros recentes ao invés de toda vida do projeto.

JunoRecuperando um commit perdido Se você alguma vez executar 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á.
JunoRecuperando um commit perdidogit reflog lista posições recentes de HEAD com hashes curtos que você pode reset --hard de volta para, então um reset que foi longe demais é geralmente recuperável. Vive só em sua própria máquina porém, então não pode resgatar um commit que um colega perdeu na deles.
JunoRecuperando um commit perdido O reflog é um registro local, cronológico de cada lugar onde HEAD esteve, o que é como um commit que reset --hard parecia apagar aparece novamente apontando um branch para a entrada correta de HEAD@{n}. Envelhece com coleta de lixo eventualmente, e cobre só seus próprios erros recentes. Um colega que perdeu um commit precisa executar a mesma recuperação em sua própria máquina.