Mostrando entradas con la etiqueta git merge. Mostrar todas las entradas
Mostrando entradas con la etiqueta git merge. Mostrar todas las entradas

lunes, 17 de octubre de 2022

GIT - 5 - Conflictos entre archivos. Git merge Vs Git rebase

Normalmente los conflictos de fusión se producirán cuando dos personas han cambiado las mismas líneas en un archivo o si una de ellas ha borrado un archivo que otra está desarrollando. En estos casos Git no sabe como solventar la situación y generará un conflicto que tendremos que resolver.

Tipos de conflictos de fusión.

- Git no inicia la fusión.

Una fusión no se iniciará si Git detecta que hay cambios en el directorio de trabajo actual, no confirmados que puedan ser sobrescritos en la fusión. Vamos a crear un ejemplo de fusión que cree un error de este tipo:

chema@mcbook:~/Prueba$ git init
Initialized empty Git repository in /home/chema/Prueba/.git/
chema@mcbook:~/Prueba$ > main.py
chema@mcbook:~/Prueba$ git add .
chema@mcbook:~/Prueba$ git commit -m "Primer commit en rama main."
[main (root-commit) b19e412] Primer commit en rama main.
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 main.py
 
chema@mcbook:~/Prueba$ git branch auxiliar
chema@mcbook:~/Prueba$ git switch auxiliar 
Switched to branch 'auxiliar'
chema@mcbook:~/Prueba$ echo "# Un comentario." > main.py
chema@mcbook:~/Prueba$ git add .
chema@mcbook:~/Prueba$ git commit -m "Añadida linea en archivo. Rama 
auxiliar."
[auxiliar e195481] Añadida linea en archivo. Rama auxiliar.
 1 file changed, 1 insertion(+)
 
chema@mcbook:~/Prueba$ git switch main 
Switched to branch 'main'
chema@mcbook:~/Prueba$ echo "segunda linea" > main.py
chema@mcbook:~/Prueba$ git merge auxiliar
Updating b19e412..e195481
error: Your local changes to the following files would be overwritten
     by merge:
	main.py
Please commit your changes or stash them before you merge.
Aborting


- Git falla durante la fusión.

Un fallo durante la fusión indica un conflicto entre la rama local actual y la rama que se está fusionando. Git hará lo posible para fusionar los archivos, pero te dejará cosas a solventar en los archivos con conflictos. 

Vamos a verlo con un ejemplo.

chema@mcbook:~/Prueba$ git init
Initialized empty Git repository in /home/chema/Prueba/.git/
chema@mcbook:~/Prueba$ echo "Esto es el texto inicial." > merge.txt
chema@mcbook:~/Prueba$ git add .
chema@mcbook:~/Prueba$ git commit -m "Confirmamos el contenido inicial."
[main (root-commit) e951c03] Confirmamos el contenido inicial.
 1 file changed, 1 insertion(+)
 create mode 100644 merge.txt

Ahora tenemos un repositorio con una única rama main y un poco de texto en el archivo merge.txt. A continuación creamos una nueva rama y la utilizaremos para crear una fusión conflictiva.

chema@macbook:~/Prueba$ git checkout -b nueva_rama_para_mezclar_luego
Switched to a new branch 'nueva_rama_para_mezclar_luego'
chema@macbook:~/Prueba$ echo "Contenido diferente para mezclar luego." > merge.txt
chema@macbook:~/Prueba$ git add .
chema@macbook:~/Prueba$ git commit -m "Editado el contenido del archivo merge.txt para crear un conflicto."
[nueva_rama_para_mezclar_luego 9a77225] Editado el contenido del archivo merge.txt para crear un conflicto.
 1 file changed, 1 insertion(+), 1 deletion(-)

Con esta nueva rama, hemos creado una confirmación que sobrescribe el contenido de merge.txt

chema@mcbook:~/Prueba$ git switch main
Switched to branch 'main'
chema@mcbook:~/Prueba$ echo "Contenido para añadir." >> merge.txt
chema@mcbook:~/Prueba$ git add .
chema@mcbook:~/Prueba$ git commit -m "añadido contenido a merge.txt"
[main 4f820aa] añadido contenido a merge.txt
 1 file changed, 1 insertion(+)

Esto ahora pone nuestro repositorio de ejemplo en un estado en el que tenemos dos nuevas confirmaciones. Una está en la rama main y la otra en la rama 'nueva_rama_para_mezclar_luego'. En este momento vamos a fusionarlas a ver que pasa.

