Ana y Bruno tienen dos funcionalidades terminadas y probadas, cada una en su rama. Y sin embargo main —el proyecto de verdad, el que se publica— sigue exactamente donde estaba hace una semana, en c5d9b1e. Aislar el trabajo era la mitad del problema; esta lección resuelve la otra mitad.

Fusionar (merge) es la operación de traer el trabajo de una rama a otra. Es la razón de ser de las ramas: si no se pudieran volver a juntar, aislar el trabajo no serviría de nada.

Git distingue dos escenarios muy distintos al fusionar, y confundirlos es el origen de la mayoría de dudas. En uno, no hay nada que fusionar de verdad y basta con mover un puntero. En el otro, hay dos líneas de trabajo divergentes y hace falta crear un commit nuevo con dos padres. Vamos a ver los dos en directo, entender cuándo se produce cada uno, y aprender a forzar o prohibir cada comportamiento cuando nos interese.

En esta lección todas las fusiones saldrán bien a la primera. Cuando dos personas tocan las mismas líneas aparecen los conflictos, y esos tienen lección propia (03-05).

Contenido

  1. Cómo se fusiona: siempre desde la rama destino
  2. Escenario 1: fast-forward, el puntero avanza
  3. Deshacer una fusión local
  4. --no-ff: forzar el commit de fusión
  5. Escenario 2: la fusión a tres bandas
  6. El ancestro común y git merge-base
  7. Anatomía del commit de fusión
  8. Leer un historial con fusiones
  9. --ff-only: prohibir los commits de fusión

  1. Cómo se fusiona: siempre desde la rama destino

Antes de nada, la regla que evita el 90 % de los errores de principiante:

Te sitúas en la rama que quiere recibir el trabajo y fusionas la que lo aporta.

git switch <rama-destino>
git merge <rama-origen>

git merge solo mueve la rama en la que estás. La rama que nombras como argumento no se toca en absoluto: sigue apuntando exactamente al mismo commit antes y después.

Aplicado al caso de Ana: quiere que main reciba el contador de tareas.

git switch main
git merge funcionalidad/contador-tareas

Al revés —estar en funcionalidad/contador-tareas y hacer git merge main— es una operación distinta y válida, pero significa otra cosa: traer a mi rama de trabajo lo último de main, algo que se hace para mantenerse al día. Cambiar el orden por descuido es un clásico. Antes de fusionar, comprueba dónde estás:

git branch --show-current
main

  1. Escenario 1: fast-forward, el puntero avanza

Situación de partida:

git log --oneline --graph --all
* b2e6d3f (funcionalidad/filtro-pendientes) Corregir el foco del campo tras añadir una tarea
* 7c1f4a9 Aplicar estilo a las tareas completadas
* 3d5b8e1 Añadir filtro de tareas pendientes
| * 9d1e4b7 (funcionalidad/contador-tareas) Marcar tareas como completadas al hacer clic
| * 6f2b9d4 Añadir el contador de tareas pendientes
|/
* c5d9b1e (HEAD -> main) Documentar la instalación en el README
* 4e7f2a9 Añadir borrado de tareas al listado
* 8b6d3c2 Añadir estilos base del listado
* 1a4c8d6 Estructura inicial del gestor de tareas

Fíjate en la relación entre main y funcionalidad/contador-tareas. main está en c5d9b1e, y c5d9b1e es un antepasado directo de 9d1e4b7. Con el vocabulario de la lección 03-01: main está por detrás, no ha divergido. No tiene ningún commit propio que la rama de Ana no tenga.

En esta situación, "fusionar" es una palabra demasiado grande. No hay nada que combinar: todo lo que hay en main está ya contenido en la otra rama. Basta con adelantar el puntero de main hasta 9d1e4b7.

git switch main
git merge funcionalidad/contador-tareas
Updating c5d9b1e..9d1e4b7
Fast-forward
 app.js     | 24 ++++++++++++++++++++++--
 estilos.css |  6 ++++++
 2 files changed, 28 insertions(+), 2 deletions(-)

Analicemos la salida línea a línea:

  • Updating c5d9b1e..9d1e4b7: de dónde a dónde ha movido main.
  • Fast-forward: el nombre del escenario. No se ha creado ningún commit.
  • El resumen de ficheros: los cambios acumulados de los dos commits de Ana, en el mismo formato de git diff --stat que ya conoces.

