Como o 'git merge' funciona nos detalhes?

97

Eu quero saber um algoritmo exato (ou perto disso) por trás do 'git merge'. As respostas, pelo menos a essas subquestões, serão úteis:

  • Como o git detecta o contexto de uma mudança não conflitante em particular?
  • Como o git descobre que há um conflito nessas linhas exatas?
  • Quais coisas o git mescla automaticamente?
  • Como o git funciona quando não há uma base comum para mesclar branches?
  • Como o git funciona quando há várias bases comuns para mesclar branches?
  • O que acontece quando mesclo várias ramificações de uma vez?
  • Qual é a diferença entre estratégias de mesclagem?

Mas a descrição de todo um algoritmo será muito melhor.

abismo 7
fonte
8
Eu acho que você poderia preencher um livro inteiro com essas respostas ...
Daniel Hilgarth
2
Ou você pode simplesmente ir e ler o código, o que levaria tanto tempo quanto "descrever todo o algoritmo"
Nevik Rehnel
3
@DanielHilgarth Eu ficaria feliz em saber, se já existe esse livro em algum lugar. Referências são bem-vindas.
abyss.7 de
5
@NevikRehnel Sim, posso. Mas pode ficar muito mais fácil, se alguém já conhece a teoria por trás deste código.
abyss.7 de
1. Qual é o "contexto de uma mudança não conflitante em particular"? Os pontos 2. e 3. são iguais, mas negados, vamos mesclar essas duas perguntas?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respostas:

67

Talvez seja melhor você procurar uma descrição de um algoritmo de mesclagem de 3 vias. Uma descrição de alto nível seria mais ou menos assim:

  1. Encontre uma base de mesclagem adequada B- uma versão do arquivo que é ancestral de ambas as novas versões ( Xe Y), e geralmente a mais recente dessa base (embora haja casos em que terá que voltar mais longe, que é um dos os recursos de mesclagem gitpadrão recursive)
  2. Execute diferenças de Xcom Be Ycom B.
  3. Percorra os blocos de mudança identificados nas duas diferenças. Se ambos os lados introduzirem a mesma mudança no mesmo local, aceite qualquer um; se um introduz uma mudança e o outro deixa essa região em paz, introduza a mudança no final; se ambos introduzirem mudanças em um ponto, mas não corresponderem, marque um conflito para ser resolvido manualmente.

