Git merge explicado: fast-forward vs 3-way merge

Qué hace git merge por dentro, cuándo produce un fast-forward y cuándo un merge commit de tres vías, y cómo leer el grafo resultante.

Git merge explicado: fast-forward vs 3-way merge

Ejecutas git merge en dos ramas distintas el mismo día. En una aparece un commit nuevo con el mensaje “Merge branch…”. En la otra no aparece nada: el historial se integra en silencio y ni te enteras. Mismo comando, resultado distinto. La razón tiene nombre: fast-forward frente a merge de tres vías, y entender cuándo pasa cada uno te ahorra la mitad de las sorpresas que da Git al integrar ramas.

¿Qué hace git merge en realidad?

git merge otra-rama coge el historial de otra-rama y lo integra en la rama en la que estás ahora mismo. El punto de partida siempre es tu rama actual, la que apunta HEAD. No mezclas dos ramas en abstracto: traes el trabajo de una hacia donde estás parado.

Para decidir cómo hacerlo, Git mira una cosa: dónde se separaron las dos ramas. A ese commit donde compartían historia se le llama ancestro común, o base del merge. A partir de ahí, solo hay dos escenarios posibles, y Git elige el que corresponde sin preguntarte.

Si trabajas con ramas a diario pero todavía te lías con cuándo crear una o dónde nace HEAD, el punto de partida está en la guía completa de ramas en Git. Aquí doy por hecho que ya sabes crear una rama y commitear en ella.

¿Qué es un fast-forward merge?

Un fast-forward ocurre cuando tu rama actual no tiene ningún commit nuevo desde que la otra rama se separó. En ese caso no hay nada que reconciliar: la otra rama simplemente va por delante en la misma línea de historia, y Git solo tiene que mover el puntero de tu rama hacia delante hasta alcanzarla.

Imagina que creas una rama de trabajo desde main, haces un par de commits, y mientras tanto nadie ha tocado main.

# Partes de main y creas una rama nueva
git switch -c feature-login

# login.js es un fichero nuevo: hay que trackearlo con git add.
# git commit -am no lo incluiría, porque -a solo mete ficheros ya trackeados
git add login.js
git commit -m "Add login form"

# Ahora login.js ya está trackeado, así que -am basta para el siguiente commit
git commit -am "Validate email field"

# Vuelves a main e integras: main no ha cambiado
git switch main
git merge feature-login

La salida de ese último comando lo dice explícitamente:

Updating a1b2c3d..e4f5a6b
Fast-forward
 login.js | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

No hay commit de merge. main no tenía commits propios que feature-login no conociera, así que Git avanza el puntero de main hasta el último commit de la rama. El historial queda como una línea recta, como si todo el trabajo se hubiera hecho directamente sobre main.

Esto es lo que confunde a mucha gente: el fast-forward no pierde nada. Tus dos commits siguen ahí, con su hash y su mensaje intactos. Lo único que no se crea es un commit extra de merge, porque no haría falta reconciliar dos historias divergentes.

¿Qué es un 3-way merge?

El merge de tres vías ocurre cuando ambas ramas han avanzado desde que se separaron. Tú hiciste commits en tu rama, y mientras tanto alguien metió commits en main. Ahora las dos historias divergen desde el ancestro común, y ninguna es un simple adelanto de la otra.

Aquí Git no puede limitarse a mover un puntero. Tiene que mirar tres commits: la base común donde las ramas coincidían, la punta de tu rama y la punta de main. Compara las dos versiones contra esa base para saber qué cambió en cada lado, combina ambos conjuntos de cambios y guarda el resultado en un commit nuevo. Ese es el merge commit, y su rasgo distintivo es que tiene dos padres en lugar de uno.

git switch main
git merge feature-checkout
Merge made by the 'ort' strategy.
 checkout.js | 40 ++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

El 'ort' que aparece ahí es el nombre de la estrategia por defecto que Git usa desde la versión 2.34 para calcular la combinación. No necesitas configurarla ni pensar en ella. Solo es la respuesta de Git diciendo “esto no ha sido un fast-forward, he tenido que fabricar un commit de unión”.

Si los cambios de ambos lados tocan líneas distintas, Git los combina solo y el merge commit sale limpio. Si ambos lados editaron las mismas líneas, aparece un conflicto y Git te pide que decidas tú. Ese caso tiene su propia mecánica, y lo cubro en cómo resolver conflictos de merge en Git. Un merge de tres vías no implica siempre un conflicto: la mayoría se resuelven sin que tengas que tocar nada.

¿Cuándo ocurre cada uno?

La regla es corta: si tu rama actual no tiene commits que la otra no conozca, fast-forward; si los dos lados avanzaron por su cuenta, merge de tres vías. Git decide por ti según el estado del historial, pero puedes forzar su comportamiento con dos flags.

Comparación de dos grafos de commits de Git: a la izquierda un fast-forward merge donde el puntero de main avanza en línea recta sin crear un commit nuevo; a la derecha un merge de tres vías donde main y feature divergen desde un ancestro común y confluyen en un nuevo commit de merge con dos padres.
Fast-forward frente a merge de tres vías: en el primero Git solo mueve el puntero de main; en el segundo crea un commit de merge con dos padres porque ambas ramas avanzaron desde el ancestro común.
Fast-forward3-way merge
Crea commit nuevoNoSí, el merge commit
Padres del último commitUnoDos
Cuándo ocurreLa rama actual no tiene commits propios desde la divergenciaAmbas ramas avanzaron desde el ancestro común
Historial resultanteLínea rectaBifurcación que se vuelve a unir
Riesgo de conflictoNingunoSolo si ambos lados editan las mismas líneas

