Cómo resolver conflictos con Git por consola

El escenario es bastante habitual cuando trabajamos con GitHub y Pull Requests. Hemos terminado nuestro trabajo en una rama, hemos abierto una Pull Request y estamos esperando a que otro compañero la revise. Mientras tanto, otro desarrollador abre su propia Pull Request, la mergea contra master y modifica alguna de las mismas líneas que nosotros habíamos cambiado. Esto genera un conflicto en GitHub.¿Cómo resolverlo? Vamos a ver como resolver conflictos en GitHub usando rebase.

Cuando volvemos a nuestra Pull Request nos encontramos con algo como esto:

Vamos a ver como hacerlo en nuestra terminal

Situarse en la rama correcta

git checkout feature/WPA-23654
# Cambiado a rama 'feature/WPA-23654'
# Tu rama está actualizada con 'origin/feature/WPA-23654'.

Iniciamos el git pull en modo rebase

Podemos ejecutar directamente:

git pull --rebase origin master

Este comando obtiene los últimos cambios de master y vuelve a aplicar nuestros commits encima de ellos.

Conceptualmente tendremos algo parecido a esto antes del rebase:

A---B---C---D  master
         \
          E---F  feature/WPA-23654

Después del rebase, Git intenta reconstruir nuestra rama así:

A---B---C---D  master
             \
              E'---F'  feature/WPA-23654

Nuestros commits se vuelven a aplicar sobre el último estado de master.

Por este motivo los commits E y F pasan a ser nuevos commits E' y F', con hashes diferentes.

¿Qué es el modo rebase en git pull? ¿Cuándo usar git pull –rebase?

I think you should use git pull --rebase when collaborating with others on the same branch. You are in your work → commit → work → commit cycle, and when you decide to push your work your push is rejected, because there’s been parallel work on the same branch. At this point I always do a pull --rebase. I do not use squash (to flatten commits), but I rebase to avoid the extra merge commits.

https://stackoverflow.com/questions/2472254/when-should-i-use-git-pull-rebase

Git detecta el conflicto

(venv) ➜  security-management git:(feature/WPA-23654) ✗ git pull --rebase origin master
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 2), reused 0 (delta 0), pack-reused 0
Desempaquetando objetos: 100% (4/4), 1.38 KiB | 472.00 KiB/s, listo.
Desde github.com:Wallapop/security-management
 * branch            master     -> FETCH_HEAD
   99ca504..9b22c5e  master     -> origin/master
Auto-fusionando infrastructure/security-alerts/template.yaml
CONFLICTO (contenido): Conflicto de fusión en infrastructure/security-alerts/template.yaml
error: no se pudo aplicar 378c547... add permissions to the lambda role to perform puts in cloudwatch
ayuda: Resolve all conflicts manually, mark them as resolved with
ayuda: "git add/rm <conflicted_files>", then run "git rebase --continue".
ayuda: You can instead skip this commit: run "git rebase --skip".
ayuda: To abort and get back to the state before "git rebase", run "git rebase --abort".
No se pudo aplicar 378c547... add permissions to the lambda role to perform puts in cloudwatch

Git no puede decidir automáticamente cuál de los dos cambios debe conservar.

El proceso de rebase queda entonces detenido.

Entender el conflicto

<<<<<<< HEAD
            Resource: '*'
=======
            Resource: "*"

          - Effect: "Allow"
            Action:
              - cloudwatch:Enable*
              - cloudwatch:Put*
              - cloudwatch:Set*
              - cloudwatch:Describe*
              - cloudwatch:List*
            Resource: "*"
>>>>>>> 378c547 (add permissions to the lambda role to perform puts in cloudwatch)
      Roles:
        - !Ref SecurityAlertsRole

Los marcadores:

<<<<<<<
=======
>>>>>>>

separan las dos versiones que Git no ha podido combinar automáticamente.

Durante un rebase hay que tener cierto cuidado al interpretar estas partes: Git está tomando nuestros commits y reproduciéndolos sobre la rama base.

Por tanto, lo importante no es simplemente escoger una parte u otra, sino decidir cómo debe quedar finalmente el fichero.


5. Resolver manualmente el conflicto

En nuestro caso queremos conservar los cambios existentes y añadir además los nuevos permisos de CloudWatch.

Eliminamos completamente los marcadores:

<<<<<<<
=======
>>>>>>>

y guardamos el fichero.

         - Effect: "Allow"
            Action:
              - config:GetResourceConfigHistory
            Resource: "*"

          - Effect: "Allow"
            Action:
              - cloudwatch:Enable*
              - cloudwatch:Put*
              - cloudwatch:Set*
              - cloudwatch:Describe*
              - cloudwatch:List*
            Resource: "*"
      Roles:
        - !Ref SecurityAlertsRole

Marcar el conflicto como resuelto

Si ejecutamos:

git status

todavía veremos algo parecido a:

Rutas no fusionadas:

  (usa "git add <archivo>..." para marcar una resolución)

        modificados por ambos:
        infrastructure/security-alerts/template.yaml