Antes y después, en disco:

ANTES:  .git/refs/heads/main → c5d9b1e…
DESPUÉS: .git/refs/heads/main → 9d1e4b7…

Eso es todo lo que ha ocurrido: 41 bytes reescritos. No hay commit nuevo, no hay objetos nuevos, no hay nada que calcular.

graph RL
    subgraph despues["DESPUÉS"]
        D2["c5d9b1e"] --> C2["4e7f2a9"]
        E2["6f2b9d4"] --> D2
        F2["9d1e4b7<br/><b>main</b> · contador-tareas"] --> E2
    end
    subgraph antes["ANTES"]
        D1["c5d9b1e<br/><b>main</b>"] --> C1["4e7f2a9"]
        E1["6f2b9d4"] --> D1
        F1["9d1e4b7<br/>contador-tareas"] --> E1
    end

El historial resultante:

git log --oneline --graph
* 9d1e4b7 (HEAD -> main, funcionalidad/contador-tareas) Marcar tareas como completadas al hacer clic
* 6f2b9d4 Añadir el contador de tareas pendientes
* c5d9b1e Documentar la instalación en el README
* 4e7f2a9 Añadir borrado de tareas al listado
* 8b6d3c2 Añadir estilos base del listado
* 1a4c8d6 Estructura inicial del gestor de tareas

Una línea perfectamente recta. Las dos ramas apuntan ahora al mismo commit.

Y ahí está el detalle que hay que sopesar: mirando este historial, es imposible saber que hubo una rama. 6f2b9d4 y 9d1e4b7 parecen dos commits normales hechos directamente sobre main. La información de que formaban una unidad de trabajo —una funcionalidad— se ha perdido.

A veces eso es exactamente lo que quieres (historial limpio y lineal). Otras veces no. Para el segundo caso está --no-ff, pero antes tenemos que deshacer lo hecho.

  1. Deshacer una fusión local

Ana se ha quedado con la duda y quiere probar la otra forma. Como la fusión es local y aún no ha salido de su portátil, deshacerla es trivial: basta con devolver el puntero de main a donde estaba.

git reset --hard c5d9b1e
HEAD is now at c5d9b1e Documentar la instalación en el README
git log --oneline --graph --all
* b2e6d3f (funcionalidad/filtro-pendientes) Corregir el foco del campo tras añadir una tarea
* 7c1f4a9 Aplicar estilo a las tareas completadas
* 3d5b8e1 Añadir filtro de tareas pendientes
| * 9d1e4b7 (funcionalidad/contador-tareas) Marcar tareas como completadas al hacer clic
| * 6f2b9d4 Añadir el contador de tareas pendientes
|/
* c5d9b1e (HEAD -> main) Documentar la instalación en el README
...

Todo como estaba. Los dos commits de Ana no se han perdido porque funcionalidad/contador-tareas sigue apuntando a ellos: son perfectamente alcanzables.

Tres advertencias sobre lo que acabamos de hacer:

  • git reset --hard es una herramienta afilada. Mueve el puntero de la rama y sobrescribe el directorio de trabajo, descartando cualquier cambio sin confirmar. Lo estudiaremos con todo detalle en la lección 09-02.
  • Aquí es seguro porque el trabajo está protegido por otra rama y porque no había nada sin confirmar.
  • Solo vale para fusiones locales. Si la fusión ya se hubiera compartido con el resto del equipo, mover el puntero hacia atrás causaría problemas a los demás; ahí la herramienta correcta es git revert (lección 05-06). Como todo este módulo ocurre en un único repositorio local, no tenemos ese problema.

Un atajo útil: Git guarda automáticamente la posición anterior en la referencia ORIG_HEAD justo antes de operaciones como merge o reset. Así que esto habría sido equivalente sin necesidad de recordar el hash:

git reset --hard ORIG_HEAD

  1. --no-ff: forzar el commit de fusión

Ana lo vuelve a intentar, esta vez pidiendo explícitamente que se cree un commit de fusión aunque no haga falta:

git merge --no-ff funcionalidad/contador-tareas

Git abre el editor con un mensaje propuesto:

Merge branch 'funcionalidad/contador-tareas'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

Ana amplía la primera línea para que diga algo útil y guarda:

Fusionar el contador de tareas pendientes

Incorpora el contador en la cabecera y el marcado de tareas
completadas al hacer clic. Revisado con Bruno.
Merge made by the 'ort' strategy.
 app.js      | 24 ++++++++++++++++++++++--
 estilos.css |  6 ++++++
 2 files changed, 28 insertions(+), 2 deletions(-)

Fíjate en la diferencia: ahora dice Merge made by the 'ort' strategy en lugar de Fast-forward. Se ha creado un commit.

git log --oneline --graph
*   8d4e6b2 (HEAD -> main) Fusionar el contador de tareas pendientes
|\
| * 9d1e4b7 (funcionalidad/contador-tareas) Marcar tareas como completadas al hacer clic
| * 6f2b9d4 Añadir el contador de tareas pendientes
|/
* c5d9b1e Documentar la instalación en el README
* 4e7f2a9 Añadir borrado de tareas al listado
* 8b6d3c2 Añadir estilos base del listado
* 1a4c8d6 Estructura inicial del gestor de tareas
gitGraph
   commit id: "1a4c8d6"
   commit id: "8b6d3c2"
   commit id: "4e7f2a9"
   commit id: "c5d9b1e"
   branch contador-tareas
   checkout contador-tareas
   commit id: "6f2b9d4"
   commit id: "9d1e4b7"
   checkout main
   merge contador-tareas id: "8d4e6b2"

El commit 8d4e6b2 no aporta ningún cambio de código: el contenido resultante es idéntico al de 9d1e4b7. Lo que aporta es información sobre la estructura del trabajo: dice que esos dos commits formaban una unidad y cuándo se integró en el proyecto.

Cuándo interesa --no-ff

A favor de --no-ff A favor del fast-forward
El historial documenta qué funcionalidades se integraron y cuándo Historial lineal, más fácil de leer commit a commit
Se puede revertir una funcionalidad entera con un solo git revert -m 1 Sin commits "vacíos" que no cambian código
git log --first-parent da un resumen limpio de las integraciones git bisect recorre menos ruido
Es la práctica que asumen flujos como Git Flow (módulo 7) Encaja con equipos que prefieren un historial de una sola línea

Muchos equipos lo fijan en la configuración para no tener que acordarse:

git config --global merge.ff false

Con eso, todas las fusiones crearán commit de fusión. Una variante más matizada, muy extendida, es dejarlo por rama o simplemente escribir --no-ff cuando se integra una funcionalidad y permitir el fast-forward para el resto.

Es una decisión de equipo, no una cuestión técnica: lo importante es que todo el mundo haga lo mismo. Volveremos sobre ello en el módulo 8.

  1. Escenario 2: la fusión a tres bandas

Ahora Ana integra el trabajo de Bruno. Y la situación ha cambiado por completo:

git log --oneline --graph --all
*   8d4e6b2 (HEAD -> main) Fusionar el contador de tareas pendientes
|\
| * 9d1e4b7 (funcionalidad/contador-tareas) Marcar tareas como completadas al hacer clic
| * 6f2b9d4 Añadir el contador de tareas pendientes
|/
| * b2e6d3f (funcionalidad/filtro-pendientes) Corregir el foco del campo tras añadir una tarea
| * 7c1f4a9 Aplicar estilo a las tareas completadas
| * 3d5b8e1 Añadir filtro de tareas pendientes
|/
* c5d9b1e Documentar la instalación en el README
...

main ya no es antepasado de funcionalidad/filtro-pendientes: tiene tres commits propios (los dos de Ana más la fusión) que la rama de Bruno no tiene. Y la rama de Bruno tiene otros tres que main no tiene. Han divergido.

Aquí no vale mover un puntero. Git tiene que combinar de verdad dos versiones distintas de los ficheros.

git merge funcionalidad/filtro-pendientes

Git abre el editor con el mensaje propuesto —Ana lo acepta tal cual esta vez— y responde:

Merge made by the 'ort' strategy.
 app.js      | 15 +++++++++++++++
 estilos.css |  4 ++++
 index.html  |  3 +++
 3 files changed, 22 insertions(+)
