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
- Cómo se fusiona: siempre desde la rama destino
- Escenario 1: fast-forward, el puntero avanza
- Deshacer una fusión local
--no-ff: forzar el commit de fusión- Escenario 2: la fusión a tres bandas
- El ancestro común y
git merge-base - Anatomía del commit de fusión
- Leer un historial con fusiones
--ff-only: prohibir los commits de fusión
- 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 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.
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:
- Escenario 1: fast-forward, el puntero avanza
Situación de partida:
* 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.
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 movidomain.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 --statque ya conoces.
Antes y después, en disco:
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:
* 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.
- 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.
* 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 --hardes 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:
--no-ff: forzar el commit de fusión
--no-ff: forzar el commit de fusiónAna lo vuelve a intentar, esta vez pidiendo explícitamente que se cree un commit de fusión aunque no haga falta:
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.
* 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:
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.
- Escenario 2: la fusión a tres bandas
Ahora Ana integra el trabajo de Bruno. Y la situación ha cambiado por completo:
* 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 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(+)
* 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 |
| Sí | No | Se coge la versión de ours |
| No | Sí | Se coge la versión de theirs |
| Sí | Sí, igual | Se coge esa versión (no hay discrepancia) |
| Sí | 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.
- El ancestro común y
git merge-base
git merge-baseGit no te pregunta cuál es el ancestro común: lo calcula. Y puedes calcularlo tú:
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 $?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-pendientesFí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:
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.
- 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.
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 |
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-parentsigue 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 1cuál de los padres es el "bueno" (lección 05-06).- Es el motivo por el que
HEAD^yHEAD~1no 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
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:
- 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. - No hay diff. Por defecto,
git showygit log -pno 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.
- 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:
Vista de alto nivel, siguiendo solo el primer padre:
* 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.
--ff-only: prohibir los commits de fusión
--ff-only: prohibir los commits de fusiónEl 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:
¿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
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:
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 cambianConsejo 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:
- Una fusión que sea fast-forward.
- 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:
- Los hashes completos de sus dos padres, usando solo comandos de fontanería.
- Cuál de los dos era la rama destino.
- Por qué
git show <fusión>no muestra ningún diff y cómo verlo de todas formas. - Cuántos commits muestra
git log --oneliney cuántosgit 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:
- Integrar en
mainuna funcionalidad de 12 commits desarrollada durante dos semanas. - Integrar una rama con un único commit que corrige una errata en el README.
- Poner al día tu rama de funcionalidad con lo último de
main, sabiendo que tú no has tocadomain. - 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 mainPredicción:
0 significa que main sí es antepasado de rama-ff: será fast-forward.
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:
1 significa "no": main tiene ahora un commit propio. Han divergido, hará falta commit de fusión.
Sin conflicto: cada rama tocó un fichero distinto.
Solución 2:
O de forma más directa:
Era main, que es donde estábamos al fusionar. El segundo padre es la punta de rama-3b.
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)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:
-
--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 congit revert -m 1. Además, en--first-parentaparecerá como una sola entrada. -
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.
-
--ff-only. Si no has tocadomain, 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. -
--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 mainy luegogit merge <rama>.git mergemueve 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-ancestorpredice el escenario; la sintaxisa...bsirve 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:^1es la rama destino y^2la traída. De ahí--first-parent,git revert -m 1y el hecho de queHEAD^yHEAD~1difieran en las fusiones.git showno muestra diff por defecto; usa--cc. --no-fffuerza el commit de fusión aunque no haga falta, para documentar la integración;--ff-onlylo prohíbe, para garantizar un historial lineal. Ambos se pueden fijar conmerge.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
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
