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

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

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

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

Tutoriais e scripts mais antigos usam git checkout -- <file> para exatamente o trabalho que git restore <file> faz hoje. checkout costumava servir tanto para "trocar de branch" quanto para "restaurar um arquivo", o que gerava confusão suficiente para o Git separar as duas coisas em switch e restore. Você ainda vai encontrar checkout em materiais mais antigos, reconheça-o, mas use restore em tudo que você escrever a partir de agora. Você também pode descartar todas as alterações fora do stage na pasta atual de uma vez com git restore ., algo que merece uma pausa antes de rodar, já que limpa todos os arquivos de uma só vez.

O desfazer do restore para firmemente no diretório de trabalho. Um commit é um objeto salvo que continua existindo até a coleta de lixo do Git removê-lo, e é por isso que um commit ruim é recuperável pelo reflog mesmo depois de um reset hard. Uma edição não commitada nunca chegou a se tornar um objeto salvo: restore recupera a última versão salva de um arquivo por cima da sua edição, e não existe nenhum estado anterior guardado em lugar algum para resgatar. Esse é o argumento prático para commitar cedo e com frequência. Um commit desorganizado quase sempre tem conserto; uma edição que você nunca salvou geralmente não tem.

JunoDescartando uma alteração não commitadagit 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.
JunoDescartando uma alteração não commitadagit restore <file> é o substituto moderno de git checkout -- <file>, e git restore . limpa todas as alterações fora do stage na pasta de uma vez, então rode git status antes para ver o que isso afetaria. Você ainda vai 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 commitadarestore nunca cria ou toca em um commit, ele apenas sobrescreve seu diretório de trabalho com a última versão salva de um arquivo. Isso significa que ele não deixa nenhuma trilha para recuperação se você errar, diferente de quase tudo no resto deste capítulo. Commite pouco e commite com frequência, e um "ops" se torna um problema que o reset ou o reflog conseguem resolver, em vez de uma alteração que nunca teve um ponto de salvamento para voltar.

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.

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 desapareceu, só 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 serve para alterações que você ainda não está pronto para commitar; ele nunca cria um commit no seu branch.

Um único stash raramente cobre uma semana real de trabalho, então conheça o resto do conjunto de ferramentas. git stash list mostra tudo que você guardou de lado, do mais recente para o mais antigo. git stash pop reaplica a entrada do topo e a remove da lista; git stash apply reaplica, mas mantém na lista, útil se você quiser a mesma alteração guardada em dois branches. Nomeie o stash na hora de criá-lo, para que list não seja mais tarde uma parede de entradas sem identificação:

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

Um stash é construído com o mesmo mecanismo de commits usado no resto do Git. git stash salva seu estado com e sem stage como alguns objetos de commit e aponta uma referência especial, refs/stash, para eles, e é por isso que git stash list parece uma pequena história por conta própria. O reflog se apoia no mesmo truque: uma referência apontando para commits que nenhum branch alcança.

JunoGuardando trabalho de ladogit 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.
JunoGuardando trabalho de ladogit stash list mostra todas as alterações guardadas de lado, pop reaplica e remove a mais recente, apply reaplica sem remover. Nomeie os stashes na hora de criá-los com push -m; uma lista de stash cheia de entradas sem identificação uma semana depois não ajuda ninguém, incluindo você.
JunoGuardando trabalho de lado Um stash é composto por alguns objetos de commit para os quais refs/stash aponta, fora de qualquer branch, e é por isso que ele se comporta como uma pequena história própria. Depois que você vê dessa forma, o stash deixa de parecer mágica; é o mesmo modelo de objetos fazendo o mesmo truque de novo.

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.

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

Reverter uma alteração que se sobrepõe a commits posteriores pode gerar um conflito, do mesmo tipo tratado em merging and conflicts: o Git marca o arquivo com <<<<<<<, ======= e >>>>>>> e espera você resolver. Corrija os marcadores, dê git add no arquivo e finalize 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 do revert em um histórico compartilhado se baseia na mesma ideia por trás de nunca fazer rebase de um branch público: reescrever um commit que outras pessoas já puxaram tira a cópia delas e a sua de sincronia, e alguém depois precisa forçar a passagem por essa divergência. O revert deixa cada commit existente exatamente como estava. Ele só adiciona um novo, então nenhuma cópia precisa ser reconciliada com a sua.

JunoDesfazendo um commit com revertgit 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.
JunoDesfazendo um commit com revert O revert pode gerar conflito do mesmo jeito que um merge quando commits posteriores tocaram nas mesmas linhas; resolva do mesmo jeito: corrija os marcadores, dê add no arquivo e depois git revert --continue. Use-o sempre que o commit que você está desfazendo já tiver sido enviado ou compartilhado.
JunoDesfazendo um commit com revert O revert nunca toca em um commit existente, então é a ferramenta de desfazer que convive bem com um branch de onde outras pessoas já estão puxando. Mantenha-o como sua opção padrão a partir do momento em que um commit ruim sai da sua própria máquina.

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 <commit> move seu branch para apontar para esse commit, e --soft, --mixed e --hard decidem o quanto mais se move junto com ele. Imagine três camadas: seu histórico de commits, onde o ponteiro do branch fica; a área de stage, o que está alinhado para commitar; e seu diretório de trabalho, os arquivos no disco. Os hashes curtos abaixo se leem da mesma forma que History mostra com git log --oneline, e o destino HEAD~1 significa "um commit antes de HEAD", a forma padrão de dizer "desfaça o commit mais recente".

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 um commit para trás e para aí. A área de stage e o diretório de trabalho não são afetados, então tudo que estava no commit desfeito fica no stage, pronto para ser commitado de novo do jeito que você quiser.

bash
$ git reset --mixed HEAD~1

