← Voltar ao blog

Recupere commits perdidos com git reflog (você provavelmente não perdeu nada)

tutorial git

"Perdi tudo" (spoiler: provavelmente não)

Respira fundo. Se o trabalho que você está procurando foi commitado em algum momento -- mesmo uma única vez, mesmo no branch errado, mesmo num commit que você "apagou" depois -- ele quase certamente ainda está no seu repositório agora mesmo. O Git é muito melhor em guardar coisas do que em perdê-las, e os próximos dez minutos de leitura muito provavelmente vão terminar com o seu código de volta na tela.

Aqui vai a verdade reconfortante sobre como o Git funciona por dentro: o Git quase nunca apaga nada imediatamente. Cada commit que você faz é armazenado como um objeto no diretório .git, identificado pelo seu SHA. Quando você roda git reset --hard, apaga um branch ou reescreve o histórico com um rebase, o Git não apaga esses objetos de commit. Ele só move ponteiros -- nomes de branch e o HEAD -- para que eles não apontem mais para esses commits. Os commits ficam "inalcançáveis", ou seja, nenhum branch ou tag leva mais até eles, mas os objetos em si continuam no disco por semanas antes que a garbage collection sequer pense em tocá-los.

Então o problema não é que seus commits sumiram. O problema é que você não tem mais um nome para eles. Você precisa do SHA. E o Git mantém um diário que vem registrando silenciosamente cada SHA por onde o seu repositório já passou: o reflog.

O que o reflog realmente é

O reflog (reference log) é um diário local, por máquina, de onde o HEAD e a ponta de cada branch apontaram ao longo do tempo. Toda vez que o HEAD se move -- porque você fez commit, checkout, merge, rebase, reset ou amend -- o Git acrescenta uma linha a esse diário. É a caixa-preta do seu repositório.

Para vê-lo, rode:

git reflog

A saída se parece com isto:

e4f5a6b HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: add payment validation
9f8e7d6 HEAD@{2}: commit: refactor checkout flow
3c4d5e6 HEAD@{3}: checkout: moving from main to feature/payments

Leia de cima para baixo como "mais recente primeiro". Cada linha mostra o SHA para onde o HEAD apontava, uma referência posicional como HEAD@{1}, a operação que moveu o HEAD e uma descrição curta. A sintaxe HEAD@{n} significa "onde o HEAD estava n movimentos atrás": HEAD@{0} é onde você está agora, HEAD@{1} é uma operação atrás, e assim por diante. Você pode usar essas referências em qualquer comando que aceite um commit, e é exatamente isso que torna as operações de resgate tão diretas.

No exemplo acima, a história é clara: você fez dois commits (HEAD@{2} e HEAD@{1}), e então um reset moveu o HEAD três commits para trás. Esses dois commits não estão perdidos -- estão bem ali, em a1b2c3d.

O HEAD não é a única referência com um log. Cada branch mantém o seu:

git reflog show feature/payments

Isso mostra cada posição que a ponta de feature/payments já ocupou -- útil quando você quer o histórico de um branch sem o ruído de cada checkout que você já fez.

Cenários de resgate, passo a passo

1. Você foi longe demais com git reset --hard

Você queria desfazer um commit e apagou três, ou fez reset para o lugar completamente errado. O reflog registrou onde você estava imediatamente antes do reset, e essa posição é HEAD@{1}:

git reset --hard HEAD@{1}

E é só isso. O próprio reset foi apenas mais um movimento do HEAD, então mover o HEAD de volta para a posição anterior restaura tudo. Se você fez outras coisas depois do reset errado, rode git reflog primeiro, encontre a linha logo antes da entrada do reset e faça reset para esse SHA. Para um tour mais amplo pelas estratégias de desfazer, veja como desfazer um commit no Git.

2. Você apagou um branch

Apagar um branch apaga o ponteiro, não os commits. Se o branch teve checkout ou commits recentes nesta máquina, a ponta dele está no reflog do HEAD. Encontre-a:

git reflog | grep "feature/payments"

Procure o último commit feito naquele branch, ou a última entrada checkout: moving from feature/payments -- o SHA naquela linha é a ponta do branch. Depois recrie o branch apontando para ela:

git branch feature/payments a1b2c3d

O branch está de volta, idêntico ao momento em que foi apagado. De bônus: quando você apaga um branch, o Git imprime o SHA da ponta (Deleted branch feature/payments (was a1b2c3d)) -- se essa mensagem ainda está no histórico do seu terminal, você pode pular a busca no reflog por completo.

3. Um rebase deu errado

Um rebase reescreve commits, e no meio de um cheio de conflitos pode parecer que o seu branch foi destruído sem conserto. Não foi: o estado pré-rebase está no reflog. Se o rebase ainda está em andamento, a saída mais limpa é git rebase --abort. Se ele já terminou e você odiou o resultado, encontre a entrada de antes de ele começar:

git reflog
# procure por: "rebase (start)" -- a entrada logo ABAIXO dela
# é onde o seu branch estava antes do rebase
git reset --hard HEAD@{5}

(Substitua {5} pela posição que a entrada pré-rebase ocupa no seu reflog.) Seu branch está exatamente como era. Se rebases vivem te colocando nessa situação, nosso guia de rebase interativo sem medo mostra como torná-los rotina em vez de risco.

4. Um commit sumiu depois de um --amend

git commit --amend não edita um commit -- ele cria um novo e move o branch para ele. O original fica inalcançável, mas intacto, e está em HEAD@{1} logo depois do amend. Para inspecioná-lo:

git show HEAD@{1}

Se você fez amend no commit errado ou precisa do original de volta, faça reset para ele ou cherry-pick para onde ele pertence.

5. Você commitou num HEAD detached e depois trocou de lugar

Você fez checkout de um commit ou tag específico, fez commits ali em estado de detached HEAD e depois fez checkout de um branch -- e seus commits aparentemente evaporaram. O Git até te avisa sobre isso na hora da troca, imprimindo o SHA órfão. De qualquer forma, o reflog tem o registro:

git reflog
# encontre o último commit que você fez antes da
# entrada "checkout: moving from <sha> to <branch>"
git branch rescued-work e4f5a6b

Seus commits detached agora vivem num branch de verdade, e você pode fazer merge ou rebase deles para onde precisarem ir.

Inspecione antes de restaurar

Antes de apontar qualquer coisa para um SHA recuperado, olhe para ele. Confirme que é o commit que você acha que é:

git show a1b2c3d

Isso imprime a mensagem do commit, o autor, a data e o diff completo. Se você quiser navegar pelo reflog com os detalhes completos de cada commit em vez de entradas de uma linha, use o modo reflog do log:

git log -g

E aqui vai o hábito mais seguro de todos: em vez de fazer reset do seu branch atual para um commit recuperado, crie antes um branch temporário nele:

git branch rescue a1b2c3d

Agora o trabalho recuperado tem um nome permanente e alcançável. Nada no seu branch atual mudou, a garbage collection nunca poderá tocar nos commits resgatados, e você pode inspecionar, fazer diff e merge com calma. Um branch é de graça; use um sempre que não tiver 100% de certeza.

Os limites, com honestidade

