Git: cómo forzar la fusión manual incluso si no hay conflicto

Esta es una pregunta que se hizo muchas veces a lo largo de los años. He encontrado varias respuestas, en particular esta:

Git: cómo forzar el conflicto de combinación y la fusión manual en el archivo seleccionado (@Dan Molding)

Esta página contiene una guía detallada sobre cómo configurar un controlador de combinación que siempre devolvería la falla y, por lo tanto, haría posible una fusión manual. Intenté adaptar esa solución para Windows:

  1. %homepath%\.gitconfig siguiente a mi %homepath%\.gitconfig :

    [merge "verify"] name = merge and verify driver driver = %homepath%\\merge-and-verify-driver.bat %A %O %B

  2. Cambié el controlador a

    cmd /K "echo Working > merge.log & git merge-file %1% %2% %3% & exit 1"

    (Se agregó echo Working > merge.log para verificar si se invocó el controlador).

  3. y, en la raíz del repository, creó un archivo .gitattributes con la siguiente línea:

    *.txt merge=verify

Lamentablemente, no funciona. Traté de combinar un archivo, feature.txt y, por desgracia, la fusión se completó con éxito. Parece que el controlador no se invocó en absoluto, ya que el archivo merge.log no se creó.

¿Hago algo mal? Cualquier solución al problema de forzar la fusión manual es bienvenida.

tl; dr: Traté de repetir lo que describiste y parece funcionar. Hubo 2 cambios en comparación con la versión suya, pero sin ellos también he fracasado en la combinación (porque el controlador no se ejecutó)

He intentado esto:

Cree un controlador de combinación $HOME/bin/errorout.bat :

 exit 1 

Crear una sección para el tipo de fusión

 [merge "errorout"] name = errorout driver = ~/bin/errorout.bat %A %O %B 

Crea el archivo .gitattributes:

 *.txt merge=errorout 

Después de eso, se informa un error ya que creo que quieres que se informe:

  $ git merge a C:\...>exit 1 Auto-merging f.txt CONFLICT (content): Merge conflict in f.txt Automatic merge failed; fix conflicts and then commit the result. 

Tengo la versión 2.11.0.rc1.windows.1 de git. No pude hacer el comando complicado como especificó ejecutar con éxito, estaba informando algunos errores de syntax.

Hay dos partes del problema. Lo relativamente fácil es escribir el controlador de combinación personalizado, como hiciste en los pasos 1 y 2. Lo difícil es que Git no se molesta en ejecutar el controlador personalizado si, en opinión de Git, no es necesario. Esto es lo que has observado en el paso 3.

Entonces, ¿cuándo ejecuta Git su controlador de combinación? La respuesta es bastante complicada, y para llegar allí tenemos que definir el término base de fusión , que veremos en un momento. También debe saber que Git identifica los archivos, de hecho, casi todo: confirmaciones, archivos, parches, etc., por sus identificadores hash . Si ya sabe todo esto, puede saltear directamente a la última sección.

ID de Hash

Los identificadores hash (o, a veces, IDs de objetos u OID) son esos grandes nombres desagradables que ves para commits:

 $ git rev-parse HEAD 7f453578c70960158569e63d90374eee06104adc $ git log commit 7f453578c70960158569e63d90374eee06104adc Author: ... 

Todo lo que almacena Git tiene una identificación hash única, calculada a partir del contenido del objeto (el archivo o confirmación o lo que sea).

Si almacena el mismo archivo dos veces (o más), obtendrá el mismo ID de hash dos (o más) veces. Dado que cada confirmación finalmente almacena una instantánea de cada archivo a partir del momento de dicha confirmación, cada confirmación por lo tanto tiene una copia de cada archivo, listado por su identificación hash. De hecho, puede ver estos:

 $ git ls-tree HEAD 100644 blob b22d69ec6378de44eacb9be8b61fdc59c4651453 README 100644 blob b92abd58c398714eb74cbe66671c7c3d5c030e2e integer.txt 100644 blob 27dfc5306fbd27883ca227f08f06ee037cdcb9e2 lorem.txt 

