crear - Git: cómo forzar la fusión manual incluso si no hay conflicto
git rebase (2)
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:
%homepath%/.gitconfig
siguiente a mi%homepath%/.gitconfig
:[merge "verify"] name = merge and verify driver driver = %homepath%//merge-and-verify-driver.bat %A %O %B
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).y, en la raíz del repositorio, 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 sintaxis.
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 ramas, podemos dibujarlas de varias maneras. El método que me gusta usar aquí (en texto, en ) 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 rama "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 rama, 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 rama real. Supongamos que hacemos una nueva rama, 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 ramas. 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 ramas 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 rama real en realidad está formada por los commits , y que los nombres de las ramas casi no tienen significado o contribución aquí. Los nombres simplemente sirven para encontrar las sugerencias de ramas : confirma C
y D
en este caso. La rama 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 ramas 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 ramas .
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 ramas, 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 ramas. Pero, de hecho, commit I
también está en ambas ramas, así que 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--E-G--I <-- br1
/ X
D---F-H--J <-- br2
Obtenemos esto comenzando con las dos ramas 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 ramas, 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:
- Identifica la confirmación actual, también conocida como
HEAD
. Esto también se llama compromiso local o--ours
. - identifica el otro compromiso, el que diste a través
branch-name
. Esa es la confirmación de sugerencia de la otra rama, 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). - 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. - Efectivamente, ejecuta dos comandos separados de
git diff
:git diff base ours
ygit diff base theirs
.
Estas dos diferencias le dicen a Git "lo que sucedió". El objetivo 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.