Skip to content
This page has been auto-translated and may contain errors.View in English

Mesclando branches e resolvendo conflitos

docs.scrimba.com

Sua branch de feature está pronta. O toggle funciona, você fez commit das mudanças, e agora quer que esse trabalho esteja em main onde o código do resto do time fica. Na maioria das vezes trazer isso é rápido e tranquilo: Git compara as duas branches, não encontra nada que se sobrepõe, e incorpora o trabalho sem passos extras. Às vezes, porém, você e um colega de trabalho mudaram exatamente as mesmas linhas em branches diferentes, e Git não consegue adivinhar qual versão você quer manter. Essa situação aparece em quase todo projeto com mais de uma pessoa fazendo commit nele. É uma parte normal e cotidiana do branching, e significa que Git encontrou duas ideias sobre as mesmas linhas e quer sua decisão sobre qual vence.

Trazendo uma branch de volta com git merge

Digamos que você construiu o toggle Fahrenheit em sua própria branch, add-fahrenheit-toggle, e main não se moveu desde que você a criou. Mude para main e execute git merge com o nome da branch que quer trazer:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Updating 4f2a891..8c3d1a0
Fast-forward
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git merge <branch> sempre mescla a branch nomeada na branch em que você está no momento, então mudar para main primeiro é importante. A branch em que você está é a que recebe o merge. Após isso rodar, main tem todos os commits de add-fahrenheit-toggle. A branch de feature em si fica intocada: você pode continuar trabalhando nela ou deletá-la agora que seu trabalho vive em main também.

Aquela linha Fast-forward na saída vale a pena ler com atenção. Significa que main não ganhou nenhum novo commit desde que você a criou, então Git não precisou combinar nada: moveu o rótulo main para frente para apontar para o mesmo commit que add-fahrenheit-toggle já apontava. Um merge fast-forward é Git alcançando um rótulo onde ele já precisava estar, nada mais.

Se main tivesse pegado outros commits enquanto você estava trabalhando, digamos que um colega mesclou uma correção de bug enquanto isso, Git não consegue mais mover o rótulo para frente, porque main e sua branch foram em direções diferentes desde que se separaram. Em vez disso, cria um novo commit que une as duas histórias, chamado merge commit:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Merge made by the 'ort' strategy.
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git log --graph --oneline após um merge assim mostra a bifurcação e a junção: duas linhas de história correndo lado a lado, se encontrando no merge commit. Nenhum resultado exige nada extra de você. Git decide qual se aplica baseado em se as branches divergiram, e de qualquer forma sua feature agora faz parte de main.

Ambos os resultados acima descansam na mesma ideia embaixo: o merge 3-way. Para combinar duas branches, Git compara mais do que suas duas pontas. Primeiro encontra o commit que ambas as branches compartilham como ponto de partida comum, a base de merge, depois olha para três snapshots: a base, e a ponta de cada branch. Comparar cada ponta contra a base compartilhada diz ao Git exatamente quais linhas cada lado mudou, o que permite combinar dois conjuntos de edições automaticamente em vez de pedir a você cada vez.

Um fast-forward é o caso onde um desses três snapshots acaba sendo redundante: a base e uma ponta são o mesmo commit, então não há nada naquele lado para combinar, e Git move o rótulo sem criar nada novo. Um merge commit é o caso geral, onde ambos os lados mudaram algo desde a base, e Git registra um novo snapshot com dois pais, um apontando de volta para o histórico de cada branch. Todo conflito de merge que você encontra na próxima seção vem dessa mesma comparação 3-way encontrando que ambos os lados tocaram as mesmas linhas, sem jeito de combiná-las por conta própria.

JunoTrazendo uma branch de volta com git mergegit merge branch-name traz os commits daquela branch para a branch em que você está no momento, então mude para main primeiro. A maioria dos merges acontece silenciosamente no fundo e você continua seu dia. Nada sobre a branch de feature em si muda; você pode continuar usando-a ou deletá-la uma vez que main tenha o trabalho.
JunoTrazendo uma branch de volta com git merge Um fast-forward significa que main não havia se movido, então Git só deslizou o rótulo para frente. Uma vez que main pegue outros commits enquanto isso, fazer merge cria um merge commit real com dois pais em vez disso. De qualquer forma o comando é o mesmo git merge branch-name; Git decide qual tipo de merge se encaixa.
JunoTrazendo uma branch de volta com git merge Um merge 3-way compara o commit base compartilhado contra cada ponta de branch para trabalhar o que mudou em cada lado, o que torna combinar duas branches automático na maioria das vezes. Fast-forward é o caso especial onde a base e uma ponta já combinam. Eu ainda me pego parando para trabalhar qual commit conta como base em um gráfico bagunçado, então desenhe se tiver dúvida.

Como um conflito de merge se parece

Um conflito acontece quando as mesmas linhas mudam nos dois lados de um merge, e Git não tem jeito de dizer qual versão você quer. Digamos que um colega mesclou um spinner de carregamento em main que mudou uma função em src/index.js, e sua branch add-fahrenheit-toggle mudou a mesma função para adicionar o toggle. Fazer merge agora para no meio em vez de terminar:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js
Automatic merge failed; fix conflicts and then commit the result.

