Git 🌙
Chapters ▾ 2nd Edition

7.8 Herramientas de Git - Fusión Avanzada

Fusión Avanzada

La fusión en Git suele ser bastante fácil. Dado que Git facilita la fusión de otra rama varias veces, significa que puede tener una rama de larga duración, pero puede mantenerla actualizada sobre la marcha, resolviendo pequeños conflictos a menudo, en lugar de sorprenderse por un conflicto enorme en el final de la serie.

Sin embargo, a veces ocurren conflictos engañosos. A diferencia de otros sistemas de control de versiones, Git no intenta ser demasiado listo para fusionar la resolución de conflictos. La filosofía de Git es ser inteligente para determinar cuándo una resolución de fusión no es ambigua, pero si hay un conflicto, no intenta ser inteligente para resolverlo automáticamente. Por lo tanto, si espera demasiado para fusionar dos ramas que divergen rápidamente, puede encontrarse con algunos problemas.

En esta sección, veremos cuáles podrían ser algunos de esos problemas y qué herramientas le dará Git para ayudarlo a manejar estas situaciones más engañosas. También cubriremos algunos de los diferentes tipos de fusión no estándar que puede hacer, y también veremos cómo deshacerse de las fusiones que ha realizado.

Conflictos de Fusión

Si bien cubrimos algunos conceptos básicos para resolver conflictos de fusión en Principales Conflictos que Pueden Surgir en las Fusiones, para conflictos más complejos, Git proporciona algunas herramientas para ayudarlo a descubrir qué está sucediendo y cómo lidiar mejor con el conflicto.

En primer lugar, si es posible, intente asegurarse de que su directorio de trabajo esté limpio antes de realizar una fusión que pueda tener conflictos. Si tiene un trabajo en progreso, hágale commit a una rama temporal o stash. Esto hace que pueda deshacer cualquier cosa que intente aquí. Si tiene cambios no guardados en su directorio de trabajo cuando intenta fusionarlos, algunos de estos consejos pueden ayudarlo a perder ese trabajo.

Veamos un ejemplo muy simple. Tenemos un archivo Ruby super simple que imprime hello world.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

En nuestro repositorio, creamos una nueva rama llamada whitespace y procedemos a cambiar todas las terminaciones de línea de Unix a terminaciones de línea de DOS, esencialmente cambiando cada línea del archivo, pero solo con espacios en blanco. Luego cambiamos la línea "hello world" a "hello mundo".

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'converted hello.rb to DOS'
[whitespace 3270f76] converted hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -w
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 1 file changed, 1 insertion(+), 1 deletion(-)

Ahora volvemos a nuestra rama master y agregamos cierta documentación para la función.

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'document the function'
[master bec6336] document the function
 1 file changed, 1 insertion(+)

Ahora tratamos de fusionarnos en nuestra rama whitespace y tendremos conflictos debido a los cambios en el espacio en blanco.

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Abortar una Fusión

Ahora tenemos algunas opciones. Primero, cubramos cómo salir de esta situación. Si tal vez no esperabas conflictos y aún no quieres lidiar con la situación, simplemente puedes salir de la fusión con git merge --abort.

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

La opción git merge --abort intenta volver a su estado antes de ejecutar la fusión. Los únicos casos en los que podría no ser capaz de hacer esto a la perfección serían si hubiera realizado cambios sin stash, no confirmados en su directorio de trabajo cuando lo ejecutó, de lo contrario, debería funcionar bien.

Si por alguna razón se encuentra en un estado horrible y solo quiere comenzar de nuevo, también puede ejecutar git reset --hard HEAD o donde quiera volver. Recuerde, una vez más, que esto hará volar su directorio de trabajo, así que asegúrese de no querer ningún cambio allí.

Ignorando el Espacio en Blanco

En este caso específico, los conflictos están relacionados con el espacio en blanco. Sabemos esto porque el caso es simple, pero también es muy fácil saberlo en casos reales, al analizar el conflicto, porque cada línea se elimina por un lado y se agrega nuevamente por el otro. De manera predeterminada, Git ve que todas estas líneas están siendo modificadas, por lo que no puede fusionar los archivos.

Sin embargo, la estrategia de combinación predeterminada puede tomar argumentos, y algunos de ellos son acerca de ignorar adecuadamente los cambios del espacio en blanco. Si ve que tiene muchos problemas con espacios en blanco en una combinación, simplemente puede cancelarla y volverla a hacer, esta vez con -Xignore-all-space o` -Xignore-space-change`. La primera opción ignora los cambios en cualquier cantidad de espacios en blanco existentes, la segunda ignora por completo todos los cambios de espacios en blanco.

$ git merge -Xignore-all-space whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Dado que en este caso, los cambios reales del archivo no eran conflictivos, una vez que ignoramos los cambios en los espacios en blanco, todo se fusiona perfectamente.

Esto es un salvavidas si tiene a alguien en su equipo a quien le gusta ocasionalmente reformatear todo, desde espacios hasta pestañas o viceversa.

Re-fusión Manual de Archivos

Aunque Git maneja muy bien el preprocesamiento de espacios en blanco, hay otros tipos de cambios que quizás Git no pueda manejar de manera automática, pero que son correcciones de secuencias de comandos. Como ejemplo, imaginemos que Git no pudo manejar el cambio en el espacio en blanco y que teníamos que hacerlo a mano.

Lo que realmente tenemos que hacer es ejecutar el archivo que intentamos fusionar a través de un programa dos2unix antes de intentar fusionar el archivo. Entonces, ¿cómo haríamos eso?

Primero, entramos en el estado de conflicto de la fusión. Luego queremos obtener copias de mi versión del archivo, su versión (de la rama en la que nos estamos fusionando) y la versión común (desde donde ambos lados se bifurcaron). Entonces, queremos arreglar su lado o nuestro lado y volver a intentar la fusión sólo para este único archivo.

Obtener las tres versiones del archivo es bastante fácil. Git almacena todas estas versiones en el índice bajo “etapas”, cada una de las cuales tiene números asociados. La etapa 1 es el ancestro común, la etapa 2 es su versión y la etapa 3 es de la MERGE_HEAD, la versión en la que se está fusionando (“suya”).

Puede extraer una copia de cada una de estas versiones del archivo en conflicto con el comando git show y una sintaxis especial.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

Si quiere ponerse un poco más intenso, también puede usar el comando de plomería ls-files -u para obtener el verdadero SHA-1s de las manchas de Git para cada uno de los archivos.

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

El :1:hello.rb es solo una clave para buscar esa mancha SHA-1.

Ahora que tenemos el contexto de estas tres etapas en nuestro directorio de trabajo, manualmente podemos arreglarlos para solucionar los problemas de espacios en blanco y volver a fusionar el archivo con el poco conocido comando git merge-file que hace exactamente eso.

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -w
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

En este punto hemos, agradablemente, fusionado el archivo. De hecho, esto en realidad funciona mejor que la opción de ignore-all-space, porque realmente soluciona los cambios de los espacios en blanco antes de la fusión, en lugar de simplemente ignorarlo. En la fusión `ignore-all-space, en realidad, terminamos con unas pocas líneas con finales de línea DOS, haciendo que las cosas se mezclen.

Si quiere tener una idea antes de finalizar este compromiso sobre qué había cambiado en realidad entre un lado y el otro, puede pedirle a git diff que compare qué hay en su directorio de trabajo que está a punto de comprometer como resultado de la fusión a cualquiera de estas etapas. Vamos a través de todas ellas.

Para comparar el resultado con lo que tenías en su rama antes de la fusión, en otras palabras, para ver lo que su fusión insertó, puede correr git diff --ours

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Así, podemos observar fácilmente lo que sucedió en nuestra rama, y si lo que en realidad estamos insertando a este archivo con esta fusión está cambiando solamente esa línea.

Si queremos ver cómo el resultado de la fusión difiere de lo que estaba del otro lado, podemos correr git diff --theirs. En este y el siguiente ejemplo, tenemos que usar -w para despojarlo de los espacios en blanco porque lo estamos comparando con lo que está en Git, no con nuestro archivo limpio hello.theirs.rb

$ git diff --theirs -w
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

Finalmente, puede observar cómo el archivo ha cambiado desde ambos lados con git diff --base.

$ git diff --base -w
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

En este punto podemos usar el comando git clean para limpiar los archivos sobrantes que creamos para hacer la fusión manual, pero que ya no necesitamos.

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

Revisando Los Conflictos

Tal vez en este punto no estemos felices con la resolución por alguna razón, o quizás manualmente editando uno o ambos lados todavía no funciona como es debido y necesitamos más contexto.

Cambiemos el ejemplo un poco. En este caso, tenemos dos ramas de larga vida las cuales cada una tiene unos pocos “commit” en ella, aparte de crear un contenido de conflicto legítimo cuando es fusionado.

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed text to hello mundo
|/
* b7dcc89 initial hello world code

Ahora tenemos tres “commit” únicos que viven solo en la rama principal y otros tres que viven en la rama mundo. Si intentamos fusionar la rama mundo, generaremos un conflicto.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Nos gustaría ver cuál es el conflicto de fusión. Si abrimos el archivo, veremos algo así:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

Ambos lados de la fusión han añadido contenido a este archivo, pero algunos de los “commit” han modificado el archivo en el mismo lugar que causó el conflicto.

Exploremos un par de herramientas que ahora tiene a su disposición para determinar cómo el conflicto resultó ser. Tal vez, no es tan obvio cómo exactamente debería solucionar este problema. Necesita más contexto.

Una herramienta útil es git checkout con la opción “--conflict”. Esto revisará el archivo de nuevo y reemplazará los marcadores de conflicto de la fusión. Esto puede ser útil si quiere reiniciar los marcadores y tratar de resolverlos de nuevo.

Puedes pasar --conflict en lugar de diff3 o merge (lo que es por defecto). Si pasa diff3, Git usará una versión un poco diferente de marcadores de conflicto, no solo dándole “ours” versión y la versión de “theirs”, sino también la versión “base” en línea para darle más contexto.

$ git checkout --conflict=diff3 hello.rb

Una vez que corremos eso, en su lugar el archivo se verá así:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

Si este formato es de su agrado, puede configurarlo como “default” para futuros conflictos de fusión al colocar el merge.conflictstyle configurándolo a diff3.

$ git config --global merge.conflictstyle diff3

El comando git checkout puede también tomar la opción de --theirs`o la `--ours, lo cual puede ser una manera mucho más rápida de escoger un lado o el otro sin tener que fusionar las cosas en lo absoluto.

Esto puede ser particularmente útil para conflictos de archivos binarios donde simplemente puede escoger un lado, o donde solo quiere fusionar ciertos archivos desde otra rama – puede hacer la fusión y luego revisar ciertos archivos de un lado o del otro antes de comprometerlos

Registro de Fusión

Otra herramienta útil al resolver conflictos de fusión es git log. Esto puede ayudarle a tener contexto de lo que pudo haber contribuido a los conflictos. Revisar un poco el historial para recordar por qué dos líneas de desarrollo estaban tocando el mismo código de área, puede ser muy útil algunas veces.

Para obtener una lista completa de “commit” únicos que fueron incluidos en cualquiera de las ramas involucradas en esta fusión, podemos usar la sintaxis “triple dot” (triple punto) que aprendimos en Tres puntos.

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo

Esa es una buena lista de los seis compromisos involucrados, así como en qué línea de desarrollo estuvo cada compromiso.

Sin embargo, podemos simplificar aún más esto para darnos un contexto mucho más específico. Si añadimos la opción --merge a git log, solo mostrará los compromisos en cualquier lado de la fusión que toque un archivo que esté actualmente en conflicto.

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo

En su lugar, si corremos eso con la opción -p obtendremos sólo los diffs del archivo que terminó en conflicto. Esto puede ser bastante útil, al darle rápidamente el contexto que necesita para ayudarle a entender por qué algo crea problemas y cómo resolverlo de una forma más inteligente.

Formato Diff Combinado

Dado que las etapas de Git clasifican los resultados que tienen éxito, cuando corre git diff mientras está en un estado de conflicto de fusión, sólo puede obtener lo que está actualmente en conflicto. Esto puede ser útil para ver lo que todavía debe resolver.

Cuando corre directamente git diff después de un conflicto de fusión, le dará la información en un formato de salida diff bastante único.

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

El formato es llamado “Diff combinado” y proporciona dos columnas de datos al lado de cada línea. La primera columna muestra si esa línea es diferente (añadida o removida) entre la rama “ours” y el archivo en su directorio de trabajo, y la segunda columna hace lo mismo entre la rama “theirs” y la copia de su directorio de trabajo.

Así que en ese ejemplo se puede observar que las líneas <<<<<<< y >>>>>>> están en la copia de trabajo, pero no en ningún lado de la fusión. Esto tiene sentido porque la herramienta de fusión las mantiene ahí para nuestro contexto, pero se espera que las removamos.

Si resolvemos el conflicto y corremos git diff de nuevo, veremos la misma cosa, pero es un poco más útil.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Esto muestra que “hola mundo” estaba de nuestro lado, pero no en la copia de trabajo, que “hello mundo” estaba en el lado de ellos, pero no en la copia de trabajo y finalmente que “hola mundo” no estaba en ningún lado, sin embargo está ahora en la copia de trabajo. Esto puede ser útil para revisar antes de comprometer la resolución.

También se puede obtener desde el git log para cualquier fusión después de realizada, para ver cómo algo se resolvió luego de dicha fusión. Git dará salida a este formato si se puede correr git show en un compromiso de fusión, o si se añade la opción --cc a un git log -p (el cual por defecto solo muestras parches para compromisos no fusionados).

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Deshaciendo Fusiones

Ahora que ya conoce como crear un “merge commit” (compromiso de fusión), probablemente haya creado algunos por error. Una de las ventajas de trabajar con Git es que está bien cometer errores, porque es posible y, en muchos casos, es fácil solucionarlos.

Los compromisos de fusión no son diferentes. Digamos que comenzó a trabajar en una rama temática accidentalmente fusionada en una rama master, y ahora el historial de compromiso se ve así:

Accidental merge commit.
Figura 138. Accidental merge commit

Existen dos formas de abordar este problema, dependiendo de cuál es el resultado que desea.

Solucionar las referencias

Si el compromiso de fusión no deseado solo existe en su repositorio local, la mejor y más fácil solución es mover las ramas para que así apunten a dónde quiere que lo hagan. En la mayoría de los casos si sigue al errante git merge con git reset --hard HEAD~, esto restablecerá los punteros de la rama, haciendo que se vea así:

History after `git reset --hard HEAD~`.
Figura 139. History after git reset --hard HEAD~

Ya vimos reset de nuevo en Reiniciar Desmitificado, así que no debería ser muy difícil averiguar lo que está sucediendo. Aquí un repaso rápido: reset --hard usualmente va a través de tres pasos:

  1. Mover los puntos de la rama HEAD. En este caso, se quiere mover la principal`a donde se encontraba antes el compromiso de fusión (`C6).

  2. Hacer que el índice parezca HEAD.

  3. Hacer que el directorio de trabajo parezca el índice.

La desventaja de este enfoque es que se reescribirá el historial, lo cual puede ser problemático con un depósito compartido. Revise Los Peligros de Reorganizar para saber más de lo que puede suceder; la versión corta es que, si otras personas tienen los compromisos que está reescribiendo, probablemente debería evitar resetear. Este enfoque tampoco funcionará si cualquiera de los otros compromisos han sido creados desde la fusión; mover los refs efectivamente perdería esos cambios.

Revertir el compromiso

Si mover los punteros de la rama alrededor no funciona para su caso, Git le proporciona la opción de hacer un compromiso (“commit”) nuevo que deshace todos los cambios de uno ya existente. Git llama a esta operación un “revert”, y en este escenario en particular, ha invocado algo así:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

La bandera -m 1 indica cuál padre es el “mainline” y debería ser mantenido. Cuando se invoque la fusión en el HEAD (git merge topic), el nuevo compromiso tiene dos padres: el primero es HEAD (C6), y el segundo es la punta de la rama siendo fusionada en (C4). En este caso, se quiere deshacer todos los cambios introducidos por el fusionamiento en el padre #2 (C4), pero manteniendo todo el contenido del padre #1 (C6).

El historial con el compromiso revertido se ve así:

History after `git revert -m 1`.
Figura 140. History after git revert -m 1

El nuevo compromiso ^M tiene exactamente los mismos contenidos que C6, así que comenzando desde aquí es como si la fusión nunca hubiese sucedido, excepto que ahora los no fusionados compromisos están todavía en HEAD's history. Git se confundirá si intenta fusionar la rama temática en la rama master:

$ git merge topic
Already up-to-date.

No hay nada en topic que no sea ya alcanzable para la master. Que es peor, si añade trabajo a topic y fusiona otra vez, Git solo traerá los cambios desde la fusión revertida:

History with a bad merge.
Figura 141. History with a bad merge

La mejor forma de evitar esto es deshacer la fusión original, dado que ahora se quiere traer los cambios que fueron revertidos, luego crear un nuevo compromiso de fusión:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
History after re-merging a reverted merge.
Figura 142. History after re-merging a reverted merge

En este ejemplo, M y ^M se cancelan. Efectivamente ^^M se fusiona en los cambios desde C3 y C4, y C8 se fusiona en los cambios desde C7, así que ahora topic está completamente fusionado.

Otros Tipos de Fusiones

Hasta hora ya cubrimos la fusión normal de dos ramas, normalmente manejado con lo que es llamado la estrategia de fusión “recursive”. Sin embargo, hay otras formas de fusionar a las ramas. Cubriremos algunas de ellas rápidamente.

Nuestra o Su preferencia

Primero que nada, hay otra cosa útil que podemos hacer con el modo de fusión “recursive”. Ya vimos las opciones ignore-all-space e ignore-space-change las cuales son pasadas con un -X, pero también le podemos decir a Git que favorezca un lado u otro cuando observe un conflicto.

Por defecto, cuando Git ve un conflicto entre dos ramas siendo fusionadas, añadirá marcadores de conflicto de fusión a los códigos, marcará el archivo como conflictivo y le dejará resolverlo. Si prefiere que Git simplemente escoja un lado específico e ignore el otro, en lugar de dejarle manualmente fusionar el conflicto, puede pasar el comando de fusión, ya sea on un -Xours o -Xtheirs.

Si Git ve esto, no añadirá marcadores de conflicto. Cualquier diferencia que pueda ser fusionable, se fusionará. Cualquier diferencia que entre en conflicto, él simplemente escogerá el lado que especifique en su totalidad, incluyendo los archivos binarios.

Si volvemos al ejemplo de “hello world” que estábamos utilizando antes, podemos ver que el fusionamiento en nuestra rama causa conflicto.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Sin embargo, si lo corremos con -Xours o -Xtheirs no lo causa.

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

En este caso, en lugar de obtener marcadores de conflicto en el archivo con “hello mundo” en un lado y “hola world” en el otro, simplemente escogerá “hola world”. Sin embargo, todos los cambios no conflictivos en esa rama se fusionaron exitosamente.

Esta opción también puede ser trasmitida al comando git merge-file que vimos antes al correr algo como esto git merge-file --ours para archivos de fusión individuales.

Si quiere realizar algo así, pero Git no ha intentado siquiera fusionar cambios desde el otro lado, hay una opción más draconiana, la cual es la estrategia de fusión “ours” merge strategy. Esto es diferente de la opción de fusión recursiva “ours” recursive merge _option.

Esto básicamente hace una fusión falsa. Registrará un nuevo compromiso de fusión con ambas ramas como padres, pero ni siquiera mirará a la rama que está fusionando. Simplemente registrará como el resultado de la fusión el código exacto en su rama actual.

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

Puede observar que no hay diferencia entre la rama en la que estábamos y el resultado de la fusión.

Esto a menudo puede ser útil para, básicamente, engañar a Git y que piense que una rama ya ha sido fusionada cuando se hace una fusión más adelante. Por ejemplo, decir que ha ramificado una rama de “release” y ha hecho un poco de trabajo que querrá fusionar de vuelta en su rama “master” en algún punto. Mientras tanto, algunos arreglos de fallos en la “master” necesitan ser adaptados en la rama de release. Se puede fusionar la rama “bugfix” en la de release y también merge -s ours, la misma rama en la principal (a pesar de que el arreglo ya se encuentre ahí). Así que, más tarde cuando fusione la de lanzamiento otra vez, no hay conflictos del “bugfix”.

Convergencia de Subárbol

La idea de la convergencia de subárboles es que usted tiene dos proyectos, de los cuales uno lleva un subdirectorio del otro y viceversa Cuando especifica una convergencia de subárbol, Git suele ser lo suficientemente inteligente para comprender que uno es un subárbol del otro y convergerá apropiadamente.

Veremos un ejemplo donde se añade un proyecto separado a un proyecto existente y luego se converge el código del segundo dentro de un subdirectorio del primero.

Primero, añadiremos la aplicación Rack a nuestro proyecto. Añadiremos el proyecto Rack como referencia remota en nuestro propio proyecto y luego lo colocaremos en su propia branch:

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

Ahora tenemos la raíz del proyecto Rack en nuestro branch rack_branch y nuestro proyecto en el branch master. Si verifica uno y luego el otro, puede observar que tienen diferentes raíces de proyecto:

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

Este concepto es algo extraño. No todas las branchs en su repositorio tendrán que ser branchs del mismo proyecto como tal. No es común, porque rara vez es de ayuda, pero es fácil que los branchs contengan historias completamente diferentes.

En este caso, queremos integrar el proyecto Rack a nuestro proyecto master como un subdirectorio. Podemos hacer eso en Git con git read-tree. Aprenderá más sobre read-tree y sus amigos en [ch10-git-internals], pero por ahora sepa que éste interpreta el árbol raíz de una branch en su 'área de staging y directorio de trabajo. Sólo cambiamos de vuelta a su branch master, e integramos la branch rack_branch al subdirectorio rack de nuestra branch master de nuestro proyecto principal:

$ git read-tree --prefix=rack/ -u rack_branch

Cuando hacemos “commit”, parece que tenemos todos los archivos Rack bajo ese subdirectorio - como si los hubiéramos copiado de un tarball. Lo interesante es que podemos facilmente converger cambios de una de las branchs a la otra. Entonces, si el proyecto Rack se actualiza, podemos atraer cambios río arriba alternando a esa branch e incorporando:

$ git checkout rack_branch
$ git pull

Luego, podemos converger de vuelta esos cambios a nuestr branch master. Para incorporar los cambios y rellenar previamente el mensaje de “commit”, utilice las opciones --squash y --no-commit, así como la estrategia de convergencia recursiva de la opción -Xsubtree. (La estrategia recursiva está aquí por defecto, pero la incluímos para aclarar.)

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack --no-commit rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

Todos los cambios del proyeto Rack se convergieron y están listos para ser encomendados localmente. También puede hacer lo opuesto - hacer cambios en el subdirectorio rack de su master branch y luego convergerlos a su branch rack_branch más adelante para entregarlos a los mantenedores o empujarlos río arriba.

Esto nos deja una manera de tener un flujo de trabajo algo similar al flujo de trabajo de submódulo sin utilizar submódulos (de los cuales hablamos en Submódulos). Podemos mantener branchs con otros proyectos relacionados en nuestro repositorio y convergerlos tipo subárbol a nuestro proyecto ocasionalmente. Esto es bueno por ciertas razones, por ejemplo, todo el códido se encomienda a un único lugar. Sin embargo, tiene el defecto de ser un poco más complejo y facilita el cometer errores al reintegrar cambios o empujar accidentalmente una branch a un repositorio con el que no guarda relación.

Otra particularidad es que para diferenciar entre lo que tiene en su subdirectorio rack y el código en su branch rack_branch - para ver si necesita convergerlos - no puede usar el comando diff normal. En lugar de esto, debe ejecutar git diff-tree con la branch que desea comparar:

$ git diff-tree -p rack_branch

O, para comparar lo que hay en su subdirectorio rack con lo que era la branch master en el servidor la última vez que usted hizo fetch, ejecute:

$ git diff-tree -p rack_remote/master
scroll-to-top