¿Cuál es la rama dominante al hacer una fusión git?
(3)
El modificador "dominante" no está definido por Git. La forma en que usa la palabra me parece que hace una suposición incorrecta, lo que creo hace que la pregunta no responda tal como es. Sin embargo, con un pequeño cambio, la respuesta se vuelve simple: no es ninguna . Ninguna rama es "dominante" aquí; ambas ramas son socios iguales, y el resultado de la fusión es el mismo, ya sea que combine A en B o B en A, pero puede cambiar esto de varias maneras.
Hay bastantes puntos útiles debajo, que esta pregunta expone, así que vamos a explorarlos. Al final, veremos cómo formular correctamente la pregunta y cuáles son las posibles respuestas.
Recordatorio: Git almacena instantáneas con vinculación principal
Cada confirmación almacena una copia completa de todos los archivos en esa confirmación, intacta (aunque comprimida). Algunos otros sistemas de control de versiones comienzan con una instantánea inicial, luego, para cada confirmación, almacenan un conjunto de cambios desde una confirmación anterior o cambios desde una instantánea anterior . Por lo tanto, estos otros VCS pueden mostrarle los cambios fácilmente (ya que eso es lo que tienen), pero tienen dificultades para obtener los archivos reales (porque tienen que reunir muchos cambios). Git adopta el enfoque opuesto, almacena los archivos cada vez y calcula los cambios solo cuando los solicita.
Esto no hace mucha diferencia en términos de uso, dado que con dos instantáneas, podemos encontrar un cambio, y con una instantánea y un cambio, podemos aplicar el cambio para obtener una nueva instantánea. Pero sí importa un poco, y me refiero a estas instantáneas a continuación. Para (mucho) más sobre esto, vea ¿Cómo almacena git los archivos? y ¿Son los deltas de archivos de paquete de Git en lugar de instantáneas?
Mientras tanto, cada commit, en Git, también registra un commit padre . Estos enlaces principales forman una cadena de confirmaciones hacia atrás, que necesitamos ya que las ID de confirmación parecen bastante aleatorias:
4e93cf3 <- 2abedd2 <- 1f0c91a <- 3431a0f
El cuarto commit "apunta de nuevo" al tercero, que apunta de nuevo al segundo, que apunta de nuevo al primero. (La primera confirmación no tiene padre, por supuesto, porque es la primera confirmación). Así es como Git encuentra las confirmaciones anteriores, dadas las últimas confirmaciones o sugerencias . La palabra consejo aparece en el glosario de Git , pero solo bajo la definición de rama .
El objetivo de una fusión.
El objetivo de cualquier fusión es combinar el trabajo. En Git, como en cualquier otro sistema moderno de control de versiones, podemos tener diferentes autores trabajando en diferentes ramas, o "nosotros" podemos ser una persona (un autor, el "nosotros" real :-)) trabajando en diferentes ramas. En esas diversas ramas, nosotros, ya sea "nosotros" significa una persona o muchas, podemos hacer diferentes cambios en nuestros archivos, con diferentes intenciones y diferentes resultados.
Eventualmente, sin embargo, decidimos que nos gustaría
combinar
algunos de estos de alguna manera.
Combinar estos diferentes conjuntos de cambios, para lograr un resultado particular y, al menos normalmente, registrar el hecho de que los combinamos, es una fusión.
En Git, esta versión verbal de merge, para fusionar varias ramas, se realiza con
git merge
, y el resultado es
una
fusión, la forma nominal de fusión.
1
El sustantivo puede convertirse en un adjetivo: un
commit de fusión
es cualquier commit con dos o más padres.
Todo esto se define correctamente en el
glosario Git
.
Cada padre de un commit de fusión es un
encabezado
anterior (ver más abajo).
El
primero de
esos padres es la cabeza que era
HEAD
(ver también a continuación).
Esto hace que el primer padre de merge commits sea especial, y es por eso que
git log
y
git rev-list
tienen una opción
--first-parent
: esto le permite ver solo la rama "principal", en la que se encuentran todas las ramas "laterales" fusionado
Para que esto funcione según lo deseado, es crucial que
todas las
fusiones (forma verbal) se realicen con cuidado y con la intención adecuada, lo que requiere que
ninguna
de ellas se realice a través de
git pull
.
(Esta es una de varias razones por las cuales las personas nuevas en Git deberían evitar el comando
git pull
. La importancia, o la falta de ella, de esta propiedad del primer
--first-parent
depende de cómo va a usar Git. Pero si es nuevo en Git, es probable que aún no sepas cómo vas a usar Git, por lo que no sabes si esta propiedad será importante para ti. El
uso de
git pull
atornilla casualmente, por
lo que debes evitar
git pull
).
1
Confusamente,
git merge
también puede implementar el verbo de acción, pero produce un commit ordinario, sin fusión, usando
--squash
.
La opción
--squash
realidad suprime el commit en sí, pero también lo hace
--no-commit
.
En cualquier caso, es el eventual
git commit
que ejecutas lo que hace el commit, y este
es
un commit de fusión a menos que
--squash
cuando
--squash
git merge
.
Por qué
--squash
implica
--no-commit
, cuando de hecho puedes ejecutar
git merge --squash --no-commit
si quieres omitir el paso de confirmación automática, es un poco misterioso.
Estrategias de fusión de Git
La documentación de
git merge
señala que hay
cinco
estrategias
integradas
, llamadas
resolve
,
recursive
,
octopus
,
ours
y
subtree
.
Notaré aquí que el
subtree
es solo un pequeño ajuste
recursive
, por lo que quizás sea mejor reclamar solo cuatro estrategias.
Además, la
resolve
y la
recursive
son bastante similares, ya que la recursiva es simplemente una variante recursiva de la
resolve
, lo que nos lleva a tres.
Las tres estrategias funcionan con lo que Git llama cabezas . Git define la palabra cabeza:
Una referencia con nombre al commit en la punta de una rama.
pero la forma en que Git usa esto con
git merge
tampoco coincide con esta definición.
En particular, puede ejecutar
git merge 1234567
para fusionar commit
1234567
, incluso si no tiene una referencia con nombre.
Simplemente se trata
como si fuera
la punta de una rama.
Esto funciona porque la
rama de
la palabra en sí misma está bastante débilmente definida en Git (ver
¿Qué queremos decir exactamente con "rama"?
): En efecto, Git crea una rama anónima, para que tenga una referencia
sin
nombre para la confirmación que es La punta de esta rama sin nombre.
Una cabeza siempre es
HEAD
El nombre
HEAD
que también se puede escribir
@
reservado en Git, y
siempre se refiere a la confirmación actual
(siempre hay una confirmación actual).
2
Su
HEAD
puede estar separada (apuntando a una confirmación específica) o adjuntada (que contiene el nombre de una rama, con el nombre de la rama a su vez nombrando la confirmación específica que, por lo tanto, es la confirmación actual).
Para todas las estrategias de fusión,
HEAD
es una de las cabezas a fusionar.
La estrategia del
octopus
es realmente un poco diferente, pero cuando se trata de resolver elementos fusionables, funciona de manera muy similar a la
resolve
excepto que no puede tolerar conflictos.
Eso le permite evitar detenerse con un conflicto de fusión en primer lugar, lo que le permite resolver más de dos cabezas.
Excepto por su intolerancia a los conflictos y su capacidad para resolver tres o más cabezas, puede considerarlo como una fusión de resolución regular, a lo que llegaremos en un momento.
La estrategia
ours
es completamente diferente:
ignora completamente
todas las demás cabezas.
Nunca hay conflictos de fusión porque no hay otras entradas: el resultado de la fusión, la instantánea en la nueva
HEAD
, es el mismo que el de la
HEAD
anterior.
Esto también permite que esta estrategia resuelva más de dos cabezas, y también nos da una forma de definir "cabeza dominante" o "rama dominante", aunque ahora la definición no es particularmente útil.
Para la estrategia
ours
, la "rama dominante" es la rama actual, pero el objetivo de una fusión
ours
es registrar, en la historia, que hubo una fusión, sin tomar nada del trabajo de las otras cabezas.
Es decir, este tipo de fusión es trivial: la forma verbal de "fusionar" no hace nada, y luego la forma sustantiva resultante de "una fusión" es una nueva confirmación cuyo primer padre tiene la misma instantánea, con los padres restantes grabando las otras cabezas.
2
Hay una excepción a esta regla, cuando estás en lo que Git llama una "rama no nacida" o una "rama huérfana".
El ejemplo que la mayoría de las personas encuentran con mayor frecuencia es el estado que tiene un repositorio recién creado: usted está en la rama
master
, pero no hay confirmaciones en absoluto.
El nombre
HEAD
todavía existe, pero el nombre
master
la rama
aún no
existe, ya que no hay confirmación a la que pueda apuntar.
Git resuelve esta situación difícil al crear el nombre de la sucursal tan pronto como crea la primera confirmación.
Puede volver a ingresar en cualquier momento utilizando
git checkout --orphan
para crear una nueva rama que aún no existe.
Los detalles están más allá del alcance de esta respuesta.
Cómo funciona la resolución / fusión recursiva
Los tipos de fusión restantes (no
ours
) son los que usualmente pensamos cuando hablamos de fusión.
Aquí, realmente estamos
combinando
cambios.
Tenemos nuestros cambios, en nuestra sucursal;
y tienen sus cambios, en su rama.
Pero dado que Git almacena instantáneas, primero tenemos que
encontrar
los cambios.
¿Cuáles son, precisamente, los
cambios
?
La única forma en que Git puede producir una lista de nuestros cambios y una lista de sus cambios es encontrar primero un
punto de partida común
.
Debe encontrar una confirmación (una instantánea) que
ambos tuvimos
y que ambos usamos.
Esto requiere mirar a través de la historia, que Git reconstruye mirando a los padres.
A medida que Git retrocede a través de la historia de
HEAD
nuestro trabajo, y de la otra cabeza, finalmente encuentra una
base de fusión
: un compromiso desde el que ambos comenzamos.
3
Estos son a menudo visualmente obvios (dependiendo de cuán cuidadosamente dibujemos el gráfico de compromiso):
o--o--o <-- HEAD (ours)
/
...--o--*
/
o--o <-- theirs
Aquí, la base de fusión es commit
*
: ambos comenzamos desde ese commit, e hicimos tres commits y ellos hicieron dos.
Dado que Git almacena instantáneas, encuentra los
cambios
ejecutando, en esencia,
git diff base HEAD
y
git diff base theirs
, siendo
base
el ID del commit de base de fusión
*
.
Git luego combina estos cambios.
3
La base de fusión se define técnicamente como el
Ancestro común más bajo
, o LCA, del Gráfico Acíclico Dirigido o DAG, formado por los arcos unidireccionales de los commits que vinculan cada commit con sus padres.
Los commits son los vértices / nodos en el gráfico.
Los LCA son fáciles en los árboles, pero los DAG son más generales que los árboles, por lo que a veces no hay un solo LCA.
Aquí es donde la fusión
recursive
difiere de la fusión de
resolve
: la
resolve
funciona eligiendo uno de estos "mejores" nodos ancestrales esencialmente de manera arbitraria, mientras que la
recursive
selecciona a
todos
, fusionándolos para formar una especie de simulación de compromiso: una
base de fusión virtual
.
Los detalles están más allá del alcance de esta respuesta, pero los he mostrado en otra parte.
Para resolver / recursivo, no hay una rama dominante por defecto
Ahora finalmente llegamos a la respuesta a su pregunta:
Dado que mi rama actual es la Rama A
La rama A contiene [en el archivo
file.txt]:
line 1 -- "i love you Foo"La rama B contiene:
line 1 -- "i love you Bar"si hago un:
git merge BranchB¿Qué obtendría?
Para responder a esto, necesitamos una información más:
¿Qué hay en la base de fusión?
¿Qué
BranchA
en
BranchA
y qué
cambiaron en
BranchB
?
No es lo que hay
en
las dos ramas, sino qué
cambió
cada uno de ustedes desde la base.
Supongamos que encontramos la ID de la base de fusión,
4
y es (de alguna manera)
ba5eba5
.
Luego corremos:
git diff ba5eba5 HEAD
para descubrir lo que cambiamos y:
git diff ba5eba5 BranchB
para descubrir qué cambiaron.
(O, de manera similar, usamos
git show ba5eba5:file.txt
y miramos la línea 1, aunque solo hacer las dos
git diff
es más fácil). Obviamente, al menos uno de nosotros cambió
algo
, de lo contrario la línea 1 sería la misma en ambos archivos
Si cambiamos la línea 1 y no lo hicieron , el resultado de la fusión es nuestra versión.
Si no cambiamos la línea 1, y lo hicieron , el resultado de la fusión es su versión.
Si ambos cambiamos la línea 1, el resultado de la fusión es que la fusión falla , con un conflicto de fusión. Git escribe ambas líneas en el archivo y detiene la fusión con un error, y nos hace limpiar el desorden:
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Con el estilo predeterminado, vemos:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
=======
i love you Bar
>>>>>>> BranchB
Si establecemos
merge.conflictStyle
en
diff3
(
git config merge.conflictStyle diff3
o
git -c merge.conflictStyle=diff3 merge BranchB
o similar),
5
Git escribe no solo nuestras dos líneas, sino también lo que originalmente estaba allí:
$ cat file.txt
<<<<<<< HEAD
i love you Foo
||||||| merged common ancestors
original line 1
=======
i love you Bar
>>>>>>> BranchB
Tenga en cuenta, por cierto, que Git
no
mira ninguno de los commits
intermedios
.
Simplemente compara la base de fusión con las dos cabezas, con dos comandos
git diff
.
4
Esto presupone que hay una única base de fusión.
Ese suele ser el caso, y no quiero entrar en bases de fusión virtuales aquí, excepto en algunas de estas notas al pie.
Podemos encontrar todas las bases de fusión con
git merge-base --all HEAD BranchB
, y generalmente imprime solo una ID de confirmación;
esa es
la
base de fusión (única).
5
Utilizo este estilo
diff3
, configurándolo en mi configuración
--global
Git, porque me parece bueno, al resolver conflictos, ver qué había en la base de fusión.
No me gusta tener que encontrar la base de fusión y verificarla;
y para una fusión verdaderamente recursiva, cuando Git construyó una base de fusión virtual, no hay nada
que
ver.
Es cierto que cuando hay una base de fusión virtual, esto puede ser bastante complicado, ya que puede haber conflictos de fusión en la base de fusión virtual.
Ver
Git - diff3 Estilo de conflicto - Rama de fusión temporal
para un ejemplo de esto.
Cómo puedes establecer el dominio
Definamos cabeza dominante , con el propósito de manejar conflictos de fusión o posibles conflictos de fusión, como la versión cuyos cambios se prefieren automáticamente. Hay una manera fácil, en las estrategias recursivas y de resolución, de establecer esto.
Por supuesto, Git nos ofrece la
estrategia de
fusión de
-s ours
, que elimina incluso el
potencial
de conflictos de fusión.
Pero si no cambiamos la línea 1
y lo hicieron
, esto usa
nuestra
línea 1 de todos modos, así que eso no es lo que queremos.
Queremos tomar
nuestra
o
su
línea 1 si solo nosotros o solo ellos la cambiaron;
solo queremos que Git
prefiera
nuestra cabeza, o su cabeza, para la línea 1 en el caso en que
ambos la
cambiamos.
Estos son los argumentos
-X ours
y
-X theirs
argumentos de
opción de estrategia
.
6
Todavía utilizamos la estrategia recursiva o de resolución, como antes, pero ejecutamos con
-X ours
para decirle a Git que, en caso de conflicto, debería preferir
nuestro
cambio.
O, corremos con
-X theirs
para preferir
su
cambio.
En cualquier caso, Git
no se
detiene con un conflicto: toma el cambio preferido (o dominante) y continúa.
Estas opciones son algo peligrosas, porque dependen de que Git tenga el contexto correcto. Git no sabe nada acerca de nuestro código o archivos de texto: solo está haciendo una diferencia línea por línea, 7 tratando de encontrar un conjunto mínimo de instrucciones: "Elimine esta línea aquí, agregue esa allí, y eso lo llevará a la versión en la base comprometerse con la versión en una de las cabezas ". Es cierto que esto también es cierto para las fusiones "sin cabeza preferida / dominante", pero en esos casos, no tomamos uno de sus cambios, y luego anulamos otro cercano "su" cambio con el nuestro, que es donde probablemente problemas, especialmente en los tipos de archivos con los que trabajo.
En cualquier caso, si ejecuta una
git merge
regular
sin
uno de estos argumentos
-X
, obtendrá un conflicto como de costumbre.
A continuación, puede ejecutar
git checkout --ours
en cualquier archivo para seleccionar
nuestra
versión del archivo (sin conflictos en absoluto, ignorando
todos
sus cambios), o
git checkout --theirs
para seleccionar
su
versión del archivo ( nuevamente sin conflictos e ignorando todos
nuestros
cambios), o
git checkout -m
para volver a crear los conflictos de fusión.
Sería bueno si hubiera un contenedor orientado al usuario para
git merge-file
que extrajera las tres versiones del índice
8
y le permitiera usar
--ours
o
--theirs
para actuar como
-X ours
o
-X theirs
solo por ese archivo
(esto es lo que significan esas banderas, en
git merge-file
).
Tenga en cuenta que esto también debería permitirle usar
--union
.
Consulte
la documentación de
git merge-file
para obtener una descripción de lo que hace.
Al
--ours
--theirs
, es un poco peligroso y debe usarse con cuidado y observación;
realmente no funciona cuando se fusionan archivos XML, por ejemplo.
6
Creo que el nombre "opción de estrategia" es una mala elección, ya que suena como el argumento de
-s
strategy
, pero en realidad es completamente diferente.
La mnemónica que uso es que
-s
toma una
Estrategia
, mientras que
-X
toma una opción de estrategia
extendida
, transmitida a cualquier estrategia que ya hayamos elegido.
7
Puede controlar el diff a través de
.gitattributes
u opciones para
git diff
, pero no estoy seguro de cómo afecta el primero a las estrategias de fusión incorporadas, ya que en realidad no lo he probado.
8
Durante una fusión en conflicto,
las tres versiones
de cada archivo en conflicto están allí en el índice, a pesar de que
git checkout
solo tiene una sintaxis especial para dos de ellas.
Puede usar la
sintaxis de
gitrevisions
para extraer la versión base.
Dado que mi rama actual es la Rama A
La rama A contiene:
line 1 -- "i love you Foo"
La rama B contiene:
line 1 -- "i love you Bar"
si hago un:
git merge Branch B
¿Qué obtendría?
Está llevando los cambios a su rama actual, por lo que suponiendo que actualmente tenga la rama A desprotegida, estaría sacando los cambios de la rama B a la rama A.
git merge Branch B
Este comando fusiona B en la rama actual (A)
caso 1 . Debe adjuntar el contenido de la rama B a A. Cuando los nombres de los archivos son diferentes o los contenidos de los mismos archivos son diferentes en las diferentes líneas de B y A.
caso 2 . Combine el conflicto si el contenido de los mismos archivos es diferente en las mismas líneas.