Las tres grandes identificaciones feos en el medio son las tres ID de hash. Esos tres archivos están en el compromiso HEAD bajo esos ID. Tengo los mismos tres archivos en varias confirmaciones más, generalmente con contenidos ligeramente diferentes.

Cómo llegar a la base de combinación: el DAG

El DAG o G rafo cíclico D ireccionado es una forma de establecer las relaciones entre los commits. Para usar Git correctamente, necesitas al menos una vaga idea de lo que es el DAG. También se llama el gráfico de compromiso , que es un término más agradable en algunos aspectos, ya que evita la jerga informática especializada.

En Git, cuando hacemos twigs, podemos dibujarlas de varias maneras. El método que me gusta usar aquí (en texto, en StackOverflow) es poner commits anteriores a la izquierda y posteriores commits a la derecha, y etiquetar cada commit con una sola letra mayúscula. Idealmente, dibujaríamos esto de la manera en que Git los mantiene, lo cual es bastante retroactivo:

 A <- B <- C <-- master 

Aquí tenemos solo tres commits, todos en master . El master nombre de twig "apunta a" el último de los tres commits. Así es como Git realmente encuentra la confirmación C , leyendo su ID hash del master nombre de twig, y ​​de hecho, el master nombre almacena de manera efectiva solo esta ID.

Git encuentra el compromiso B leyendo la confirmación C Commit C tiene, dentro de él, la identificación hash de commit B Decimos que C "apunta a" B , de ahí la flecha hacia atrás. Del mismo modo, B "apunta a" A Como A es la primera confirmación, no tiene una confirmación previa, por lo que no tiene un puntero atrás.

Estas flechas internas le dicen a Git sobre el compromiso padre de cada compromiso. La mayoría de las veces, no nos importa que todos estén al revés, por lo que podemos dibujar esto más simplemente como:

 A--B--C <-- master 

lo que nos permite pretender que es obvio que C viene después de B , aunque de hecho eso es bastante difícil en Git. (Compare con la afirmación " B viene antes que C ", lo cual es muy fácil en Git: es fácil retroceder, porque las flechas internas son todas hacia atrás).

Ahora dibujemos una twig real. Supongamos que hacemos una nueva twig, comenzando en la confirmación B , y hacemos una cuarta confirmación D (no está claro exactamente cuándo lo hacemos, pero al final no importa de todos modos):

 A--B--C <-- master \ D <-- sidebr 

El nombre sidebr ahora apunta a cometer D , mientras que el nombre master apunta a cometer C

Un concepto clave de Git aquí es que el compromiso B está en ambas twigs. Está en master y sidebr . Esto también es cierto para el compromiso A En Git, cualquier compromiso dado puede ser, y a menudo lo es, en muchas twigs simultáneamente.

Hay otro concepto clave escondido en Git aquí que es bastante diferente de la mayoría de los otros sistemas de control de versiones, que mencionaré de pasada. Esto es que la twig real en realidad está formada por los commits , y que los nombres de las twigs casi no tienen significado o contribución aquí. Los nombres simplemente sirven para encontrar las sugerencias de twigs : confirma C y D en este caso. La twig en sí es lo que obtenemos al dibujar las líneas de conexión, yendo desde las confirmaciones más recientes (secundarias) hasta las confirmaciones más antiguas (parentales).

También vale la pena señalar, como un punto aparte, que este extraño vínculo hacia atrás le permite a Git nunca, nunca, cambiar nada sobre cualquier compromiso . Tenga en cuenta que tanto C como D son hijos de B , pero no sabíamos necesariamente, cuando hicimos B , que íbamos a hacer C y D Pero, como el padre no "conoce" a sus hijos, Git no tuvo que almacenar los ID de C y D dentro de B en absoluto. Simplemente almacena la ID de B que definitivamente existía para entonces, dentro de cada uno de C y D cuando crea cada uno de C y D

