Como Resolver Conflitos de Merge Difíceis no Git Passo a Passo

Resolva conflitos de merge difíceis no Git lendo as versões ours, theirs e base, lidando com renomeações, rebases, binários e testes.

Como Resolver Conflitos de Merge Difíceis no Git Passo a Passo

Um conflito de merge difícil no Git raramente é difícil por causa dos marcadores de conflito em si. É difícil porque você precisa preservar a intenção de duas alterações diferentes ao mesmo tempo. Um branch renomeou uma função enquanto outro alterou seu comportamento. Um branch moveu um arquivo enquanto outro o editou. Um branch alterou uma sequência de migração de banco de dados. O Git pode mostrar a sobreposição, mas não pode decidir o que o software deve significar depois.

Quando um merge para com conflitos, não comece a deletar marcadores imediatamente. Primeiro, oriente-se.

git status

O Git listará caminhos não mesclados. Pode dizer both modified, deleted by us, deleted by them, both added ou algo similar. Essas frases indicam a forma do conflito.

Se o merge parecer errado ou você não estiver pronto para resolvê-lo, cancele antes de fazer mais alterações:

git merge --abort

Para um conflito de rebase, o equivalente é:

git rebase --abort

Isso não é fracasso. É um reset limpo para o estado anterior à operação, que muitas vezes é a jogada mais inteligente quando você percebe que precisa de mais contexto.

Leia o Conflito como Três Versões

Um marcador de conflito normal se parece com isso:

<<<<<<< HEAD
versão do branch atual
=======
versão do branch de entrada
>>>>>>> feature-branch

Durante um merge, HEAD é o branch que você tinha ativo quando executou git merge. O lado inferior é o branch sendo mesclado.

Para conflitos difíceis, use os três estágios que o Git mantém no índice:

git show :1:caminho/para/arquivo   # ancestral comum
git show :2:caminho/para/arquivo   # nosso
git show :3:caminho/para/arquivo   # deles

O ancestral comum é a versão da qual ambos os branches partiram. É útil porque mostra o que cada branch realmente alterou. Sem ele, você pode comparar duas versões finais e perder o motivo por trás delas.

Você também pode usar:

git diff
git diff --ours -- caminho/para/arquivo
git diff --theirs -- caminho/para/arquivo
git diff --base -- caminho/para/arquivo

É aqui que muitos vão rápido demais. O objetivo não é escolher "nosso" ou "deles" como um voto de lealdade à equipe. O objetivo é produzir o arquivo final correto.

Um Fluxo de Trabalho Manual Seguro

Use esta rotina para cada arquivo em conflito:

  1. Abra o arquivo e encontre todos os marcadores de conflito.
  2. Leia o código ao redor, não apenas as linhas marcadas.
  3. Verifique os commits que tocaram o arquivo em ambos os branches.
  4. Edite o arquivo para a versão final pretendida.
  5. Remova todos os marcadores de conflito.
  6. Execute o menor teste relevante ou verificação de build.
  7. Prepare o arquivo.

Comandos úteis ao fazer isso:

git log --oneline --left-right --merge -- caminho/para/arquivo
git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

git diff --check captura problemas de espaçamento residuais. git grep captura marcadores de conflito esquecidos antes que cheguem à CI.

Após resolver um arquivo:

git add caminho/para/arquivo

Quando todos os conflitos estiverem preparados:

git status
git commit

Durante um rebase, use:

git rebase --continue

Quando Ambos os Branches Alteraram a Mesma Função

Este é o caso comum. Suponha que um branch adiciona validação e o outro renomeia um parâmetro:

<<<<<<< HEAD
function createUser(email) {
  return db.users.insert({ email });
}
=======
function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}
>>>>>>> normalize-email

A resposta correta pode combinar ambos:

function createUser(rawEmail) {
  const email = rawEmail.trim().toLowerCase();
  return db.users.insert({ email });
}

Mas apenas se os chamadores foram atualizados para passar rawEmail, e apenas se a normalização ainda for desejada. Procure pela função:

git grep -n 'createUser'

Conflitos difíceis frequentemente exigem verificar arquivos próximos. Um conflito de assinatura de função em um arquivo pode exigir atualizações em testes, rotas, tipos, mocks ou documentação.

Conflitos de Renomeação e Edição

Conflitos de renomeação são irritantes porque o arquivo que você deseja pode não estar onde você espera. Comece com o status:

git status --short

Em seguida, inspecione as informações de nome-status:

git diff --name-status --diff-filter=R

Se um branch renomeou src/user.js para src/account.js e o outro editou src/user.js, você geralmente quer o conteúdo editado aplicado ao novo caminho. Uma ferramenta de merge visual pode ajudar, mas o conceito é simples: preserve a renomeação e preserve as edições significativas.