chema@mcbook:~/Prueba$ git branch
* main
  nueva_rama_para_mezclar_luego
chema@mcbook:~/Prueba$ git merge nueva_rama_para_mezclar_luego 
Auto-merging merge.txt
CONFLICT (content): Merge conflict in merge.txt
Automatic merge failed; fix conflicts and then commit the result.

Como no podía ser de otra forma Git nos indica que existe un conflicto puesto que las dos ramas intentan escribir en la misma línea del archivo merge.txt.


Como identificar conflictos de fusión.


Tal como hemos visto anteriormente Git al intentar la fusión nos avisa de que existe un conflicto. 
Podemos ver más detalles usando el siguiente comando:

chema@macbook:~/Prueba$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   merge.txt

no changes added to commit (use "git add" and/or "git commit -a")

El resultado de "git status" nos indica que existen ramas a fusionar a causa de un conflicto. El archivo merge.txt aparece ahora como modificado. Vamos a ver el archivo para ver que es lo que se ha modificado. 

chema@macbook:~/Prueba$ cat merge.txt
<<<<<<< HEAD
Esto es el texto inicial.
Contenido para añadir.
=======
Contenido diferente para mezclar luego.
>>>>>>> nueva_rama_para_mezclar_luego

Se nos muestra lo que aporta cada rama  <<<<<<< HEAD y >>>>>> nueva_rama_para_mezclar_luegoseparado por los signos ==========. 


Como resolver conflictos de fusión mediante la línea de comandos.


La forma de resolver en nuestro caso el archivo es editar el archivo merge.txt. Como las líneas que intentamos fusionar no son excluyentes, quitamos los signos añadidos por Git y lo dejamos como está.
El contenido del archivo merge.txt tendría el siguiente aspecto:

Esto es el texto inicial.
Contenido para añadir.
Contenido diferente para mezclar luego.

Cuando hayas editado el archivo, utiliza git add merge.txt para prepararlo para la fusión. Finalmente terminamos la fusión con una nueva confirmación:

chema@macbook:~/Prueba$ git add merge.txt
chema@macbook:~/Prueba$ git commit -m "fusionados y resueltos los conflictos de merge.txt"
[main d1d0197] fusionados y resueltos los conflictos de merge.txt

Git merge Vs Git rebase.

Lo primero que hay que comentar sobre estos dos comandos es que ambos están diseñados para la misma función. Integrar las modificaciones o cambios que se hayan hecho en una rama con los de otra. En lo que varían es que lo hacen de forma diferente.

Imaginemos que tenemos un repositorio con dos ramas. Una llamada "main" y otra llamada "nuevas_caracteristicas". En la rama main un miembro del equipo ha hecho modificaciones y ha agregado varios commits. Por nuestra parte en la rama "nuevas_caracteristicas" nosotros hemos hecho algunas aportaciones con sus correspondientes confirmaciones. Este escenario da lugar a un historial bifurcado típico de git, tal como este:


escenario log gits
Digamos que ahora las nuevas confirmaciones en la rama main son relevantes para nuestro rama y queremos utilizarlas. Tenemos para ello dos opciones:


1) GIT MERGE

$ git switch nuevas_caracteristicas
o
$ git checkout nuevas_caracteristicas
(Para cambiar de rama.)

$ git merge main

Esta instrucción crea una nueva confirmación de mezcla en la rama nuevas_caracteristicas que une o fusiona las historias de ambas ramas, lo que hace que quede una estructura como esta:


git merge


Usar "git merge" hace que no cambien las ramas de ninguna forma y evita los problemas que puede causar la otra opción como veremos luego.

Por otro lado esto también significa, que la rama "nuevas_características" tendrá una confirmación de commits extraña cada vez que se necesiten incorporar cambios ascendentes. Si la rama main es muy activa esto puede contaminar bastante el historial de la rama "nueva_caracteristicas" al extremo de dificultar la compresión del proyecto.

2) GIT REBASE

Como alternativa a la fusión, podemos rebasar la rama "nuevas_caracteristicas" en la rama "main" usando el siguiente comando:

$ git checkout nuevas_caracteristicas
o
$ git switch nuevas_caracteristicas
(Para cambiar de rama.)

$ git rebase main