Añadimos el archivo corregido al staging:

git add infrastructure/security-alerts/template.yaml

Con esto le estamos diciendo a Git:

He resuelto este conflicto y esta es la versión que quiero conservar.


Continuar el rebase

Ahora podemos continuar:

git rebase --continue

Dependiendo de nuestra configuración de Git, puede abrirse el editor mostrando el mensaje del commit que Git está volviendo a aplicar.

Normalmente podemos mantener el mensaje existente, guardar y cerrar el editor.

Git continuará procesando nuestros commits.

En este caso obtenemos:

[HEAD desacoplado e89e874]
add permissions to the lambda role to perform puts in cloudwatch

1 file changed, 11 insertions(+), 1 deletion(-)

Rebase aplicado satisfactoriamente y actualizado
refs/heads/feature/WPA-23654.

Si hubiera más commits con conflictos, Git volvería a detenerse y repetiríamos el mismo proceso:

resolver conflicto
        ↓
git add <archivo>
        ↓
git rebase --continue
        ↓
siguiente commit

Hasta completar el rebase.


¿Y si queremos cancelar el rebase?

Si algo sale mal o queremos volver al estado inicial podemos ejecutar:

git rebase --abort

Git devolverá nuestra rama al estado que tenía antes de iniciar el rebase.

Existe también:

git rebase --skip

pero hay que usarlo con mucha precaución porque hace que Git ignore el commit que está intentando aplicar.

Normalmente, ante un conflicto, lo que queremos es resolverlo y ejecutar:

git rebase --continue

Subir nuevamente nuestra rama a GitHub

Aquí hay una diferencia importante respecto a un flujo normal.

Un rebase reescribe el historial de nuestra rama.

Los commits originales han sido sustituidos por nuevos commits con hashes diferentes.

Por ello un simple:

git push

normalmente será rechazado.

Podríamos hacer un force push, pero en lugar de:

git push --force

es preferible utilizar:

git push --force-with-lease origin feature/WPA-23654

--force-with-lease añade una protección importante.

Antes de sobrescribir la rama remota comprueba que esta sigue estando en el estado que esperamos. Si otro compañero ha subido nuevos commits a nuestra rama desde la última vez que la consultamos, Git rechazará el push en lugar de sobrescribir esos cambios accidentalmente.

Después del push, GitHub actualizará automáticamente nuestra Pull Request con el nuevo historial de la rama.

El conflicto debería desaparecer y la Pull Request volverá a estar preparada para ser revisada o mergeada.


Resumen

El proceso completo sería:

git checkout feature/WPA-23654
git fetch origin
git rebase origin/master

Si aparece un conflicto:

git status

# Editamos manualmente los archivos afectados

git add infrastructure/security-alerts/template.yaml
git rebase --continue

Repetimos el proceso si aparecen nuevos conflictos.

Finalmente:

git push --force-with-lease origin feature/WPA-23654

Y si queremos cancelar todo el proceso:

git rebase --abort

Rebase vs merge

También podríamos actualizar nuestra feature branch haciendo un merge de master.

Por ejemplo:

git fetch origin
git merge origin/master

Esto también resolvería el problema, pero introduciría un merge commit adicional en nuestra rama.

Con rebase, en cambio, reconstruimos nuestros commits sobre la versión más reciente de master, obteniendo un historial lineal:

master
   |
A--B--C--D
          \
           E'--F'

Este es precisamente uno de los motivos por los que se utiliza rebase habitualmente antes de actualizar una Pull Request.

No significa que rebase sea siempre mejor que merge: depende de la estrategia Git que utilice el equipo.

Lo importante es recordar que rebase reescribe el historial, por lo que debemos tener especial cuidado cuando trabajamos sobre ramas compartidas.

Y listo, conflictos resueltos

(venv) ➜  security-management git:(feature/WPA-23654) ✗ ggpush -f
Enumerando objetos: 81, listo.
Contando objetos: 100% (81/81), listo.
Compresión delta usando hasta 16 hilos
Comprimiendo objetos: 100% (67/67), listo.
Escribiendo objetos: 100% (67/67), 7.90 KiB | 2.63 MiB/s, listo.
Total 67 (delta 45), reusados 0 (delta 0), pack-reusados 0
remote: Resolving deltas: 100% (45/45), completed with 12 local objects.
To github.com:Wallapop/security-management.git
 + f60f7b6...19b0000 feature/WPA-23654 -> feature/WPA-23654 (forced update)

Links

¿Te ha ayudado este artículo?

☕ Invítame a un café

Deja una respuesta

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

Rubenortiz.es
Información sobre privacidad

Este sitio web utiliza cookies para ofrecerte la mejor experiencia de usuario posible. La información sobre cookies se almacena en tu navegador y realiza funciones como reconocerte cuando vuelve a nuestro sitio web y ayudarme a comprender qué secciones de la web resultan más interesantes y útiles.