Estos dibujos que hacemos muestran (parte de) el gráfico de compromiso .

La base de fusión

Una definición adecuada de las bases de fusión es demasiado larga para entrar aquí, pero ahora que hemos dibujado el gráfico, una definición informal es muy fácil y visualmente obvia. La base de fusión de dos twigs es el punto en el que se unen por primera vez , cuando trabajamos hacia atrás como lo hace Git. Es decir, es el primer compromiso de este tipo en ambas twigs .

Por lo tanto, en:

 A--B--C <-- master \ D <-- sidebr 

la base de combinación es commit B Si hacemos más commits:

 A--B--C--F <-- master \ D--E--G <-- sidebr 

la base de fusión permanece confirmada B Si realizamos una fusión exitosa, la nueva combinación de fusión tiene dos confirmaciones principales en lugar de solo una:

 A--B--C--F---H <-- master \ / D--E--G <-- sidebr 

Aquí, commit H es la fusión, que realizamos en master ejecutando git merge sidebr , y sus dos padres son F (el commit que solía ser la punta del master ) y G (el commit que aún es la punta del sidebr ) .

Si ahora continuamos realizando confirmaciones y luego decidimos hacer otra fusión, G será la nueva base de fusión:

 A--B--C--F---H--I <-- master \ / D--E--G--J <-- sidebr 

H tiene dos padres, y nosotros (y Git) seguimos a ambos padres "simultáneamente" cuando miramos hacia atrás. Por lo tanto, commit G es el primero que está en ambas twigs, si y cuando ejecutamos otra fusión.

Aparte: fusión cruzada

Tenga en cuenta que F no está, en este caso, en sidebr : tenemos que seguir los enlaces padres a medida que los encontramos, por lo que J conduce de nuevo a G , lo que nos lleva de vuelta a E , etc., de modo que nunca llegamos a F cuando comenzamos desde sidebr . Sin embargo, si hacemos nuestra próxima fusión de master en sidebr :

 A--B--C--F---H--I <-- master \ / \ D--E--G--J---K <-- sidebr 

Ahora commit F está en ambas twigs. Pero, de hecho, commit I también está en ambas twigs, por lo tanto, aunque esto hace que las fusiones vayan en ambos sentidos, estamos bien aquí. Podemos meternos en problemas con las llamadas "fusiones entrecruzadas", y dibujaré una para ilustrar el problema, pero no entrar aquí:

 A--B--C--EG--I <-- br1 \ X D---FH--J <-- br2 

Obtenemos esto comenzando con las dos twigs que salen hacia E y F respectivamente, y luego hacia git checkout br1; git merge br2; git checkout br2; git merge br1 git checkout br1; git merge br2; git checkout br2; git merge br1 git checkout br1; git merge br2; git checkout br2; git merge br1 para hacer G (una fusión de E y F , agregada a br1 ) y luego también inmediatamente H (una fusión de F y E , agregada a br2 ). Podemos seguir comprometiéndonos con ambas twigs, pero eventualmente, cuando vayamos a fusionar nuevamente, tenemos un problema al elegir una base de combinación, porque tanto E como F son "mejores candidatos".

Por lo general, incluso esto "simplemente funciona", pero a veces las fusiones cruzadas crean problemas que Git intenta manejar de una manera elegante utilizando su estrategia de fusión "recursiva" predeterminada. En estos (raros) casos, puede ver algunos conflictos de fusión de apariencia extraña, especialmente si establece merge.conflictstyle = diff3 (que normalmente recomiendo: le muestra la versión base de fusión en fusiones conflictivas).

¿Cuándo se ejecuta el controlador de combinación?

Ahora que hemos definido la base de combinación y visto la forma en que los hashes identifican los objetos (incluidos los archivos), ahora podemos responder la pregunta original.

