git reset e git revert: como desfazer um commit sem quebrar o histórico do time
O git reset move o ponteiro da branch para outro commit e reescreve o histórico, por isso serve apenas para commits locais que ainda não foram enviados. O git revert cria um novo commit com as alterações inversas e não mexe no histórico, por isso é a forma segura de desfazer um commit que já está no servidor.
Resumindo: o git reset volta a branch para trás e finge que os commits extras nunca existiram. O git revert deixa o histórico como está e adiciona um novo commit que desfaz as alterações do antigo. O primeiro serve para o trabalho local; o segundo, para tudo o que já foi para o repositório compartilhado. Se você lembrar só dessa regra, já vai evitar a maioria dos desastres com Git.
A seguir veremos como os dois comandos funcionam, qual a diferença entre os modos do reset, como desfazer um commit depois do push, como recuperar commits "perdidos" e onde entra o git clean.
As três áreas que os comandos de desfazer alteram
Para entender a diferença, lembre das três áreas do Git: o diretório de trabalho (os arquivos no disco), o índice (staging area, o que vai entrar no próximo commit) e o histórico (os commits apontados pela branch e pelo HEAD). Cada comando de desfazer atua sobre um conjunto diferente dessas áreas.
| Comando | Histórico | Índice | Diretório de trabalho | Seguro em branch compartilhada |
|---|---|---|---|---|
git restore arquivo | não | não | sim | sim |
git restore --staged arquivo | não | sim | não | sim |
git reset --soft | sim | não | não | não |
git reset --mixed (padrão) | sim | sim | não | não |
git reset --hard | sim | sim | sim | não |
git revert | adiciona um commit | — | — | sim |
git clean | não | não | apaga arquivos não rastreados | sim, mas é irreversível |
Como funciona o git reset
O comando git reset <commit> move o ponteiro da branch atual (e o HEAD junto) para o commit indicado. Os commits "depois" dele não são apagados fisicamente, mas deixam de aparecer na branch. O modo define o que acontece com o índice e com os arquivos.
git reset --soft: desfazer o commit mantendo as alterações no índice
A branch se move, mas o índice e os arquivos continuam como estavam. Todas as alterações dos commits desfeitos ficam prontas para commit. É a forma ideal de reescrever a mensagem ou juntar os últimos commits em um só.
git reset --soft HEAD~3
git commit -m "feat: formulário de cadastro"
Os três últimos commits viraram um só, e o conteúdo dos arquivos não mudou.
git reset --mixed: desfazer o commit e o índice
É o modo padrão: git reset HEAD~1 equivale a git reset --mixed HEAD~1. A branch se move, o índice é limpo e as alterações ficam no diretório de trabalho como não preparadas. Útil quando você commitou coisa demais e quer decidir de novo o que adicionar e em quais partes.
git reset HEAD~1
git status
# Changes not staged for commit:
# modified: app.py
# modified: config.yml
git reset --hard: voltar tudo
Branch, índice e diretório de trabalho ficam iguais ao commit indicado. Todas as alterações não commitadas em arquivos rastreados são perdidas sem aviso. É o único modo do reset que pode realmente destruir o seu trabalho.
git reset --hard HEAD~1 # descartar o último commit e todas as alterações
git reset --hard origin/main # deixar a branch igual à do servidor
Antes de um --hard, vale rodar git status e git stash se houver algo importante. O que foi commitado dá para recuperar (veja abaixo); o que não foi, não.
git reset em um único arquivo
Com um caminho, o reset não move a branch, só altera o índice: git reset app.py tira o arquivo da staging area. O equivalente moderno é git restore --staged app.py, que é mais claro. As flags --soft e --hard não funcionam com caminhos.
Como funciona o git revert
O git revert <commit> calcula as alterações do commit indicado, aplica o inverso delas e cria um novo commit. Linhas adicionadas são removidas, linhas removidas voltam. O histórico só cresce para frente, nada é reescrito, então os colegas não precisam consertar nada do lado deles.
git log --oneline
# a3f9c21 (HEAD -> main) docs: README
# 7be41d0 feat: cache de respostas
# 1c02e8f fix: timeout das requisições
git revert 7be41d0
# [main 5d8e2aa] Revert "feat: cache de respostas"
# 2 files changed, 3 insertions(+), 41 deletions(-)
O Git abre o editor com uma mensagem do tipo Revert "..." e a linha This reverts commit 7be41d0.... Uma boa prática é acrescentar o motivo da reversão.
Flags úteis do revert
--no-edit— aceita a mensagem padrão sem abrir o editor.-n(--no-commit) — aplica as alterações inversas no índice sem criar commit. Assim dá para desfazer vários commits com um único commit.git revert A..B— desfaz um intervalo de commits (cada um em um commit de revert separado, do mais novo para o mais antigo).-m 1— obrigatório para commits de merge: indica qual pai é a linha principal.
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert: desfazer três commits do experimento"
Revert de um commit de merge
Um merge tem dois pais, e o Git não sabe em relação a qual calcular as alterações "inversas"; por isso, sem -m, ele dá o erro is a merge but no -m option was given. Normalmente a branch principal é o primeiro pai:
git revert -m 1 9f1e2ab
A pegadinha: se depois você quiser fazer merge da mesma branch de novo, o Git vai considerar os commits dela já integrados e não vai trazer as alterações. A solução é primeiro fazer revert do próprio commit de revert e só então trazer as novas alterações.
git reset vs git revert: qual escolher
| Critério | git reset | git revert |
|---|---|---|
| O que faz com o histórico | reescreve (os commits somem da branch) | adiciona um novo commit |
| Commit já enviado (push) | exige push --force, atrapalha os colegas | git push normal |
| Desfazer um commit do meio do histórico | só junto com todos os seguintes | pode ser pontual |
| Rastro no histórico | não fica | fica, dá para ver quem desfez e por quê |
| Cenário típico | limpar commits locais antes do push | reverter na main o commit que quebrou produção |
Regra simples: se alguém além de você já viu o commit, use revert; se não, reset. A exceção é a sua feature branch pessoal que ninguém baixou: nela é aceitável reescrever o histórico e fazer git push --force-with-lease.
Por que reset --hard em uma branch compartilhada é má ideia
Imagine: você fez git reset --hard HEAD~2 na main e git push --force. Os colegas continuam com esses dois commits. No próximo git pull, o Git vai tentar fazer merge da branch deles com a sua, e os commits "apagados" voltam, agora com um commit de merge inútil. Pior ainda se alguém já tinha enviado commits por cima: o force push simplesmente os apaga.
Por isso, em branches protegidas (main, release), o force push costuma ser bloqueado nas configurações da plataforma. Se reescrever for inevitável, use git push --force-with-lease: ele recusa o envio se surgiram no servidor commits que você ainda não viu.
Cenários comuns para desfazer
Como desfazer o último commit que ainda não foi enviado
git reset --soft HEAD~1 # alterações no índice, dá para commitar de novo
git reset HEAD~1 # alterações nos arquivos, índice vazio
git reset --hard HEAD~1 # alterações descartadas
Se você só precisa corrigir a mensagem ou adicionar um arquivo esquecido, não precisa de reset: git add esquecido.txt e git commit --amend.
Como desfazer um commit depois do push
git revert <hash>
git push
Essa é a resposta para a busca comum "git reverse commit": não existe um comando git reverse no Git; quem cria o commit "inverso" é justamente o git revert.
Como deixar a branch igual à do servidor
git fetch origin
git reset --hard origin/main
Útil quando a branch local ficou bagunçada depois de um rebase malsucedido ou de experimentos. Todos os commits locais que não estão no servidor somem da branch.
Como recuperar commits depois de um reset errado
O Git registra cada movimento do HEAD no reflog. Depois do reset, os commits continuam no banco de objetos por pelo menos algumas semanas (por padrão, entradas inalcançáveis do reflog são mantidas por 30 dias).
git reflog
# 1c02e8f HEAD@{0}: reset: moving to HEAD~2
# a3f9c21 HEAD@{1}: commit: docs: README
# 7be41d0 HEAD@{2}: commit: feat: cache de respostas
git reset --hard a3f9c21 # ou HEAD@{1}
O reflog é local: existe só no seu clone e não é enviado ao servidor.
git clean: apagar arquivos não rastreados
O git reset --hard não mexe nos arquivos que o Git não rastreia: arquivos novos que não foram adicionados ao índice, artefatos de build, logs. Para eles existe o git clean. O comando é irreversível: os arquivos apagados não vão para o reflog nem para a lixeira.
git clean -n # dry run: mostra o que será apagado
git clean -f # apaga arquivos não rastreados
git clean -fd # o mesmo, mais pastas não rastreadas
git clean -fdx # mais arquivos ignorados (.gitignore): node_modules, dist
git clean -i # escolha interativa
Sem -f, o Git se recusa a rodar por padrão (configuração clean.requireForce). Comece sempre com -n, principalmente com -x: ele apaga até o seu .env local, se estiver no ignore.
A combinação para voltar totalmente ao último commit: git reset --hard && git clean -fd.
Erros comuns
- reset --hard no lugar de --soft. O que foi commitado o reflog salva; o que não foi está perdido. O hábito de fazer
git stashantes de experimentar resolve o problema. - revert sem entender as dependências. Se commits posteriores mexeram nas mesmas linhas, o revert gera conflito. Resolva como qualquer outro: edite os arquivos,
git add,git revert --continue; desistiu —git revert --abort. - Confundir restore e reset. Para desfazer alterações em um arquivo, não é preciso dar reset na branch inteira:
git restore arquivo. - force push na main. Melhor um revert e um push normal, mesmo que o histórico fique um pouco mais longo.
Resumo
O git reset é a ferramenta para o seu próprio histórico, ainda não publicado: --soft mantém as alterações no índice, --mixed nos arquivos, --hard descarta tudo. O git revert é a forma segura de desfazer em branches compartilhadas, deixando um rastro honesto no histórico. O git clean termina de limpar os arquivos não rastreados, mas não perdoa erros. Os cenários de desfazer com restore, reset e revert são praticados no curso de Git.
Perguntas frequentes
O git reset apaga commits para sempre?
Não, os commits continuam no banco de objetos e podem ser acessados pelo git reflog até que o garbage collector os remova — por padrão, depois de 30 dias para entradas inalcançáveis.
Dá para desfazer um git revert?
Sim, o commit de revert é um commit normal: dá para desfazê-lo com outro git revert ou, se ainda não foi enviado, removê-lo com git reset.
Qual a diferença entre git reset e git checkout?
O git reset move o ponteiro da branch atual, enquanto o git checkout (ou git switch) muda o HEAD para outra branch ou commit sem alterar a branch em si.
O que significam HEAD~1 e HEAD^?
Os dois indicam o pai do commit atual; HEAD~3 é o terceiro ancestral seguindo os primeiros pais, e HEAD^2 é o segundo pai de um commit de merge.
Como desfazer um git clean?
Pelo Git, não tem como: os arquivos não rastreados apagados nunca estiveram no repositório, por isso rode sempre primeiro git clean -n.
Este tema é abordado em profundidade, com prática, no curso “Git do zero ao desenvolvedor confiante”.
Ir para o curso