Guía del comando jq
Comparando TOML, JSON y YAML

Resolver conflictos en Git. Merge, Squash, Rebase o Pull

Merge Squash Rebase Pull

Merge, squash, rebase y pull. Resolver conflictos en git es un desafío continuo, así que vamos a ver ejemplos para evitar un lío de fusión utilizando estas opciones de git.

Un repositorio Git es un sistema de control de versiones que registra los cambios en los archivos. En un artículo previo hay una pequeña guía de comandos con algunos trucos y, en otro, se describen estrategias de fusión.

Hay que tener en cuenta que Git puede llegar a manejarlo un gran número de desarrolladores de software, trabajando en diferentes ramas, y esto puede causar conflictos a la hora de fusionar y se deben resolver día a día en un constante desafío. Vamos a ver los ejemplos de Jennifer Fu.

Índice de contenidos:

Un repo con conflictos para resolver con merge, squash, rebase y pull

Jennifer Fu creó un repositorio con conflictos, que puede ser clonado mediante el siguiente comando:

git clone https://github.com/JenniferFuBook/git-merge-conflicts.git

Hay dos ramas, main y feature, en el repositorio. Las podemos ver con git branch -av que tendrá el siguiente output.

* main                   d5ba99b main adds a new file
  remotes/origin/HEAD    -> origin/main
  remotes/origin/feature d64444a feature increases every number by 20
  remotes/origin/main    d5ba99b main adds a new file

Con git log –graph podemos ver una representación gráfica basada en texto del historial de confirmaciones en la parte izquierda de la salida. Cada * representa un commit, y las líneas entrantes bajo * representan los commits padres. Múltiples líneas entrantes indican una fusión, mientras que las líneas salientes marcan un ancestro común.

La opción –all muestra el historial de todas las ramas. Podemos hacerlo así

git log --all --graph
#Output
* commit d5ba99b96eedc106040aa63973e9fc7c08ee6f78 (HEAD -> main, origin/main, origin/HEAD)
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:35:22 2021 -0800
|
|     main adds a new file
|
* commit 9ff737a22d918f5781363b244104f394ef4a67e3
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:15:56 2021 -0800
|
|     main increases every number by 10
|
| * commit d64444a78401995781a3efce9ab250d43c19a022 (origin/feature)
|/  Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
|   Date:   Wed Mar 10 19:32:00 2021 -0800
|
|       feature increases every number by 20
|
* commit a6cd6c726a7d9bca0aec7d2af88914d79ceeedf3
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:13:08 2021 -0800
|
|     Create file.txt
|
* commit 78444c6cd523a305de2b7e8874b1ea6cc22f2afd
  Author: Jennifer Fu <54613999+JenniferFuBook@users.noreply.github.com>
  Date:   Wed Mar 10 19:06:25 2021 -0800
Initial commit

(En los trucos del anterior artículo tenemos git log con unas opciones maravillosas).

Después de que feature se ramificara de main, ambos modificaron file.txt (línea 14 y línea 8). Cuando feature se sincroniza con main, obtiene dos cambios:

  • newFile.txt que main crea (línea 2): No hay conflicto.
  • archivo.txt que main modifica (línea 8): Hay un conflicto.

Nos encontramos en la situación de que main y feature divergen con los nuevos cambios.

Tenemos varias formas de resolver este conflicto. Vamos a explorar y comparar las opciones.

git merge

git merge no hace cambios en el historial del repositorio. Simplemente crea una confirmación extra para la fusión.

Paso 1. Clonar el repositorio

Clona el repositorio en conflicto:

git clone https://github.com/JenniferFuBook/git-merge-conflicts.git

Con este repositorio de ejemplo se puede seguir casi todos los pasos. El push al remoto fallará porque se necesita estar autorizado con permiso de fusión. Sin embargo, puedes explorar todo en las ramas locales o incluso subir el código en tu propia forja de repos.

Por ejemplo, en github:

Restablece el upstream de tu repositorio clonado, y empuja todo el código.

git remote set-url origin https://github.com/
git switch main
git push
git switch feature
git push

Cada paso de este artículo se puede realizar tal cual en local.

Paso 2. Realizar la fusión

git switch feature

git merge origin/main

En el output se muestran los conflictos:
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.

Pueden ser verificados con git status

On branch feature
Your branch is up to date with 'origin/feature'.
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)
Changes to be committed:
 new file:   newFile.txt
Unmerged paths:
  (use "git add ..." to mark resolution)
 both modified:   file.txt

El conflicto con el archivo lo puedes ver si lo imprimes en pantalla con cat