Esto mueve toda la rama nuevas_caracteristicas para que comience en la punta de la rama main, incorporando efectivamente todas las nuevas confirmaciones de main. Pero en lugar de utilizar una confirmación de fusión, la reorganización vuelve a escribir el historial del proyecto mediante la creación de nuevas confirmaciones para cada confirmación de la rama main.


git rebase

El mayor beneficio es que se obtiene un historial del proyecto mucho más limpio. Primero, se eliminan todas las confirmaciones de combinación innecesarias requeridas por "git merge". En segundo lugar, como se puede ver en el diagrama anterior, da como resultado un historial del proyecto perfectamente lineal. Puedes seguir el proyecto desde la punta hasta el inicio sin ninguna bifurcación. Esto facilita la navegación por el proyecto.

Pero también hay dos desventajas. La primera es que volver a escribir el historial del proyecto puede ser catastrófico para el flujo de trabajo cuando se esta trabajando en equipo, ya que se unifican las ramas perdiendo el historial de los commits. Además monta los commits de una rama en otra sin importar la cronología. Y aunque menos importante, la reorganización pierde el contexto proporcionado por una confirmación de combinación: no se puede ver cuando se incorporaron los cambios anteriores a la función.


Debido a esto es SUPERIMPORTANTE:

Nunca usar si este comando si se esta trabajando en un repositorio público en el que se colabora con más personas y donde las confirmaciones y su historial son muy importantes.


lunes, 3 de octubre de 2022

GIT - 2 - Ramas.

Usando una analogía, podemos decir que las ramas son como cuando hacíamos una tarea en el colegio. El cuaderno principal que entregamos al profesor sería la rama master o main. Los cuadernos auxiliares con notas, borradores etc. serían las ramas auxiliares.

NOTA: Al comenzar un proyecto, debido a que la palabra "master" en inglés no es políticamente correcta, se prefiere utilizar como nombre de la rama principal "main". Por ello al iniciar un proyecto podemos transformar la rama master en main con:

$ git checkout -b main

Aunque sea adelantarnos esta instrucción lo que hace es crear la rama e ir a ella directamente.

Dicho esto, el comando con el que trabajaremos al utilizar las ramas es:

$ git branch

Con ella podemos crear, eliminar y manipular ramas.

Con:

$ git branch 

Se muestran todas las ramas de un repositorio.

$ git branch [nombre_nueva_rama]

Se creará una nueva rama.

Por ejemplo. Creemos una nueva rama con el nombre de "Prueba". Luego indicaremos al programa que queremos ver todas las ramas existentes.

chema@lenovo:~/proyecto$ git branch Prueba
chema@lenovo:~/proyecto$ git branch
  Prueba
* master

Como puedes ver el asterisco * nos indica en que rama estamos en este momento.


Para cambiar de una rama a otra:

$ git checkout [nombre_rama]

chema@lenovo:~/proyecto$ git checkout Prueba 
Switched to branch 'Prueba'
chema@lenovo:~/proyecto$ git branch
* Prueba
  master

El utilizar esta instrucción tiene su lógica ya que este comando hace que volvamos a la última instantánea que haya de ese sitio, ya se un commit, un archivo o una rama.

Al crear una rama y movernos a ella, hace que todos los archivos que creemos o modificaciones que hagamos se queden en esta rama (podemos decir que es otra versión distinta del repositorio). Si volvemos a movernos a la rama principal, vemos que no están los archivos o modificaciones que hayamos confirmado en la rama auxiliar. No se han perdido, si no que simplemente están ahora en los commits de esa rama.

Para clarificar un poco el concepto y como la instrucción "git checkout" se utiliza para muchas cosas, git (switch = cambiar) recientemente añadió este comando que hace lo mismo:

$ git switch [nombre_rama] 


Eliminar una rama.

$ git branch -d [rama_a_eliminar]

Si la rama esta vacía, no tiene contenido, el programa no realiza ningún aviso, las borra y ya está. Por el contrario si la rama tiene contenido, porque previamente hemos hecho en ella algún commit, al intentar borrarla, si antes no la hemos mezclado con la rama principal (cosa que veremos en breve), nos realizará un aviso.

Para borrar una rama, previamente tenemos que cambiarnos a una rama diferente.

Ejemplo de una rama con contenido que queremos borrar:

chema@lenovo:~/proyecto$ git branch -d Prueba 
error: The branch 'Prueba' is not fully merged.
If you are sure you want to delete it, run 'git branch -D Prueba'.

Fusión de ramas.

Tenemos que estar en la rama a la que queremos fusionar otra. Por ejemplo si tenemos una rama auxiliar que queremos fusionar con la rama principal del repositorio entonces tenemos que estar en esta última para realizar la fusión.

El comando a utilizar es:

$ git merge [rama_a_fusionar]

La fusión va a combinar tanto los datos de las ramas como el historial.

GIT utiliza dos algoritmos diferentes para realizar una fusión.

1) fast-forward (fusión de avance rápido)

2) three-way merge (fusión de tres vías)


1) Fast-Forward.

Cuando todas las confirmaciones en la rama extraída también están en la que se está fusionando, es decir hay una ruta lineal directa desde la rama de origen a la rama de destino. En este tipo de fusión, Git mueve el puntero de la rama de origen al puntero de la rama de destino sin crear una confirmación de fusión adicional. 

Veamos un ejemplo:

Tenemos una rama principal con tres confirmaciones (commits).

proyecto en git con tres confirmaciones

A continuación creamos una rama llamada auxiliar. En Git realmente una rama no es más que un puntero a una determinada confirmación. En este momento tanto la rama principal como la rama auxiliar apuntan a la misma confirmación.

proyecto en git con tres confimaciones y dos ramas

Ahora cambiemos a la rama auxiliar y realicemos un par de confirmaciones. 

rama principal y rama auxiliar


Para fusionar los cambios en la rama principal, todo lo que tenemos que hacer es cambiar el puntero de la rama principal hacia adelante. Por eso este método se llama fusión de avance rápido.

fusión fast-fordward


Como entenderlo puede ser un poco farragoso vamos a verlo con un ejemplo teórico y luego otro real.

Digamos que tenemos un directorio llamado MAIN_V_1.0. Es un programa que estamos desarrollando y ahí estarán todos sus archivos. Ahora imaginemos que queremos añadirle nuevas funcionalidades. Como no sabemos si estas nueva funcionalidades estropearán el programa principal lo que hacemos es copiar esta carpeta y renombrarla como MAIN_FIX_V_1.0. El código de esta última carpeta es idéntico al del directorio original.  Ahora digamos que hicimos cambios en esta carpeta para mejorar el programa, no hay errores de ejecución y llegamos a MAIN_FIX_V_2.0. La pregunta que se plantea es ¿Cómo llevamos esos cambios a la carpeta Principal MAIN_V_1.0?

Solución 1: Copiamos todos los archivos de MAIN_FIX a MAIN. Esta no es la mejor solución ya que las operaciones de copia pueden llevar bastante tiempo si tenemos muchos archivos en la carpeta MAIN_FIX. Debería haber una mejor solución.

Solución 2: Dado que no hemos modificado el directorio Principal "MAIN_V_1.0" y todo en este directorio es esencialmente la primera versión de MAIN_FIX, simplemente podemos cambiar el nombre de MAIN_FIX a MAIN_V_2.0. De ahora en adelante, podemos decir que este es nuestro nuevo directorio Principal. El concepto de avance rápido en GIT es muy similar a esta solución.

Ejemplo PRACTICO:

$ git init
$ echo hola>hola.txt
$ git add .
$ git commit -m 'primer commit'
$ echo mundo>>hola.txt
$ git add .
$ git commit -m 'segundo commit'
$ echo '¿Que tal?'>>hola.txt
$ git commit -a -m 'tercer commit'

- Después de hacer tres confirmaciones creamos una nueva rama.

$ git branch auxiliar
$ git switch auxiliar

- Añadimos una nueva línea a hola.txt y creamos un archivo nuevo 
  en esta nueva rama.
$ echo 'Estamos en una nueva rama'>>hola.txt
$ touch nuevo_archivo.txt
$ git add .
$ git commit -m 'auxiliar 1'
$ echo 'ultimo texto'>>hola.txt
$ git add .
$ git commit -m 'auxiliar 2'

- Puedes probar a cambiar entre ramas y ver los directorios. Verás
que en la rama master solo esta el archivo hola.txt y el texto que hemos  
añadido en los commits suyos. En la rama auxiliar, sin embargo, hay 
un archivo más, nuevo_archivo.txt y el archivo hola.txt tiene más texto.