git log --oneline --graph
*   f7a3e92 (HEAD -> main) Merge branch 'funcionalidad/filtro-pendientes'
|\
| * b2e6d3f (funcionalidad/filtro-pendientes) Corregir el foco del campo tras añadir una tarea
| * 7c1f4a9 Aplicar estilo a las tareas completadas
| * 3d5b8e1 Añadir filtro de tareas pendientes
* |   8d4e6b2 Fusionar el contador de tareas pendientes
|\ \
| * | 9d1e4b7 (funcionalidad/contador-tareas) Marcar tareas como completadas al hacer clic
| * | 6f2b9d4 Añadir el contador de tareas pendientes
|/ /
* / c5d9b1e Documentar la instalación en el README
|/
* 4e7f2a9 Añadir borrado de tareas al listado
* 8b6d3c2 Añadir estilos base del listado
* 1a4c8d6 Estructura inicial del gestor de tareas
gitGraph
   commit id: "1a4c8d6"
   commit id: "8b6d3c2"
   commit id: "4e7f2a9"
   commit id: "c5d9b1e"
   branch contador-tareas
   checkout contador-tareas
   commit id: "6f2b9d4"
   commit id: "9d1e4b7"
   checkout main
   merge contador-tareas id: "8d4e6b2"
   branch filtro-pendientes
   checkout filtro-pendientes
   commit id: "3d5b8e1"
   commit id: "7c1f4a9"
   commit id: "b2e6d3f"
   checkout main
   merge filtro-pendientes id: "f7a3e92"

(En el diagrama, filtro-pendientes se dibuja saliendo de main; recuerda que en realidad parte de c5d9b1e, antes de la primera fusión.)

El proyecto tiene por fin las dos funcionalidades. app.js contiene el contador de Ana y el filtro de Bruno, y ninguno de los dos ha tenido que copiar nada a mano.

Por qué se llama "a tres bandas"

Porque Git usa tres versiones de cada fichero para decidir el resultado:

Versión De dónde sale Papel
base El ancestro común (c5d9b1e) Punto de referencia neutral
ours La rama en la que estás (main, en 8d4e6b2) Tu lado
theirs La rama que fusionas (b2e6d3f) El otro lado

Y la regla, línea a línea, es la que anticipamos en la lección 03-01:

Cambió en ours Cambió en theirs Resultado
No No Se conserva la línea de la base
No Se coge la versión de ours
No Se coge la versión de theirs
Sí, igual Se coge esa versión (no hay discrepancia)
Sí, distinto Conflicto: decide una persona (lección 03-05)

En este caso no ha habido conflictos porque Ana tocó la cabecera y la lógica del contador, mientras que Bruno añadió el filtro más abajo y unas reglas nuevas en estilos.css. Zonas distintas de los mismos ficheros.

Fíjate en la consecuencia importante: que dos personas toquen el mismo fichero no provoca un conflicto. El conflicto aparece cuando tocan las mismas líneas (o líneas muy próximas) de formas distintas. Es una fuente de miedo innecesario muy extendida.

  1. El ancestro común y git merge-base

Git no te pregunta cuál es el ancestro común: lo calcula. Y puedes calcularlo tú:

git merge-base main funcionalidad/filtro-pendientes
c5d9b1e7a3f2d8b4e6c1a9f5d3b7e2c8a4f6d1b9
git log --oneline -1 c5d9b1e
c5d9b1e Documentar la instalación en el README

Efectivamente: el último commit que las dos ramas tienen en común, el punto donde se separaron.

Algunas variantes útiles:

# ¿Es A antepasado de B? Responde con el código de salida, sin imprimir nada
git merge-base --is-ancestor main funcionalidad/filtro-pendientes
echo $?
1

Un 1 significa "no". Si diera 0, la fusión sería un fast-forward. Es la comprobación exacta que hace git merge para decidir el escenario, y resulta muy práctica en scripts.

# Ver la base de fusión de forma legible, junto con las puntas
git log --oneline --graph --boundary main...funcionalidad/filtro-pendientes

Fíjate en los tres puntos. Ya viste a..b en la lección 02-06 (lo que hay en b y no en a); a...b es la diferencia simétrica: lo que tiene cada una y la otra no. Es la forma natural de responder a "¿en qué se diferencian estas dos ramas?".

