git

¿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.