Armadura de titânio: recuperação de vários desastres

Nicola Paolucci
Nicola Paolucci
Voltar para a lista

O git é uma ferramenta avançada. Ele apresenta uma filosofia valiosa para mim: tratar os desenvolvedores como pessoas inteligentes e responsáveis. Isso significa que muita energia está ao seu alcance. O poder de também dar um tiro no próprio pé - sem dúvida com um colete de titânio - mas atirar em si mesmo assim.

O tópico deste post é confirmar o que meu colega Charles O'Farrell pontuou muito bem no popular artigo "Why Git?" há algum tempo:

[...] Na verdade, o Git é a mais segura de todas as opções de DVCS. Como vimos acima, o Git nunca permite que você altere nada, apenas cria novos objetos.

[...] Na verdade, o Git acompanha todas as alterações que você faz, armazenando-as no reflog. Como cada commit é único e imutável, tudo o que o reflog precisa fazer é armazenar uma referência a eles. Ou seja: você está protegido contra danos e seu código está sempre preservado, mas há situações em que você pode precisar de alguma conjuração para recuperá-lo.

Deixe-me dar alguns exemplos do mundo real de como se recuperar de problemas, passando do simples ao avançado:

Sumário

  1. Como desfazer uma redefinição (suave) e recuperar um arquivo excluído
  2. Se você perder um commit durante um rebase interativo
  3. Como desfazer o reset --hard se você apenas fez o staging das alterações

Como desfazer uma redefinição (suave) e recuperar um arquivo excluído

Se você excluir um arquivo com git rm e imediatamente depois usar o comando git reset no seu diretório de trabalho (que é chamado de redefinição suave), você vai ter um arquivo ausente e um diretório de trabalho sujo, como o seguinte:

 On branch main Changes not staged for commit: (use "git add/rm file..." to update what will be committed) (use "git checkout -- file..." to discard changes in working directory) deleted: test.txt

Existem algumas maneiras simples de restaurar o arquivo. Uma é usar um git reset --hard, que vai recriar os arquivos ausentes, mas vai excluir quaisquer modificações locais que você possa ter em seu diretório de trabalho.

A segunda é apenas verificar novamente o arquivo com git checkout test.txt.

Em um cenário um pouco diferente, se o arquivo foi removido em um commit anterior, você pode recuperá-lo anotando o commit exato em que o arquivo foi excluído e usar a referência para escolher o commit imediatamente antes: git checkout commit_id~ --test.txt

Onde o sinal ~ (til) significa o anterior a este.

Se você perder um commit durante um rebase interativo

O rebase interativo é uma das ferramentas mais úteis no arsenal git; ele permite editar, esmagar e excluir commits interativamente.

Pessoalmente, adoro limpar e agilizar os commits antes de compartilhá-los com outras pessoas. É bom quando cada commit é uma parte compreensível e lógica do trabalho. No calor do desenvolvimento, ou em um branch privado local, posso fazer commits com afinco por um tempo e, então, quando fico satisfeito, passo um tempo limpando o histórico.

Mas e se, durante a sessão de rebase interativo, você remover acidentalmente uma linha de commit sem perceber e salvar?

Essa situação é um pouco irritante, porque você acabou de remover um commit do seu histórico!

Se você criou um branch descartável antes de iniciar o rebase – o que você deve sempre fazer – está tudo certo e você pode reiniciar o processo todo. Mas e se você não o fizesse?

Não se preocupe, o git tem o que você precisa. Nunca se perde nada. Esta é uma boa oportunidade para usar o [reflog] [6], que é um mecanismo automático de gravação onde a ponta de cada branch esteve nos últimos 30 dias.

Digamos que no início do rebase eu tinha 4 commits como os seguintes:

 $ git log cfdf880 [2 seconds ago] (HEAD, main) wrote fifth change ab446e6 [34 seconds ago] changed one line 6e1a130 [25 minutes ago] third change 6566977 [26 minutes ago] second change 5feeb33 [26 minutes ago] initial commit

Eu faço rebase dos últimos 4 commits e acidentalmente removo uma linha:

$ git rebase -i HEAD~4

O editor abre em:

pick 6566977 second change pick 6e1a130 third change pick ab446e6 changed one line pick cfdf880 wrote fifth change Rebase 5feeb33..cfdf880 onto 5feeb33

Comandos:

  • p, pick = usar commit
  • r, reword = usar commit, mas editar a mensagem do commit
  • e, edit = usar commit, mas parar para emendar
  • s, squash = usar commit, mas mesclar no commit anterior
  • f, fixup = como "squash", mas descarte a mensagem de log deste commit
  • x, exec = executar comando (o resto da linha) usando shell

Essas linhas podem ser reordenadas; elas são executadas de cima para baixo.

