Desfazer no Git: reset, revert e restore
1. Visão geral
Quem usa Git em algum momento precisa voltar atrás: arrumar um arquivo errado, fazer git add desnecessário, ou commitar algo que não deveria. Git tem três comandos principais para isso: restore, reset e revert. Seus nomes parecem semelhantes, mas cada comando afeta uma área diferente.
Este artigo passa por cada comando usando um repositório de exemplo. Depois de ler, você saberá qual comando usar em cada situação. Você também aprenderá como recuperar commits quando acidentalmente executa reset --hard.
2. Preparando o repositório de exemplo
Antes de tudo, lembre-se que Git tem três áreas:
- Working tree: os arquivos que você está modificando no disco.
- Staging area (index): o que entrará no próximo commit.
- Commit history: os commits salvos, com
HEADapontando para o commit atual.
Vamos criar um repositório pequeno com quatro commits:
mkdir order-service && cd order-service
git init -b main
echo "class Order {}" > Order.java
git add Order.java && git commit -m "Adicionar classe Order"
echo "class Customer {}" > Customer.java
git add Customer.java && git commit -m "Adicionar classe Customer"
echo "class Order { int total; }" > Order.java
git commit -am "Adicionar total para Order"
echo "class PaymentService {}" > PaymentService.java
git add PaymentService.java && git commit -m "Adicionar PaymentService"
git log --oneline
O comando git commit -am faz stage de todos os arquivos já rastreados e faz o commit ao mesmo tempo. O resultado de git log será semelhante a este (o hash na sua máquina será diferente):
d4e5f60 (HEAD -> main) Adicionar PaymentService
c3d4e5f Adicionar total para Order
b2c3d4e Adicionar classe Customer
a1b2c3d Adicionar classe Order
3. git restore: desfazer mudanças não commitadas
git restore foi introduzido no Git 2.23. Este comando funciona apenas com arquivos. Nunca move HEAD nem modifica o histórico de commits. Em versões antigas do Git, essa funcionalidade era feita por git checkout -- <file>.
3.1. Descartar mudanças na working tree
Suponha que você modificou Order.java e mudou de ideia:
echo "class Order { int total; String note; }" > Order.java
git status --short
git restore Order.java
cat Order.java
M Order.java
class Order { int total; }
M significa que o arquivo foi modificado mas ainda não foi feito stage. git restore Order.java copia o conteúdo da staging area de volta para a working tree. Como nada foi feito stage, este conteúdo é idêntico ao HEAD.
Aviso: As mudanças descartadas com
git restoresão perdidas completamente. Git nunca as salvou, entãoreflognão consegue recuperá-las. Se ainda tiver dúvida, executegit stashem vez derestore.
3.2. Remover arquivo da staging area
Você acidentalmente fez git add de um arquivo que não queria commitar:
echo "class Order { int total; String note; }" > Order.java
git add Order.java
git restore --staged Order.java
git status --short
M Order.java
A opção --staged copia o conteúdo de HEAD de volta para a staging area. O resultado é que o arquivo é removido do stage, mas as mudanças que você fez permanecem na working tree. O comando antigo equivalente é git reset HEAD Order.java.
Para remover tanto do stage quanto da working tree ao mesmo tempo, use:
git restore --staged --worktree Order.java
3.3. Recuperar um arquivo de um commit antigo
A opção --source permite obter o conteúdo do arquivo de qualquer commit:
git restore --source=HEAD~2 Order.java
cat Order.java
git status --short
class Order {}
M Order.java
HEAD~2 é o commit “Adicionar classe Customer”. Naquele commit, Order.java ainda não tinha total. Por padrão, o comando escreve apenas na working tree, então o arquivo agora é M (não feito stage). Você pode revisar e fazer git add e commit se quiser manter. Caso contrário, execute git restore Order.java para voltar.
4. git reset: mover o ponteiro da branch
git reset <commit> move a branch atual (e HEAD) para o commit especificado. Os três modos --soft, --mixed e --hard diferem em se a staging area e a working tree são sobrescritas ou não.
| Modo | Branch/HEAD | Staging area | Working tree |
|---|---|---|---|
--soft |
move para o commit | mantém | mantém |
--mixed (padrão) |
move para o commit | reseta para o commit | mantém |
--hard |
move para o commit | reseta para o commit | reseta para o commit |
Todos os exemplos abaixo começam a partir de um estado limpo no commit d4e5f60.
4.1. –soft: remover commit, manter mudanças feitas stage
git reset --soft HEAD~1
git log --oneline -1
git status --short
c3d4e5f (HEAD -> main) Adicionar total para Order
A PaymentService.java
O commit “Adicionar PaymentService” desaparece da branch. O arquivo PaymentService.java ainda existe e está no estado A (feito stage). Isso é útil quando você quer editar a mensagem ou consolidar vários commits em um:
git commit -m "Adicionar PaymentService para processar pagamentos"
4.2. –mixed: remover commit e tirar do stage
git reset HEAD~1
git status --short
?? PaymentService.java
Este é o modo padrão. A branch volta um commit, a staging area é resetada. PaymentService.java é um arquivo novo, então agora está untracked (??). Use --mixed quando quiser dividir um commit: faça stage de partes e commite como vários commits menores.
4.3. –hard: descartar tudo
git add PaymentService.java && git commit -m "Adicionar PaymentService"
git reset --hard HEAD~1
ls
HEAD is now at c3d4e5f Adicionar total para Order
Customer.java Order.java
O commit, o conteúdo da staging area e da working tree de todos os arquivos rastreados são revertidos para c3d4e5f. PaymentService.java desaparece.
Aviso:
reset --hardtambém deleta as mudanças não commitadas em arquivos rastreados. Essas mudanças não podem ser recuperadas. Por outro lado, não deleta arquivos untracked. Para deletar arquivos untracked, você precisa usargit clean -fd, e esse comando é igualmente perigoso.
4.4. Recuperar commits com reflog
Commits removidos por reset não são perdidos imediatamente. Git registra cada movimento de HEAD no reflog:
git reflog -3
c3d4e5f HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: Adicionar PaymentService
c3d4e5f HEAD@{2}: commit: Adicionar PaymentService para processar pagamentos
Você apenas precisa resetar para o commit que deseja recuperar:
git reset --hard 9f8e7d6
git log --oneline -1
9f8e7d6 (HEAD -> main) Adicionar PaymentService
Reflog existe apenas na sua máquina. Por padrão, entradas não referenciadas expiram após 30 dias e são removidas pelo garbage collector. Portanto, recupere o quanto antes possível.
4.5. Quando NÃO usar reset
Não faça reset de commits já pushed para branches compartilhadas. Após um reset, o histórico local e remoto divergem, então você é forçado a fazer git push --force. Colegas que fizeram pull desses commits enfrentarão um histórico confuso e podem acidentalmente pushá-los novamente.
Se você absolutamente precisar fazer uma reescrita, por exemplo em uma branch pessoal, use um comando mais seguro:
git push --force-with-lease
--force-with-lease recusa fazer push se o remote tem commits novos que você ainda não fez fetch. Assim você não sobrescreve o trabalho de outras pessoas.
5. git revert: desfazer com um novo commit
git revert não apaga o histórico. Este comando cria um novo commit com conteúdo oposto ao commit que está sendo desfeito. O histórico apenas fica mais longo, não é reescrito, portanto este é o caminho seguro para branches já pushed.
5.1. Revertendo um commit
git revert --no-edit HEAD
git log --oneline -3
ls
1a2b3c4 (HEAD -> main) Revert "Adicionar PaymentService"
9f8e7d6 Adicionar PaymentService
c3d4e5f Adicionar total para Order
Customer.java Order.java
--no-edit usa a mensagem padrão sem abrir o editor. O commit original 9f8e7d6 permanece no histórico. O novo commit 1a2b3c4 deleta PaymentService.java. O corpo do novo commit contém a linha This reverts commit 9f8e7d6..., ajudando outros a rastrear a origem.
Você pode revertear commits antigos no meio do histórico, não precisa ser HEAD:
git revert --no-edit c3d4e5f
cat Order.java
class Order {}
Se commits subsequentes modificaram as mesmas linhas de código, Git relatará um conflito. Você resolve o conflito, executa git add, depois executa git revert --continue. Para cancelar no meio do processo, use git revert --abort.
5.2. Reverter múltiplos commits em um único commit
Por padrão, cada commit revertido cria um commit separado. Para consolidá-los, use --no-commit:
git revert --no-commit HEAD~3..HEAD~1
git commit -m "Desfazer mudanças em Customer e Order"
A sintaxe A..B seleciona commits após A até B. Git aplica cada commit ao contrário na staging area, então você faz um único commit.
5.3. Revertendo um merge commit
Um merge commit tem dois pais. Portanto, Git precisa saber qual branch você quer manter como “linha principal”:
git revert -m 1 <hash-merge-commit>
-m 1 seleciona o primeiro pai, geralmente a branch em que você fez merge (por exemplo, main). O resultado é que todas as mudanças trazidas pela outra branch serão desfeitas.
Aviso: Depois de revertear um merge, se você fizer merge da mesma branch novamente, Git considerará os commits antigos como “já merged”. Essas mudanças não voltarão. Para trazê-las de volta, você precisa revertear o commit de revert em si.
6. Qual comando escolher?
| Situação | Comando para usar |
|---|---|
| Descartar mudanças não feitas stage de um arquivo | git restore <file> |
| Remover arquivo do stage | git restore --staged <file> |
| Obter versão antiga de um arquivo | git restore --source=<commit> <file> |
| Editar/consolidar último commit, não pushed | git reset --soft HEAD~1 |
| Dividir commit novamente, não pushed | git reset HEAD~1 |
| Descartar completamente o commit local | git reset --hard <commit> |
| Desfazer commit já pushed | git revert <commit> |
Uma regra simples para lembrar:
restoretrabalha com arquivos.resettrabalha com branches, usado localmente.reverttrabalha com histórico compartilhado.
7. Erros comuns
- Confundir
resetcomrevertem branches compartilhadas. Este é o erro mais comum. Se já fez push, use revert. - Executar
reset --hardcom mudanças não commitadas. Antes de executar, verifiquegit status. Se houver mudanças, executegit stashprimeiro. - Pensar que
reset --hardlimpa o diretório completamente. Arquivos untracked permanecem. Você precisa degit clean -npara visualizar primeiro, depoisgit clean -fdpara deletar. - Esquecer que
HEAD~1eHEAD^são diferentes em merge commits.HEAD^2é o segundo pai, enquantoHEAD~2é o avô, ou seja, pai do pai. Com histórico linear,HEAD~1eHEAD^são o mesmo. - Usar
git checkout <file>por hábito. Este comando ainda funciona, mascheckoutassume muitos papéis e é fácil digitar errado e trocar de branch. No Git moderno,restoreeswitchsão mais claros.
8. Conclusão
Neste artigo, você viu três formas de desfazer no Git:
git restorerestaura o conteúdo do arquivo na working tree ou staging area sem tocar no histórico.git resetmove a branch para outro commit, com três níveis:--soft,--mixed,--hard. Este comando deve ser usado apenas para commits não pushed.git revertcria um commit invertido, a escolha segura para branches compartilhadas.
Quando acidentalmente fizer algo errado, lembre-se de git reflog. A maioria dos commits “perdidos” ainda estão lá esperando você recuperá-los.