Si quieres que quede constancia explícita de cada integración, aunque el fast-forward fuera posible, fuerza el merge commit:

git merge --no-ff feature-login

Con --no-ff Git crea un commit de merge siempre, incluso cuando podría haber avanzado el puntero sin más. Muchos equipos lo usan para que cada rama de trabajo deje una marca visible en el historial de main, útil para revisar después qué entró y cuándo.

El caso contrario es --ff-only, y conviene entenderlo bien antes de ponerlo en un script:

git merge --ff-only feature-login

Este flag le dice a Git: “haz el merge solo si puede ser un fast-forward; si no, no hagas nada y avísame con un error”. Si main ha avanzado por su cuenta y el fast-forward ya no es posible, el comando falla y sale con código distinto de cero, sin tocar tu rama. Es la forma de garantizar que tu historial se mantenga lineal y de enterarte pronto cuando ya no lo es.

¿Cómo leo el resultado?

El comando que despeja la duda es git log --graph --oneline. Dibuja el historial como un grafo a la izquierda, y ahí se ve de un vistazo si hubo bifurcación o no.

git log --graph --oneline

Tras un fast-forward verás una columna recta, sin ramificaciones:

* e4f5a6b Validate email field
* a1b2c3d Add login form
* 9f8e7d6 Previous main commit

Tras un merge de tres vías verás el commit de merge y las dos líneas que confluyen en él:

*   b7c8d9e Merge branch 'feature-checkout'
|\
| * c3d4e5f Add checkout button
| * f6a7b8c Wire checkout API
* | d9e0f1a Fix header on main
|/
* 9f8e7d6 Common ancestor

Ese commit b7c8d9e con las dos líneas que entran por debajo es un merge commit: la prueba visual de que Git hizo una combinación de tres vías. Si algo sale mal a mitad de un merge con conflictos y quieres volver al estado anterior sin dejar rastro, git merge --abort deshace la operación y te devuelve justo donde estabas.

Errores comunes

Esperar un merge commit que nunca llega. Haces git merge esperando ver “Merge branch…” en el log, y en su lugar Git dice Fast-forward. No es un fallo. Simplemente tu rama actual no había avanzado, y Git prefirió mover el puntero. Si necesitas la marca del merge commit, usa --no-ff.

Poner --ff-only en un script sin plan B. El flag aborta cuando el fast-forward no es posible, que es exactamente su propósito. El problema es cuando lo metes en automatización y no gestionas el fallo: el pipeline se para y no siempre queda claro por qué. Si usas --ff-only, decide qué pasa cuando falla antes de que falle.

Creer que el fast-forward te ha comido trabajo. Es el miedo más habitual, y no tiene fundamento. Todos tus commits siguen ahí con su hash original. Lo único que no existe es un commit de merge adicional, y no existe porque no había dos historias que reconciliar.

Confundir merge commit con conflicto. Ver “Merge made by the ‘ort’ strategy” asusta a quien espera problemas, pero ese mensaje es la ruta feliz: significa que Git combinó los cambios él solo. El conflicto se anuncia de otra forma, con un CONFLICT bien visible y ficheros marcados para que edites.

Cuándo elegir merge y cuándo otra cosa

Merge es la forma más honesta de integrar historia: conserva el grafo real de quién hizo qué y cuándo. A cambio, el historial se llena de bifurcaciones que a algunos equipos les resultan ruidosas. La alternativa habitual es rebase, que reescribe tus commits sobre la punta de la otra rama para mantener una línea recta. Cada una tiene su precio, y la decisión de fondo la comparo en merge vs rebase.

Entender fast-forward frente a tres vías es lo que hace que esa elección deje de ser cuestión de fe. Cuando sabes exactamente qué commit se mueve y cuál se crea, el grafo de Git deja de ser un misterio y pasa a ser algo que puedes predecir antes de pulsar Enter.

Todo esto se entiende de verdad cuando lo practicas con un repositorio delante y ves el puntero moverse. En el curso de Git de cero a profesional montas estos dos escenarios con las manos, provocas un fast-forward y un merge de tres vías a propósito, y lees el grafo hasta que te sale solo.

Un concepto nuevo cada semana

Preguntas Frecuentes

¿Un fast-forward pierde commits?

No, ninguno. Tus commits siguen en el historial con su hash y su mensaje intactos; lo único que no se crea es un commit de merge adicional, porque en un fast-forward no hay dos historias divergentes que reconciliar.

¿Por qué mi git merge no creó un commit nuevo?

Porque fue un fast-forward. Tu rama actual no tenía commits propios desde que la otra rama se separó, así que Git avanzó el puntero en lugar de fabricar un commit de merge. Si quieres forzar siempre un commit de merge, ejecuta git merge --no-ff.

¿Cómo fuerzo siempre un merge commit?

Con el flag --no-ff: git merge --no-ff nombre-rama. Le dice a Git que cree un commit de merge aunque el fast-forward fuera posible. Es habitual en equipos que quieren que cada rama de trabajo deje una marca visible en el historial.

¿git merge y git merge —no-ff dan el mismo historial?

Solo cuando el merge ya era de tres vías. Si Git iba a hacer un fast-forward, git merge deja una línea recta sin commit de merge, mientras que --no-ff inserta un commit de merge con dos padres y crea una bifurcación visible. Cuando ambas ramas divergieron, las dos formas producen el mismo merge commit.

¿Un merge de tres vías siempre da conflicto?

No, la mayoría se resuelven solos. Git combina automáticamente los cambios cuando cada lado tocó líneas distintas. El conflicto aparece únicamente cuando ambas ramas editaron las mismas líneas del mismo fichero, y entonces Git te pide que decidas qué versión conservar.