Se você remover uma linha aqui, ESSE COMMIT VAI SER PERDIDO.

No entanto, se você remover tudo, o rebase vai ser abortado.

Observe que os commits vazios não são comentados

Eu removi uma linha de commit (pick ab446e6 changed one line), salvei e obtive isso:

 Successfully rebased and updated refs/heads/main.

Agora, se eu verificar a lista de commits, vejo que perdi um:

 $ git log 3dd6845 [6 minutes ago] (HEAD, main) wrote fifth change 6e1a130 [32 minutes ago] third change 6566977 [32 minutes ago] second change 5feeb33 [32 minutes ago] initial commit

Mas o reflog tem nosso commit perdido:

 $ git reflog 3dd6845 HEAD@{0}: rebase -i (finish): returning to refs/heads/main 3dd6845 HEAD@{1}: rebase -i (pick): wrote fifth change 6e1a130 HEAD@{2}: checkout: moving from main to 6e1a130 cfdf880 HEAD@{3}: commit: wrote fifth change ----}} ab446e6 HEAD@{4}: commit: changed one line [...]

Aqui ab446e6 é o sha-1 para o commit que perdemos. Armados com esse id, podemos simplesmente selecioná-lo de volta no código:

 $ git cherry-pick ab446e6 [main 032c6a6] changed one line 1 file changed, 1 insertion(+), 1 deletion(-)

E como você pode ver, o commit está de volta no nosso histórico:

 $ git log 032c6a6 [12 minutes ago] (HEAD, main) changed one line 3dd6845 [12 minutes ago] wrote fifth change 6e1a130 [37 minutes ago] third change 6566977 [38 minutes ago] second change 5feeb33 [38 minutes ago] initial commit

Como desfazer o reset --hard se você apenas fez o staging das alterações

Vou poupar o discurso sobre fazer commits com frequência — seja em um branch privado de curta duração ou em um compartilhado — para evitar muitos problemas.

Mas digamos que você não fez o commit desta vez e se encontra no seguinte cenário: você fez algum trabalho, não fez o commit ainda, mas usou o comando git added para a área de staging.

Então acontece uma tragédia: você digita git reset --hard e imediatamente percebe que zerou suas alterações locais (!!) e trouxe de volta (na totalidade) o commit anterior.

É uma situação difícil. Como você recupera seu trabalho? Isso é possível? A resposta é: sim! A solução envolve algumas pesquisas de baixo nível e pode ser abordada de algumas maneiras.

Como o git cria novos objetos na pasta .git/objects assim que algo for adicionado à área de staging, podemos procurar os objetos criados mais recentemente:

No OSX (e nos sistemas BSD), você pode fazer:

find .git/objects/ -type f | xargs stat -f "%Sm %N" | sort | tail -5

No Linux (com ferramentas GNU), isso deve funcionar:

find .git/objects/ -type f -printf '%TY-%Tm-%Td %TT %p\n' | sort | tail -5

Com tail -n, você pode cortar para os últimos n resultados se seu repositório for muito grande:

O resultado deve ser algo assim:

Abr 2 12:26:33 2013 .git/objects//pack/pack-4cf657357973915f6fb6f90a41f69a88c0b08bb7.pack Abr 2 12:29:56 2013 .git/objects//3d/d6845a69cd59dac0851e1ae0bd69dde950c46b Abr 2 12:29:56 2013 .git/objects//68/c983f0cefb4bee2f753516ab6352ee2f2b7d29 2 de abril 12:35:23 2013 .git/objects//03/2c6a653cc00c302020293e020d8d68326b112e 2 de abril 15:34:55 2013 .git/objects//df/485610889e98ff773b4440a032ea2ede1338b9

No meu caso, meu arquivo de amostra foi perdido exatamente por volta das 15:34 para que eu possa inspecioná-lo e recuperá-lo com:

show git: df485610889e98ff773b4440a032ea2ede1338b9

Lembre-se de que a pasta faz parte da referência sha-1, então remova a barra / para calcular o id correto.

Observe que o git tem um comando específico de baixo nível para explorar objetos pendentes e verificar sua conectividade: git fsck. Para uma solução alternativa para esse problema usando o fsck, confira esta resposta muito legal no Stack Overflow.

Conclusão

Existem muitos outros exemplos e cenários a serem abordados sobre esse tópico, para que eu possa voltar mais tarde com casos mais interessantes.

Uma coisa a tirar disso deve ser a sensação de que é muito difícil estragar tudo e perder seus dados ao usar o git. Isso é tudo por enquanto!

Siga a incrível equipe @Bitbucket ou eu @durdn para mais agitar o Git.

Pronto(a) para aprender Git?

Tente este tutorial interativo.

Comece agora mesmo