git rebase: cómo funciona, modo interactivo y squash de commits
git rebase traslada los commits de la rama actual a una nueva base: Git aparta temporalmente tus commits, coloca la rama al final de la indicada y los vuelve a aplicar como commits nuevos. Así se consigue un historial lineal sin merge commits, y con git rebase -i se pueden combinar (squash), renombrar o eliminar commits; solo debe hacerse con commits que aún no hayas publicado para otros.
Qué hace git rebase
git rebase «trasplanta» tu rama a otro punto del historial. Supongamos que creaste feature a partir de main hace una semana y que desde entonces main ha avanzado. Tras el rebase, tu rama parece haberse creado ahora mismo, a partir del commit más reciente de main.
git switch feature
git fetch origin
git rebase origin/main
Por debajo ocurren tres pasos. Git encuentra el ancestro común de las ramas, guarda uno a uno los cambios de cada commit tuyo posterior a él, mueve la rama a origin/main y vuelve a aplicar los cambios guardados, uno tras otro. El resultado son commits nuevos con el mismo contenido y los mismos mensajes, pero con otros hashes.
Antes: A---B---C---F---G main
\
D---E feature
Después: A---B---C---F---G main
\
D'---E' feature
Los apóstrofos de D' y E' no son decoración. Son objetos distintos, con hashes distintos y con otro padre. De ahí sale la regla principal de rebase, que vemos a continuación.
La regla de oro: no hagas rebase de lo que ya has publicado
Rebase reescribe el historial. Mientras los commits solo los tengas tú, es seguro. Pero si tus compañeros ya descargaron la rama, después del rebase ellos seguirán teniendo los antiguos D y E y tú los nuevos D' y E'; en el siguiente pull el historial se duplicará y habrá que volver a resolver los conflictos.
Regla práctica: rebase se aplica a tus ramas locales y a las ramas de feature personales en las que trabaja una sola persona. Las ramas compartidas (main, develop, las de release) no se rebasan nunca.
Cómo subir la rama después de un rebase
Si la rama ya estaba en el servidor, un git push normal será rechazado: el historial local ya no continúa el remoto. Hace falta un push forzado, y en su variante segura:
git push --force-with-lease
A diferencia de --force, la opción --force-with-lease se niega a sobrescribir la rama si en el servidor han aparecido commits que no has visto. Así no borrarás el trabajo de otra persona por accidente.
Conflictos durante el rebase
Como los commits se aplican de uno en uno, puede surgir un conflicto en cualquiera de ellos, incluso varias veces seguidas. Git se detiene e indica qué commit no ha podido aplicar.
$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Añadir caché de peticiones
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
Qué hacer:
- Abre el archivo en conflicto, elige la variante correcta y quita los marcadores
<<<<<<<,=======,>>>>>>>. - Marca el archivo como resuelto:
git add src/api.js. No hace falta hacer commit. - Continúa:
git rebase --continue. Git aplicará el siguiente commit.
El resto de comandos para controlar el proceso:
| Comando | Qué hace |
|---|---|
git rebase --continue | Continuar tras resolver el conflicto |
git rebase --skip | Saltar el commit actual completo (sus cambios se pierden) |
git rebase --abort | Interrumpir el rebase y devolver la rama a su estado original |
Ojo con la confusión entre «nuestro» y «suyo». Durante un rebase, HEAD es la base sobre la que trasladas la rama, y los cambios «entrantes» son tus propios commits. Por eso git checkout --ours en un rebase toma la versión de main, y --theirs, la tuya. En un merge es justo al revés.
Conflictos repetidos: rerere
Si rebasas a menudo una rama de larga duración y resuelves una y otra vez el mismo conflicto, activa git config --global rerere.enabled true. Git recordará cómo lo resolviste y la próxima vez aplicará la solución solo.
Rebase interactivo: arreglar el historial antes de publicarlo
Lo más valioso de rebase es su modo interactivo. Te permite ordenar tus commits antes de enseñarlos en la revisión: combinar «fix», «fix typo», «otro fix» en un único commit con sentido, reescribir mensajes, cambiar el orden o tirar lo que sobra.
git rebase -i HEAD~4 # los últimos 4 commits
git rebase -i origin/main # todos los commits de la rama que no están en main
Git abrirá el editor con la lista de commits, de los más antiguos a los más nuevos (al revés que git log):
pick 1a2b3c4 Añadir formulario de inicio de sesión
pick 5d6e7f8 fix
pick 9a0b1c2 Validación del e-mail
pick 3d4e5f6 errata en la validación
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit
Cambias la palabra al principio de la línea, guardas y cierras el editor, y Git ejecuta el plan de arriba abajo.
| Comando | Efecto |
|---|---|
pick | Dejar el commit tal cual |
reword | Mantener los cambios pero reescribir el mensaje |
edit | Detenerse en el commit para cambiar su contenido (git commit --amend) o dividirlo en varios |
squash | Fusionarlo con el commit anterior, combinando y editando los mensajes |
fixup | Fusionarlo con el commit anterior, descartando el mensaje de este commit |
drop | Eliminar el commit junto con sus cambios |
Reordenar las líneas cambia el orden de los commits, y borrar una línea equivale a drop.
Git squash: cómo unir varios commits en uno
En Git no existe un comando git squash como tal. Los commits se pueden «squashear» de tres maneras, y el rebase interactivo es la más flexible.
Opción 1. Rebase interactivo
Con el ejemplo anterior, convertimos los cuatro commits en dos bien hechos:
pick 1a2b3c4 Añadir formulario de inicio de sesión
fixup 5d6e7f8 fix
pick 9a0b1c2 Validación del e-mail
fixup 3d4e5f6 errata en la validación
El primer commit del grupo tiene que seguir siendo pick: squash y fixup fusionan el commit con el anterior, y el primero no tiene con qué fusionarse.
Opción 2. Commits fixup y autosquash
Si ya sabes qué commit estás corrigiendo, crea la corrección marcada desde el principio:
git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main
Git colocará el commit fixup justo debajo del que corresponde y lo marcará como fixup; solo te queda guardar el plan. Este comportamiento se activa por defecto con git config --global rebase.autoSquash true.
Opción 3. Comprimir toda la rama en un commit
git reset --soft $(git merge-base HEAD origin/main)
git commit -m "Inicio de sesión por e-mail con validación"
reset --soft devuelve el puntero de la rama al punto donde se bifurcó, pero deja todos los cambios en el índice: solo queda hacer un commit. Otra opción es git merge --squash al fusionar con la rama principal, o el botón «Squash and merge» del pull request.
Variantes útiles de rebase
git pull --rebase
Un pull normal fusiona la rama remota con la tuya y crea un merge commit si has hecho commits en paralelo con tus compañeros. git pull --rebase, en cambio, recoloca tus commits locales encima de los remotos recientes. Para hacerlo el comportamiento por defecto: git config --global pull.rebase true. Esta es precisamente la zona segura de rebase: nadie ha visto todavía tus commits locales.
--onto: trasladar parte de una rama
Creaste feature-b a partir de feature-a y ahora quieres que feature-b salga directamente de main, sin los commits de feature-a:
git rebase --onto main feature-a feature-b
Se lee así: toma los commits de feature-b que no están en feature-a y trasplántalos sobre main.
--exec: comprobar cada commit
git rebase -i --exec "npm test" origin/main
Después de aplicar cada commit se lanzan los tests; si fallan, el rebase se detiene en el commit problemático. Muy útil cuando importa que cada commit del historial compile.
--update-refs: una pila de ramas dependientes
Si mantienes una cadena de ramas que dependen unas de otras, la opción --update-refs (Git 2.38+) mueve también las ramas intermedias al rebasar la de arriba.
Cómo deshacer un rebase
Mientras el proceso está en curso: git rebase --abort. Si el rebase ya terminó y el resultado no te gusta, los commits antiguos no han desaparecido: el registro de referencias los recuerda.
$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Validación del e-mail
...
c3d4e5f HEAD@{5}: commit: errata en la validación
$ git reset --hard HEAD@{5}
Busca en git reflog la entrada anterior al inicio del rebase y resetea la rama a ella. Justo después del rebase también funciona el atajo git reset --hard ORIG_HEAD. El reflog se guarda en local y por defecto conserva las entradas unos 90 días, así que no hay motivo para el pánico tras un rebase fallido.
Errores típicos
- Rebasar una rama compartida y hacer force push: a los compañeros se les descuadra el historial. Solución: ponerse de acuerdo, restaurar la rama desde el reflog de quien la tenga bien y prohibir el force push en las ramas protegidas del servidor.
- Usar
--forceen vez de--force-with-leasey machacar un commit que un compañero acababa de subir a la misma rama. - Poner squash en la primera línea del plan interactivo: Git responderá con el error «cannot 'squash' without a previous commit».
- Empezar un rebase con el árbol de trabajo sucio. Git se negará a arrancar. Haz commit de los cambios, guárdalos con
git stasho usagit rebase --autostash. - Resolver un conflicto y hacer
git commiten lugar degit rebase --continue. Saldrá un commit de más; después de--continuepuedes quitarlo con otro rebase interactivo.
Conclusión
git rebase traslada tus commits a una nueva base y los vuelve a crear, así que el historial queda lineal y limpio. El modo interactivo convierte los commits de borrador en un historial cuidado: squash y fixup unen correcciones, reword arregla mensajes y drop elimina lo que sobra. La regla principal es una sola: rebasa únicamente lo que aún no has publicado para otros y, tras rebasar tu rama remota, súbela con --force-with-lease. El trabajo con rebase y la limpieza del historial de commits se practican en el curso de Git.
Preguntas frecuentes
¿git rebase cambia el contenido del código?
Si no hay conflictos, el contenido final de los archivos es el mismo que con un merge, pero los commits se recrean con hashes y padres nuevos. Si en un conflicto eliges otra variante, los commits intermedios diferirán de los originales.
¿Cómo hacer rebase hasta el primer commit del repositorio?
Usa git rebase -i --root: el plan incluirá todos los commits, también el raíz. Rara vez hace falta, por ejemplo para reescribir el mensaje del primer commit.
¿Rebase conserva el autor y la fecha de los commits?
Se conservan el autor y la fecha de autoría, mientras que el committer y la fecha del commit pasan a ser los actuales. La opción --committer-date-is-author-date mantiene la fecha del commit igual a la de autoría.
¿Se pueden rebasar merge commits?
Por defecto, rebase endereza el historial y descarta los merge commits. Para conservar la estructura de fusiones, usa git rebase --rebase-merges.
¿Cómo saber si hay un rebase en curso?
git status mostrará la línea interactive rebase in progress o You are currently rebasing e indicará qué comandos están disponibles. Muchos shells e IDE también lo muestran en el prompt.
Este tema se explica a fondo y con práctica en el curso «Git desde cero hasta desarrollador seguro».
Ir al curso