Git · 40 min

Desembarace um Histórico Git Bagunçado

Pegue um repositório cheio de erros reais — um segredo commitado, um merge ruim, um commit errado — e conserte com segurança usando restore, revert, reset, rebase interativo e reflog. O complemento prático do Git Essentials.

Problema

Todo mundo consegue commitar num dia tranquilo. A habilidade que separa um desenvolvedor confiante é o que você faz quando o histórico está uma bagunça: você commitou uma senha, o último commit estava errado, um merge deu ruim e você tem medo de que "consertar" vá destruir trabalho. Neste lab você vai criar de propósito um repositório com esses exatos problemas e então reparar cada um com a ferramenta certa — aprendendo a diferença crucial entre `restore`, `revert` e `reset`, resolvendo um conflito de merge real na mão, limpando commits locais com um rebase interativo e resgatando trabalho "perdido" com `reflog`. Você vai terminar sabendo que, no Git, quase nada é de fato irrecuperável — se você souber onde procurar.

Objetivos

Pré-requisitos

O que você vai construir

Você vai construir um repositório quebrado de propósito e então consertá-lo. Cada problema mapeia
para uma habilidade de recuperação do Git, então no fim você terá uma árvore de decisão mental:
"a mudança não foi commitada → restore; o commit ruim já foi enviado → revert; é só local → reset
ou rebase; perdi um commit → reflog."

A regra de ouro de reescrever o histórico

Uma ideia governa tudo abaixo:

Reescreva livremente o que você não compartilhou. Nunca reescreva o que outros possam ter puxado.

reset e rebase reescrevem o histórico — mudam os IDs dos commits. Isso é perfeito para limpar
seu próprio trabalho local antes de enviar, e perigoso num branch compartilhado (força todo mundo a
conflitos). revert é o oposto: adiciona um novo commit que desfaz um antigo, então o histórico é
preservado e seguro de compartilhar. Manter essa linha clara é todo o ponto do lab.

Como trabalhar neste lab

Rode cada comando e leia a saída — especialmente git status e git log entre os passos, para
você ver cada reparo fazer efeito. Faça os passos em ordem; cada um monta o cenário para o próximo.