- Ahora cambiamos a la rama master y realizamos la fusión.
$ git switch master
$ git merge auxiliar

Ya esta la rama master actualizada con lo últimos commits de la rama
auxiliar. 


2) Three-way merge.

Se utiliza cuando el historial de las ramas de la fusión ha divergido de alguna manera. Vamos a verlo con un ejemplo. En el siguiente gráfico vemos como la rama Auxiliar está dos commits o confirmaciones por delante de la rama principal.

Fusión tres vías


Antes de fusionarla con la rama principal, digamos que hemos modificado algo en la misma (un archivo nuevo, algún dato a mayores etc) y hemos realizado una confirmación adicional, como se muestra en el siguiente gráfico.

Fusión tres vias

Debido a esta confirmación en la rama principal, ahora ambas ramas están separadas. Esto significa que tenemos algunos cambios en la rama Principal que no están presentes en la rama Auxiliar. Si en esta situación intentamos una fusión, Git no puede mover el puntero de la rama Principal hacia la rama Auxiliar.  Si Git simplemente moviese el puntero de la rama Principal a la Auxiliar, se perderá la última confirmación C6 realizada.

Entonces, ¿Cómo realizamos la fusión si ambas ramas divergen?

Cuando queremos fusionar ramas que divergen, Git crea una nueva confirmación (merge commit) y combina los cambios de estas dos ramas tal como se muestra en el siguiente diagrama.

merge commit



La razón por la que se llama fusión de tres vías es porque se basa en tres confirmaciones diferentes.

1.- El ancestro común de todas las ramas que en nuestro caso es el commit C3. Esta confirmación contiene el código del repositorio antes de que divergiéramos en diferentes ramas.

2.- La punta de la rama Principal que es la ultima confirmación realizada, es decir C6.

3.- La punta de la rama Auxiliar que es la última confirmación realizada en esta rama es decir la C5.

Para fusionar los cambios de ambas ramas, Git mira las tres confirmaciones diferentes. Basándose en estas instantáneas, Git combina los cambios mediante la creación de una nueva confirmación denominada Merge Commit.

Veámoslo con un ejemplo:

$ git init
$ echo uno>1.txt
$ git add .
$ git commit -m 'c1'
$ echo dos>2.txt
$ git add .
$ git commit -m 'c2'
$ echo tres>3.txt
$ git add .
$ git commit -m 'c3'
$ git branch auxiliar
$ git switch auxiliar
$ echo cuatro>4.txt
$ git add .
$ git commit -m 'c4'
$ echo cinco>5.txt
$ git add .
$ git commit -m 'c5'
$ git switch master
$echo seis>6.txt
$ git add .
$ git commit -m 'c6'
$ git merge auxiliar
$ git log --oneline --all --graph

Salida

Merge made by the 'ort' strategy.
 4.txt | 1 +
 5.txt | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 4.txt
 create mode 100644 5.txt

*   fc9005e (HEAD -> master) Merge branch 'auxiliar'
|\  
| * d5002e6 (auxiliar) c5
| * a66d859 c4
* | 3e7c402 c6
|/  
* 1c0f647 c3
* 9c7f558 c2
* 5b7bd0c c1

Nota:

$ git log --oneline --graph

git log --oneline hace que los diferentes log se nos muestren de una forma más bonita, en una sola línea y mostrando solo una versión abreviada del Id del commit, rama y texto del commit.

git log --graph nos muestra las ramas en forma de gráfico.


IMPORTANTE:  El fusionar ramas puede dar lugar a problemas o conflictos de fusión si hay cambios en un mismo archivo en las dos ramas. Luego veremos porque se producen y como solventarlas.


Deshacer una Fusión.

Si nos surge cualquier problema a la hora de realizar una fusión, es decir nos salga un error en que se nos diga que hay un conflicto de fusión, como salvavidas de escape tenemos el siguiente comando:

$ git merge --abort

Si como en el ejemplo previo, la fusión se realizó correctamente, pero por la razón que sea queremos volver a como estaban las cosas antes de la misma:

Comprobamos que estamos en la rama principal:

chema@lenovo:~/proyecto$ git branch 
  auxiliar
* master

y ejecutamos el siguiente comando:

$ git reset --hard [ultimo_commit_antes_de_la_fusión]


Como en el ejemplo anterior el último commit es el 6, haríamos:

$ git reset --hard 3e7c402