Você dá pull na última versão da main e o Git para com CONFLICT (content): Merge conflict in checkout.js. Você resolve o conflito e o log agora mostra um commit chamado Merge branch 'main' into feature/checkout, acima dos três commits que você realmente escreveu. Quatro commits, uma única mudança real. Um colega que ler esse histórico no mês seguinte não consegue saber qual é qual.
git merge e git rebase respondem à mesma pergunta: como eu atualizo meu branch com as mudanças feitas em outro lugar. Eles deixam rastros muito diferentes, e escolher o errado num branch compartilhado causa dano real.
O que merge e rebase fazem de diferente no grafo de commits
Digamos que a main recebeu os commits D, E, F enquanto você trabalhava na feature, que tem seus próprios commits A, B, C, ramificados a partir da ponta antiga da main:
| |
git merge main, executado a partir da feature, cria um novo commit com dois pais: a ponta da feature e a ponta da main. Nada muda em A, B ou C. Mesmos hashes, mesmos pais, mesmo lugar no grafo:
| |
git rebase main, também executado a partir da feature, faz o oposto. Ele deixa a main intocada e reaplica A, B e C um de cada vez em cima de F, gerando um novo commit para cada um — A’, B’, C’ — com hashes e pais novos:
| |
O resultado parece exatamente como se você tivesse ramificado a partir de F desde o início e nunca tivesse divergido. É todo o apelo: um histórico linear, sem commits de merge no log. O custo é que A, B e C deixam de existir para o Git. A’, B’ e C’ os substituem, e quem já tinha dado pull em A, B ou C fica com commits que não batem mais com os seus.
O que é um fast-forward merge e quando o Git o usa no lugar de um commit de merge
Um merge só precisa de um commit de merge quando os dois branches se moveram. Se a feature ramificou a partir de F e a main não se moveu desde então, unir a feature à main não tem nada para conciliar: o Git simplesmente avança o ponteiro da main até C e para. Isso é um fast-forward, e deixa o mesmo histórico achatado que um rebase deixaria:
| |
O Git só cria um commit de merge quando os dois lados adicionaram commits que o outro não tem. Alguns times forçam um mesmo assim, para que cada branch de feature deixe uma marca visível na main no ponto em que foi integrado:
| |
Como limpar seus commits com o rebase interativo
O rebase também pode editar seu próprio histórico, sem envolver nenhum outro branch. git rebase -i abre seus últimos N commits em um editor, uma linha por commit:
| |
| |
Mude a palavra no início de uma linha e o Git age de acordo quando o rebase roda:
| Comando | Efeito |
|---|---|
pick | Mantém o commit como está |
reword | Mantém o commit, edita sua mensagem |
squash | Funde com o commit anterior, combinando as duas mensagens |
fixup | Funde com o commit anterior, descartando esta mensagem |
drop | Remove o commit por completo |
edit | Pausa aqui para você ajustar o commit manualmente |
Transforme essa lista em squash e4f5a6b fix typo e fixup c7d8e9f WIP: still debugging e você acaba com um único commit, Add checkout validation, em vez de três que só fazem sentido na ordem em que você os escreveu. Faça isso antes de abrir o pull request, não depois de alguém já ter dado pull no seu branch — o squash reescreve o hash de cada commit seguinte, como qualquer outro rebase.
Se você vai corrigindo commits anteriores ao longo do caminho, git commit --fixup <hash> seguido de git rebase -i --autosquash <base> move cada fixup para perto do seu alvo e o marca automaticamente, então você nunca precisa reordenar a lista manualmente.
Como git pull --rebase mantém seu branch achatado
A mesma escolha aparece toda vez que você dá pull. Um git pull simples, num branch onde você tem commits locais e o remoto avançou, faz um fetch seguido de um merge. É assim que você acaba com um commit Merge branch 'main' into feature/checkout por nenhum motivo além de um timing ruim. git pull --rebase busca os mesmos commits e reaplica os seus por cima:
| |
Alterações não commitadas fazem o rebase recusar a começar. Adicione --autostash e o Git as guarda de lado, faz o rebase e depois as reaplica:
| |
Configure isso uma vez, em vez de digitar a flag toda vez, por branch ou globalmente:
| |
Isso é seguro porque só reescreve commits que existem exclusivamente no seu branch local. No momento em que isso deixa de ser verdade, o rebase deixa de ser seguro.
A regra de ouro: nunca faça rebase de commits que outra pessoa já baixou
O rebase reescreve os hashes dos commits. Se você faz rebase de um branch cujos commits antigos outra pessoa já tem — ela deu pull no seu branch, ou é a main, ou é qualquer coisa com mais de um colaborador — o histórico dela e o seu agora discordam sobre o que é aquele branch. O Git não tem como saber que seu A e o A’ dela são a mesma mudança lógica. Ele vê dois commits sem relação, e o próximo merge entre as duas cópias duplica tudo o que divergiu.
A regra que evita isso: faça rebase só de branches sobre os quais mais ninguém já construiu nada. Seu próprio branch de feature, antes de você dar a alguém um hash sobre o qual construir, é terreno livre. Um branch que você já deu push e indicou a colegas, a main, qualquer branch compartilhado de longa duração: esses você funde, nunca rebase.
Fazer rebase de um branch que você já deu push exige um push forçado para que o remoto aceite o histórico reescrito:
| |
--force-with-lease recusa o push se o remoto tiver commits que você ainda não buscou — exatamente a situação em que alguém construiu sobre o histórico antigo e um --force puro descartaria o trabalho dessa pessoa silenciosamente. Nunca use --force puro, e só num branch que você tem certeza de que é só seu.
Como se recuperar de um rebase malfeito com git reflog
Um rebase feito sobre a base errada, ou um rebase interativo em que você removeu o commit errado, parece irrecuperável: os commits que você tinha um minuto atrás não aparecem mais no git log. Eles não sumiram. git reflog registra cada posição para a qual HEAD já apontou na sua máquina, rebases incluídos:
| |
| |
HEAD@{3} é para onde seu branch apontava logo antes do rebase começar. Dê reset para esse ponto e você volta exatamente ao estado anterior, commits removidos incluídos:
| |
As entradas do reflog expiram depois de 90 dias por padrão, ou 30 dias para commits que nenhuma outra referência aponta mais. É uma rede de segurança para a última hora ruim, não um armazenamento permanente. Também é só local: nunca sai da sua máquina, e não consegue desfazer uma reescrita que outra pessoa já tenha baixado.
O mesmo instinto — voltar a um estado conhecido e funcional depois de uma mudança ruim — também se aplica fora do histórico do Git. Um Deployment do Kubernetes mantém seu próprio histórico de rollouts pelo mesmo motivo, e kubectl rollout undo faz por um deploy ruim o que git reset --hard contra uma entrada do reflog faz por um rebase ruim.
Git rebase vs merge: qual usar e quando
| Situação | Use |
|---|---|
Atualizar seu branch local, ainda sem push, com a main | rebase (ou pull --rebase) |
| Limpar seus próprios commits antes de abrir um pull request | rebase -i |
Integrar um branch de feature terminado na main | merge (com ou sem --no-ff, conforme a convenção do seu time) |
O branch é compartilhado, já teve push, ou é a própria main | merge — nunca rebase |
Você precisa saber exatamente quando uma feature chegou na main | merge --no-ff — o commit de merge marca isso |
| Você quer um histórico linear sem commits de merge no log | rebase do seu lado, antes de abrir o pull request |
A decisão é, na verdade, sobre quem mais já viu esses commits. Só você? Rebase, e mantenha o histórico limpo. Pelo menos mais uma pessoa? Merge. Um log um pouco mais bagunçado não custa nada; um histórico compartilhado reescrito custa uma tarde inteira a quem já baixou.
Se o seu time ainda não tem uma convenção, comece por aqui: pull.rebase = true localmente, rebase -i para organizar seus commits antes de abrir o pull request, e depois um merge simples para levá-los à main. Isso dá um histórico limpo por branch sem nunca reescrever um commit do qual outra pessoa dependa, CI incluído. Um workflow como Construir e Publicar uma Imagem Docker com GitHub Actions marca cada build com o SHA do commit, e esses SHAs precisam continuar existindo depois que o pipeline já os usou. Configure git config --global pull.rebase true hoje mesmo: sozinho, isso elimina a maioria dos commits de merge acidentais do seu log.