La herramienta más evitada de Git
Pregunta en una sala llena de desarrolladores qué comando de Git les pone nerviosos, y git rebase -i aparecerá entre los primeros. El rebase interactivo tiene fama: reescribe el historial, abre una extraña todo list en tu editor, y parece peligrosamente fácil romper algo. Así que la mayoría simplemente lo evita -- y sus pull requests llegan cargadas de commits como "wip", "fix typo" y "actually fix it".
Es una lástima, porque el rebase interactivo es la mejor herramienta que existe para convertir una branch desordenada en un historial limpio y fácil de revisar. En esta guía vamos a desmitificarlo con un ejemplo concreto que seguiremos de principio a fin: una branch de funcionalidad con 9 commits desordenados que remodelaremos en 3 commits limpios antes de abrir una pull request. Si todavía te preguntas si el rebase es siquiera la opción correcta para tu flujo de trabajo, empieza por nuestra comparativa de rebase vs merge -- este artículo asume que has decidido hacer rebase y quieres hacerlo bien.
Por qué importa un historial limpio
Limpiar los commits antes de una pull request no es una cuestión de estética. Un historial ordenado compensa de tres maneras muy prácticas:
- Revisabilidad. Un revisor que lee tres commits lógicos -- "Add login form with validation", "Add remember me option", "Add error messages" -- puede revisar cada cambio de forma aislada. El mismo código repartido en nueve commits a medio terminar le obliga a revisar todo el diff de una vez, y la calidad de la revisión cae.
- git bisect. Cuando un bug aparece semanas después,
git bisectrecorre el historial para encontrar el commit que lo introdujo. Eso solo funciona si cada commit compila y tiene sentido por sí mismo. Un historial lleno de commits "wip" que ni siquiera compilan hace que bisect sea casi inútil. - Revertir. Si hay que retirar una funcionalidad, revertir un commit autocontenido es trivial. Revertir una funcionalidad esparcida por nueve commits entrelazados es una tarde de arqueología.
Nuestro ejemplo de referencia: 9 commits desordenados
Esta es la branch que vamos a limpiar. Es un historial de trabajo perfectamente normal -- así es como todo el mundo programa en realidad:
$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton
No hay nada de malo en hacer commits así mientras trabajas. El problema es solo si se publica así. Nuestro objetivo es terminar con tres commits:
- Add login form with validation
- Add remember me option
- Add error messages with styling
Iniciar el rebase: git rebase -i HEAD~9
Para reescribir los últimos 9 commits, ejecuta:
git rebase -i HEAD~9
HEAD~9 significa "el commit 9 pasos antes del actual" -- todo lo que viene después de ese punto queda disponible para editar. Git abre tu editor con la todo list:
pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file
Dos cosas que entender antes de tocar nada. Primero, esto es solo un archivo de texto: no pasa nada hasta que lo guardas y lo cierras, y si borras todas las líneas o lo cierras sin guardar cambios significativos, el rebase se cancela sin consecuencias. Segundo, el orden es del más antiguo al más reciente -- lo contrario de git log. Git reproducirá los commits de arriba abajo, aplicando el verbo que hay al principio de cada línea.
Los seis verbos
pick
Mantener el commit exactamente como está. Es el valor por defecto para cada línea.
reword
Mantener los cambios del commit, pero detenerse para que edites el mensaje del commit. Perfecto para corregir "wip 2" sin tocar el código.
edit
Pausar el rebase en este commit. Puedes hacer amend, dividirlo en varios commits o probarlo, y luego continuar con git rebase --continue. Es el verbo más avanzado y el que menos usarás.
squash
Fusionar este commit con el de arriba, y abrir el editor para que combines los dos mensajes de commit en uno.
fixup
Como squash, pero descartando por completo el mensaje de este commit. El commit de arriba conserva su mensaje sin cambios. Es lo que quieres para los commits "fix typo" -- sus mensajes no aportan ninguna información que merezca conservarse.
drop
Eliminar el commit y sus cambios por completo. Borrar la línea de la todo list tiene el mismo efecto, pero escribir drop hace tu intención explícita.
Limpiar la branch, paso a paso
Ahora apliquemos esto a nuestros 9 commits. Hacen falta dos operaciones: reordenar y cambiar verbos.
Primero, el reordenamiento. Mirando los diffs, "oops forgot the css file" es en realidad la hoja de estilos del checkbox de remember me, no la de los mensajes de error. En la todo list, reordenar es simplemente mover una línea: corta la línea c9d0e1f y pégala justo después del grupo de remember me. Git reproducirá los commits en el nuevo orden.
Después, los verbos. Todo lo que hay hasta "actually fix it" pertenece al formulario de login, así que se colapsa en el primer commit. Usamos squash en "add form validation" porque queremos fusionar su mensaje en el final, y fixup en los commits de ruido cuyos mensajes merecen desaparecer. Esta es la todo list editada:
pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages
Guarda y cierra. Git reproduce los commits y se detiene dos veces: una para que escribas el mensaje combinado del primer grupo (escribe "Add login form with validation"), y otra para el reword (escribe "Add error messages with styling"). Para el commit de remember me, un reword rápido también habría funcionado -- aquí lo dejamos como pick y su mensaje original ya era decente. El resultado:
$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation
Nueve commits se convirtieron en tres, cada uno autocontenido y fácil de revisar. Fíjate en que todos los hashes cambiaron -- son commits completamente nuevos. Ten ese detalle presente; es el corazón de la regla de oro de más abajo.
El movimiento de profesional: --autosquash
Cuando ya te sientas cómodo, puedes preparar la limpieza mientras trabajas en lugar de resolverla al final. Cuando corrijas algo de un commit anterior, regístralo así:
git commit --fixup a1b2c3d
Git crea un commit cuyo mensaje es fixup! add login form skeleton. Más tarde, ejecuta:
git rebase -i --autosquash main
Git rellena previamente la todo list con cada commit fixup! ya movido bajo su objetivo y marcado como fixup. Tú solo guardas y cierras. Para hacer de esto el comportamiento por defecto, configúralo una sola vez:
git config --global rebase.autosquash true
La regla de oro: nunca hagas rebase de commits compartidos
El rebase no modifica los commits -- los reemplaza por otros nuevos. Si tus compañeros ya han hecho pull de los commits antiguos y han basado su trabajo en ellos, reescribir esos commits crea dos historiales divergentes, y el siguiente merge se convierte en un desastre de duplicados y conflictos.
La regla es simple: haz rebase solo de commits que no hayan sido pusheados, o que vivan en una branch en la que solo trabajas tú. Limpiar tu propia branch de funcionalidad antes de abrir una pull request es el caso seguro de manual. Si ya has pusheado la branch y sigues siendo su único autor, pushea el historial reescrito con git push --force-with-lease, que se niega a sobrescribir trabajo que otra persona haya pusheado entretanto. Nunca reescribas main ni ninguna branch de la que tu equipo haga pull.
Cuando algo sale mal
En mitad de un rebase pueden pasar dos cosas. Si un commit reproducido entra en conflicto con un cambio anterior, Git se pausa y te pide que resuelvas el conflicto, luego hagas git add de los archivos y ejecutes git rebase --continue. Y si en algún momento te sientes perdido, existe un botón de expulsión total:
git rebase --abort
Esto devuelve la branch al estado exacto en que estaba antes de empezar, sin hacer preguntas. Mientras recuerdes que --abort existe, un rebase en curso nunca puede atraparte.
¿Y si el rebase terminó y te das cuenta de que el resultado está mal? Incluso entonces, no se pierde nada. Los commits antiguos siguen existiendo -- Git los conserva durante semanas, y el reflog registra cada posición a la que tu branch ha apuntado alguna vez, así que puedes volver al estado previo al rebase con un solo reset. Cubrimos esa red de seguridad en detalle en recuperar commits perdidos con git reflog. Y si tu error es más pequeño -- solo hay que cambiar el último commit -- no necesitas un rebase en absoluto: consulta cómo deshacer un commit de Git.
El rebase interactivo en GitSquid
La todo list es potente, pero editar verbos en un archivo de texto es exactamente la parte que pone nerviosa a la gente. GitSquid la reemplaza por un rebase interactivo visual: reordenas los commits con drag & drop, y eliges la acción para cada commit -- pick, squash, fixup, reword o drop -- desde un selector desplegable. Todo el plan es visible de un vistazo antes de que nada se ejecute, lo que elimina la mayor parte del factor miedo.
GitSquid también te ayuda antes incluso de empezar: el Conflict Predictor, incluido en el plan gratuito, muestra qué archivos entrarán en conflicto antes de que lances un rebase, así sabes a qué te comprometes en lugar de descubrir los conflictos a mitad de camino. El rebase está disponible directamente desde los menús contextuales de branches y commits en el grafo de commits.
Referencia rápida
| Comando / verbo | Qué hace |
|---|---|
git rebase -i HEAD~n |
Reescribe interactivamente los últimos n commits |
pick |
Mantiene el commit tal cual |
reword |
Mantiene los cambios, edita el mensaje |
edit |
Se detiene en este commit para hacer amend o dividirlo |
squash |
Fusiona con el commit anterior, combina los mensajes |
fixup |
Fusiona con el commit anterior, descarta este mensaje |
drop |
Elimina el commit por completo |
git commit --fixup <hash> |
Registra una corrección destinada a un commit anterior |
git rebase -i --autosquash |
Coloca automáticamente los commits fixup! en la todo list |
git rebase --continue |
Reanuda tras resolver un conflicto |
git rebase --abort |
Cancela y restaura el estado previo al rebase |
El rebase interactivo recompensa un poco de práctica con mucho poder. Empieza en una branch desechable, ten presente --abort, respeta la regla de oro, y convertir "wip, fix typo, actually fix it" en un historial limpio se vuelve rutina. Y si prefieres ver el plan en lugar de editar un archivo de texto, descarga GitSquid gratis y prueba el rebase visual en tu próxima branch de funcionalidad.