O algoritmo completo lida com isso com muito mais detalhes e ainda tem alguma documentação ( https://github.com/git/git/blob/master/Documentation/technical/trivial-merge.txt para um, junto com as git help XXXpáginas , onde XXX é um dos merge-base, merge-file, merge, merge-one-filee possivelmente alguns outros). Se isso não for profundo o suficiente, sempre há código-fonte ...

Twalberg
fonte
11

Como o git funciona quando há várias bases comuns para mesclar branches?

Este artigo foi muito útil: http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html (aqui está a parte 2 ).

Recursivo usa diff3 recursivamente para gerar uma ramificação virtual que será usada como ancestral.

Por exemplo:

(A)----(B)----(C)-----(F)
        |      |       |
        |      |   +---+
        |      |   |
        |      +-------+
        |          |   |
        |      +---+   |
        |      |       |
        +-----(D)-----(E)

Então:

git checkout E
git merge F

Existem 2 melhores ancestrais comuns (ancestrais comuns que não são ancestrais de nenhum outro) Ce D. O Git os mescla em um novo branch virtual Ve os usa Vcomo base.

(A)----(B)----(C)--------(F)
        |      |          |
        |      |      +---+
        |      |      |
        |      +----------+
        |      |      |   |
        |      +--(V) |   |
        |          |  |   |
        |      +---+  |   |
        |      |      |   |
        |      +------+   |
        |      |          |
        +-----(D)--------(E)

Suponho que o Git apenas continuaria com o se houvesse mais ancestrais comuns melhores, se fundindo Vcom o próximo.

O artigo diz que se houver um conflito de mesclagem durante a geração do branch virtual, o Git apenas deixa os marcadores de conflito onde estão e continua.

O que acontece quando mesclo várias ramificações de uma vez?

Como @Nevik Rehnel explicou, depende da estratégia, está bem explicado na man git-merge MERGE STRATEGIESseção.

Apenas octopuse ours/ theirssuporta mesclar vários ramos de uma vez, recursivepor exemplo, não.

octopusrecusa-se a mesclar se houver conflitos, e oursé uma mesclagem trivial, portanto não pode haver conflitos.

Esses comandos geram um novo commit que terá mais de 2 pais.

Eu fiz um merge -X octopusno Git 1.8.5 sem conflitos para ver como funciona.

Estado inicial:

   +--B
   |
A--+--C
   |
   +--D

Açao:

git checkout B
git merge -Xoctopus C D

Novo estado:

   +--B--+
   |     |
A--+--C--+--E
   |     |
   +--D--+

Como esperado, Etem 3 pais.

TODO: exatamente como o polvo opera em modificações de um único arquivo. Mesclagens de três vias recursivas dois por dois?

Como o git funciona quando não há uma base comum para mesclar branches?

@Torek menciona que desde o 2.9, a mesclagem falha sem --allow-unrelated-histories.

Eu tentei empiricamente no Git 1.8.5:

git init
printf 'a\nc\n' > a
git add .
git commit -m a

git checkout --orphan b
printf 'a\nb\nc\n' > a
git add .
git commit -m b
git merge master

a contém:

a
<<<<<<< ours
b
=======
>>>>>>> theirs
c

Então:

git checkout --conflict=diff3 -- .

a contém:

<<<<<<< ours
a
b
c
||||||| base
=======
a
c
>>>>>>> theirs

Interpretação:

  • a base está vazia
  • quando a base está vazia, não é possível resolver nenhuma modificação em um único arquivo; apenas coisas como a adição de um novo arquivo podem ser resolvidas. O conflito acima seria resolvido em uma fusão de 3 vias com base a\nc\ncomo uma adição de linha única
  • Eu acho que uma mesclagem de 3 vias sem um arquivo de base é chamada de mesclagem de 2 vias, que é apenas uma diferença
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte
1
Há um novo link do SO para esta pergunta, então eu examinei esta resposta (que é muito boa) e notei que uma mudança recente no Git desatualizou um pouco a última seção. Desde a versão 2.9 do Git (confirmação e379fdf34fee96cd205be83ff4e71699bdc32b18), o Git agora se recusa a mesclar se não houver uma base de mesclagem a menos que você adicione --allow-unrelated-histories.
Torek em
1
Aqui está o artigo de acompanhamento daquele postado por @Ciro
adam0101 01 de
A menos que o comportamento tenha mudado desde a última vez que tentei: --allow-unrelated-historiespode ser omitido se não houver caminhos de arquivo comuns entre as ramificações que você está mesclando.
Jeremy List
Pequena correção: há oursestratégia de fusão, mas nenhuma theirsestratégia de fusão. recursive+ theirsestratégia só pode resolver dois ramos. git-scm.com/docs/git-merge#_merge_strategies
nekketsuuu
9

Também estou interessado. Não sei a resposta, mas ...

Um sistema complexo que funciona invariavelmente evoluiu a partir de um sistema simples que funcionou

Acho que a fusão do git é altamente sofisticada e será muito difícil de entender - mas uma maneira de abordar isso é partindo de seus precursores e focar no centro de sua preocupação. Ou seja, dados dois arquivos que não têm um ancestral comum, como git merge funciona como mesclá-los e onde estão os conflitos?

Vamos tentar encontrar alguns precursores. De git help merge-file:

git merge-file is designed to be a minimal clone of RCS merge; that is,
       it implements all of RCS merge's functionality which is needed by
       git(1).

Da wikipedia: http://en.wikipedia.org/wiki/Git_%28software%29 -> http://en.wikipedia.org/wiki/Three-way_merge#Three-way_merge -> http: //en.wikipedia .org / wiki / Diff3 -> http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf

O último link é um pdf de um artigo que descreve o diff3algoritmo em detalhes. Aqui está uma versão do visualizador de pdf do google . Tem apenas 12 páginas, e o algoritmo tem apenas algumas páginas - mas um tratamento matemático completo. Isso pode parecer um pouco formal, mas se você quiser entender o merge do git, você precisará entender a versão mais simples primeiro. Eu não verifiquei ainda, mas com um nome como diff3, você provavelmente também precisará entender diff (que usa um algoritmo de subsequência comum mais longo ). No entanto, pode haver uma explicação mais intuitiva do que está diff3por aí, se você tiver um google ...


Agora, acabei de fazer uma experiência comparando diff3e git merge-file. Eles tomam os mesmos três arquivos de entrada version1 OldVersion version2 e conflitos marcam o caminho mesma, com <<<<<<< version1, =======, >>>>>>> version2( diff3também tem ||||||| oldversion), mostrando o seu património comum.

Eu usei um arquivo vazio para OldVersion e arquivos quase idênticas para version1 e version2 com apenas uma linha extra adicionado ao version2 .

Resultado: git merge-fileidentificou a única linha alterada como o conflito; mas diff3tratou os dois arquivos inteiros como um conflito. Portanto, por mais sofisticado que o diff3 seja, o merge do git é ainda mais sofisticado, mesmo para os casos mais simples.

Aqui estão os resultados reais (usei a resposta de @twalberg para o texto). Observe as opções necessárias (consulte as respectivas páginas de manual).

$ git merge-file -p fun1.txt fun0.txt fun2.txt

You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.
<<<<<<< fun1.txt
=======
THIS IS A BIT DIFFERENT
>>>>>>> fun2.txt

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...

$ diff3 -m fun1.txt fun0.txt fun2.txt

<<<<<<< fun1.txt
You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...
||||||| fun0.txt
=======
You might be best off looking for a description of a 3-way merge algorithm. A
high-level description would go something like this:

    Find a suitable merge base B - a version of the file that is an ancestor of
both of the new versions (X and Y), and usually the most recent such base
(although there are cases where it will have to go back further, which is one
of the features of gits default recursive merge) Perform diffs of X with B and
Y with B.  Walk through the change blocks identified in the two diffs. If both
sides introduce the same change in the same spot, accept either one; if one
introduces a change and the other leaves that region alone, introduce the
change in the final; if both introduce changes in a spot, but they don't match,
mark a conflict to be resolved manually.
THIS IS A BIT DIFFERENT

The full algorithm deals with this in a lot more detail, and even has some
documentation (/usr/share/doc/git-doc/technical/trivial-merge.txt for one,
along with the git help XXX pages, where XXX is one of merge-base, merge-file,
merge, merge-one-file and possibly a few others). If that's not deep enough,
there's always source code...
>>>>>>> fun2.txt

Se você estiver realmente interessado nisso, é uma espécie de toca de coelho. Para mim, parece tão profundo quanto expressões regulares, o algoritmo de subsequência comum mais longo de diff, gramáticas livres de contexto ou álgebra relacional. Se você quiser chegar ao fundo disso, acho que pode, mas vai exigir um estudo determinado.

13ren
fonte
0

Como o git detecta o contexto de uma mudança não conflitante em particular?
Como o git descobre que há um conflito nessas linhas exatas?

Se a mesma linha mudou em ambos os lados da fusão, é um conflito; se não o fizeram, a mudança de um lado (se existente) é aceita.

Quais coisas o git mescla automaticamente?

Mudanças que não entram em conflito (veja acima)

Como o git funciona quando há várias bases comuns para mesclar branches?

Pela definição de uma base de mesclagem Git , existe apenas um (o último ancestral comum).

O que acontece quando mesclo várias ramificações de uma vez?

Isso depende da estratégia de fusão (apenas as estratégias octopuse ours/ theirssuportam a fusão de mais de dois ramos).

Qual é a diferença entre estratégias de mesclagem?

Isso é explicado na página de git mergemanual .

Nevik Rehnel
fonte
2
O que significa 'mesma linha'? Se eu inserir uma nova linha não vazia entre duas outras e mesclar - quais linhas são iguais? Se eu excluir algumas linhas em uma ramificação, quais são as 'mesmas' em outra ramificação?
abyss.7 de
1
É um pouco complicado responder em texto. Git usa [diffs] (en.wikipedia.org/wiki/Diff) para expressar a diferença entre dois arquivos (ou duas revisões de um arquivo). Ele pode detectar se as linhas foram adicionadas ou removidas comparando o contexto (por padrão, três linhas). "Mesma linha" significa então por contexto, mantendo as adições e exclusões em mente.
Nevik Rehnel
1
Você sugere que a mudança da "mesma linha" indicaria um conflito. O motor automerge é realmente baseado em linha? Ou é baseado em pedaços? Existe apenas um ancestral comum? Se sim, por que git-merge-recursiveexiste?
Edward Thomson
1
@EdwardThomson: Sim, a resolução é baseada em linhas (pedaços podem ser divididos em pedaços menores até que apenas uma linha seja deixada). A estratégia de mesclagem padrão usa o ancestral comum mais recente como referência, mas existem outros se você quiser usar outra coisa. E eu não sei o que git-merge-recursivedeveria ser (não existe uma página de manual e o google não produz nada). Mais informações sobre isso podem ser encontradas nas páginas de manual git mergee git merge-base.
Nevik Rehnel
1
A git-mergepágina do git-merge-basemanual e as páginas do manual que você aponta discutem vários ancestrais comuns e mesclagem recursiva. Sinto que sua resposta está incompleta sem uma discussão sobre isso.
Edward Thomson