Come funziona "git merge" nei dettagli?


95

Voglio conoscere un algoritmo esatto (o vicino a quello) dietro "git merge". Le risposte almeno a queste domande secondarie saranno utili:

  • In che modo git rileva il contesto di una particolare modifica non in conflitto?
  • Come fa Git a scoprire che c'è un conflitto in queste righe esatte?
  • Quali cose git si auto-merge?
  • Come si comporta git quando non esiste una base comune per l'unione dei rami?
  • Come si comporta git quando ci sono più basi comuni per l'unione di rami?
  • Cosa succede quando unisco più rami contemporaneamente?
  • Qual è la differenza tra le strategie di unione?

Ma la descrizione di un intero algoritmo sarà molto migliore.


8
Immagino che potresti riempire un intero libro con queste risposte ...
Daniel Hilgarth,

2
Oppure potresti semplicemente andare a leggere il codice, che impiegherebbe circa il tempo di "descrivere l'intero algoritmo"
Nevik Rehnel

3
@DanielHilgarth sarei felice di scoprirlo, se esiste già un libro del genere da qualche parte. Le referenze sono benvenute.
abisso 7

5
@NevikRehnel Sì, posso. Ma può diventare molto più semplice, se qualcuno conosce già la teoria alla base di questo codice.
abisso 7

1. Qual è "il contesto di un particolare cambiamento non conflittuale"? I punti 2. e 3. sono uguali ma negati, uniamo queste due domande?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Risposte:


65

Potrebbe essere meglio cercare una descrizione di un algoritmo di unione a 3 vie. Una descrizione di alto livello sarebbe qualcosa del genere:

  1. Trova una base di unione adatta B: una versione del file che è un antenato di entrambe le nuove versioni ( Xe Y), e di solito la base più recente (sebbene ci siano casi in cui dovrà tornare indietro, che è una delle le caratteristiche di gits default recursivemerge)
  2. Esegui differenze di Xcon Be Ycon B.
  3. Esamina i blocchi di modifica identificati nelle due differenze. Se entrambe le parti introducono la stessa modifica nello stesso punto, accettare una delle due; se una introduce una modifica e l'altra lascia da sola quella regione, introduce la modifica in finale; se entrambi introducono modifiche in un punto, ma non corrispondono, contrassegnare un conflitto da risolvere manualmente.

L'algoritmo completo si occupa di questo in modo molto più dettagliato e ha anche della documentazione ( https://github.com/git/git/blob/master/Documentation/technical/trivial-merge.txt per uno, insieme alle git help XXXpagine , dove XXX è uno dei merge-base, merge-file, merge, merge-one-filee forse pochi altri). Se non è abbastanza profondo, c'è sempre il codice sorgente ...


11

Come si comporta git quando ci sono più basi comuni per l'unione di rami?

Questo articolo è stato molto utile: http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html (qui è la parte 2 ).

Ricorsivo usa diff3 in modo ricorsivo per generare un ramo virtuale che verrà utilizzato come antenato.

Per esempio:

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

Poi:

git checkout E
git merge F

Ci sono 2 migliori antenati comuni (antenati comuni che non sono antenati di nessun altro) Ce D. Git li unisce in un nuovo ramo virtuale Ve quindi li usa Vcome base.

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

Suppongo che Git continuerebbe con il se ci fossero più migliori antenati comuni, fondendosi Vcon quello successivo.

L'articolo dice che se c'è un conflitto di unione durante la generazione del ramo virtuale, Git lascia i marcatori di conflitto dove sono e continua.

Cosa succede quando unisco più rami contemporaneamente?

Come ha spiegato @Nevik Rehnel, dipende dalla strategia, è ben spiegato nella man git-merge MERGE STRATEGIESsezione.

Solo octopuse ours/ theirssupportano l'unione di più rami contemporaneamente, recursivead esempio no.

octopusrifiuta di fondersi in caso di conflitti, ed oursè un'unione banale quindi non possono esserci conflitti.

Quei comandi generano un nuovo commit avranno più di 2 genitori.

Ne ho fatto uno merge -X octopussu Git 1.8.5 senza conflitti per vedere come va.

Stato iniziale:

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

Azione:

git checkout B
git merge -Xoctopus C D

Nuovo stato:

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

Come previsto, Eha 3 genitori.

TODO: come funziona esattamente il polpo sulle modifiche di un singolo file. Unioni ricorsive due per due a 3 vie?

Come si comporta git quando non esiste una base comune per l'unione dei rami?

@Torek afferma che dalla 2.9, l'unione fallisce senza --allow-unrelated-histories.

L'ho provato empiricamente su 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 contiene:

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

Poi:

git checkout --conflict=diff3 -- .

a contiene:

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

Interpretazione:

  • la base è vuota
  • quando la base è vuota non è possibile risolvere alcuna modifica su un singolo file; solo cose come l'aggiunta di nuovi file possono essere risolte. Il conflitto di cui sopra verrebbe risolto con un'unione a 3 vie con la base a\nc\ncome aggiunta di una singola riga
  • Io penso che una fusione a 3 vie senza un file di base si chiama una fusione a 2 vie, che è solo un diff

1
C'è un nuovo collegamento SO a questa domanda, quindi ho analizzato questa risposta (che è abbastanza buona) e ho notato che una recente modifica di Git ha un po 'superato l'ultima sezione. Dalla versione 2.9 di Git (commit e379fdf34fee96cd205be83ff4e71699bdc32b18), Git ora rifiuta di unire se non c'è una base di unione a meno che tu non aggiunga --allow-unrelated-histories.
torek

