En la lección anterior, cada vez que Git creó un commit de fusión nos dijo lo mismo: Merge made by the 'ort' strategy. Pasamos de largo, pero esa palabra esconde una parte importante de cómo funciona Git.
Una estrategia de fusión es el algoritmo que Git emplea para combinar dos (o más) líneas de trabajo. No hay una sola: Git trae varias, elige una por defecto según el caso, y te permite imponer otra. Además, cada estrategia admite opciones que modifican su comportamiento en detalles concretos, como qué hacer ante un conflicto o si ignorar cambios de espacios en blanco.
En la práctica, el 95 % de las fusiones que harás usarán la estrategia por defecto sin que tengas que pensar en ello. Pero el 5 % restante —una fusión con cientos de conflictos triviales, una rama abandonada que hay que "cerrar" formalmente, una funcionalidad hecha con veinte commits caóticos que no quieres meter en main— es exactamente donde conocer las alternativas ahorra horas.
Esta lección cubre también una decisión que va más allá del algoritmo: qué forma de integrar elegir según el historial que quieras acabar teniendo. Fusión normal, --no-ff, squash… o rebase, que veremos en el módulo 5 pero que conviene situar ya en el mapa.
Contenido
- Qué es exactamente una estrategia de fusión
ort: la estrategia por defectoresolve: la veteranaoctopus: varias ramas de una vez- La estrategia
ours: descartar el contenido, conservar la historia subtree: fusionar proyectos anidados- Opciones de estrategia con
-X - La trampa: estrategia
oursfrente a opción-X ours - El squash merge
--no-commit: revisar antes de sellar- Tabla de decisión: qué forma de integrar elegir
- Qué es exactamente una estrategia de fusión
Cuando git merge tiene que combinar de verdad —es decir, cuando no es un fast-forward—, necesita responder a una pregunta por cada fichero y por cada línea: dado lo que había en el ancestro común y lo que hay en cada lado, ¿qué pongo aquí?
El conjunto de reglas que responde a esa pregunta es la estrategia. Se elige con -s o --strategy:
Y sus opciones internas con -X o --strategy-option:
Las dos se pueden combinar, y -X se puede repetir:
Comparemos primero todas las estrategias disponibles, y después veamos cada una:
| Estrategia | Ramas que admite | Cuándo la usa Git | Para qué sirve |
|---|---|---|---|
ort |
Dos | Por defecto desde Git 2.34 | Fusión general; es la que quieres casi siempre |
recursive |
Dos | Por defecto de Git 2.0 a 2.33 | Predecesora de ort; hoy es un alias de ort |
resolve |
Dos | Nunca por defecto | Algoritmo antiguo y simple; un único ancestro común |
octopus |
Tres o más | Por defecto al fusionar 3+ ramas | Integrar varias ramas triviales de golpe |
ours |
Dos o más | Nunca por defecto | Registrar la fusión descartando el contenido de la otra rama |
subtree |
Dos | Nunca por defecto | Fusionar un proyecto dentro de un subdirectorio |
ort: la estrategia por defecto
ort: la estrategia por defectoort significa Ostensibly Recursive's Twin ("el gemelo aparente de recursive"), un nombre con sentido del humor que delata su origen: es una reescritura desde cero de la estrategia recursive, diseñada para dar los mismos resultados pero mucho más rápido y con menos casos raros.
Es la estrategia por defecto desde Git 2.34 (noviembre de 2021). Antes lo era recursive. Hoy, si escribes -s recursive, Git usa ort de todas formas.
Lo que hace, resumido:
- Calcula el ancestro común de las dos ramas.
- Hace la fusión a tres bandas fichero a fichero y línea a línea, con la tabla de reglas que vimos en la lección 03-03.
- Detecta renombrados: si en una rama moviste
estilos.cssacss/estilos.cssy en la otra lo modificaste, aplica los cambios al fichero en su nueva ubicación en lugar de darte un conflicto absurdo. - Si hay varios ancestros comunes, los fusiona entre sí recursivamente hasta obtener uno virtual, y usa ese como base.
Ese punto 4 es el que da nombre a la familia y merece un momento. Puede haber más de un ancestro común cuando el historial tiene fusiones cruzadas:
graph RL
A["A"]
B["B"] --> A
C["C"] --> A
D["D"] --> B
D --> C
E["E"] --> C
E --> B
F["F<br/>rama-1"] --> D
G["G<br/>rama-2"] --> E
Aquí, tanto D como E son ancestros comunes de rama-1 y rama-2, y ninguno es antepasado del otro. Una estrategia simple tendría que elegir uno arbitrariamente, con el riesgo de producir conflictos falsos. ort los fusiona entre sí para construir una base virtual que combine la información de ambos.
No necesitas provocar esta situación para trabajar: solo saber que existe y que Git la resuelve solo. Es la razón de que las fusiones en historiales complejos funcionen sorprendentemente bien.
resolve: la veterana
resolve: la veteranaresolve es la estrategia clásica: fusión a tres bandas con un único ancestro común, elegido si hay varios. No hace recursión y detecta renombrados de forma mucho más limitada.
¿Cuándo usarla? Prácticamente nunca. Su nicho es el caso raro en el que ort produce un resultado que no te convence en un historial con fusiones cruzadas y quieres probar una base distinta. Es más un recurso de diagnóstico que una herramienta de trabajo.
Está en el temario porque la verás mencionada en documentación antigua y porque conocerla ayuda a entender por qué ort hace lo que hace.
octopus: varias ramas de una vez
octopus: varias ramas de una vezgit merge no se limita a un argumento. Puedes nombrar varias ramas:
Cuando hay tres o más, Git cambia automáticamente a la estrategia octopus, que crea un único commit de fusión con tantos padres como ramas más una.
Imaginemos que el equipo ha acumulado tres correcciones diminutas e independientes, cada una en su rama:
Trying simple merge with correccion/typo-readme Trying simple merge with correccion/favicon Trying simple merge with correccion/idioma-html Merge made by the 'octopus' strategy. README.md | 2 +- index.html | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-)
tree 5f8b2e1c9a3d7f4b6e2a8c1d5f9b3e7a4c6d2f8b parent f7a3e92b5d1c8f4a6e3b7d9f2a5c1e8b4d6f3a9c parent 2f6a3c8e9b1d5f7a3c2e8b4d6f1a9c5e7b3d2f4a parent 7d2f9a1c5e8b3d6f2a4c9e1b7d5f3a8c2e6b4d9f parent 3c9d4a2f8e1b5d7c3a9f2e6b4d8c1a5f7e3b9d2c
Cuatro padres. El primero es main y los otros tres, las puntas de las ramas fusionadas.
gitGraph commit id: "f7a3e92" branch typo-readme commit id: "2f6a3c8" checkout main branch favicon commit id: "7d2f9a1" checkout main branch idioma-html commit id: "3c9d4a2" checkout main merge typo-readme merge favicon merge idioma-html id: "pulpo"
La limitación crucial de octopus: se niega a trabajar si hay cualquier conflicto.
No hay resolución posible, no hay marcadores en los ficheros, no hay nada que arreglar: la operación se aborta entera. La razón es de diseño: resolver conflictos entre cuatro versiones simultáneas sería inmanejable para una persona.
Por eso su uso realista es muy concreto: integrar de golpe varias ramas que no se pisan entre sí. Si alguna conflictúa, tendrás que fusionarlas de una en una. En el día a día de un equipo pequeño la verás poco; en proyectos con muchísimas aportaciones independientes (el núcleo de Linux es el ejemplo canónico, y no por casualidad, ya que fue el proyecto para el que se escribió Git) tiene todo el sentido.
- La estrategia
ours: descartar el contenido, conservar la historia
ours: descartar el contenido, conservar la historiaEsta es la estrategia más peculiar del conjunto y la que más se malinterpreta.
Lo que hace: crea un commit de fusión con los dos padres normales, pero el árbol resultante es exactamente el de la rama actual. El contenido de la otra rama se descarta por completo.
Suena absurdo. ¿Para qué fusionar algo que vas a tirar? Para registrar en el historial que esa rama ya está considerada, sin traer su código.
El caso de uso real en el proyecto de Ana: el equipo abrió hace un mes la rama experimento/almacenamiento-indexeddb para probar un sistema de almacenamiento distinto. El experimento se descartó —al final se quedaron con localStorage—, pero la rama sigue ahí y de vez en cuando alguien pregunta si hay que integrarla.
git switch main
git merge -s ours experimento/almacenamiento-indexeddb -m "Descartar el experimento con IndexedDB
Se probó IndexedDB para el almacenamiento de tareas. Se descarta:
la complejidad no compensa para el volumen de datos que manejamos.
Se mantiene localStorage. La rama queda registrada como integrada."Fíjate en que no lista ningún fichero cambiado: no ha cambiado nada.
Cero diferencias respecto al commit anterior de main. El código es idéntico. Pero:
La rama consta como fusionada, con lo que aparecerá en las listas de ramas que se pueden borrar sin perder nada (lección 03-06), dejará de salir en los avisos de "ramas pendientes de integrar", y el historial contiene una explicación permanente de por qué se descartó.
Otro uso clásico: cuando dos ramas han divergido tanto que reconciliarlas a mano es inviable y quieres que gane una entera, dejando constancia de la decisión.
Advertencia importante: -s ours descarta todo el contenido de la otra rama sin preguntar y sin avisar. No hay conflictos, no hay revisión, no hay marcha atrás automática. Es una decisión deliberada, no un atajo para salir de una fusión difícil. Y no la confundas con -X ours, que es algo completamente distinto y que veremos en el apartado 8.
subtree: fusionar proyectos anidados
subtree: fusionar proyectos anidadossubtree es una variante de ort para el caso en que una rama contiene un proyecto que en la otra vive dentro de un subdirectorio.
Supón que el equipo desarrolla aparte una pequeña biblioteca de componentes, en su propio repositorio, y que en gestor-tareas esa biblioteca vive en vendor/componentes/. Al fusionar la rama de la biblioteca, una estrategia normal intentaría casar sus ficheros con la raíz del proyecto y produciría un desastre. subtree detecta el desplazamiento de rutas y aplica los cambios donde corresponde.
También se puede indicar el prefijo a mano, que es más fiable que dejar que Git lo adivine:
Este mecanismo es la base del comando git subtree, una de las dos formas de anidar repositorios. La otra son los submódulos, que tienen lección propia en el módulo 6. Por ahora basta con que sepas que la estrategia existe y para qué escenario está pensada.
- Opciones de estrategia con
-X
-XLas opciones no cambian el algoritmo: lo ajustan. Estas son las que valen la pena:
| Opción | Efecto |
|---|---|
-X ours |
Ante un conflicto, resuelve automáticamente a favor de la rama actual |
-X theirs |
Ante un conflicto, resuelve automáticamente a favor de la rama fusionada |
-X ignore-space-change |
Ignora cambios en la cantidad de espacios al comparar |
-X ignore-all-space |
Ignora todos los espacios en blanco |
-X ignore-space-at-eol |
Ignora espacios al final de línea |
-X ignore-cr-at-eol |
Ignora el retorno de carro final (útil entre Windows y Unix) |
-X renormalize |
Normaliza los finales de línea antes de comparar, según .gitattributes |
-X find-renames=<n> |
Ajusta el umbral de similitud para detectar renombrados |
-X no-renames |
Desactiva la detección de renombrados |
-X patience |
Usa el algoritmo de diff "paciencia", que a veces alinea mejor |
-X diff-algorithm=histogram |
Elige otro algoritmo de comparación |
El caso de los espacios en blanco
Este es el que más veces te va a salvar. Situación real: Bruno ha configurado su editor para reindentar automáticamente al guardar, y ha reindentado app.js entero en su rama. Ana ha modificado tres líneas del mismo fichero en la suya.
Auto-merging app.js CONFLICT (content): Merge conflict in app.js Automatic merge failed; fix conflicts and then commit the result.
Ochenta conflictos, y ninguno real: son todos diferencias de sangrado. Solución:
Auto-merging app.js Merge made by the 'ort' strategy. app.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+)
Los ochenta conflictos falsos desaparecen y quedan solo los cambios reales.
Dos matices importantes:
- Estas opciones afectan a cómo se comparan las líneas, no al resultado escrito. El fichero final conserva el sangrado de una de las versiones (la de
oursen los tramos ambiguos), así que revisa el resultado. - Si el problema del sangrado es recurrente en el equipo, la solución de fondo no es
-Xsino.gitattributesy un formateador automático compartido. Es el tema de la lección 08-04.
-X theirs en la práctica
Un ejemplo típico: un fichero generado automáticamente (un bundle.js, un fichero de bloqueo de dependencias) que conflictúa siempre y en el que la versión de la rama entrante es la correcta:
Es cómodo, pero usa -X ours/-X theirs con la cabeza: resuelven todos los conflictos del mismo lado, sin distinguir cuáles eran importantes. Si el resultado te importa, es mejor resolver a mano (lección 03-05) o limitar el atajo a los ficheros concretos que lo merecen, que también se puede hacer y veremos allí.
- La trampa: estrategia
ours frente a opción -X ours
ours frente a opción -X oursSe escriben casi igual y hacen cosas radicalmente distintas. Es una de las confusiones más caras de Git, así que vamos a dejarla clavada.
-s ours (estrategia) |
-X ours (opción) |
|
|---|---|---|
| Qué es | Un algoritmo de fusión completo | Un ajuste de la estrategia ort |
| Qué hace | Descarta todo el contenido de la otra rama | Fusiona normal, y solo ante un conflicto elige tu lado |
| Cambios sin conflicto de la otra rama | Se pierden todos | Se incorporan con normalidad |
| Resultado | El árbol es idéntico al de tu rama | El árbol combina las dos ramas |
| Cuándo usarla | Cerrar formalmente una rama descartada | Resolver en bloque conflictos sin importancia |
Un ejemplo concreto. La rama funcionalidad/filtro-pendientes aporta dos cosas: un fichero nuevo filtros.js (que no conflictúa con nada) y una modificación de la línea 12 de app.js (que sí conflictúa con un cambio de Ana).
Resultado: no aparece filtros.js y la línea 12 es la de Ana. Se ha descartado todo, incluso lo que no molestaba a nadie.
Resultado: filtros.js sí aparece, porque no había conflicto con él, y la línea 12 es la de Ana, porque ahí sí lo había. Fusión real con desempate automático.
Regla mnemotécnica:
-ses la estrategia, y una estrategia decide todo.-Xes una opción, y una opción solo decide los empates.
Y una precisión sobre el vocabulario, porque también confunde: en una fusión, ours es siempre la rama en la que estás y theirs la que has nombrado en el comando. Suena obvio, pero durante un rebase los papeles se invierten respecto a lo que uno esperaría, y ese es uno de los motivos por los que el rebase merece su propia lección (05-01).
- El squash merge
Cambiamos de tercio. El squash merge no es una estrategia: es una forma distinta de integrar, y en muchos equipos es la más usada de todas.
Situación en el proyecto: Bruno ha desarrollado la exportación del listado a CSV en la rama funcionalidad/exportar-csv. Ha funcionado, pero el historial de la rama es un desastre:
e9a2c5f Ahora sí 7e2a9c4 Quitar el console.log 1d6b4f8 wip 2 5a8e2b9 Arreglar el separador 9f3a2c1 wip 4a1c7e3 Primer intento de exportación
Seis commits, cuatro de los cuales no aportan nada a nadie que lea el historial dentro de seis meses. Bruno no quiere meter eso en main.
Updating f7a3e92..e9a2c5f Fast-forward Squash commit -- not updating HEAD app.js | 34 ++++++++++++++++++++++++++++++++++ index.html | 2 ++ 2 files changed, 36 insertions(+)
Lee bien: Squash commit -- not updating HEAD. Git ha calculado el resultado de la fusión y lo ha dejado en el área de preparación, pero no ha confirmado nada.
On branch main Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: app.js modified: index.html
Ahora Bruno escribe un mensaje decente y confirma:
git commit -m "Añadir exportación del listado de tareas a CSV
Genera un fichero CSV con el título, el estado y la fecha de
creación de cada tarea. Se descarga desde el botón nuevo de la
cabecera."En qué se diferencia de una fusión normal
graph TB
subgraph squash["SQUASH MERGE"]
S1["f7a3e92<br/>main"] --> S2["3b9e7d1<br/>main (un solo commit,<br/>UN padre)"]
S3["4a1c7e3 → e9a2c5f<br/>exportar-csv<br/>(desconectada)"]
end
subgraph normal["FUSIÓN NORMAL"]
N1["f7a3e92"] --> N3["merge<br/>main (DOS padres)"]
N2["4a1c7e3 → e9a2c5f<br/>exportar-csv"] --> N3
end
La diferencia esencial:
| Fusión normal | Squash merge | |
|---|---|---|
Commits nuevos en main |
1 (de fusión) | 1 (normal) |
| Padres de ese commit | Dos | Uno |
Commits de la rama en el historial de main |
Sí, los seis | No, ninguno |
| ¿Queda registrada la relación con la rama? | Sí | No |
¿Qué dice git branch --merged? |
Que está fusionada | Que no lo está |
| Autoría de los commits originales | Se conserva | Se pierde (queda quien confirma) |
Ese "no queda registrada la relación" tiene una consecuencia práctica muy concreta. Después de un squash, main no es descendiente de funcionalidad/exportar-csv:
La rama de Bruno no aparece, aunque su contenido esté íntegramente en main. Si intentas borrarla:
error: The branch 'funcionalidad/exportar-csv' is not fully merged. If you are sure you want to delete it, run 'git branch -D funcionalidad/exportar-csv'.
Git no miente: desde el punto de vista del grafo, esos seis commits no están en main. Habrá que borrarla con -D sabiendo lo que se hace. Volveremos sobre esto en la lección 03-06.
Cuándo usar squash
A favor:
maintiene un commit por funcionalidad, limpio y con un mensaje escrito con calma.- El desorden de la rama de trabajo (los
wip, los "ahora sí") no contamina el proyecto. git bisect(lección 06-02) funciona mejor: cada commit demaines un estado completo y funcional.- Es el modelo por defecto del botón "Squash and merge" de las plataformas de alojamiento, y por tanto el más extendido hoy en equipos que trabajan con propuestas de cambio.
En contra:
- Se pierde el detalle del desarrollo. Si la funcionalidad son 40 commits bien escritos, aplastarlos es destruir información útil.
- Se pierde la autoría individual: un commit hecho con aportaciones de tres personas queda a nombre de quien lo confirma.
- Las ramas hay que borrarlas con
-D, sin la red de seguridad de-d. - Si sigues trabajando en la rama después del squash y vuelves a fusionar, Git no sabe que aquello ya estaba integrado y puede reproducir conflictos ya resueltos.
Regla práctica: squash para ramas cortas de una sola persona con commits desordenados; fusión con --no-ff para funcionalidades grandes con un historial que merece conservarse.
--no-commit: revisar antes de sellar
--no-commit: revisar antes de sellargit merge --no-commit hace la fusión de verdad pero se detiene justo antes de crear el commit:
On branch main All conflicts fixed but you are still merging. (use "git commit" to conclude merge) Changes to be committed: modified: app.js modified: estilos.css modified: index.html
En este punto puedes:
# Ver exactamente qué va a entrar
git diff --cached
# Ejecutar las pruebas con el resultado ya combinado
npm test
# Ajustar algo antes de confirmarY después, confirmar o abortar:
git commit # sella la fusión (con sus dos padres)
# o bien
git merge --abort # deshace la fusión y vuelve al estado anteriorLa diferencia con --squash es fundamental y a menudo se confunde, porque las dos "paran antes de confirmar":
--no-commit |
--squash |
|
|---|---|---|
| ¿Se registra la fusión? | Sí: al confirmar sale un commit con dos padres | No: sale un commit normal |
| ¿Se puede abortar? | Sí, con git merge --abort |
No: no hay fusión en curso que abortar |
Estado de .git/MERGE_HEAD |
Existe | No existe |
| Para qué sirve | Revisar o probar antes de sellar | Aplastar una rama en un commit |
Ese fichero MERGE_HEAD es el detalle técnico que lo explica todo: con --no-commit hay una fusión en curso que Git recuerda, y por eso el commit resultante tendrá dos padres y --abort funciona. Con --squash no hay ninguna fusión en curso: solo hay unos cambios preparados en el índice.
Usos habituales de --no-commit:
- Ejecutar la batería de pruebas sobre el resultado combinado antes de dejarlo en
main. - Revisar una fusión grande fichero a fichero.
- Ajustar detalles menores (un
importduplicado, un espacio) y meterlos en el propio commit de fusión.
- Tabla de decisión: qué forma de integrar elegir
Recopilamos todo lo visto en este módulo, más el rebase que llega en el módulo 5, en una única tabla de decisión:
| Quiero… | Uso | Historial resultante |
|---|---|---|
| Integrar y que no se note que hubo rama | git merge (fast-forward) |
Lineal, sin nodo de fusión |
| Dejar constancia de que se integró una funcionalidad | git merge --no-ff |
Con nodo de fusión y burbuja de commits |
| Un solo commit limpio por funcionalidad | git merge --squash + git commit |
Lineal, un commit por rama |
Prohibir los nodos de fusión en main |
git merge --ff-only |
Lineal, o falla |
| Probar el resultado antes de sellarlo | git merge --no-commit |
El que decidas después |
| Integrar varias ramas triviales a la vez | git merge a b c (octopus) |
Un nodo con muchos padres |
| Cerrar una rama descartada sin traer su código | git merge -s ours |
Con nodo de fusión, sin cambios |
Poner mi rama al día con main sin nodo de fusión |
git rebase main → 05-01 |
Lineal, con commits reescritos |
Sobre la última fila, lo justo para situarla: el rebase no fusiona, sino que reescribe los commits de tu rama como si los hubieras hecho a partir del estado actual de main. El resultado es un historial lineal sin nodos de fusión, a cambio de que los commits cambian de hash y por tanto son commits nuevos. Eso tiene implicaciones serias cuando el trabajo ya se ha compartido, y por eso tiene lección propia. Aquí solo necesitas saber que es la tercera alternativa junto a la fusión y el squash.
Y una recomendación de partida
Si estás empezando y no tienes una política de equipo que seguir:
- Fusión normal para todo, dejando que Git decida entre fast-forward y tres bandas.
--no-ffcuando integres algo que quieras poder identificar como una unidad en el futuro.--squashcuando la rama sea tuya, corta y desordenada.- No toques
-sni-Xhasta que un problema concreto te pida una de esas opciones.
Y sobre todo, que el equipo entero haga lo mismo. Un historial coherente vale más que un historial óptimo pero irregular. Las convenciones de flujo con nombre —Git Flow, GitHub Flow, Trunk Based Development— son precisamente eso: paquetes de decisiones ya tomadas. Tienen su sitio en el módulo 7.
Errores Comunes y Consejos
Error 1: confundir -s ours con -X ours. Es el error más caro de esta lección. -s ours descarta silenciosamente todo el trabajo de la otra rama, incluidos los ficheros que no daban ningún problema. Si lo que querías era desempatar conflictos, la opción es -X ours. Si alguna vez dudas, git diff HEAD~1 HEAD después de la fusión te dirá enseguida si has traído algo o nada.
Error 2: usar -X theirs como atajo universal. Resuelve todos los conflictos del mismo lado, incluidos aquellos en los que tu versión era la correcta. Se pierde trabajo sin que nada avise. Reserva estas opciones para conflictos que sabes que son irrelevantes.
Error 3: creer que --squash fusiona. No lo hace: prepara los cambios y se va. Si olvidas el git commit posterior, te quedas con un montón de cambios preparados en main y una sensación rara de que "la fusión no ha funcionado". Y aunque confirmes, la rama seguirá figurando como no fusionada.
Error 4: hacer squash de una rama y seguir trabajando en ella. Como el commit resultante no tiene relación con los originales, la siguiente fusión de esa rama volverá a traer todo lo anterior y reproducirá conflictos ya resueltos. Tras un squash, la rama se cierra: se borra y se abre otra.
Error 5: buscar la estrategia perfecta para un conflicto complicado. No existe. Si ort da conflictos, es porque hay una decisión humana que tomar. Cambiar de estrategia solo desplaza el problema o lo esconde. La respuesta correcta es la lección siguiente.
Consejo 1: -X ignore-space-change es el que más veces usarás. Cuando una fusión reviente con decenas de conflictos que al mirarlos son solo sangrado, aborta y reinténtalo con esa opción. La mejora es espectacular.
Consejo 2: comprueba lo que ha hecho una fusión rara. Después de cualquier fusión con -s o -X, mira el resultado antes de seguir:
Consejo 3: --no-commit es tu red de seguridad. En fusiones grandes o delicadas, párate antes de confirmar, ejecuta las pruebas y revisa. Cuesta treinta segundos y evita meter en main una combinación que nadie ha comprobado.
Consejo 4: documenta la política del equipo. Escribe en el README.md o en el CONTRIBUTING.md del proyecto cómo se integra en main. Es la clase de decisión que, sin escribir, cada persona interpreta de una forma.
Ejercicios
Ejercicio 1: demostrar la diferencia entre -s ours y -X ours
Monta un repositorio de pruebas donde una rama aporte dos cosas: un fichero nuevo que no conflictúa y una modificación que sí conflictúa con la rama principal. Fusiona de las dos formas (en dos intentos independientes) y demuestra con comandos qué queda en cada caso.
Ejercicio 2: squash frente a fusión normal
Crea una rama con tres commits y intégrala en main de las dos formas, en dos repositorios distintos o deshaciendo entre medias. Después responde:
- ¿Cuántos commits tiene
mainen cada caso? - ¿Cuántos padres tiene el último commit de
mainen cada caso? - ¿Qué dice
git branch --mergeden cada caso? - ¿Se puede borrar la rama con
-den cada caso?
Ejercicio 3: elegir la forma de integrar
Para cada situación, elige entre fusión normal, --no-ff, --squash, -s ours, octopus o --no-commit, y justifícalo:
- Una rama de un compañero con 25 commits bien escritos y mensajes cuidados, que implementa el módulo de informes.
- Tu propia rama con 7 commits, cuatro de los cuales se llaman
wip. - Cinco ramas de traducción, cada una tocando un fichero de idioma distinto.
- Una rama de un experimento que se ha decidido no llevar adelante, y de la que quieres dejar constancia.
- Una fusión que afecta a la lógica de facturación y que quieres validar con las pruebas antes de que entre en
main.
Soluciones
Solución 1:
mkdir /tmp/practica-ours && cd /tmp/practica-ours
git init -b main
echo "linea original" > comun.txt
git add . && git commit -m "Base"
# La rama aporta un fichero nuevo Y un cambio conflictivo
git switch -c aportacion
echo "contenido nuevo" > exclusivo.txt
echo "version de la rama" > comun.txt
git add . && git commit -m "Añadir exclusivo.txt y cambiar comun.txt"
# main cambia la MISMA línea de comun.txt
git switch main
echo "version de main" > comun.txt
git commit -am "Cambiar comun.txt en main"Intento A, con la estrategia:
exclusivo.txt no está. Se ha descartado todo el contenido de la rama, incluido el fichero que no daba ningún problema.
Cero cambios respecto a main. Es la firma inconfundible de -s ours.
Intento B, con la opción:
git reset --hard HEAD~1 # deshacemos el intento A
git merge -X ours aportacion -m "Fusión con opción -X ours"
ls
cat comun.txtexclusivo.txt sí está, porque no había conflicto con él. Y comun.txt tiene la versión de main porque ahí sí lo había y -X ours desempató a nuestro favor.
Conclusión: -s ours descarta todo; -X ours fusiona de verdad y solo desempata.
Solución 2:
mkdir /tmp/practica-squash && cd /tmp/practica-squash
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"
git switch -c funcionalidad
echo "a" >> f.txt && git commit -am "Paso 1"
echo "b" >> f.txt && git commit -am "Paso 2"
echo "c" >> f.txt && git commit -am "Paso 3"
git switch main
echo "cambio en main" > otro.txt && git add . && git commit -m "Avanzar main"Fusión normal:
Squash (repitiendo el montaje desde cero o deshaciendo con git reset --hard HEAD~1):
git merge --squash funcionalidad
git commit -m "Añadir la funcionalidad completa"
git log --oneline | wc -lUn solo padre.
Respuestas:
- 6 commits con fusión normal (base + 3 de la rama + avance de main + fusión); 3 con squash (base + avance de main + el commit aplastado).
- Dos padres con fusión normal; uno con squash.
- Con fusión normal, la rama aparece como fusionada; con squash, no.
- Con fusión normal,
-dfunciona; con squash, hay que usar-D.
Solución 3:
-
Fusión con
--no-ff. Veinticinco commits bien escritos son información valiosa que no hay que destruir, y el nodo de fusión documenta la integración del módulo como una unidad identificable. -
--squash. Rama corta, propia y desordenada: el caso de manual. Un commit limpio enmaincon un mensaje escrito con calma vale más que siete commits de los que cuatro no dicen nada. -
octopus(git merge trad-es trad-ca trad-en trad-fr trad-de). Cinco ramas que tocan ficheros distintos y por tanto no pueden conflictuar: es exactamente el escenario para el que sirve. Si alguna conflictuara,octopusfallaría y habría que integrarlas de una en una. -
-s ours. Registra en el historial que la rama se ha considerado y descartado, sin traer su código, y deja de aparecer como pendiente de integrar. Aprovecha el mensaje del commit para explicar por qué se descartó. -
--no-commit. Permite ejecutar la batería de pruebas sobre el resultado ya combinado y, si algo falla, salir limpiamente congit merge --abortsin quemainllegue a contener nada.
Conclusión
Ya sabes que "fusionar" no es una sola cosa:
- Una estrategia es el algoritmo que combina las ramas, y se elige con
-s.ortes la de por defecto desde Git 2.34: fusión a tres bandas con detección de renombrados y resolución recursiva cuando hay varios ancestros comunes.resolvees su predecesora simple;octopusfusiona tres o más ramas de golpe pero falla ante cualquier conflicto;subtreecasa proyectos que viven en subdirectorios. - La estrategia
ours(-s ours) crea un commit de fusión y descarta todo el contenido de la otra rama. Sirve para cerrar formalmente una rama que se ha decidido no integrar. - Las opciones
-Xajustan la estrategia, no la sustituyen.-X ours/-X theirsdesempatan solo los conflictos;-X ignore-space-changeelimina de un plumazo los conflictos falsos por sangrado. -s oursy-X oursno tienen nada que ver. La estrategia decide todo; la opción decide solo los empates.- El squash merge (
--squash) deja el resultado preparado en el índice sin confirmar y sin registrar la fusión: el commit final tiene un solo padre y la rama no consta como fusionada. Ideal para ramas cortas y desordenadas; malo para funcionalidades cuyo historial merece conservarse. --no-commithace la fusión de verdad pero se detiene antes de confirmar, permitiendo revisar y probar. A diferencia de--squash, la fusión sigue en curso y--abortfunciona.- La elección de cómo integrar es una decisión de equipo sobre el historial que se quiere tener, no una cuestión técnica.
Lo que viene
Hemos dado por hecho durante toda la lección algo que en el mundo real no siempre ocurre: que la fusión sale bien. Pero Ana y Bruno no van a tener tanta suerte la próxima vez. Los dos están a punto de tocar las mismas líneas de la función que pinta el listado de tareas en app.js, cada uno en su rama y por motivos distintos.
Cuando eso pasa, Git no puede decidir. Se detiene, escribe unos marcadores extraños dentro del fichero y te pasa la pelota. En la siguiente lección, Resolviendo Conflictos de Fusión, provocaremos un conflicto de verdad y lo resolveremos paso a paso: aprenderás a leer los marcadores <<<<<<<, ======= y >>>>>>> (y el estilo diff3, que además te enseña qué había en el ancestro común), a orientarte con git status y git diff mientras la fusión está a medias, a usar los atajos --ours/--theirs, a abortar con git merge --abort cuando la cosa se tuerza, y a configurar una herramienta gráfica para los conflictos difíciles.
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
