Resolvendo conflitos
Um conflito significa que o git encontrou duas alterações no mesmo lugar e se recusa a adivinhar. Uma resolução é uma decisão sobre a intenção, e o padrão seguro é manter a intenção de ambos os lados. Resolver excluindo um lado faz os marcadores desaparecerem e piora o problema: a compilação ainda passa, e a alteração de um colega desaparece.
1. Saiba em qual operação você está — ours e theirs dependem disso
git status # "rebase in progress" / "You have unmerged paths" / "cherry-pick in progress"
| Operação | Lado --ours / HEAD | Lado --theirs |
|---|---|---|
merge | o branch em que você está | o branch sendo mesclado |
rebase | o branch para o qual você está fazendo rebase (upstream) | seu próprio commit sendo reproduzido |
cherry-pick, revert, stash pop | o branch em que você está | o commit sendo aplicado |
A linha do rebase é a armadilha. Durante git rebase main, "ours" é main e "theirs" é o trabalho do próprio usuário, o inverso de um merge. git checkout --theirs file em um rebase mantém sua alteração e descarta a do upstream. Releia esta tabela antes de usar qualquer flag.
2. Veja o conflito completo, não apenas os marcadores
git diff --name-only --diff-filter=U # arquivos não mesclados
git status # o tipo: both modified / deleted by us / both added
git config merge.conflictStyle zdiff3 # mostra o ancestral comum também (git 2.35+; use diff3 em versões antigas)
git checkout --conflict=zdiff3 -- <file> # re-renderiza UM arquivo com a base visível (descarta edições já feitas nele)
Com a base visível, os marcadores têm três partes: seu lado, o original, o lado deles. A maioria dos conflitos aparentes se torna óbvia quando você pode ver do que cada lado mudou a partir de.
Para entender por que cada lado mudou:
git log --merge -p -- <file> # commits de ambos os lados tocando o arquivo (merge ou rebase em andamento)
git show REBASE_HEAD # durante um rebase: o commit sendo reproduzido
git show :1:<file> :2:<file> :3:<file> # base, "ours", "theirs" como arquivos completos
Leia as mensagens de commit. Uma resolução que você não consegue explicar em uma frase por lado é um palpite.
3. Resolva combinando
- Ambos os lados editaram as mesmas linhas por razões diferentes — escreva a versão que faz
ambos. Por exemplo, um lado adicionou um parâmetro
tax_ratee o outro um parâmetrodiscount: a função resolvida recebe ambos e aplica ambos. - Um lado moveu ou renomeou, o outro editou — aplique a edição no novo local. Não mantenha o arquivo antigo e o novo.
- Excluído por um lado, modificado pelo outro — esta é uma decisão real, não mecânica. Descubra por que foi excluído; se a modificação ainda importa, ela deve ser transportada, caso contrário a exclusão vence. Pergunte ao usuário se não estiver claro.
- Arquivos gerados e lockfiles (
package-lock.json,poetry.lock,go.sum, snapshots, saída compilada) — não faça merge manual. Pegue um lado, depois regenere com a ferramenta (npm install,poetry lock,go mod tidy, o comando de snapshot). - Arquivos binários — não há merge; escolha um lado deliberadamente (lembre-se da tabela acima) e informe isso.
O texto pode ser mesclado de forma limpa e ainda assim estar errado: se um lado renomeou uma função e o outro adicionou uma chamada para o nome antigo, o git não vê conflito. É por isso que a etapa 4 inclui compilar.
Flags cegas escondem decisões. git checkout --ours ., -X ours / -X theirs (que
resolvem cada trecho conflitante para um lado) e -s ours (que descarta o outro branch
inteiramente) cada um joga trabalho fora sem olhar para ele. Use-os apenas quando o usuário
pediu exatamente isso e sabe o que está sendo descartado.
4. Verifique, depois continue
git diff --check # falha em marcadores de conflito restantes e erros de espaço em branco
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # cinto e suspensórios, em arquivos que o git não verifica diff
# compile e execute os testes relevantes mais rápidos
git add <arquivos-resolvidos>
Continue com o comando correspondente. Adicione GIT_EDITOR=true onde um editor abriria, já que
um editor trava em um shell não interativo:
git merge --continue # ou: git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
Um rebase reproduz commits um por um, então pode parar novamente no próximo commit. Repita o ciclo. Resolva no commit mais antigo onde um conflito aparece; commits posteriores podem ser o mesmo conflito se repetindo.
Se os testes falharem após a resolução, a falha é uma informação sobre a resolução. Não continue além dela e não "corrija" editando testes até saber de qual lado comportamento o teste está protegendo.
5. Quando parar e perguntar
Pare em vez de adivinhar quando:
- ambos os lados mudaram a lógica de negócios de maneiras incompatíveis (duas regras fiscais diferentes, duas políticas de repetição diferentes);
- você não consegue dizer em uma frase por que cada lado fez sua alteração;
- a resolução faz um teste falhar e a causa não está clara.
Declare as duas intenções, diga o que você escolheria e por quê, e deixe o usuário decidir. Tudo
antes de --continue é reversível: git merge --abort, git rebase --abort,
git cherry-pick --abort retornam a árvore para onde começou.
Conflitos repetidos
Para um rebase longo onde o mesmo conflito ocorre novamente, ou um branch que é repetidamente mesclado e abandonado:
git config rerere.enabled true # registra cada resolução e a reproduz na próxima vez
git rerere diff # o que foi registrado
git rerere forget-path <file> # descarta uma resolução registrada que estava errada
rerere reaplica uma resolução sem perguntar, então uma resolução ruim se propaga. Após um
conflito resolvido por rerere, ainda verifique o resultado.
Quando um branch é antigo e muito desatualizado, mesclar o tronco uma vez geralmente significa uma rodada de conflitos; rebase os reproduz por commit. Essa escolha é do usuário.
Barreiras de proteção
- Nunca resolva escolhendo um lado por completo para fazer os conflitos desaparecerem. O outro lado é trabalho de alguém.
- Não aborte um rebase que o usuário pediu para você terminar só porque é tedioso; aborte apenas se estiver dando errado, e informe isso.
- Não faça push após resolver a menos que solicitado. Um rebase precisa de um force-push; siga
git-safe-history-rewrite. - Relate cada arquivo em conflito: o que ambos os lados fizeram, o que você manteve e qualquer coisa que não pôde verificar.