1
Ecco l'articolo di follow-up di quello pubblicato da @Ciro
adam0101

A meno che il comportamento non sia cambiato dall'ultima volta che l'ho provato: --allow-unrelated-historiespuò essere omesso se non ci sono percorsi di file comuni tra i rami che stai unendo.
Jeremy List

Piccola correzione: esiste una oursstrategia di unione, ma nessuna theirsstrategia di unione. recursive+ la theirsstrategia può risolvere solo due rami. git-scm.com/docs/git-merge#_merge_strategies
nekketsuuu

9

Anche a me interessa. Non so la risposta, ma ...

Si scopre invariabilmente che un sistema complesso che funziona si è evoluto da un sistema semplice che funzionava

Penso che l'unione di git sia altamente sofisticata e sarà molto difficile da capire, ma un modo per avvicinarsi a questo è dai suoi precursori e concentrarsi sul cuore della tua preoccupazione. Cioè, dati due file che non hanno un antenato comune, come funziona git merge come unirli e dove sono i conflitti?

Proviamo a trovare alcuni precursori. Da 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

Quest'ultimo collegamento è un pdf di un documento che descrive diff3in dettaglio l' algoritmo. Ecco una versione del visualizzatore di pdf di Google . È lungo solo 12 pagine e l'algoritmo è solo un paio di pagine, ma un trattamento matematico completo. Potrebbe sembrare un po 'troppo formale, ma se vuoi capire l'unione di git, dovrai prima capire la versione più semplice. Non ho ancora controllato, ma con un nome come diff3, probabilmente dovrai anche capire diff (che utilizza un algoritmo di sottosequenza comune più lungo ). Tuttavia, potrebbe esserci una spiegazione più intuitiva diff3là fuori, se hai un google ...


Ora, ho appena fatto un esperimento confrontando diff3e git merge-file. Prendono le stesse tre file di input version1 oldversion version2 e conflitti segnare la strada stessa, con <<<<<<< version1, =======, >>>>>>> version2( diff3ha anche ||||||| oldversion), mostrando il loro patrimonio comune.

Ho usato un file vuoto per oldversion e file quasi identici per version1 e version2 con solo una riga in più aggiunta a version2 .

Risultato: git merge-fileidentificato la singola riga modificata come conflitto; ma ha diff3trattato i due file interi come un conflitto. Così, sofisticato come diff3, l'unione di git è ancora più sofisticata, anche per questo dei casi più semplici.

Ecco i risultati effettivi (ho usato la risposta di @ twalberg per il testo). Annotare le opzioni necessarie (vedere le rispettive manpage).

$ 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 sei veramente interessato a questo, è un po 'una tana di coniglio. A me sembra profondo quanto le espressioni regolari, l' algoritmo di sottosequenza comune più lungo di diff, grammatiche libere dal contesto o algebra relazionale. Se vuoi andare fino in fondo, penso che tu possa farlo, ma ci vorrà uno studio determinato.



0

In che modo git rileva il contesto di una particolare modifica non in conflitto?
Come fa Git a scoprire che c'è un conflitto in queste righe esatte?

Se la stessa riga è cambiata su entrambi i lati dell'unione, è un conflitto; in caso contrario, viene accettato il cambiamento da un lato (se esistente).

Quali cose git si auto-merge?

Modifiche che non sono in conflitto (vedi sopra)

Come si comporta git quando ci sono più basi comuni per l'unione di rami?

Secondo la definizione di una base di unione Git , ce n'è solo una (l'ultimo antenato comune).

Cosa succede quando unisco più rami contemporaneamente?

Dipende dalla strategia di unione (solo le strategie octopuse ours/ theirssupportano l'unione di più di due rami).

Qual è la differenza tra le strategie di unione?

Questo è spiegato nella git mergemanpage .


2
Cosa significa la "stessa linea"? Se inserisco una nuova riga non vuota tra altre due e mi unisco, quali righe sono le stesse? Se elimino alcune righe in un ramo, quali sono le "stesse" in un altro ramo?
abisso 7

1
È un po 'complicato rispondere nel testo. Git usa [diffs] (en.wikipedia.org/wiki/Diff) per esprimere la differenza tra due file (o due revisioni di un file). Può rilevare se le righe sono state aggiunte o rimosse confrontando il contesto (per impostazione predefinita, tre righe). "Stessa riga" significa quindi per contesto, tenendo presente le aggiunte e le eliminazioni.
Nevik Rehnel

1
Suggerisci che la modifica della "stessa riga" indicherebbe un conflitto. Il motore di automerge è davvero basato sulla linea? O è basato sul fusto? C'è sempre un solo antenato comune? Se è così, perché git-merge-recursiveesiste?
Edward Thomson

1
@EdwardThomson: Sì, la risoluzione è basata sulla riga (i blocchi possono essere suddivisi in blocchi più piccoli fino a quando rimane solo una riga). La strategia di unione predefinita utilizza l'ultimo antenato comune come riferimento, ma ce ne sono altri se vuoi usare qualcos'altro. E non so cosa git-merge-recursivedovrebbe essere (non c'è una pagina di manuale e Google non produce nulla). Maggiori informazioni su questo possono essere trovate nelle pagine man di git mergee git merge-base.
Nevik Rehnel

1
La git-mergepagina di manuale e le git-merge-basepagine di manuale che indichi discutono di più antenati comuni e di unioni ricorsive. Sento che la tua risposta è incompleta senza una discussione di questo tipo.
Edward Thomson
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.