Log:
main creates file.txt
<<<<<<<< HEAD
feature increases every number by 20
Content:
Sunday is 20
Monday is 21
Tuesday is 22
Wednesday is 23
Thursday is 24
Friday is 25
Saturday is 26
=======
main increases every number by 10
Content:
Sunday is 10
Monday is 11
Tuesday is 12
Wednesday is 13
Thursday is 14
Friday is 15
Saturday is 16
>>>>>>> origin/main

Step 3. Resolve the conflicts

Resolvemos los conflictos manualmente, editando el fichero con tu editor favorito (Espero que digas vim) y escogiendo entre la parte del <<<<<<< HEAD o a partir de la marca ======.  Si hubiese varios conflictos en el mismo documento lo iría marcando. Quedaría algo así:

Log:
main creates file.txt
main increases every number by 10
feature increases every number by 20
Content:
Sunday is 30
Monday is 31
Tuesday is 32
Wednesday is 33
Thursday is 34
Friday is 35
Saturday is 36

Ahora ya podemos confirmar los cambios y comitear. Se puede indicar en el mensaje del commit lo que hemos hecho.

git add .

git commit -m "feature resolves conflicts"

Paso 4. Push el merge al remoto

Ejecute git push y quedará solucionado con esta opción.

Paso 5. Examinar el historial de fusiones

Ahora podemos ver los cambios finales en la forja o en local con git log –all –graph.

*   commit cf1129a756bd0f2ef1641e4f5fd4c65684286ffa (origin/feature, feature)
|\  Merge: d64444a d5ba99b
| | Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| | Date:   Thu Mar 11 20:23:21 2021 -0800
| |
| |     feature resolves conflicts
| |
| * commit d5ba99b96eedc106040aa63973e9fc7c08ee6f78 (HEAD -> main, origin/main, origin/HEAD)
| | Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| | Date:   Wed Mar 10 19:35:22 2021 -0800
| |
| |     main adds a new file
| |
| * commit 9ff737a22d918f5781363b244104f394ef4a67e3
| | Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| | Date:   Wed Mar 10 19:15:56 2021 -0800
| |
| |     main increases every number by 10
| |
* | commit d64444a78401995781a3efce9ab250d43c19a022
|/  Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
|   Date:   Wed Mar 10 19:32:00 2021 -0800
|
|       feature increases every number by 20
|
* commit a6cd6c726a7d9bca0aec7d2af88914d79ceeedf3
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:13:08 2021 -0800
|
|     Create file.txt
|
* commit 78444c6cd523a305de2b7e8874b1ea6cc22f2afd
  Author: Jennifer Fu <54613999+JenniferFuBook@users.noreply.github.com>
  Date:   Wed Mar 10 19:06:25 2021 -0800
Initial commit

La representación gráfica basada en el texto anterior muestra la confirmación de fusión adicional cf1129a756bd0f2ef1641e4f5fd4c65684286ffa

El siguiente es el diagrama conceptual del historial de confirmaciones.

Bonito ¿Verdad? Vamos a por el siguiente.

git squash

En Git merge cada fusión genera un commit extra. Las fusiones más frecuentes tendrán más commit extra, lo que puede ser bastante tedioso para resolver.

git merge tiene la opción –squash que produce el árbol de trabajo y index state de la misma manera que un merge real, pero en el historial del merge se descarta.

El merge anterior de cinco pasos es igual excepto por lo siguiente:

Paso 2. Realizar la fusión con squash

git merge --squash origin/main

Paso 5. Examinar el historial de fusiones

Los cambios finales se verán así con git-squash. En local con git log –all –graph.

* commit 8fe1ca0b47100ad13cf577efe364e2c3aed69008 (origin/feature, feature)
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Thu Mar 11 21:38:23 2021 -0800
|
|     feature resolves conflicts
|
* commit d64444a78401995781a3efce9ab250d43c19a022
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:32:00 2021 -0800
|
|     feature increases every number by 20
|
| * commit d5ba99b96eedc106040aa63973e9fc7c08ee6f78 (HEAD -> main, origin/main, origin/HEAD)
| | Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| | Date:   Wed Mar 10 19:35:22 2021 -0800
| |
| |     main adds a new file
| |
| * commit 9ff737a22d918f5781363b244104f394ef4a67e3
|/  Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
|   Date:   Wed Mar 10 19:15:56 2021 -0800
|
|       main increases every number by 10
|
* commit a6cd6c726a7d9bca0aec7d2af88914d79ceeedf3
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:13:08 2021 -0800
|
|     Create file.txt
|
* commit 78444c6cd523a305de2b7e8874b1ea6cc22f2afd
  Author: Jennifer Fu <54613999+JenniferFuBook@users.noreply.github.com>
  Date:   Wed Mar 10 19:06:25 2021 -0800
Initial commit

En la representación gráfica que se muestra aparece el commit de fusión extra 8fe1ca0b47100ad13cf577efe364e2c3aed69008. El commit parece un commit normal en feature. Aplasta las confirmaciones en main en un solo commit y no tiene un path commit desde main.

