Saltar al contenido
sudoquest@root
Git

Conflictos

Cuando dos ramas cambian lo mismo: leer el conflicto, resolverlo (con una versión, con las dos o a mano), echarse atrás, y el rebase.

35 min de lectura

En el tema anterior, git merge juntaba dos ramas solo. Funciona mientras cada rama toca cosas distintas. Pero si las dos cambian las mismas líneas de un archivo, Git no sabe con cuál quedarse: es un conflicto. No es un error ni algo roto: Git para y te pregunta.

En este tema trabajas en la redacción de Mareas, una revista que guarda sus artículos en Git. Varios redactores tocan la misma portada a la vez, cada uno en su rama, y al juntarlas chocan. Como siempre, cada sección tiene un ejercicio con una terminal.

Un conflicto: qué dice Git

El redactor propuso otro titular en la rama titular; mientras, la directora cambió el de main. Al juntarlas:

tecnico@srv-pruebas:~/revista$ git merge titular
Auto-merging articulos/portada.md
CONFLICT (content): Merge conflict in articulos/portada.md
Automatic merge failed; fix conflicts and then commit the result.

git status dice qué archivos chocan (en Unmerged paths, como both modified, porque las dos ramas lo cambiaron) y qué hacer:

tecnico@srv-pruebas:~/revista$ 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:   articulos/portada.md

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

En corto, git status -s lo marca con UU. Y dentro del archivo, Git deja las dos versiones entre marcas: de <<<<<<< HEAD a =======, lo de tu rama (la de ahora); de ======= a >>>>>>> titular, lo de la otra. Lo que no chocó (el subtítulo, la foto) ya está juntado:

tecnico@srv-pruebas:~/revista$ cat articulos/portada.md
Portada

<<<<<<< HEAD
Titular: Agosto en llamas
=======
Titular: Cuarenta grados
>>>>>>> titular
Subtítulo: Crónica de una ola de calor
Foto: playa.jpg
Pie: Bañistas en Cádiz

git diff, durante un conflicto, enseña un diff combinado: dos columnas de + y -, una por rama. Y mientras no lo resuelvas, Git no deja seguir:

tecnico@srv-pruebas:~/revista$ git commit -m 'Sin resolver'
U	articulos/portada.md
error: Committing 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.

Resolverlo quedándote con una versión

A veces una de las dos versiones gana entera. git checkout --ours archivo deja la de tu rama (la de HEAD); git checkout --theirs archivo, la de la otra. (git restore --ours y --theirs hacen lo mismo, sin avisar.)

tecnico@srv-pruebas:~/revista$ git checkout --ours articulos/portada.md
Updated 1 path from the index

Eso solo cambia el archivo. Para decirle a Git que ya está resuelto, se prepara con git add; entonces status dice que solo falta terminar el merge:

tecnico@srv-pruebas:~/revista$ git add articulos/portada.md && git status
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)

nothing to commit, working tree clean

Que no te despiste el nothing to commit: los archivos ya eran los de tu rama, pero el merge sigue a medias. git commit lo termina: abre el editor con el mensaje ya escrito (Merge branch 'titular', y en comentarios, qué archivos chocaron). Guardas, sales, y queda el commit de merge, con sus dos padres, como cualquier otro merge.

Ejercicio

Uno de los dos titulares

Un redactor propuso otro titular en la rama titular; la directora cambió el de main. Junta titular con main: chocarán. La directora manda: se queda su titular (el de main).

Preparando la máquina…

Resolverlo juntando las dos

Otras veces lo bueno es un poco de cada: Marta escribió en la agenda los conciertos, y Luis, el cine. Los dos crearon el mismo archivo, así que choca (add/add, y en corto AA):

tecnico@srv-pruebas:~/revista$ cat articulos/agenda.md
Agenda

<<<<<<< HEAD
Cine de verano
=======
Conciertos en el parque
>>>>>>> agenda

Aquí no sirve --ours ni --theirs: se edita el archivo (con nano, por ejemplo) hasta que quede como debe, quitando las tres marcas. Después, git add y git commit, como antes.

¿Ya se guardaron? No hace falta deshacer el merge: deja el archivo como debe (con nano, o con git checkout <rama o commit> -- archivo, que trae la versión de ese commit) y guárdalo en un commit nuevo, encima.

Ejercicio

Las dos agendas

Marta (en la rama agenda) y Luis (en main) escribieron cada uno la agenda, en articulos/agenda.md. Junta agenda con main y quédate con las dos cosas: primero los conciertos y después el cine, sin las marcas del conflicto.

Preparando la máquina…

Varios archivos a la vez

Un merge puede chocar en más de un archivo. Git los lista todos:

tecnico@srv-pruebas:~/revista$ git merge fotos
Auto-merging articulos/portada.md
CONFLICT (content): Merge conflict in articulos/portada.md
Auto-merging sumario.txt
CONFLICT (content): Merge conflict in sumario.txt
Automatic merge failed; fix conflicts and then commit the result.

Se resuelven uno a uno, cada uno a su manera, y cada uno con su git add; git status va diciendo cuáles faltan. Si intentas terminar antes de tiempo, Git dice cuál queda:

tecnico@srv-pruebas:~/revista$ git merge --continue
U	articulos/portada.md
error: Committing 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.

git merge --continue es lo mismo que git commit para terminar un merge, solo que se lee mejor.

Ejercicio

Un conflicto en dos archivos

La rama fotos y main cambian la foto de la portada y el sumario. Junta fotos con main: de la portada, la foto de fotos (calle.jpg); del sumario, el de main ("Agenda cultural").

Preparando la máquina…

Echarse atrás: git merge –abort

Si el merge no tocaba (la rama no estaba lista, o te has liado resolviendo), git merge --abort lo deja todo como antes del merge: la rama, el índice y los archivos. No dice nada:

tecnico@srv-pruebas:~/revista$ git merge --abort
tecnico@srv-pruebas:~/revista$ git status
On branch main
nothing to commit, working tree clean

Sin un merge a medias, Git avisa: fatal: There is no merge to abort (MERGE_HEAD missing). (MERGE_HEAD es el archivo de .git donde Git apunta qué se estaba juntando.)

¿Te echaste atrás de más, con un git reset --hard que se llevó commits de main? No se han borrado: git merge <hash> con el último que tenía main (git reflog, del tema 2, te lo dice) los trae de vuelta.

Ejercicio

Un merge que no tocaba

Alguien empezó a juntar la rama borrador con main y lo dejó a medias, con un conflicto. Esa rama aún no está aprobada: deja main como estaba antes de ese merge.

Preparando la máquina…

Borrado en una rama, cambiado en la otra

No todos los conflictos son de líneas. Si una rama borra un archivo y la otra lo cambia, Git no sabe si quieres el archivo o no:

tecnico@srv-pruebas:~/revista$ git merge recorte
CONFLICT (modify/delete): articulos/entrevista.md deleted in recorte and modified in HEAD.  Version HEAD of articulos/entrevista.md left in tree.
Automatic merge failed; fix conflicts and then commit the result.

Deja en la carpeta la versión cambiada, y status lo marca como deleted by them (UD en corto). Tú decides: git rm archivo si se va, o git add archivo si se queda. Y después, git commit.

Ejercicio

La entrevista que se cae

En la rama recorte se quitó la entrevista (no cabe en este número); en main, mientras, alguien corrigió una errata en ella. Junta recorte con main: la entrevista se cae del número.

Preparando la máquina…

Rebase: rehacer una rama encima de otra

Juntar con merge deja en la historia cada rama y su commit de merge. Hay otra forma: git rebase main coge los commits de tu rama y los rehace uno a uno encima de lo último de main, como si hubieras empezado la rama ahora. La historia queda en línea, sin merge.

tecnico@srv-pruebas:~/revista$ git log --oneline --graph --all
* 7e0e0c1 (HEAD -> columna) La columna
| * b20e8a2 (main) El sumario, con el tema
|/  
* d2996e8 Las dos agendas
tecnico@srv-pruebas:~/revista$ git rebase main
Successfully rebased and updated refs/heads/columna.
tecnico@srv-pruebas:~/revista$ git log --oneline --graph --all
* f72bb6d (HEAD -> columna) La columna
* b20e8a2 (main) El sumario, con el tema
* d2996e8 Las dos agendas

Fíjate en el hash: La columna ya no es 7e0e0c1 sino f72bb6d, porque es un commit nuevo (con otro padre). Si ya estaba encima, Git dice Current branch columna is up to date.

Si al rehacer un commit choca, el rebase para en ese commit, con el conflicto como en un merge (git status dice por dónde va). Lo resuelves igual (editar o --ours/--theirs, y git add) y sigues con git rebase --continue. También puedes saltarte ese commit (git rebase --skip) o dejarlo todo como antes (git rebase --abort). Un rebase parado se queda a medias, aunque cierres la consola, hasta que alguien lo siga o lo deshaga: mientras, git status empieza por interactive rebase in progress y Git no deja cambiar de rama.

Ejercicio

La columna, al día

La rama columna salió de main hace días, y main ha avanzado. Pon la columna encima de lo último de main, sin un commit de merge (que la historia quede en línea); main no se toca.

Preparando la máquina…

Y el revert con conflicto

En el tema 2, git revert deshacía un commit… salvo cuando lo que vino después tocaba las mismas líneas. Ahora ya sabes qué pasa: es un conflicto, con su git revert --continue (para terminar), --skip y --abort, igual que en un rebase.

Resumen

Orden Qué hace
git status qué archivos chocan (both modified, UU…) y qué hacer
git diff durante un conflicto, el diff combinado de las dos ramas
git checkout --ours / --theirs archivo quedarse con lo de tu rama / con lo de la otra
git add archivo marcarlo como resuelto
git rm archivo resolver un borrado/cambiado borrándolo
git commit / git merge --continue terminar el merge
git merge --abort echarse atrás: todo como antes del merge
git rebase main rehacer tu rama encima de main, en línea
git rebase --continue / --skip / --abort seguir, saltar ese commit o echarse atrás