Depois de decidir o caminho final, remova o caminho obsoleto se necessário e prepare o final:

git rm old/path.js
git add new/path.js

Não prepare ambos os arquivos a menos que o projeto final realmente deva conter ambos.

Deletado por Nós ou Deletado por Eles

Um conflito de delete/modify significa que um branch deletou um arquivo enquanto o outro o alterou. O Git não pode saber se a exclusão tornou a alteração irrelevante.

Se o arquivo deve permanecer deletado:

git rm caminho/para/arquivo

Se o arquivo deve permanecer, escolha a versão que você deseja e prepare-a:

git checkout --theirs caminho/para/arquivo
git add caminho/para/arquivo

ou:

git checkout --ours caminho/para/arquivo
git add caminho/para/arquivo

Tenha cuidado com --ours e --theirs durante o rebase. Em um rebase, os rótulos podem parecer invertidos porque o Git está reproduzindo seus commits em outra base. Quando estiver em dúvida, inspecione os estágios:

git show :2:caminho/para/arquivo
git show :3:caminho/para/arquivo

Conflitos de Arquivos Binários

O Git não pode mesclar a maioria dos arquivos binários. Se dois branches alteraram a mesma imagem, arquivo, documento ou ativo compilado, você tem que escolher uma versão ou criar um novo arquivo manualmente.

Para pegar nossa versão:

git checkout --ours caminho/para/arquivo.bin
git add caminho/para/arquivo.bin

Para pegar a versão deles:

git checkout --theirs caminho/para/arquivo.bin
git add caminho/para/arquivo.bin

Se o binário é gerado, a melhor resposta pode ser regenerá-lo a partir da fonte após resolver os arquivos de texto. Se o binário é um ativo de design ou documento, converse com a pessoa que alterou o outro lado. Adivinhar pode destruir trabalho.

Use uma Ferramenta de Merge Quando o Arquivo For Muito Difícil de Ler

Uma boa ferramenta de merge mostra quatro coisas: a versão base, sua versão, a versão deles e o resultado. Configure uma que você realmente goste. O Visual Studio Code é comum:

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

Depois execute:

git mergetool

Outras equipes preferem Meld, KDiff3, Beyond Compare ou ferramentas integradas à IDE. A ferramenta importa menos do que entender as três versões. Não clique em "aceitar entrada" em um conflito complexo apenas para fazer os marcadores vermelhos desaparecerem.

Após usar um mergetool, verifique se há arquivos de backup como .orig:

git status --short

Você pode desabilitar arquivos de backup do mergetool globalmente se não quiser:

git config --global mergetool.keepBackup false

Opções de Estratégia Não São Mágicas

Você pode ver conselhos como:

git merge -X theirs feature

Isso não significa "substitua meu branch por feature." Significa que quando a estratégia de merge do Git vê pedaços conflitantes, ela deve preferir o outro lado para esses pedaços. Alterações não conflitantes de ambos os branches ainda são mescladas. Isso pode ser útil para lockfiles gerados ou conflitos de formatação mecânicos, mas é arriscado para lógica de negócios.

-X ours e -X theirs são opções de estratégia. A estratégia de merge ours é diferente:

git merge -s ours old-branch

Isso registra um merge enquanto mantém a árvore atual. É uma ferramenta especializada, frequentemente usada para marcar um branch como mesclado sem pegar seu conteúdo. Não use para resolução normal de conflitos a menos que você tenha muita certeza.

Conflitos de Rebase

Durante o rebase, o Git reproduz commits um de cada vez. Isso significa que você pode resolver vários conflitos menores em vez de um grande conflito de merge.

O loop é:

git status
# editar arquivos
git add arquivo-resolvido
git rebase --continue

Se um commit sendo reproduzido não for mais necessário porque a nova base já contém a alteração, use:

git rebase --skip

Use skip com cuidado. Ele descarta esse commit do branch rebaseado. Leia o commit primeiro:

git show

Novamente, --ours e --theirs podem ser confusos no rebase. Inspecione :2: e :3: quando estiver em dúvida.

Teste a Resolução, Não Apenas o Merge

Um merge pode ser sintaticamente resolvido e ainda estar errado. Após preparar os arquivos, execute testes que toquem a área alterada. Para um conflito de frontend, isso pode ser uma verificação de tipos e um teste de componente focado. Para um conflito de backend, pode ser um teste de serviço ou verificação de migração. Para um conflito de lockfile, reinstale as dependências e execute o comando de verificação do gerenciador de pacotes.

No mínimo:

git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'

Depois execute a verificação específica do projeto que teria capturado uma combinação ruim.