git merge –squash altera el historial de confirmaciones pero produce un historial más limpio. Parece que todo el desarrollo ocurre en feature.

Esta estrategia se utiliza a menudo cuando se fusiona un Pull Request – fusionar a main desde una feature. Este ejemplo está en la dirección opuesta, aunque el concepto es el mismo. En cierto modo, no importa cuántos commits de fusión tiene la rama feature. Cuando se fusionan en la rama main con squash, se muestra todo el desarrollo de la característica como un solo commit.

El siguiente es el diagrama de concepto del historial de commits con squash.

git rebase

git rebase cambia el historial de commits pero crea un historial lineal moviendo feature a la punta del main.

Step 1. Clone the repository

Igual que el paso 1 de antes, empezamos de nuevo.

Step 2. Perform rebase

Con los siguientes comandos

git switch feature

git rebase origin/main

Tendremos este output con el conflicto en mayúsculas.

Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
error: could not apply d64444a... feature increases every number by 20
Resolve all conflicts manually, mark them as resolved with
"git add/rm ", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply d64444a... feature increases every number by 20

Se puede verificar por git status

interactive rebase in progress; onto d5ba99b
Last command done (1 command done):
   pick d64444a feature increases every number by 20
No commands remaining.
You are currently rebasing branch 'feature' on 'd5ba99b'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged ..." to unstage)
  (use "git add ..." to mark resolution)
	both modified:   file.txt

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

Tanto la salida de git rebase como de git status listan tres opciones:

  1. Arreglar los conflictos y luego ejecutar git rebase –continue.
  2. Usar git rebase –skip para omitir este parche.
  3. Usar git rebase –abort para revisar la rama original.

Estamos en medio de rebase. Lo podemos ver con git branch

* (no branch, rebasing feature)
feature
main

No está permitido dejar un rebase inacabado.

git switch -f feature
#Output
fatal: cannot switch branch while rebasing
Consider "git rebase --quit" or "git worktree add".
---
git switch -f feature
#Output
fatal: cannot switch branch while rebasing
Consider "git rebase --quit" or "git worktree add".

Paso 3. Resolver los conflictos

Debemos elegir una de las tres opciones. La opción 1 es para resolver los conflictos manualmente. Como hicimos con git merge. Pero después de confirmar los cambios y comitear tendremos que continuar el rebase.

git add .

git commit -m "feature resolves conflicts"

git rebase --continue
#Output
Successfully rebased and updated refs/heads/feature.

Paso 4. Push rebase al remote

Estamos listos para el push.

git push
#Output
To https://github.com/JenniferFuBook/git-rebase
 ! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to 'https://github.com/JenniferFuBook/git-rebase'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

¿Qué ha pasado? 🤯

¿Debemos seguir el consejo del output y hacer un pull? 🤔

git pull origin master
#Output
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint: git config pull.rebase false # merge (the default strategy)
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
error: Pulling is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.

La salida del pull muestra claramente que el consejo es engañoso. 😩

Después de ejecutar el comando rebase, la rama remota no puede ser adelantada al commit fusionado. Con lo cual, git push –force es necesario para el rebase. 👍

git push --force
#Output
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 396 bytes | 396.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/JenniferFuBook/git-rebase
+ d64444a...ba4c3fb feature -> feature (forced update)

Ahora si vamos al repositorio remoto de git-rebase veremos que el resultado de nuestro git rebase ha sido pusheado

Paso 5. Examinar el historial de rebase

Vamos a revisar el historial con git log –all –graph.

* commit ba4c3fbd670c08f30d7a16371719a81714ea8899 (HEAD -> feature, origin/feature)
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Sat Mar 13 12:24:24 2021 -0800
|
|     feature resolves conflicts
|
* commit d5ba99b96eedc106040aa63973e9fc7c08ee6f78 (origin/main, origin/HEAD, main)
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:35:22 2021 -0800
|
|     main adds a new file
|
* commit 9ff737a22d918f5781363b244104f394ef4a67e3
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:15:56 2021 -0800
|
|     main increases every number by 10
|
* commit a6cd6c726a7d9bca0aec7d2af88914d79ceeedf3
| Author: Jennifer Fu <jennifer.fu@dominodatalab.com>
| Date:   Wed Mar 10 19:13:08 2021 -0800
|
|     Create file.txt
|
* commit 78444c6cd523a305de2b7e8874b1ea6cc22f2afd
  Author: Jennifer Fu <54613999+JenniferFuBook@users.noreply.github.com>
  Date:   Wed Mar 10 19:06:25 2021 -0800

      Initial commit

En la representación gráfica del git rebase vemos las rutas de commits lineales. La confirmación en la rama se reescribe para ser ba4c3fbd670c08f30d7a16371719a81714ea8899, desde la punta de main. En lugar de crear un commit de fusión adicional, rebase reescribe la historia del proyecto creando nuevos commits para cada commit en la rama main. El commit original, d64444a78401995781a3efce9ab250d43c19a022, es reemplazado por ba4c3fbd670c08f30d7a16371719a81714ea8899.

El siguiente es el diagrama conceptual del historial de commits.

Precioso ¿Verdad? Pues aun nos queda otra opción.

git pull

git pull [<repositorio>] [<referencia>] obtiene y se integra con otro repositorio o con una rama local. En su modo por defecto, git pull es la abreviatura de git fetch seguido de git merge FETCH_HEAD.

git fetch actualiza con <repositorio><referencia> la rama seguida en remoto. Guarda temporalmente la punta remota en FETCH_HEAD. La posterior fusión o rebase ocurre en la referencia, FETCH_HEAD.

Tres estrategias de pull

Clonar nuestro repositorio de ejemplo y ejecutar git pull.

git pull origin main
#Output
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint: git config pull.rebase false # merge (the default strategy)
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
From https://github.com/JenniferFuBook/git-merge-conflicts
* branch main -> FETCH_HEAD
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.
Automatic merge failed; fix conflicts and then commit the result.
and the repository exists.

El output nos muestra las tres estrategias posibles para realizar git pull:

  1. git pull –no-rebase (la estrategia por defecto)
  2. git pull –rebase
  3. git pull –ff-only

git pull origin main realiza la estrategia por defecto, que es la misma que git pull –no-rebase origin main. También es lo mismo que git merge origin/main.

Si se continúa, el resultado será el mismo que se ha descrito en la sección git merge anterior. Vamos a dar marcha atrás del merge con git merge –abort.

Probamos la segunda estrategia con rebase:

git pull --rebase origin main
#Output
From https://github.com/JenniferFuBook/git-merge-conflicts
* branch main -> FETCH_HEAD
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
error: could not apply d64444a... feature increases every number by 20
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply d64444a... feature increases every number by 20

Si se continúa por este camino el resultado será el mismo que se ha descrito en la sección git rebase. Así que vamos a retroceder del rebase con git rebase –abort.

Echando un vistazo a la tercera estrategia para el fast-forward solamente (–ff-only). git pull tiene tres ajustes para especificar cómo se maneja una fusión:

  • –ff es la configuración por defecto. Cuando es posible, sólo realiza fast-forward de la rama para que coincida con la rama fusionada (no crea un commit de fusión). Cuando esto no es posible, crea un commit de fusión.
  • –no-ff crea un commit de fusión en todos los casos, incluso cuando la fusión puede resolverse como un fast-forward.
  • –ff-only resuelve la fusión como un fast-forward cuando es posible. Cuando no es posible, rechaza la fusión y sale con un estado distinto de cero.
git pull --ff-only origin main
#Output
From https://github.com/JenniferFuBook/git-merge-conflicts
* branch main -> FETCH_HEAD
fatal: Not possible to fast-forward, aborting.

git pull –ff-only es la opción más segura. Cuando el comando pull falla, puedes decidir qué método utilizar para resolver los conflictos.

git config

Se puede utilizar git config para obtener información y establecer opciones globales o del repositorio en concreto con local. La configuración se puede ver con git config -l, en este caso es la configuración por defecto.

user.name=Jennifer Fu
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=https://github.com/JenniferFuBook/git-merge-conflicts.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.main.remote=origin
branch.main.merge=refs/heads/main
branch.feature.remote=origin
branch.feature.merge=refs/heads/feature

Puedes elegir una de las tres estrategias de pull para que se establezca en el archivo de configuración:

  1. git config pull.rebase false # merge (la estrategia por defecto)
  2. git config pull.rebase true # rebase
  3. git config pull.ff only # sólo fast-forward

Con las anteriores opciones en config, se realizará la estrategia git pull. Pero también se puede ejecutar git config –unset pull.rebase o git config –unset pull.ff para eliminarlo del archivo de configuración.

Conclusión

Para resolver los conflictos de fusión, puedes elegir entre git merge, git merge –squash, git rebase o git pull <opción>. Según cuál sea tu preferencia o la política de tu empresa o del repositorio en concreto.

Gracias de nuevo a Jennifer Fu por mostrar el camino.

Más apuntes

Invítame a un café con bitcoins:
1QESjZDPxWtZ9sj3v5tvgfFn3ks13AxWVZ

Bitcoins para café
También puedes invitarme a algo para mojar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Rellena este campo
Rellena este campo
Por favor, introduce una dirección de correo electrónico válida.
Tienes que aprobar los términos para continuar