Un truco muy usado para ver qué aporta una rama respecto al punto en que se separó, sin que aparezca todo lo que ha avanzado main mientras tanto:

git diff main...funcionalidad/filtro-pendientes

Con tres puntos en git diff, Git compara el ancestro común con la punta de la segunda rama. Es decir, muestra solo lo que ha hecho Bruno, ignorando lo que haya cambiado en main desde entonces. Es lo que enseñan las plataformas de revisión de código cuando muestran los cambios de una propuesta.

  1. Anatomía del commit de fusión

Un commit de fusión es un objeto commit normal y corriente con una única peculiaridad: tiene más de una línea parent. Recuerda el modelo de datos de la lección 01-04, donde vimos que el campo parent puede repetirse.

git cat-file -p f7a3e92
tree 3f7b2e9c1a5d8b4f2e6a9c3d7b1f5e8a4c2d9b6f
parent 8d4e6b2f1a3c5e7b9d2f4a6c8e0b1d3f5a7c9e2b
parent b2e6d3f8a1c5e9d2b4f7a3c6e8d1b5f9a2c4e7d3
author Ana Ferrer <[email protected]> 1754049600 +0200
committer Ana Ferrer <[email protected]> 1754049600 +0200

Merge branch 'funcionalidad/filtro-pendientes'

Dos líneas parent. Y el orden importa muchísimo:

Posición Nombre Qué es Se referencia como
Primer parent first parent El commit en el que estaba main antes de fusionar f7a3e92^1 o f7a3e92^
Segundo parent second parent La punta de la rama fusionada f7a3e92^2
git log --oneline -1 f7a3e92^1
8d4e6b2 Fusionar el contador de tareas pendientes
git log --oneline -1 f7a3e92^2
b2e6d3f Corregir el foco del campo tras añadir una tarea

Ese orden es lo que hace que la rama destino sea siempre ^1 y la rama traída sea ^2. Tiene consecuencias prácticas concretas:

  • git log --first-parent sigue solo el primer padre y produce un resumen limpio del proyecto principal.
  • git revert -m 1 <merge> deshace una fusión conservando la línea principal, indicando con -m 1 cuál de los padres es el "bueno" (lección 05-06).
  • Es el motivo por el que HEAD^ y HEAD~1 no son sinónimos en un commit de fusión, algo que ya se apuntó en la lección 02-06.

git show sobre una fusión

git show f7a3e92
commit f7a3e92b5d1c8f4a6e3b7d9f2a5c1e8b4d6f3a9c (HEAD -> main)
Merge: 8d4e6b2 b2e6d3f
Author: Ana Ferrer <[email protected]>
Date:   Fri Aug 1 12:20:00 2026 +0200

    Merge branch 'funcionalidad/filtro-pendientes'

Dos cosas que sorprenden:

  1. Aparece una línea Merge: con los hashes abreviados de los dos padres. Es el indicador visual de que estás ante una fusión.
  2. No hay diff. Por defecto, git show y git log -p no muestran cambios en los commits de fusión.

No es un fallo. Un commit de fusión tiene dos padres, así que "el diff" es ambiguo: ¿respecto a cuál? Y, en una fusión limpia como esta, respecto a cualquiera de los dos el resultado sería simplemente todo lo que aportaba la otra rama, que ya está en sus propios commits. Mostrarlo duplicaría información.

Si aun así lo necesitas:

# Diff respecto al primer padre (lo que la fusión trajo a main)
git show --first-parent f7a3e92

# Diff combinado: solo las líneas que difieren de AMBOS padres,
# es decir, lo que se decidió a mano al resolver conflictos
git show --cc f7a3e92

# Un diff por cada padre, por separado
git show -m f7a3e92

--cc es la opción realmente valiosa y merece que la recuerdes: en una fusión limpia no muestra prácticamente nada, pero en una fusión con conflictos resueltos muestra exactamente lo que decidió la persona, que es justo lo que interesa auditar. La usaremos en la lección 03-05.

  1. Leer un historial con fusiones

El historial de main ya no es una línea recta, y merece la pena saber mirarlo desde dos alturas distintas.

Vista completa, con todo el detalle:

git log --oneline --graph

Vista de alto nivel, siguiendo solo el primer padre:

git log --oneline --graph --first-parent
*   f7a3e92 (HEAD -> main) Merge branch 'funcionalidad/filtro-pendientes'
*   8d4e6b2 Fusionar el contador de tareas pendientes
* c5d9b1e Documentar la instalación en el README
* 4e7f2a9 Añadir borrado de tareas al listado
* 8b6d3c2 Añadir estilos base del listado
* 1a4c8d6 Estructura inicial del gestor de tareas

Seis entradas en lugar de once. Este es el historial desde el punto de vista del proyecto: qué se integró en main y en qué orden, sin el detalle interno de cada funcionalidad. Para un responsable de producto o para redactar unas notas de versión, es infinitamente más útil que la vista completa.

Y aquí se ve por qué merece la pena escribir buenos mensajes en los commits de fusión: Fusionar el contador de tareas pendientes informa; Merge branch 'funcionalidad/filtro-pendientes' obliga a adivinar.

Otros filtros relacionados:

git log --merges       # solo los commits de fusión
git log --no-merges    # solo los commits de trabajo real

--no-merges es especialmente útil combinado con --author para ver lo que ha escrito realmente una persona sin contar sus integraciones.

  1. --ff-only: prohibir los commits de fusión

El opuesto de --no-ff. Con --ff-only, Git fusiona solo si puede hacerlo adelantando el puntero; si haría falta un commit de fusión, se niega y no toca nada:

git switch main
git merge --ff-only funcionalidad/filtro-pendientes
fatal: Not possible to fast-forward, aborting.

¿Para qué sirve negarse a fusionar? Para garantizar un historial lineal. Hay equipos que no quieren ver ni un solo commit de fusión en main y prefieren que quien integra ponga primero su rama al día (con rebase, que veremos en la lección 05-01) y después fusione en fast-forward.

También es una red de seguridad excelente contra fusiones accidentales. Y de hecho ya lo tienes activo sin saberlo: en la lección 01-06 configuraste

git config --global pull.ff only

que aplica exactamente esta política a git pull (que, como veremos en el módulo 4, es una obtención seguida de una fusión). La configuración equivalente para git merge sería:

git config --global merge.ff only

Las tres políticas, comparadas

Opción Cuando se puede adelantar Cuando las ramas han divergido
Por defecto Fast-forward, sin commit nuevo Crea commit de fusión
--no-ff Crea commit de fusión igualmente Crea commit de fusión
--ff-only Fast-forward, sin commit nuevo Falla y no hace nada

Un par de opciones más que conviene conocer:

# Editar el mensaje aunque Git no lo pidiera
git merge --edit funcionalidad/filtro-pendientes

# Aceptar el mensaje propuesto sin abrir el editor
git merge --no-edit funcionalidad/filtro-pendientes

# Fusionar pero NO confirmar: deja el resultado preparado para revisarlo
git merge --no-commit funcionalidad/filtro-pendientes

--no-commit es especialmente interesante y lo retomaremos en la lección siguiente, cuando hablemos de estrategias: permite inspeccionar y ajustar el resultado de la fusión antes de sellarlo.

Errores Comunes y Consejos

Error 1: fusionar en la dirección equivocada. Estar en funcionalidad/contador-tareas y ejecutar git merge main cuando lo que querías era lo contrario. La rama de funcionalidad recibe main, main no se entera de nada, y luego no entiendes por qué el proyecto sigue igual. Comprueba siempre con git branch --show-current antes de fusionar.

Error 2: creer que fusionar mueve las dos ramas. git merge mueve solo la rama en la que estás. Tras integrar funcionalidad/filtro-pendientes en main, esa rama sigue exactamente en b2e6d3f. Sigue existiendo y sigue apuntando a lo mismo; borrarla cuando ya no haga falta es tema de la lección 03-06.

Error 3: temer que fusionar dos ramas que tocan el mismo fichero dé conflicto. No lo da si tocan zonas distintas. Git trabaja línea a línea, no fichero a fichero.

Error 4: no entender por qué git show de una fusión no muestra cambios. Es el comportamiento por defecto y tiene sentido: el diff sería ambiguo con dos padres. Usa --cc para ver lo que se decidió a mano o --first-parent para ver lo que la fusión trajo.

