Cómo resolver conflictos de merge en git sin pánico

Aprende a resolver conflictos de merge en git paso a paso: qué significan los marcadores, cómo elegir el código correcto y cómo abortar si algo sale mal.

Cómo resolver conflictos de merge en git sin pánico

Haces git merge esperando que todo encaje y en su lugar aparece CONFLICT (content): Merge conflict in config.js. El corazón se acelera y llega la tentación de siempre: borrar la carpeta, clonar de nuevo y hacer como que nada ha pasado. No hace falta. Un conflicto de merge no es un error de git, es una pregunta: cuando dos cambios chocan sobre las mismas líneas, git te pide que decidas cuál gana. Aquí explico qué te está diciendo git y un procedimiento que funciona siempre, sin romper nada.

¿Qué es exactamente un conflicto de merge?

Un conflicto de merge aparece cuando dos ramas modifican las mismas líneas del mismo archivo y git no tiene forma de saber cuál de las dos versiones debe quedar. Mientras los cambios de cada rama toquen zonas distintas, git los combina solo, sin preguntarte nada. El problema es cuando se solapan.

Piensa en un escenario típico. Tú estás en main y un compañero abre feature/subir-timeout desde el mismo punto. En un config.js, tú dejas timeout: 3000 y él lo cambia a timeout: 5000. Cada uno commitea en su rama. Cuando integras la suya con git merge feature/subir-timeout, git llega a esa línea y se encuentra dos valores incompatibles para el mismo sitio. No puede quedarse con los dos. Y no va a inventarse cuál prefieres.

Ahí es donde para y te marca el archivo como conflictivo. Si te interesa el detalle de cómo git decide qué combinar y qué no, lo cuento en qué es un merge en git. Para resolver el que tienes delante, lo importante es entender que git no ha fallado. Ha hecho todo lo que podía y te ha dejado la última palabra a ti.

¿Qué significan los marcadores <<<<<<<, ======= y >>>>>>>?

Cuando hay conflicto, git edita el archivo e inserta tres marcadores que envuelven las dos versiones en disputa. Abres el archivo y te encuentras algo así:

const config = {
  apiUrl: "https://api.ejemplo.com",
<<<<<<< HEAD
  timeout: 3000,
=======
  timeout: 5000,
>>>>>>> feature/subir-timeout
  retries: 3,
};

Cada marcador significa una cosa concreta:

  • <<<<<<< HEAD abre tu versión, la de la rama en la que estás ahora mismo. HEAD es simplemente el commit donde estás posicionado, así que todo lo que va justo debajo es tu código actual.
  • ======= es la frontera: separa lo tuyo (arriba) de lo que viene (abajo), sin ser código en sí, solo el separador.
  • >>>>>>> feature/subir-timeout cierra el bloque e indica de qué rama viene la versión de abajo.

Lo que git te muestra es literal: arriba tienes timeout: 3000 (lo que había en main), abajo timeout: 5000 (lo que trae la rama entrante), y tú decides. El resto del archivo, la línea del apiUrl y la de retries, no aparece marcado porque nadie las tocó en conflicto. Solo se marca la zona que choca.

Este es el estilo por defecto de git, el que hereda del programa merge clásico. Existe otro (diff3) que además te enseña el ancestro común, pero salvo que lo configures a mano vas a ver siempre estos tres marcadores. Reconocerlos es la mitad del trabajo. La otra mitad es decidir y limpiar.

Diagrama que señala las tres líneas de marcador dentro de un bloque de conflicto de merge en git y explica qué versión de código delimita cada una.
Anatomía de un bloque de conflicto: cada marcador delimita una zona distinta del archivo.

¿Cómo lo resuelvo paso a paso?

Resolver un conflicto son cuatro pasos: localizar, decidir, limpiar y confirmar. Ninguno tiene misterio una vez sabes qué hace cada comando.

1. Localiza qué archivos están en conflicto. El primer comando siempre es git status. Durante un merge conflictivo te muestra una sección clara:

$ 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:   config.js

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

La clave está en “Unmerged paths” y en el both modified. Eso te dice exactamente qué archivos tocaron las dos ramas. Si hay cinco en conflicto, aquí salen los cinco.