Cuando ejecutas git merge branch-name , Git:

  1. Identifica la confirmación actual, también conocida como HEAD . Esto también se llama compromiso local o --ours .
  2. identifica el otro compromiso, el que diste a través branch-name . Esa es la confirmación de sugerencia de la otra twig, y ​​se llama diversamente la otra, - --theirs , o a veces la confirmación remota ("remota" es un nombre muy pobre ya que Git usa ese término para otros fines también).
  3. Identifica la base de combinación. Llamemos a esto "base" de compromiso. La letra B también es buena pero con un controlador de fusión, %A y %B refieren a --ours y --theirs versiones respectivamente, con %O refiriéndose a la base.
  4. Efectivamente, ejecuta dos comandos separados de git diff : git diff base ours y git diff base theirs .

Estas dos diferencias le dicen a Git "lo que sucedió". El objective de Git, recuerde, es combinar dos conjuntos de cambios : "lo que hicimos en la nuestra" y "lo que hicieron en la suya". Eso es lo que muestran los dos git diffs : "base versus la nuestra" es lo que hicimos, y "base versus la suya" es lo que hicieron. (Esta es también la forma en que Git descubre si se agregaron, eliminaron y / o renombraron archivos, en base a los nuestros y / o de base a los suyos), pero esta es una complicación innecesaria en este momento, que ignoraremos).

Es la mecánica real de combinar estos cambios lo que invoca a los controladores de fusión o, como en nuestros casos problemáticos, no.

Recuerde que Git tiene cada objeto catalogado por su identificación hash. Cada ID es única en función del contenido del objeto. Esto significa que puede decir instantáneamente si dos archivos son 100% idénticos: son exactamente iguales si y solo si tienen el mismo hash.

Esto significa que si, en base-vs-nuestra o base-vs-suya, los dos archivos tienen los mismos valores hash, entonces o bien no hicimos cambios, o no hicieron ningún cambio. Si no hicimos cambios y ellos hicieron cambios, ¿por qué entonces, obviamente, el resultado de combinar estos cambios es su archivo ? O bien, si no hicieron cambios y nosotros hicimos cambios, el resultado es nuestro archivo.

Del mismo modo, si el nuestro y el suyo tienen el mismo hash, los dos hicimos los mismos cambios. En este caso, el resultado de combinar los cambios es cualquiera de los archivos, son los mismos, por lo que ni siquiera importa cuál escoja Git.

Por lo tanto, para todos estos casos, Git simplemente selecciona cualquier archivo nuevo que tenga un hash diferente (si corresponde) de la versión base. Ese es el resultado de la fusión, y no hay conflicto de fusión, y Git termina de fusionar ese archivo. Nunca ejecuta su controlador de combinación porque evidentemente no es necesario.

Solo si los tres archivos tienen tres valores hash diferentes, Git tiene que hacer una fusión real en tres direcciones. Aquí es cuando ejecutará su controlador de combinación personalizado, si tiene uno definido.

Hay una forma de evitar esto, pero no es para los débiles de corazón. Git ofrece no solo controladores de combinación personalizados, sino también estrategias de combinación personalizadas . Hay cuatro estrategias de fusión integradas, todas seleccionadas mediante la opción -s ours : -s ours , -s recursive , -s resolve y -s octopus . Sin embargo, puede usar -s custom-strategy para invocar la suya propia.

El problema es que para escribir una estrategia de fusión, debe identificar la (s) base (es) de fusión, hacer cualquier combinación recursiva que desee (a la -s recursive ) en el caso de bases de combinación ambiguas, ejecutar las dos git diff , resolver archivo agregar / eliminar / cambiar el nombre de las operaciones, y luego ejecutar sus diversos controladores. Como esto se hace cargo de toda la megillah , puedes hacer lo que quieras, pero debes hacer muchas cosas. Hasta donde sé, no hay una solución enlatada que use esta técnica.