Haces pull del último main y Git se detiene con CONFLICT (content): Merge conflict in checkout.js. Resuelves el conflicto y el log muestra ahora un commit llamado Merge branch 'main' into feature/checkout, sentado encima de los tres commits que en realidad escribiste tú. Cuatro commits, un solo cambio real. Un compañero que lea ese historial el mes que viene no puede saber cuál es cuál.
git merge y git rebase responden a la misma pregunta: cómo pongo mi rama al día con los cambios hechos en otro sitio. Dejan rastros muy distintos, y elegir el equivocado en una rama compartida causa daño real.
Qué hacen distinto merge y rebase en el grafo de commits
Supón que main recibió los commits D, E, F mientras tú trabajabas en feature, que tiene tus propios commits A, B, C, ramificados desde la punta antigua de main:
| |
git merge main, ejecutado desde feature, crea un nuevo commit con dos padres: la punta de feature y la punta de main. Nada cambia en A, B o C. Mismos hashes, mismos padres, mismo lugar en el grafo:
| |
git rebase main, ejecutado también desde feature, hace lo contrario. Deja main intacto y reproduce A, B y C uno a uno encima de F, generando un commit nuevo para cada uno — A’, B’, C’ — con hashes y padres nuevos:
| |
El resultado parece exactamente como si hubieras ramificado desde F desde el principio y nunca te hubieras separado de main. Ese es todo el atractivo: un historial lineal, sin commits de merge en el log. El coste es que A, B y C dejan de existir para Git. A’, B’ y C’ los sustituyen, y cualquiera que ya tuviera A, B o C se queda con commits que ya no coinciden con los tuyos.
Qué es un fast-forward merge y cuándo lo usa Git en vez de un commit de merge
Un merge necesita un commit de merge solo cuando las dos ramas se han movido. Si feature se ramificó desde F y main no se ha movido desde entonces, unir feature a main no tiene nada que conciliar: Git desliza el puntero de main hacia adelante hasta C y se detiene. Eso es un fast-forward, y deja el mismo historial plano que dejaría un rebase:
| |
Git crea un commit de merge solo cuando ambos lados han añadido commits que el otro no tiene. Algunos equipos fuerzan uno de todos modos, para que cada rama de feature deje una marca visible en main en el punto donde se integró:
| |
Cómo limpiar tus commits con el rebase interactivo
El rebase también puede editar tu propio historial, sin que intervenga ninguna otra rama. git rebase -i abre tus últimos N commits en un editor, una línea por commit:
| |
| |
Cambia la palabra al principio de una línea y Git actúa en consecuencia cuando arranca el rebase:
| Comando | Efecto |
|---|---|
pick | Mantiene el commit tal cual |
reword | Mantiene el commit, edita su mensaje |
squash | Lo fusiona con el commit anterior, combinando ambos mensajes |
fixup | Lo fusiona con el commit anterior, descartando este mensaje |
drop | Elimina el commit por completo |
edit | Pausa aquí para que puedas modificar el commit a mano |
Convierte esa lista en squash e4f5a6b fix typo y fixup c7d8e9f WIP: still debugging y terminas con un solo commit, Add checkout validation, en vez de tres que solo tienen sentido en el orden en que los escribiste. Hazlo antes de abrir la pull request, no después de que alguien ya haya descargado tu rama: el squash reescribe el hash de cada commit posterior, igual que cualquier otro rebase.
Si vas corrigiendo commits anteriores sobre la marcha, git commit --fixup <hash> seguido de git rebase -i --autosquash <base> mueve cada fixup junto a su objetivo y lo marca por ti, así que nunca tienes que reordenar la lista a mano.
Cómo git pull --rebase mantiene tu rama plana
La misma decisión aparece cada vez que haces pull. Un git pull normal, en una rama donde tienes commits locales y el remoto ha avanzado, hace un fetch seguido de un merge. Así es como acabas con un commit Merge branch 'main' into feature/checkout por ningún motivo salvo una mala coincidencia de tiempos. git pull --rebase descarga los mismos commits y reproduce los tuyos encima:
| |
Los cambios sin commit hacen que el rebase se niegue a empezar. Añade --autostash y Git los guarda aparte, hace el rebase y luego los reaplica:
| |
Configúralo una vez en lugar de escribir el flag cada vez, por rama o globalmente:
| |
Esto es seguro porque solo reescribe commits que existen únicamente en tu rama local. En el momento en que eso deja de ser cierto, el rebase deja de ser seguro.
La regla de oro: nunca hagas rebase de commits que alguien más ya haya descargado
El rebase reescribe los hashes de los commits. Si haces rebase de una rama cuyos commits antiguos alguien más ya tiene — los descargó de tu rama, o es main, o es cualquier cosa con más de un colaborador — su historial y el tuyo ahora no coinciden sobre qué es esa rama. Git no tiene forma de saber que tu A y su A’ son el mismo cambio lógico. Ve dos commits sin relación, y el próximo merge entre las dos copias duplica todo lo que divergió.
La regla que evita esto: haz rebase solo de ramas sobre las que nadie más haya construido nada. Tu propia rama de feature, antes de darle a alguien un hash sobre el que apoyarse, es terreno libre. Una rama que ya has pusheado y que has señalado a tus compañeros, main, cualquier rama compartida de larga vida: esas se unen con merge, nunca con rebase.
Hacer rebase de una rama ya pusheada exige un push forzado para que el remoto acepte el historial reescrito:
| |
--force-with-lease rechaza el push si el remoto tiene commits que tú aún no has descargado — justo la situación en la que alguien construyó sobre el historial antiguo y un --force a secas descartaría su trabajo en silencio. Nunca uses --force a secas, y solo en una rama de la que estés seguro de que es solo tuya.
Cómo recuperarte de un rebase mal hecho con git reflog
Un rebase sobre la base equivocada, o un rebase interactivo en el que eliminaste el commit equivocado, parece irrecuperable: los commits que tenías hace un minuto ya no aparecen en git log. No han desaparecido. git reflog registra cada posición en la que HEAD ha apuntado en tu máquina, rebases incluidos:
| |
| |
HEAD@{3} es donde apuntaba tu rama justo antes de que empezara el rebase. Haz reset a ese punto y vuelves exactamente al estado anterior, commits eliminados incluidos:
| |
Las entradas del reflog caducan a los 90 días por defecto, o a los 30 días para commits a los que ninguna otra referencia apunta. Es una red de seguridad para la última hora mala, no un almacenamiento permanente. Además es solo local: nunca se pushea, y no puede deshacer una reescritura que alguien más ya haya descargado.
El mismo instinto — volver a un estado conocido y funcional tras un cambio malo — se aplica también fuera del historial de Git. Un Deployment de Kubernetes mantiene su propio historial de rollouts por el mismo motivo, y kubectl rollout undo hace para un deploy fallido lo que git reset --hard contra una entrada del reflog hace para un rebase fallido.
Git rebase vs merge: cuál usar y cuándo
| Situación | Usa |
|---|---|
Poner tu rama local, aún no pusheada, al día con main | rebase (o pull --rebase) |
| Limpiar tus propios commits antes de abrir una pull request | rebase -i |
Integrar una rama de feature terminada en main | merge (con o sin --no-ff, según la convención de tu equipo) |
La rama es compartida, está pusheada, o es main misma | merge — nunca la rebasees |
Necesitas saber exactamente cuándo aterrizó una feature en main | merge --no-ff — el commit de merge lo marca |
| Quieres un historial lineal sin commits de merge en el log | rebase de tu lado, antes de abrir la pull request |
La decisión es en realidad sobre quién más ha visto ya esos commits. ¿Solo tú? Rebase, y mantén el historial limpio. ¿Al menos otra persona? Merge. Un log algo más desordenado no te cuesta nada; un historial compartido reescrito le cuesta una tarde a cualquiera que lo haya descargado.
Si tu equipo todavía no tiene una convención, empieza por aquí: pull.rebase = true en local, rebase -i para ordenar tus commits antes de abrir la pull request, y luego un merge simple para llevarlos a main. Consigues un historial limpio por rama sin reescribir nunca un commit del que dependa otra persona, la CI incluida. Un flujo como Construir y Publicar una Imagen Docker con GitHub Actions etiqueta cada build con el SHA del commit, y esos SHA tienen que seguir existiendo después de que el pipeline los haya usado. Configura git config --global pull.rebase true hoy mismo: por sí solo elimina la mayoría de los commits de merge accidentales de tu log.