O reflog é uma rede de segurança notável, mas tem fronteiras reais, e é melhor conhecê-las antes de precisar delas:

  • Ele é estritamente local. O reflog vive na sua máquina e nunca passa por push, pull ou clone. Um clone novo começa com o reflog vazio. Se você perdeu commits no seu laptop, o reflog que importa é o do seu laptop -- o clone de um colega não pode ajudar, nem a cópia do servidor.
  • As entradas expiram. Por padrão, entradas do reflog para commits ainda alcançáveis a partir de um branch expiram depois de 90 dias, e entradas de commits inalcançáveis expiram depois de 30 dias. Depois disso, a garbage collection pode apagar os objetos inalcançáveis de verdade. Na prática é tempo de sobra -- mas significa que o reflog resgata o erro do mês passado, não o do ano passado.
  • Ele não recupera o que nunca foi commitado. Mudanças não commitadas destruídas por git reset --hard ou git checkout -- <file> nunca foram objetos do Git, então nenhum diário as registrou. A única exceção parcial: arquivos que pelo menos passaram pelo staging com git add existem como objetos blob, e git fsck --lost-found consegue desenterrar esses blobs soltos para dentro de .git/lost-found/ -- conteúdo sem nomes de arquivo nem histórico, mas melhor que nada. É um último recurso, não um workflow.
  • Stashes descartados têm sua própria saída de emergência. Um stash que você dropou ou limpou não está no reflog do HEAD, mas os commits do stash costumam sobreviver como objetos soltos. git fsck --unreachable | grep commit mais git show nos candidatos podem encontrá-los. Se você usa muito o stash, nosso guia sobre git stash cobre os workflows mais seguros.

Prevenção: torne os próximos pânicos entediantes

Todos os cenários acima tinham um pré-requisito: o trabalho tinha sido commitado. Daí vem o único conselho de prevenção que importa: faça commit cedo e faça commit com frequência. Commits pequenos, toscos, work-in-progress não custam nada -- você pode fazer squash, reescrever as mensagens ou reordená-los depois. No momento em que algo é commitado, está no banco de objetos e o reflog cuida dele. No momento em que não está, você depende de sorte.

Para trocas rápidas de contexto em que um commit parece pesado demais, use o stash em vez de ficar equilibrando mudanças não commitadas entre checkouts -- um stash também é um objeto de verdade que o Git consegue encontrar de novo.

Onde o GitSquid entra (e onde a CLI ganha)

Resposta honesta: o reflog é um daqueles lugares onde a linha de comando é a ferramenta certa. O GitSquid não tem um navegador de reflog, e quando você precisa do git reflog, deve abrir um terminal e usá-lo -- os comandos deste artigo são o caminho de recuperação.

O que o GitSquid faz, em vez disso, é deixar o workflow ao redor mais seguro, para que você precise do reflog com menos frequência. A linha do tempo de arquivos (clique com o botão direito em qualquer arquivo e escolha Show history) te dá um histórico visual por arquivo, o que muitas vezes responde "para onde foi aquele código?" sem nenhuma recuperação. Operações destrutivas como reset e exclusão de branch passam por menus de contexto explícitos no grafo de commits, então você vê exatamente o que está prestes a fazer antes de fazer. E quando você está reconstruindo "o que acabou de acontecer", o log de comandos (Cmd/Ctrl+Shift+L) mostra cada comando Git que o GitSquid executou no seu repositório, com argumentos completos e códigos de saída -- que é exatamente a evidência que você quer ao lado de um reflog na hora de remontar um incidente.

Referência rápida

Situação Comando
Ver por onde o HEAD passou git reflog
Ver por onde a ponta de um branch passou git reflog show <branch>
Desfazer um reset --hard errado git reset --hard HEAD@{1}
Restaurar um branch apagado git branch <name> <sha>
Desfazer um rebase concluído git reset --hard HEAD@{n} (entrada pré-rebase)
Inspecionar um commit recuperado git show <sha>
Navegar pelo reflog com detalhes completos git log -g
Estacionar um resgate em segurança git branch rescue <sha>
Último recurso para arquivos que passaram pelo staging mas nunca foram commitados git fsck --lost-found

O reflog transforma "perdi tudo" em "perdi um ponteiro de vista por dez minutos". Faça commit com frequência, confira o reflog antes de entrar em pânico e estacione as recuperações num branch temporário antes de tocar em qualquer outra coisa. E se você quer um cliente Git que deixa as operações destrutivas explícitas e mantém um log completo de cada comando que executa, baixe o GitSquid grátis -- ele não vai substituir o seu reflog, mas vai te ajudar a precisar menos dele.