2. Abre el archivo y decide. Vas a cada archivo marcado y eliges qué código debe quedar. A veces te quedas con tu versión, a veces con la entrante, y muchas veces combinas las dos porque cada rama aportaba algo distinto. En el ejemplo, supongamos que el valor correcto es el nuevo, 5000, porque la API real tarda más de lo que pensábamos.

3. Limpia los marcadores. Aquí está el paso que más gente se salta: hay que borrar las tres líneas de marcadores, no solo elegir un lado. El archivo resuelto queda como código normal, sin rastro de <<<<<<<, ======= ni >>>>>>>:

const config = {
  apiUrl: "https://api.ejemplo.com",
  timeout: 5000,
  retries: 3,
};

Si dejas cualquier marcador dentro, has metido código inválido en el archivo. Un buen truco es buscar <<<<<<< en todo el proyecto antes de continuar: si no aparece nada, están todos limpios.

4. Marca el archivo como resuelto y cierra el merge. Le dices a git que ese archivo ya está listo con git add, y luego confirmas la resolución:

git add config.js
git merge --continue

git add es lo que mueve el archivo de “unmerged” a “resuelto”. git merge --continue sella el merge: comprueba que había un merge en curso y crea el commit de fusión por ti. Prefiero este comando a un git commit normal porque expresa la intención con claridad y falla si no hay ningún merge pendiente, lo que te avisa si te equivocaste de estado.[1]

Con eso, el conflicto está resuelto y la rama entrante queda integrada en tu historial.

Diagrama de flujo que muestra los pasos para resolver un conflicto de merge en git: git status, editar y limpiar marcadores, git add, git merge --continue, con git merge --abort como salida.
El camino para resolver un conflicto de merge, con la salida de emergencia de git merge —abort.

Este flujo se interioriza haciéndolo, no leyéndolo. Prueba a resolver un conflicto aquí mismo antes de seguir:

Cargando ejercicio...

¿Y si me quiero echar atrás?

Si te has liado y prefieres empezar de cero, git merge --abort cancela el merge en curso y reconstruye el estado que tenías justo antes de lanzarlo. Los marcadores desaparecen, los archivos vuelven a como estaban y es como si nunca hubieras ejecutado el merge.

git merge --abort

Hay un matiz importante: git puede reconstruir ese estado siempre que tuvieras el trabajo confirmado o guardado antes de empezar el merge. Si arrancaste el merge con cambios sin commitear en el árbol de trabajo, en algunos casos no podrá recuperarlos. Por eso conviene commitear o hacer stash antes de integrar nada.

Cuando el conflicto está en un solo archivo y sabes que quieres una versión entera sin combinar nada, tienes un atajo. git checkout --ours config.js deja la versión de tu rama actual, y git checkout --theirs config.js deja la de la rama entrante. En un merge, “ours” es siempre HEAD (donde estás) y “theirs” es lo que estás integrando. Cuidado en un rebase, donde los dos lados aparecen invertidos.[2]

Aquí tienes qué hace cada comando durante un conflicto, para tenerlo a mano:

ComandoQué hace
git statusMuestra los archivos en conflicto bajo “Unmerged paths”
git merge --abortCancela el merge y vuelve al estado anterior
git add <archivo>Marca un archivo como resuelto tras editarlo
git merge --continueCierra el merge y crea el commit de fusión
git checkout --ours <archivo>Se queda con tu versión (HEAD), descartando la entrante
git checkout --theirs <archivo>Se queda con la versión entrante, descartando la tuya

¿Cómo evito conflictos innecesarios?

Los conflictos no se pueden eliminar del todo, pero la mayoría son evitables con un par de hábitos. La raíz casi siempre es la misma: dos ramas viven separadas demasiado tiempo tocando lo mismo, y cuanto más divergen, más doloroso es juntarlas.

Integra a menudo. Una rama que se sincroniza con main cada día genera conflictos pequeños y fáciles. Una que lleva tres semanas sin mirar atrás acumula decenas de choques de golpe. Commits pequeños también ayudan: cuando algo falla, el conflicto queda acotado a un cambio concreto y no a un mega-commit imposible de leer.

Hablar con tu equipo vale más que cualquier comando. Si sabes que otra persona está reescribiendo el mismo config.js, coordináis quién va primero y os ahorráis el choque.