--mixed é o que roda se você não colocar nenhuma flag. Ele move o ponteiro do branch para trás e também limpa a área de stage, então as alterações do commit desfeito voltam como edições fora do stage no seu diretório de trabalho. Nada se perde; rode git add de novo antes de commitar.

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 corresponder a ele, então qualquer trabalho não commitado e o commit desfeito desaparecem de vista de uma só vez, sem nenhuma confirmação perguntando se você tem certeza. Esse é o modo para o qual você deve fazer uma pausa antes de executar.

Isso também explica o que o git commit --amend estava fazendo de volta no ciclo de commit: o amend é parecido com um reset --soft HEAD~1 seguido imediatamente por um novo commit, e é por isso que ele substitui seu último commit em vez de se acumular sobre ele.

O reset só move ponteiros e, no modo --hard, o diretório de trabalho para corresponder a eles. Ele nunca exclui um objeto de commit de fato. O commit que um reset --hard deixou para trás continua no banco de objetos do Git, inalcançável a partir de qualquer branch, o que importa porque existe uma forma de alcançá-lo de novo: git reflog, o próximo assunto. Em um branch compartilhado, porém, mover seu branch para trás e fazer push significa forçar o push por cima de commits que seus colegas já têm, então o próximo pull deles vai divergir do histórico reescrito. Essa é a razão prática pela qual o reset permanece uma ferramenta de limpeza local, enquanto o revert cobre qualquer coisa que já saiu da sua máquina.

JunoVoltando no tempo com git reset Você não vai precisar recorrer ao 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.
JunoVoltando no tempo com git resetgit reset --soft mantém no stage as alterações do commit desfeito, --mixed (o padrão) tira elas do stage mas mantém as edições, --hard descarta as edições completamente. O commit --amend é basicamente um reset soft seguido imediatamente por um novo commit, e é por isso que ele substitui o último commit em vez de se acumular sobre ele.
JunoVoltando no tempo com git reset O reset só move ponteiros, e com --hard, também seu diretório de trabalho para corresponder a eles; ele nunca exclui o próprio objeto de commit. Esse objeto permanece no banco de dados até que o reflog e a coleta de lixo o alcancem. Se você fizer push de um branch reescrito, o próximo pull dos seus colegas colide com um histórico que não corresponde mais ao deles.

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.

"Alguém já fez pull dele" na verdade significa "esse commit é alcançável a partir de uma referência que outra pessoa tem", o que na prática significa se ele está em um branch que já foi enviado. Um commit em um branch local que você inventou cinco minutos atrás e nunca enviou é território livre para o reset. No momento em que você o envia, trate-o como compartilhado: resetá-lo localmente e forçar o push significa combinar antes com quem mais toca naquele branch.

JunoEscolhendo entre revert e reset Faça uma pergunta antes de desfazer um commit: alguém mais já fez pull dele? Se sim, use revert, que é sempre seguro. Se o commit existe só na sua máquina, o reset também funciona bem. Quando você não tiver certeza de qual se aplica, revert é a escolha padrão mais segura.
JunoEscolhendo entre revert e reset Depois que um commit é enviado e qualquer pessoa pode tê-lo, use revert. Enquanto ele ainda é puramente local, o reset serve bem e costuma ser a correção mais limpa, já que não deixa uma entrada de "revert de um revert" parada no log.
JunoEscolhendo entre revert e reset O verdadeiro teste é a alcançabilidade: esse commit está em uma referência que alguém mais já puxou? Local e não enviado, o reset é território livre. Enviado e compartilhado, trate resetá-lo como algo para combinar antes, já que um force-push posterior colide com o que seus colegas já têm.

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.

Resumindo: git reflog lista posições recentes de HEAD, cada uma com um hash curto, e você pode fazer git reset --hard para qualquer um desses hashes e voltar exatamente para aquele estado. Ele se lê como um segundo histórico: todos os lugares onde HEAD já esteve, incluindo commits para os quais seus branches não apontam mais.

Um commit para o qual nenhum branch, tag ou HEAD mais aponta é inalcançável: à deriva no repositório, ainda salvo, só sem um rótulo que leve de volta a ele. O reflog é uma lista cronológica de todo lugar para onde HEAD já se moveu na sua máquina, essa troca de branch, aquele reset, aquele rebase, cada entrada identificada por uma referência curta como HEAD@{2}. Encontre a entrada de imediatamente antes do erro e aponte um branch de volta para ela:

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 o reset --hard parecia ter apagado, restaurado. Dois limites vale a pena conhecer. Primeiro, o reflog existe só na sua própria máquina, push e pull nunca o afetam, então ele não consegue recuperar um commit que um colega perdeu na máquina dele. Segundo, ele não dura para sempre: o Git eventualmente roda a coleta de lixo e limpa commits inalcançáveis depois que eles passam de um limite padrão de algumas semanas, então essa rede de segurança cobre erros recentes, não a vida inteira do projeto.

JunoRecuperando um commit perdido Se algum dia você rodar 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.
JunoRecuperando um commit perdidogit reflog lista posições recentes de HEAD com hashes curtos para os quais você pode fazer reset --hard de volta, então um reset que foi longe demais geralmente é recuperável. Porém, ele existe só na sua própria máquina, então não consegue resgatar um commit que um colega perdeu na dele.
JunoRecuperando um commit perdido O reflog é um registro local e cronológico de todo lugar onde HEAD já esteve, e é assim que um commit que o reset --hard parecia ter apagado ressurge, apontando um branch para a entrada HEAD@{n} correta. Ele expira com a coleta de lixo eventualmente, e cobre apenas os seus próprios erros recentes. Um colega que perdeu um commit precisa fazer a mesma recuperação na própria máquina dele.