Terminaste una funcionalidad en tu rama, pero main avanzó mientras trabajabas. También hay una corrección que necesita llegar a una versión de mantenimiento, sin incluir la funcionalidad completa. En ambos casos quieres integrar trabajo; la diferencia está en qué cambios necesitas y qué historial quieres conservar.
merge, rebase y cherry-pick no son tres nombres para la misma operación. Vamos a comparar sus efectos sobre un repositorio pequeño, inspeccionar los resultados y provocar un conflicto para resolverlo de forma deliberada. No necesitas memorizar todas las opciones de Git: primero hay que entender qué está conectado con qué.
Commits y ramas: el mapa antes de los comandos
Un commit registra una instantánea del contenido preparado en el índice, junto con metadatos y referencias a sus padres. Un commit normal tiene un padre; el inicial no tiene ninguno, y un commit de merge puede tener varios. El índice, o staging area, contiene lo que preparas con git add para el siguiente commit.
Una rama es una referencia que apunta a un commit. Al crear nuevos commits en ella, esa referencia avanza. No es una carpeta ni una copia independiente de todos los archivos. Habitualmente, HEAD indica la rama que tienes seleccionada. Pro Git: ramas en pocas palabras.
En este dibujo, las letras representan commits y las líneas conectan padres con descendientes, de izquierda a derecha:
A---D main
\
B---C featuremain y feature comparten A, pero después siguieron caminos diferentes. Eso es divergencia. El nombre de una rama no expresa su calidad ni su estado de despliegue: en este laboratorio main es solo nuestra línea de referencia.
También conviene distinguir identidad y contenido. El identificador de un commit depende de su objeto completo, incluidos sus padres. Dos commits pueden dejar archivos iguales y tener identificadores diferentes. Esta distinción será visible al hacer rebase. Pro Git: objetos de Git.
Un laboratorio local para comparar las tres operaciones
Necesitas Git 2.28 o superior y una terminal Bash, Zsh o Git Bash. Los comandos usan git switch y git init -b. Ejecuta los bloques del laboratorio en orden, en la misma terminal. Usaremos carpetas y archivos de prueba; no hace falta configurar un remoto ni hacer push.
Crea un directorio nuevo fuera de tus proyectos. Si git-integracion-lab ya existe, elige otro nombre antes de continuar:
mkdir git-integracion-lab
cd git-integracion-lab
git init -b main
git config user.name "Persona de prueba"
git config user.email "persona@example.invalid"
git config commit.gpgsign false
printf 'modo=base\n' > config.txt
git add config.txt
git commit -m "A: configuración inicial"
git tag base
git switch -c feature
printf 'Catálogo disponible\n' > catalogo.txt
git add catalogo.txt
git commit -m "B: añadir catálogo"
printf 'soporte@example.invalid\n' > contacto.txt
git add contacto.txt
git commit -m "C: añadir contacto de soporte"
git tag cambio-contacto
git switch main
printf 'Guía de instalación\n' > README.md
git add README.md
git commit -m "D: documentar instalación"
git log --graph --oneline --decorate --allLa identidad y la desactivación de firma se configuran únicamente para este repositorio de prueba. base y cambio-contacto son etiquetas que nos permiten referenciar commits sin copiar hashes.
El resultado tiene la estructura del dibujo anterior. Los hashes serán distintos en tu máquina y el orden visual de las líneas puede variar. Lo importante es que B y C descienden de A, mientras D está en la otra rama. En los siguientes ejemplos crearemos ramas auxiliares para conservar ese punto de comparación.
Merge: integrar conservando las líneas de historia
git merge feature integra feature en la rama actual. No te cambia automáticamente a main. Cuando hay divergencia, Git combina las líneas de desarrollo tomando en cuenta su ancestro común; si puede completar la operación, registra la integración en un commit de merge. Documentación de git merge.
Fast-forward: avanzar una referencia
Primero prueba un caso sin divergencia. Creamos una rama en A y la llevamos hasta C:
git switch -c demo-ff base
git merge --ff-only feature
git rev-parse demo-ff feature
git log --graph --oneline --decorate demo-ffgit rev-parse imprime dos hashes iguales. El recorrido de esta rama queda así:
A---B---C feature, demo-ffNo apareció un commit adicional: demo-ff podía alcanzar feature avanzando por el historial existente. Eso es un fast-forward. La rama main sigue en D; no se ha modificado.
--ff-only exige esa posibilidad. Comprueba ahora qué sucede desde main, que sí tiene un commit propio:
git switch -c demo-merge main
git merge --ff-only featureEste último comando debe fallar: Git informa de que no puede hacer fast-forward y devuelve un código distinto de cero. No es un conflicto de archivos, sino una negativa a crear una integración que no cumpla esa condición. Continúa con el siguiente bloque; la rama sigue en D.
Merge commit: registrar el encuentro
git merge --no-ff feature -m "M: integrar catálogo y contacto"
git show -s --format='%s%nPadres: %p' HEAD
git log --graph --oneline --decorate demo-merge
git ls-tree --name-only HEADAhora hay un commit M con dos padres. La forma de este historial es:
A---D-------M demo-merge
\ /
B---C---/ featureLos archivos son README.md, catalogo.txt, config.txt y contacto.txt. Los commits originales B y C mantienen su identidad y forman parte del historial de demo-merge.
En esta integración de dos ramas, M permite reconocer dónde se reunieron los trabajos. --no-ff también solicita un commit de merge cuando sería posible avanzar directamente; --ff-only lo impide si hace falta ese commit. No confundas un fast-forward con un squash: avanzar la referencia conserva los commits que ya había. Opciones de fast-forward de git merge.
Rebase: volver a aplicar trabajo sobre otra base
Ahora queremos ver cómo quedaría el trabajo de feature después de D, sin crear el commit M. Creamos otra rama a partir de feature:
git switch -c demo-rebase feature
git rebase main
git log --graph --oneline --decorate demo-rebase
git rev-parse feature demo-rebase
git diff demo-merge demo-rebaseEn este caso, rebase vuelve a aplicar los cambios de B y C sobre D. El resultado es:
A---D---B'---C' demo-rebase
\
B----C featureLas primas indican commits nuevos. La rama feature conserva los originales porque no la hemos reescrito: hicimos el rebase en demo-rebase. main también permanece en D. Documentación de git rebase.
git rev-parse imprime hashes diferentes. En cambio, git diff demo-merge demo-rebase no produce salida: en este ejercicio, ambas estrategias dejan el mismo contenido final. Puedes llegar a archivos iguales mediante historiales diferentes.
Si después quisiéramos integrar la rama rebasada mediante fast-forward, haríamos una operación aparte. Para no alterar el main del laboratorio:
git switch -c demo-integracion main
git merge --ff-only demo-rebase
git rev-parse demo-integracion demo-rebaseEsta vez los dos hashes coinciden. Hacer rebase de una rama no la había fusionado por sí solo en main; primero actualizamos su base y después avanzamos otra referencia hasta ella.
Qué significa «reescribir el historial»
Rebase no edita el objeto de un commit existente. Construye otro recorrido y mueve la referencia de la rama hacia su resultado. Al cambiar un padre, cambia la identidad del commit, aunque su intención sea la misma. Los commits anteriores pueden seguir accesibles por otras referencias; no son eliminados inmediatamente por el mero hecho de hacer rebase. Pro Git: rebase.
Tampoco debes interpretar rebase como una copia mecánica de absolutamente todo: Git puede omitir cambios equivalentes ya presentes en la base. Por defecto no conserva la topología de los merges; --rebase-merges aborda ese caso y requiere una revisión más cuidadosa. Nuestro laboratorio usa una secuencia lineal de dos commits independientes. Comportamiento y opciones de rebase.
Cherry-pick: llevar un cambio seleccionado a otro contexto
Supón que una rama de mantenimiento necesita el contacto de soporte, pero no el catálogo. En nuestro ejemplo esos cambios están separados: C añade contacto.txt sin depender de catalogo.txt.
git switch -c demo-pick main
git cherry-pick -x cambio-contacto
git log -1 --format='%s%n%b'
git ls-tree --name-only HEAD
git rev-parse cambio-contacto demo-pickObtendrás README.md, config.txt y contacto.txt. No aparece catalogo.txt, aunque el commit original de contacto tenga a B como padre.
A---D---C* demo-pick
\
B---C featureC* aplica el cambio seleccionado sobre D; no establece una relación de merge con C. Los hashes son distintos y, en este caso sin conflictos, el mensaje incluye una línea cherry picked from commit … por la opción -x. Esa referencia ayuda a rastrear un backport entre ramas que el equipo puede consultar. Documentación de git cherry-pick.
El comando aplica el cambio introducido por un commit, no su instantánea completa ni toda su ascendencia. Por eso debes revisar sus dependencias: una corrección que llama a una función añadida en B puede aplicarse sin conflictos y aun así romper la aplicación si solo llevas C.
Para trasladar una funcionalidad completa, repetir cherry-picks puede dificultar la trazabilidad. Para una corrección acotada en una versión mantenida, puede ser adecuado. La guía específica de cherry-pick amplía sus usos; aquí interesa compararlo con integrar una rama entera.
Los conflictos necesitan una decisión, no un comando mágico
Un conflicto aparece cuando Git no puede resolver automáticamente una combinación de cambios. No significa que el repositorio esté corrupto. Vamos a modificar de dos maneras la misma línea del archivo inicial:
git switch -c conflicto-local base
printf 'modo=rapido\n' > config.txt
git add config.txt
git commit -m "Elegir modo rápido"
git switch -c conflicto-remoto base
printf 'modo=seguro\n' > config.txt
git add config.txt
git commit -m "Elegir modo seguro"
git switch -c resolver-merge conflicto-local
git merge conflicto-remotoLos nombres son ilustrativos: ambas ramas son locales. El último comando debe detenerse con un conflicto. Examina qué está pendiente:
git status --short
git diff -- config.txt
cat config.txtgit status --short muestra UU config.txt: el archivo tiene modificaciones incompatibles de ambos lados. Con el estilo de conflicto habitual verás:
<<<<<<< HEAD
modo=rapido
=======
modo=seguro
>>>>>>> conflicto-remotoSi tienes configurado otro estilo, como diff3, también puede aparecer la versión del ancestro. En este merge, la parte de HEAD corresponde a resolver-merge y la otra a conflicto-remoto.
Decidimos que el resultado correcto del ejercicio sea modo=seguro. Reemplaza el archivo, revisa lo preparado y confirma la integración:
printf 'modo=seguro\n' > config.txt
git add config.txt
git diff --cached -- config.txt
git diff --check
git commit -m "Resolver integración usando modo seguro"
git status --shortEl último comando no debería imprimir nada. Marcar con git add comunica que resolviste el archivo; no demuestra que la decisión sea correcta. En un proyecto real, revisa la intención de ambos cambios y ejecuta las pruebas pertinentes antes de dar la integración por válida. Pro Git: conflictos de merge.
El mismo conflicto durante un rebase
Vamos a volver a aplicar el cambio «rápido» sobre la rama que eligió «seguro»:
git switch -c resolver-rebase conflicto-local
git rebase conflicto-remotoLa operación vuelve a detenerse. Inspecciona el parche en curso y resuelve con la decisión del ejercicio:
git rebase --show-current-patch
git diff -- config.txt
printf 'modo=seguro-con-cache\n' > config.txt
git add config.txt
git diff --cached -- config.txt
git rebase --continue
git status --short
cat config.txtPuede abrirse tu editor para confirmar el mensaje del commit: guárdalo y ciérralo. El valor final será modo=seguro-con-cache. Es una decisión de contenido ficticia para el laboratorio, no una opción que Git interprete.
Durante rebase no conviene elegir «ours» o «theirs» a ciegas: ours corresponde a la base y al trabajo ya reaplicado; theirs, al commit que Git está reaplicando. Esa perspectiva puede resultar opuesta a la que esperabas desde tu rama original. Rebase y los lados del conflicto.
Continuar o cancelar según la operación
| Operación detenida | Después de resolver y ejecutar git add | Para cancelarla |
|---|---|---|
| Merge | git merge --continue o git commit | git merge --abort |
| Rebase | git rebase --continue | git rebase --abort |
| Cherry-pick | git cherry-pick --continue | git cherry-pick --abort |
Son alternativas según el estado actual, no comandos para ejecutar todos seguidos. En rebase y cherry-pick, --skip omite el cambio en curso: no es una forma de «aceptar» una resolución. Las opciones están descritas en los manuales de merge, rebase y cherry-pick.
Empieza estas operaciones con el árbol de trabajo limpio. En particular, abortar un merge no garantiza reconstruir cambios sin commit que ya existían y se mezclaron con la operación. No uses reset --hard como reflejo para salir de un conflicto: puedes perder trabajo que sí querías conservar.
Historial compartido: el límite es la coordinación
Reorganizar commits locales antes de abrir una propuesta de cambios suele afectar solo a tu trabajo. Reescribir una rama sobre la que otras personas ya construyeron commits cambia las bases que ellas utilizan. Pueden terminar con historiales duplicados o necesitar una recuperación manual. Esa es la razón de la advertencia de Pro Git sobre rebase de trabajo compartido.
Una rama subida a un remoto no siempre es una rama compartida activamente, pero tampoco debes asumir que nadie la usa. Antes de reescribirla, acuerda quién puede hacerlo, avisa a quienes colaboran y respeta las protecciones del repositorio. En una rama de integración compartida, conservar el historial suele ser una decisión más predecible.
Si un push es rechazado por no ser fast-forward, no lo soluciones automáticamente con --force. --force-with-lease añade una comprobación del valor esperado de la referencia remota; no certifica que tu reescritura sea apropiada ni que nadie dependa del historial anterior. Su forma sin valor esperado explícito se apoya en referencias de seguimiento locales y puede verse afectada por un fetch automático en segundo plano. Advertencias de git push.
Cuando lo que necesitas es deshacer un cambio publicado, considera git revert, que registra una compensación mediante otro commit, en lugar de retirar commits de una historia compartida.
Elegir según el trabajo que necesitas integrar
| Situación | Opción que conviene evaluar | Qué revisar |
|---|---|---|
| Incorporar una rama completa y conservar su recorrido | Merge | Política de merge commits y validación conjunta |
| Avanzar una rama que no tiene trabajo divergente | Merge con --ff-only | Si se rechaza, investigar la divergencia |
| Actualizar tu secuencia local sobre una base reciente | Rebase | Que nadie dependa de los commits que vas a reescribir |
| Llevar una corrección a una versión de mantenimiento | Cherry-pick | Dependencias del cambio y trazabilidad del origen |
| Integrar trabajo en una rama que varias personas usan como base | Merge suele simplificar la coordinación | Acuerdos del equipo y protecciones de rama |
Estas son recomendaciones de flujo, no una clasificación de comandos «buenos» y «malos». Un equipo puede preferir merge commits para conservar puntos de integración; otro puede preparar ramas mediante rebase e integrarlas por fast-forward. El historial lineal facilita ciertos recorridos, pero no es una prueba de calidad del código.
En colaboración, separa las decisiones: actualiza primero tu información del remoto con git fetch, inspecciona la divergencia, elige cómo integrar y ejecuta las comprobaciones del proyecto. Después publica la rama mediante el PR o MR del equipo. Un git pull puede combinar descarga e integración según su configuración; no lo trates como una operación que solo «trae información».
Los comandos del laboratorio dejan observar una diferencia concreta: merge reúne historiales; rebase reconstruye una secuencia sobre otra base; cherry-pick traslada cambios seleccionados. Antes de elegir, pregunta qué trabajo debe llegar al destino y quién depende de los commits actuales. Esa respuesta vale más que perseguir un gráfico siempre lineal.