Y elige bien cómo integras. merge no es la única forma de traer cambios de una rama a otra; rebase reescribe tu historial encima del de la otra rama y genera conflictos distintos. Cuál usar depende del caso, y lo comparo a fondo en merge o rebase para integrar ramas. Si todavía te bailan los conceptos de rama, HEAD y punto de divergencia, empieza por cómo funcionan las ramas en git: un conflicto se entiende mucho mejor cuando el modelo mental de las ramas está claro.

Errores comunes al resolver un conflicto

Commitear con los marcadores dentro

Es el fallo más caro y el más silencioso. Eliges un lado, guardas y haces git add sin borrar los <<<<<<< y >>>>>>>. El resultado es un archivo con líneas basura que no compila o, peor, que sí compila y falla en runtime. Antes de cada git add, busca <<<<<<< en el proyecto. Si aparece algo, no has terminado.

Hacer git add antes de resolver de verdad

git add le dice a git “este archivo ya está bien”. Si lo ejecutas sobre un archivo que aún tiene conflictos sin decidir, estás mintiendo. Git te dejará continuar y te llevarás a producción una mezcla a medias. Añade solo lo que ya has leído línea a línea.

Borrar el repo y clonar de nuevo

El clásico pánico. Clonar de cero no resuelve nada: el conflicto reaparece en cuanto repitas el merge, y por el camino puedes perder trabajo local sin commitear. git merge --abort hace lo que crees que hace clonar, pero de forma segura y en un segundo.

Usar --ours y --theirs sin saber quién es quién

Aquí se pierde código de verdad. Alguien quiere conservar los cambios del compañero, escribe git checkout --ours pensando que “ours” es “lo bueno”, y descarta justo la versión que quería guardar. Recuérdalo con una frase: en un merge, ours eres tú (HEAD) y theirs es lo que traes. En un rebase van al revés.

Checklist para resolver un conflicto

  • Ejecuté git status y anoté todos los archivos bajo “Unmerged paths”
  • Abrí cada archivo y decidí qué código debe quedar, combinando cuando hacía falta
  • Borré las tres líneas de marcadores (<<<<<<<, =======, >>>>>>>) en cada archivo
  • Busqué <<<<<<< en el proyecto y no queda ningún marcador suelto
  • Hice git add solo de los archivos ya resueltos
  • Cerré el merge con git merge --continue (o git commit)
  • Si me bloqueé, usé git merge --abort en vez de borrar el repo

Tener el procedimiento por escrito ayuda, pero la calma de verdad llega cuando lo has hecho unas cuantas veces. En el curso Git de cero a profesional practicas conflictos reales en un entorno guiado, con ramas, merges y rebases, hasta que resolverlos deja de darte respeto.

Fuentes

  1. Documentación oficial de git-merge: comportamiento de --continue, --abort y el estilo de marcadores de conflicto por defecto.
  2. Documentación oficial de git-checkout: significado de --ours y --theirs durante un conflicto y su inversión en rebase.

Preguntas Frecuentes

¿Qué es HEAD en el marcador <<<<<<< HEAD?

HEAD es la referencia al commit en el que estás posicionado ahora mismo, normalmente la punta de tu rama actual. Cuando ves <<<<<<< HEAD, el código que viene justo debajo es el de tu rama, la que tenías activa al lanzar el merge.

¿Puedo usar una herramienta visual en vez de editar a mano?

Sí. git mergetool abre una herramienta de tres paneles (como Meld, VS Code o KDiff3) que te muestra tu versión, la entrante y el resultado, y limpia los marcadores por ti al guardar. Es cómodo para conflictos grandes. Aun así, resolver a mano un par de veces te da el modelo mental de lo que ocurre por debajo.

¿Uso git merge --continue o git commit al terminar?

Los dos funcionan y crean el mismo commit de merge. git merge --continue es más explícito: comprueba que hay un merge en curso antes de commitear y falla si no lo hay, lo que te avisa si te confundiste de estado. git commit a secas sirve igual porque git ya sabe que estás cerrando una fusión.

En un merge, ¿“ours” y “theirs” quién es quién?

En un merge, “ours” es tu rama actual, la que apunta HEAD, y “theirs” es la rama que estás integrando. Así que git checkout --ours archivo conserva tu versión y git checkout --theirs archivo la del compañero. Ojo: durante un git rebase esto aparece invertido, porque git reproduce tus commits encima de la otra rama y cambia los papeles.

Un concepto nuevo cada semana