Passos

  1. Construa o repositório quebrado

    Você vai criar a bagunça você mesmo para ela ser reproduzível. Rode estes comandos numa pasta vazia.

    mkdir git-rescue && cd git-rescue
    git init
    echo "# Notes App" > README.md
    git add README.md && git commit -m "Initial commit"
    
    echo "print('hello')" > app.py
    git add app.py && git commit -m "Add app"
    
    # Erro A: commitar um segredo
    echo "API_KEY=sk-live-supersecret-123" > config.env
    git add config.env && git commit -m "Add config"
    
    # Erro B: uma mensagem de commit ruim + arquivo lixo
    echo "temporary junk" > scratch.tmp
    git add scratch.tmp && git commit -m "asdfasdf"
    
    echo "print('goodbye')" >> app.py   # uma mudança não commitada que você vai decidir o que fazer
    

    Veja a bagunça

    git log --oneline --graph
    git status
    

    Você agora tem quatro commits e uma mudança não commitada. Os problemas:

    • config.env com um segredo está no histórico.
    • O último commit tem uma mensagem lixo (asdfasdf) e adiciona um arquivo lixo scratch.tmp.
    • Há uma edição não commitada em app.py sobre a qual você ainda não decidiu.

    Mantenha esta janela aberta — você vai consertar isso um de cada vez.

    Entregável deste passo: a saída de git log --oneline mostrando a bagunça de quatro commits.

  2. Desfaça mudanças não commitadas com restore

    O caso mais fácil primeiro: uma mudança que você não commitou e agora quer descartar.

    Descarte uma mudança do worktree

    Você adicionou print('goodbye') ao app.py e desistiu. Ela não está no stage, então:

    git status                 # app.py aparece como "modified", fora do stage
    git restore app.py         # descarta a mudança na árvore de trabalho
    git status                 # limpo de novo; app.py voltou ao estado commitado
    

    git restore <arquivo> sobrescreve o arquivo com sua versão do último commit. Isso é
    destrutivo
    para trabalho não commitado — não há desfazer, porque o Git nunca gravou aquela
    edição. Use só quando você realmente quer a mudança fora.

    Tire do stage sem perder trabalho

    A outra metade do restore cuida de arquivos no stage. Suponha que você adiciona algo por acidente:

    echo "draft" > draft.txt
    git add draft.txt          # agora no stage
    git restore --staged draft.txt   # tira do stage, mas MANTÉM o arquivo e o conteúdo
    git status                 # draft.txt voltou a "untracked", não sumiu
    

    A distinção para lembrar:

    • git restore <arquivo> → descarta mudanças do worktree (destrutivo).
    • git restore --staged <arquivo> → tira um arquivo da área de stage, mantendo suas edições.

    Apague o arquivo solto para a árvore ficar limpa: rm draft.txt.

    Entregável deste passo: a saída de git status mostrando a árvore limpa após o restore.

  3. Reverta um commit ruim: revert vs reset

    Agora os erros já commitados. Há duas filosofias, e escolher certo é a habilidade central.

    revert — seguro, mantém o histórico (use para commits compartilhados)

    O segredo em config.env é um erro commitado. Se este branch já tivesse sido enviado, você
    não pode reescrever o histórico. git revert cria um novo commit que desfaz o alvo:

    git log --oneline                 # ache o hash de "Add config"
    git revert <hash-do-add-config>   # abre um editor para a mensagem do revert; salve
    git log --oneline                 # o commit original continua lá + um novo commit "Revert ..."
    

    O arquivo config.env agora é removido pelo novo commit, mas o commit antigo ainda existe no
    histórico. (Na vida real um segredo vazado também precisa ser rotacionado — reverter o
    esconde do HEAD, não do passado do repo. Mais sobre isso na reflexão.)

    reset — reescreve o histórico (só para commits locais, não compartilhados)

    O commit lixo asdfasdf (com scratch.tmp) ainda é só local, então você pode reescrevê-lo
    para fora com limpeza. Mova o ponteiro do branch para trás, mantendo as mudanças fora do stage:

    git reset --soft HEAD~1    # desfaz o último commit, MANTÉM as mudanças no stage
    # ou:
    git reset HEAD~1           # (mixed, o padrão) desfaz o commit, mantém as mudanças FORA do stage
    

    Após um reset mixed, scratch.tmp volta como arquivo untracked — apague-o e ele some do
    histórico pretendido:

    rm scratch.tmp
    git status                 # limpo; o commit ruim sumiu
    git log --oneline
    

    Os três sabores do reset, que vale memorizar:

    Comando Commit desfeito Stage Árvore de trabalho
    reset --soft sim mantido no stage intocada
    reset (mixed) sim fora do stage intocada
    reset --hard sim descartado sobrescrita (destrutivo)

    Regra prática: revert em tudo que já foi enviado; reset só em commits locais.

    Entregável deste passo: git log --oneline após o revert + reset, mostrando o segredo revertido e o commit lixo fora.

  4. Resolva um conflito de merge real

    Conflitos assustam porque interrompem um merge no meio. Crie um de propósito e finalize-o.

    Monte dois branches divergentes

    git switch -c feature-title
    # mude a mesma linha que o branch main também vai mudar
    echo "print('hello from feature')" > app.py
    git commit -am "Feature: muda a saudação"
    
    git switch main
    echo "print('hello from main')" > app.py
    git commit -am "Main: muda a saudação"
    

    Os dois branches editaram a mesma linha de app.py. Agora faça o merge:

    git merge feature-title
    

    O Git para com CONFLICT (content): Merge conflict in app.py. Isso não é um erro — é o Git
    pedindo que você decida, porque ele não tem como saber qual versão está certa.

    Leia e resolva os marcadores

    Abra app.py. Você verá os marcadores de conflito:

    <<<<<<< HEAD
    print('hello from main')
    =======
    print('hello from feature')
    >>>>>>> feature-title
    
    • <<<<<<< HEAD … ======= → seu branch atual (main).
    • ======= … >>>>>>> feature-title → o branch que está entrando.

    Edite o arquivo para o conteúdo final que você quer — mantenha um lado, o outro, ou combine — e
    apague as três linhas de marcador. Por exemplo:

    print('hello from main and feature')
    

    Depois marque como resolvido e complete o merge:

    git add app.py
    git commit           # completa o merge (a mensagem padrão de merge serve)
    git log --oneline --graph   # veja o commit de merge juntando as duas linhas de histórico
    

    Se quiser desistir no meio do conflito, git merge --abort te devolve ao estado antes do merge.

    Entregável deste passo: o conteúdo resolvido de app.py e o git log --graph mostrando o merge.

  5. Limpe commits locais com rebase interativo, e recupere com reflog

    Duas ferramentas poderosas para fechar: polir o histórico local antes de compartilhar, e resgatar trabalho perdido.

    Rebase interativo — arrume seus próprios commits

    Faça alguns commits pequenos e bagunçados num branch:

    git switch -c cleanup-demo
    echo "line 1" > notes.txt && git commit -am "wip"
    echo "line 2" >> notes.txt && git commit -am "more wip"
    echo "line 3" >> notes.txt && git commit -am "fix typo"
    

    Três commits barulhentos que deveriam ser um só. Reescreva-os antes de alguém ver:

    git rebase -i HEAD~3
    

    Um editor abre listando os três commits com pick na frente de cada. Para combiná-los, mantenha
    o primeiro como pick e mude os outros para squash (ou s):

    pick   <hash> wip
    squash <hash> more wip
    squash <hash> fix typo
    

    Salve e feche; o Git então te deixa escrever uma mensagem combinada limpa (ex.: Add notes).
    Use reword no lugar de squash se você só quer corrigir uma mensagem. Confirme:

    git log --oneline    # os três commits agora são um
    

    Lembre da regra de ouro: isso reescreve os IDs dos commits, então só faça em commits que você não enviou.

    reflog — recupere um commit "perdido"

    Rebase e reset movem ponteiros de branch; os commits antigos não são deletados na hora.
    reflog é o diário privado do Git de todo lugar por onde o HEAD passou. Simule um desastre e recupere:

    git reset --hard HEAD~1     # "ops, destruí meu último commit"
    git log --oneline           # sumiu do branch
    git reflog                  # cada movimento do HEAD, cada um com um hash
    # ache a linha do commit que você perdeu, então:
    git reset --hard <hash-do-reflog>   # ou: git cherry-pick <hash>
    git log --oneline           # recuperado
    

    Essa é a rede de segurança que torna o Git perdoável: enquanto um commit existiu, o reflog
    geralmente consegue te levar de volta a ele por semanas, mesmo depois de um reset --hard.

    Entregável deste passo: o git log --oneline após o squash, mais um trecho de git reflog mostrando a recuperação.

  6. Entregue: documente seu resgate

    Capture o antes/depois e seu raciocínio num SUBMISSION.md dentro do repo.

    Monte sua entrega

    # Lab de Resgate Git — <seu nome>
    
    ## 1. A bagunça (Passo 1)
    <cole o `git log --oneline` inicial com os quatro commits ruins>
    
    ## 2. restore
    O que descartei e a árvore limpa no `git status` depois.
    
    ## 3. revert vs reset
    - O commit do segredo que revertí (hash) e por que usei revert.
    - O commit lixo que resetei e por que o reset foi seguro aqui.
    <cole `git log --oneline` após os dois>
    
    ## 4. Conflito de merge
    A linha final resolvida de app.py, e como eu a escolhi.
    
    ## 5. Rebase + reflog
    <cole `git log --oneline` após o squash>
    <cole a linha do `git reflog` que usei para recuperar>
    
    ## Reflexão (3-4 frases)
    Nas minhas palavras: quando usar revert vs reset, e por que o reflog
    significa que quase nada no Git é de fato perdido.
    

    Entregue

    git add SUBMISSION.md
    git commit -m "Entrega do lab de resgate Git"
    # faça push para um repo público ou gist, depois entregue essa URL
    

    Critério de submissão (autoverificação)

    • Você usou git restore (worktree) e git restore --staged e sabe explicar a diferença
    • Você reverteu o commit do segredo com git revert (histórico preservado)
    • Você removeu o commit lixo com git reset e explicou por que foi seguro (só local)
    • Você resolveu um conflito de merge real editando marcadores e completando o merge
    • Você fez squash de 3 commits em 1 com git rebase -i
    • Você recuperou um commit resetado com --hard usando git reflog
    • Sua reflexão diz corretamente a regra revert-vs-reset

    O que vem a seguir

    Você já consegue reparar quase qualquer problema de histórico local com confiança. No Git
    Advanced
    você leva isso além — stash, worktrees, hooks e recuperação de conflitos complexos.