Esse é um conflito de merge. Abra o arquivo e você encontrará que Git marcou exatamente onde as duas versões discordam:

<<<<<<< HEAD
  showLoadingSpinner(true);
=======
  const tempF = celsiusToFahrenheit(tempC);
>>>>>>> add-fahrenheit-toggle

<<<<<<< HEAD marca o início do que está atualmente em sua branch (main, neste caso). ======= divide as duas versões. >>>>>>> add-fahrenheit-toggle marca o fim da versão da branch que vem entrando. Tudo entre os marcadores é o mesmo punhado de linhas, escrito de duas formas diferentes.

Um conflito significa que Git precisa de seu julgamento; nada está quebrado. Git encontrou duas mudanças nas mesmas linhas e não tem jeito de adivinhar qual você quer, então pausa e pede que você decida. Edite o arquivo até ele ler do jeito que você quer, mantendo uma versão, ambas, ou algo novo que as combine, depois delete as linhas de marcador em si:

  showLoadingSpinner(true);
  const tempF = celsiusToFahrenheit(tempC);

Uma vez que o arquivo pareça certo, faça stage e commit exatamente como qualquer outra mudança:

bash
$ git add src/index.js
$ git commit

Rodar git commit sem mensagem aqui abre seu editor com uma mensagem que Git já preparou, descrevendo o merge. Salve e feche, e o merge está completo.

Enquanto um conflito está aberto, git status é a primeira coisa que vale a pena checar. Lista cada arquivo ainda precisando de atenção sob "Unmerged paths", então em um merge maior com vários arquivos em conflito você sabe exatamente quantos faltam corrigir:

bash
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   src/index.js

git add diz ao Git que o conflito de um arquivo foi resolvido. Não verifica que você realmente removeu os marcadores, então leia o arquivo antes de fazer stage dele. Se o merge parece mais bagunçado do que esperava, ou você escolheu a branch errada, git merge --abort volta completamente e retorna seu diretório de trabalho para como parecia antes de você rodar git merge, sem nada deixado pela metade.

Conflitos não são únicos para git merge. Rebase é a outra forma de trazer uma linha de histórico atualizada com outra, e atinge o mesmo tipo de choque, com um resultado diferente uma vez que é resolvido. Um merge une duas histórias com um merge commit que tem dois pais, preservando o fato de que uma branch existiu e exatamente quando se reincorporou. Um rebase em vez disso toca novamente seus commits de branch, um de cada vez, na ponta da outra branch, produzindo uma linha reta de histórico sem merge commit e sem traço de que as duas jamais divergiram:

bash
$ git switch add-fahrenheit-toggle
$ git rebase main

O trade-off é real. Uma histórico linear de rebase lê limpo em git log, um commit após outro, o que alguns times preferem para uma história arrumada. Um merge mantém a verdadeira forma do que aconteceu, incluindo o ponto exato onde uma branch de feature se reincorporou em main, o que alguns times preferem pela precisão. Nenhuma escolha está errada; escolha uma convenção por time e mantenha consistência.

Uma regra se mantém independente de qual convenção você escolhe: nunca faça rebase de uma branch que outras pessoas já fizeram pull ou construíram trabalho em cima. Rebase reescreve commits em novos com novos IDs, e se o histórico de uma branch compartilhada muda embaixo de um colaborador, sua cópia local e a remota reescrita não combinam mais, forçando uma correção manual do lado deles. Faça rebase livremente em sua própria branch antes que ninguém mais tenha feito pull dela. Uma vez que é compartilhada, use merge.

JunoComo um conflito de merge se parece Um conflito de merge significa que duas branches mudaram as mesmas linhas, e Git precisa que você escolha o resultado. Abra o arquivo, procure por <<<<<<<, =======, e >>>>>>>, edite até ler como você quer, depois delete essas linhas de marcador. Termine com git add e git commit. Meu primeiro conflito pareceu uma emergência; acabou sendo Git me fazendo uma pergunta bem ordinária.
JunoComo um conflito de merge se parecegit status lista cada arquivo não resolvido sob unmerged paths, útil no momento que um conflito toca mais de um. git add marca um arquivo resolvido sem checar seu trabalho, então leia-o antes de fazer stage. Se um merge vai de lado, git merge --abort o coloca de volta em um estado limpo sem nada deixado pela metade.
JunoComo um conflito de merge se parece Rebase encontra os mesmos choques que merge; toca novamente commits em uma base nova em vez de unir históricos, trocando uma forma de branch preservada por uma linha reta. Mantenha esse trade para branches apenas que você está usando. No momento uma branch é compartilhada, reescrever seus commits com rebase torna a vida difícil para quem já fez pull dela, então recorra a merge em vez disso.

Trazendo de volta junto

Mergear é o que torna branching a pena fazer: você experimenta seguro em sua própria linha de histórico, coberto em Branches, depois dobra esse trabalho de volta em main uma vez que está pronto, conflitos e tudo. Em GitHub, a mesma operação geralmente acontece através de um pull request em vez de um git merge local na sua máquina; o capítulo fluxo de pull request cobre propor, revisar, e mesclar uma mudança em um projeto compartilhado. Se um merge pousar em algum lugar que você não pretendia após já ter feito commit, desfazendo coisas cobre como voltar seguro.