Reduza Conflitos Futuros

O melhor conflito é aquele que você nunca cria. Mantenha os branches de curta duração, faça rebase ou merge do branch principal regularmente e evite misturar alterações mecânicas com alterações de funcionalidade. Um PR apenas de formatação não deve também alterar lógica. Uma movimentação de arquivo não deve também reescrever o arquivo se você puder evitar.

Para arquivos que são sempre dolorosos, considere mudanças de propriedade ou estrutura. Grandes arquivos de configuração, snapshots gerados, lockfiles, listas de migração e registros centrais de rotas frequentemente criam conflitos repetidos porque todos editam a mesma área. Às vezes a correção é processo. Às vezes a correção é dividir o arquivo ou gerá-lo a partir de fontes menores.

Use .gitattributes para arquivos que precisam de comportamento de merge especial. Por exemplo, alguns lockfiles gerados podem ter drivers de merge específicos do gerenciador de pacotes. Não invente um casualmente, mas verifique se seu ecossistema tem um driver recomendado.

Conflitos de merge são parte trabalho técnico e parte comunicação. Se você não entender a intenção do outro branch, pergunte. Dez minutos com o autor são mais baratos do que mesclar silenciosamente código que passa nos testes mas remove a funcionalidade que eles estavam construindo.

Lockfiles, Migrações e Outros Arquivos de Alta Fricção

Alguns arquivos entram em conflito com mais frequência porque muitos branches editam a mesma pequena área. Lockfiles de dependência são um exemplo comum. Se dois branches adicionam pacotes, o conflito de lockfile pode ser tecnicamente grande mas conceitualmente simples: regenere-o com o gerenciador de pacotes após resolver o arquivo de manifesto.

Para um projeto Node, isso pode significar resolver package.json, depois executar o gerenciador de pacotes que possui o lockfile:

npm install
# ou pnpm install
# ou yarn install

Depois prepare tanto o manifesto quanto o lockfile. Não edite manualmente um lockfile complexo a menos que você entenda seu formato. O gerenciador de pacotes tem menos probabilidade de cometer um erro sutil no gráfico de dependências.

Migrações de banco de dados precisam de mais cuidado. Se dois branches criam migrações com suposições de ordenação, aceitar ambos os arquivos pode não ser suficiente. Verifique os timestamps das migrações, números de sequência, dependências e se ambas as migrações modificam a mesma tabela ou dados. Às vezes a resolução correta é uma nova migração de acompanhamento que reconcilia os dois branches.

Snapshots gerados e arquivos golden têm o mesmo padrão: resolva a alteração de fonte primeiro, regenere a saída, depois revise o diff gerado. Se o diff gerado for enorme, pergunte se ele pertence ao mesmo commit de merge. Alterações geradas enormes podem esconder uma resolução manual ruim.

Quando um conflito abrange arquivos, escreva o comportamento final pretendido antes de editar. Uma nota curta como "manter a nova validação da funcionalidade A, manter o serviço renomeado da funcionalidade B, regenerar tipos do cliente" evita que você resolva cada arquivo localmente enquanto perde o design geral.

Para merges especialmente arriscados, crie um branch temporário antes de começar:

git switch -c merge-test/main-com-feature
git merge feature

Se a resolução ficar bagunçada, você pode abandonar o branch temporário sem perturbar seu branch original. Esse pequeno hábito torna conflitos difíceis menos estressantes porque você sempre tem um caminho limpo de volta.

Revise o Merge Final Como uma Alteração Própria

Uma resolução de conflito é um trabalho novo. Trate-a assim na revisão. O diff final deve mostrar não apenas as alterações de ambos os branches, mas também qualquer código de cola que você escreveu para fazê-los funcionar juntos. Se o commit de merge for grande, explique a resolução na mensagem do commit ou no comentário do pull request. Os revisores não devem ter que fazer engenharia reversa para entender por que um lado foi escolhido.

Antes de enviar, compare o resultado final com ambos os pais quando possível:

git diff HEAD^1..HEAD -- caminho/para/arquivo
git diff HEAD^2..HEAD -- caminho/para/arquivo

Para um merge não commitado, inspecione as alterações preparadas:

git diff --cached

Procure por exclusão acidental de testes, imports que não são mais usados, entradas de configuração duplicadas e caminhos de código onde ambos os branches adicionaram lógica similar sob nomes diferentes. Esses são os erros que o Git não pode identificar para você.

Se o conflito envolveu comportamento, adicione ou atualize um teste que falharia se você tivesse escolhido o lado errado. Esse teste faz mais do que provar o merge de hoje. Ele protege a decisão de ser desfeita no próximo refatoramento.