Error 5: dejar el mensaje de fusión por defecto en integraciones importantes. Merge branch 'funcionalidad/x' no dice nada que el grafo no diga ya. En una vista --first-parent, ese mensaje es toda la información disponible sobre la integración: aprovéchalo para explicar qué se integra y por qué.

Consejo 1: comprueba antes de fusionar qué te vas a traer. Dos comandos que cuestan un segundo:

git log --oneline main..funcionalidad/filtro-pendientes   # qué commits llegan
git diff main...funcionalidad/filtro-pendientes --stat    # qué ficheros cambian

Consejo 2: fusiona con el directorio de trabajo limpio. Git se niega a fusionar si hay cambios sin confirmar que pudieran perderse, pero incluso cuando te deja, mezclar tus cambios a medias con el resultado de una fusión es una receta para no saber qué es tuyo y qué ha traído la fusión.

Consejo 3: fusiona a menudo en la dirección "de main hacia tu rama". Traer main a tu rama de funcionalidad cada pocos días mantiene la divergencia pequeña y convierte los conflictos gordos en conflictos triviales. Es la mejor prevención que existe frente a la lección 03-05.

Consejo 4: usa --first-parent para presentar el trabajo. Cuando alguien te pregunte "¿qué ha entrado en el proyecto este mes?", git log --oneline --first-parent --since="1 month ago" da la respuesta exacta.

Ejercicios

Ejercicio 1: los dos escenarios, en un repositorio de pruebas

Crea un repositorio nuevo y provoca deliberadamente los dos escenarios:

  1. Una fusión que sea fast-forward.
  2. Una fusión a tres bandas, sin conflictos.

Antes de ejecutar cada git merge, predice con git merge-base --is-ancestor cuál de los dos va a ocurrir.

Ejercicio 2: la anatomía del commit de fusión

Sobre la fusión a tres bandas del ejercicio anterior, averigua:

  1. Los hashes completos de sus dos padres, usando solo comandos de fontanería.
  2. Cuál de los dos era la rama destino.
  3. Por qué git show <fusión> no muestra ningún diff y cómo verlo de todas formas.
  4. Cuántos commits muestra git log --oneline y cuántos git log --oneline --first-parent.

Ejercicio 3: elegir la política

Para cada situación, di qué opción de fusión usarías (--ff por defecto, --no-ff o --ff-only) y justifícalo en una frase:

  1. Integrar en main una funcionalidad de 12 commits desarrollada durante dos semanas.
  2. Integrar una rama con un único commit que corrige una errata en el README.
  3. Poner al día tu rama de funcionalidad con lo último de main, sabiendo que tú no has tocado main.
  4. Un script de integración continua que debe fallar si alguien intenta introducir una fusión inesperada en main.

Soluciones

Solución 1:

mkdir /tmp/practica-merge && cd /tmp/practica-merge
git init -b main
echo "linea 1" > fichero.txt
git add . && git commit -m "Primer commit"

Escenario fast-forward:

git switch -c rama-ff
echo "linea 2" >> fichero.txt
git commit -am "Añadir la línea 2"
git switch main

Predicción:

git merge-base --is-ancestor main rama-ff; echo $?
0

0 significa que main es antepasado de rama-ff: será fast-forward.

git merge rama-ff
Updating a1b2c3d..e4f5a6b
Fast-forward
 fichero.txt | 1 +
 1 file changed, 1 insertion(+)

Escenario a tres bandas:

git switch -c rama-3b
echo "aportacion de la rama" > otro.txt
git add . && git commit -m "Añadir otro.txt en la rama"

git switch main
echo "aportacion de main" > tercero.txt
git add . && git commit -m "Añadir tercero.txt en main"

Predicción:

git merge-base --is-ancestor main rama-3b; echo $?
1

1 significa "no": main tiene ahora un commit propio. Han divergido, hará falta commit de fusión.

git merge --no-edit rama-3b
Merge made by the 'ort' strategy.
 otro.txt | 1 +
 1 file changed, 1 insertion(+)

Sin conflicto: cada rama tocó un fichero distinto.

Solución 2:

# 1. Padres, con fontanería
git cat-file -p HEAD | grep "^parent"
parent 7f3c9a2e5b1d8f4a6c2e9b3d7f1a5c8e4b2d6f9a
parent 2b1a3c5e7d9f0b2a4c6e8d0f2a4b6c8e0d2f4a6b

O de forma más directa:

git rev-parse HEAD^1 HEAD^2
# 2. La rama destino es el PRIMER padre
git log --oneline -1 HEAD^1
7f3c9a2 Añadir tercero.txt en main

Era main, que es donde estábamos al fusionar. El segundo padre es la punta de rama-3b.

# 3. Sin diff por defecto...
git show HEAD
commit 5e8b1d4f2a6c9e3b7d1f5a8c4e2b6d9f3a7c1e5b (HEAD -> main)
Merge: 7f3c9a2 2b1a3c5
...

Porque con dos padres el diff es ambiguo. Para verlo:

git show --first-parent HEAD    # qué trajo la fusión a main
git show --cc HEAD              # solo lo resuelto a mano (aquí, nada)
# 4. Recuento
git log --oneline | wc -l
4
git log --oneline --first-parent | wc -l
3

La segunda vista omite el commit de la rama fusionada y muestra solo la línea principal: primer commit, tercero.txt y la fusión.

Solución 3:

  1. --no-ff. Doce commits son una unidad de trabajo con identidad propia. El commit de fusión documenta cuándo entró la funcionalidad y permite revertirla entera con git revert -m 1. Además, en --first-parent aparecerá como una sola entrada.

  2. Por defecto (fast-forward). Una errata no es una unidad de trabajo que merezca un nodo en el grafo. Un commit de fusión aquí solo añade ruido.

  3. --ff-only. Si no has tocado main, tu rama debería estar simplemente por detrás y el fast-forward funcionará. Si falla, es que había divergencia inesperada, y el fallo es una información valiosa: te avisa antes de crear una fusión que no esperabas.

  4. --ff-only. Es exactamente el caso de uso: convertir la política de historial lineal en una comprobación automática que falla en lugar de crear el commit de fusión.

Conclusión

Ya sabes juntar el trabajo que las ramas mantenían separado:

  • Se fusiona desde la rama destino: git switch main y luego git merge <rama>. git merge mueve solo la rama en la que estás.
  • Fast-forward: si la rama destino es antepasada de la que fusionas, no hay nada que combinar y Git se limita a adelantar el puntero. No se crea ningún commit y el historial queda lineal, a costa de perder la traza de que hubo una rama.
  • Fusión a tres bandas: si las ramas han divergido, Git combina las tres versiones de cada fichero (base, ours, theirs) y crea un commit de fusión con dos padres. Las líneas que solo cambió un lado se toman de ese lado; las que cambiaron los dos de forma distinta son un conflicto.
  • El ancestro común lo calcula Git y lo puedes consultar con git merge-base. --is-ancestor predice el escenario; la sintaxis a...b sirve para comparar dos ramas por su punto de separación.
  • El commit de fusión es un commit normal con dos líneas parent. El orden importa: ^1 es la rama destino y ^2 la traída. De ahí --first-parent, git revert -m 1 y el hecho de que HEAD^ y HEAD~1 difieran en las fusiones. git show no muestra diff por defecto; usa --cc.
  • --no-ff fuerza el commit de fusión aunque no haga falta, para documentar la integración; --ff-only lo prohíbe, para garantizar un historial lineal. Ambos se pueden fijar con merge.ff.

main contiene ya el contador de tareas y el filtro de pendientes. El proyecto está entero por primera vez desde que Bruno se incorporó.

Lo que viene

En las salidas de esta lección ha aparecido una y otra vez una palabra que hemos dejado pasar: Merge made by the **'ort'** strategy. ¿Qué es ort? ¿Hay otras? ¿Se pueden elegir?

Sí, y en algunos casos elegir bien ahorra mucho trabajo. En la siguiente lección, Estrategias de Fusión, veremos las estrategias que Git sabe aplicar (ort, resolve, octopus, ours, subtree), las opciones de estrategia con -X —incluida la trampa clásica de confundir la estrategia ours con la opción -X ours—, el squash merge, que aplasta una rama entera en un solo commit sin registrar la fusión, y una tabla de decisión para saber qué forma de integrar elegir según el historial que quieras acabar teniendo.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados