-
1. Inicio - Sobre el Control de Versiones
-
2. Fundamentos de Git
-
3. Ramificaciones en Git
-
4. Git en el Servidor
- 4.1 Los Protocolos
- 4.2 Configurando Git en un servidor
- 4.3 Generando tu clave pública SSH
- 4.4 Configurando el servidor
- 4.5 El demonio Git
- 4.6 HTTP Inteligente
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Git en un alojamiento externo
- 4.10 Resumen
-
5. Git en entornos distribuidos
-
6. GitHub
-
7. Herramientas de Git
- 7.1 Revisión por selección
- 7.2 Organización interactiva
- 7.3 Guardado rápido y Limpieza
- 7.4 Firmando tu trabajo
- 7.5 Buscando
- 7.6 Reescribiendo la Historia
- 7.7 Reiniciar Desmitificado
- 7.8 Fusión Avanzada
- 7.9 Rerere
- 7.10 Haciendo debug con Git
- 7.11 Submódulos
- 7.12 Agrupaciones
- 7.13 Replace
- 7.14 Almacenamiento de credenciales
- 7.15 Resumen
-
8. Personalización de Git
-
9. Git y Otros Sistemas
- 9.1 Git como Cliente
- 9.2 Migración a Git
- 9.3 Resumen
-
10. Los entresijos internos de Git
-
A1. Apéndice A: Git en otros entornos
- A1.1 Interfaces gráficas
- A1.2 Git en Visual Studio
- A1.3 Git en Eclipse
- A1.4 Git con Bash
- A1.5 Git en Zsh
- A1.6 Git en Powershell
- A1.7 Resumen
-
A2. Apéndice B: Integrando Git en tus Aplicaciones
- A2.1 Git mediante Línea de Comandos
- A2.2 Libgit2
- A2.3 JGit
-
A3. Apéndice C: Comandos de Git
- A3.1 Configuración
- A3.2 Obtener y Crear Proyectos
- A3.3 Seguimiento Básico
- A3.4 Ramificar y Fusionar
- A3.5 Compartir y Actualizar Proyectos
- A3.6 Inspección y Comparación
- A3.7 Depuración
- A3.8 Parcheo
- A3.9 Correo Electrónico
- A3.10 Sistemas Externos
- A3.11 Administración
- A3.12 Comandos de Fontanería
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í:
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í:
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:
-
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
). -
Hacer que el índice parezca HEAD.
-
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í:
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:
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
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