En la lección anterior usamos git rebase para una sola cosa: cambiar la base de una rama. Pero el mecanismo que hay debajo —descomponer una rama en una lista de commits y volver a aplicarlos uno a uno— permite mucho más si alguien te deja editar esa lista antes de que se ejecute.

Eso es git rebase -i. Git te abre un fichero de texto con los commits que va a reaplicar, uno por línea, y espera a que tú decidas qué hacer con cada uno: dejarlo tal cual, cambiarle el mensaje, unirlo con el anterior, partirlo en dos, moverlo de sitio o borrarlo. Cuando guardas y cierras, Git ejecuta tus instrucciones al pie de la letra.

Es la herramienta que convierte una rama real —con sus dudas, sus vueltas atrás y sus wip— en una secuencia de commits que otra persona puede leer y entender. Bruno la necesita ahora mismo: su rama funcionalidad/exportacion-csv tiene seis commits, tres de ellos llamados arreglo, wip 2 y wip 3, y quiere publicarla para que la revisen.

Y con ella vuelve, más fuerte que nunca, la regla de oro: el rebase interactivo reescribe commits, así que solo se aplica a trabajo que aún no has publicado (o a una rama publicada que solo tú usas y de la que puedes avisar).

Contenido

  1. Qué es la lista de tareas
  2. Las órdenes disponibles
  3. El punto de partida: la rama de Bruno
  4. Sesión completa: de seis commits a dos
  5. squash frente a fixup
  6. Reordenar y eliminar commits
  7. Dividir un commit en dos con edit
  8. Cambiar solo un mensaje con reword
  9. --autosquash: commit --fixup y commit --squash
  10. --exec: validar cada commit
  11. break, label, reset y merge
  12. Qué hacer si te pierdes a mitad

  1. Qué es la lista de tareas

Al lanzar un rebase interactivo, Git prepara la lista de commits que va a reaplicar y te la abre en tu editor:

git rebase -i main
pick b1c4f80 Empezar exportacion
pick 3d6e9a1 Anadir boton de exportar
pick c8f2b47 mas cosas del csv
pick 2a7f4c1 arreglo
pick 9e3b8d6 wip 2
pick 5c1a9f2 wip 3

# Rebase e91d4a8..5c1a9f2 onto e91d4a8 (6 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later)
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.

Tres detalles que hay que asimilar antes de tocar nada:

  • El orden es cronológico ascendente: el commit más antiguo arriba. Es al revés que git log, que muestra el más reciente primero. Esta inversión es la primera fuente de errores de quien empieza.
  • Las líneas se ejecutan de arriba abajo. Reordenar líneas reordena commits.
  • Borrar una línea equivale a drop: ese commit desaparece. Git te lo avisa en mayúsculas por algo.

Y una precisión sobre el argumento: git rebase -i main significa "reaplica sobre main todo lo que hay en mi rama y no en main", igual que en la lección anterior. Si solo quieres tocar los últimos N commits de la rama en la que estás, sin cambiar de base, se usa:

git rebase -i HEAD~4     # los 4 últimos commits
git rebase -i 8d4e6b2    # todo lo que hay DESPUÉS de ese commit

Ojo con esto último: git rebase -i <commit> no incluye <commit> en la lista, porque <commit> es la base. Para tocar los últimos cuatro commits, la base es el quinto empezando por el final: HEAD~4.

Si tu editor por defecto no es el que quieres, se cambia con git config --global core.editor "code --wait" (o nano, vim, lo que uses). Se explicó en la lección 01-05, y aquí lo vas a agradecer.

  1. Las órdenes disponibles

Orden Abreviatura Qué hace
pick p Aplica el commit tal cual. Es el valor por defecto
reword r Aplica el commit y abre el editor para cambiar su mensaje
edit e Aplica el commit y se detiene para que lo modifiques (o lo partas)
squash s Une el commit con el anterior y abre el editor para combinar los dos mensajes
fixup f Une el commit con el anterior y descarta su mensaje
fixup -C Une con el anterior pero conserva este mensaje en lugar del anterior
drop d Elimina el commit. Equivale a borrar la línea
exec x Ejecuta un comando de shell en ese punto de la secuencia
break b Se detiene ahí sin hacer nada, para que mires
label l Pone un nombre temporal al HEAD actual
reset t Vuelve a un label previo
merge m Crea un commit de fusión con un label

Las cuatro primeras cubren el 95 % del uso real. drop y exec aparecen de vez en cuando. break es una comodidad. label, reset y merge solo aparecen cuando usas --rebase-merges para reconstruir un historial con fusiones, y no vamos a necesitarlas en el día a día: basta con saber qué significan si te las encuentras.

Un apunte sobre squash y fixup: unen con el commit anterior de la lista, es decir, con la línea de encima. Por eso nunca pueden ser la primera línea (no hay nada con lo que unirse) y por eso el orden importa tanto.

  1. El punto de partida: la rama de Bruno

Bruno lleva una semana con la exportación de tareas a CSV. La rama funciona, pero el historial es un desastre:

git switch funcionalidad/exportacion-csv
git log --oneline main..HEAD
5c1a9f2 wip 3
9e3b8d6 wip 2
2a7f4c1 arreglo
c8f2b47 mas cosas del csv
3d6e9a1 Anadir boton de exportar
b1c4f80 Empezar exportacion

Con --stat se ve qué hay realmente en cada uno:

git log --oneline --stat main..HEAD
5c1a9f2 wip 3
 app.js | 4 ++--
9e3b8d6 wip 2
 app.js | 7 +++++--
2a7f4c1 arreglo
 app.js | 2 +-
c8f2b47 mas cosas del csv
 app.js | 18 ++++++++++++++++++
3d6e9a1 Anadir boton de exportar
 index.html |  3 +++
 estilos.css | 8 ++++++++
b1c4f80 Empezar exportacion
 app.js | 6 ++++++

Leído con calma, la rama hace dos cosas independientes:

  1. Una función que convierte la lista de tareas a CSV y la descarga (app.js): commits 1, 3, 4, 5 y 6.
  2. Un botón en la interfaz que la invoca (index.html + estilos.css): commit 2.

El objetivo de Bruno es acabar con exactamente esos dos commits, en ese orden, con mensajes que expliquen qué hacen. Y lo importante: el contenido final de la rama no va a cambiar ni una línea. Un rebase interactivo bien hecho reorganiza cómo se cuenta la historia, no lo que hay al final.

# La huella del estado final, para comprobarlo después
git rev-parse HEAD^{tree}
7f4b2c9e1d8a3b6f5c2e9d4a7b1f8c3e6d5a2b9f

Ese es el hash del árbol de la punta de la rama. Al terminar el rebase debe ser el mismo. Es el mejor control de calidad que existe para esta operación.

  1. Sesión completa: de seis commits a dos

Bruno lanza el rebase interactivo:

git rebase -i main

Y edita la lista que le aparece. Lo primero es reordenar para juntar lo que va junto, y después decidir la orden de cada línea. Así queda el fichero antes de guardar:

pick b1c4f80 Empezar exportacion
squash c8f2b47 mas cosas del csv
fixup 2a7f4c1 arreglo
fixup 9e3b8d6 wip 2
fixup 5c1a9f2 wip 3
pick 3d6e9a1 Anadir boton de exportar

Léelo de arriba abajo como una receta:

  1. pick b1c4f80 — aplica el primer commit tal cual. Será la base del commit final de la exportación.
  2. squash c8f2b47 — únelo al anterior, y déjame combinar los mensajes.
  3. fixup 2a7f4c1 — únelo al anterior y tira su mensaje (arreglo no aporta nada).
  4. fixup 9e3b8d6 — igual con wip 2.
  5. fixup 5c1a9f2 — igual con wip 3.
  6. pick 3d6e9a1 — el commit del botón, movido al final, se queda como está de momento.

Al guardar y cerrar, Git empieza a ejecutar. En el paso del squash se detiene y abre el editor con los dos mensajes:

# This is a combination of 2 commits.
# This is the 1st commit message:

Empezar exportacion

# This is the commit message #2:

mas cosas del csv

# Please enter the commit message for your changes.

Bruno borra todo eso y escribe un mensaje decente:

Añadir la exportación de tareas a CSV

Genera un fichero CSV con el título, el estado y la fecha de creación
de cada tarea y lo descarga desde el navegador sin llamadas al servidor.

Las comillas y los puntos y coma del título se escapan según RFC 4180.

Guarda, cierra, y el rebase continúa. Los tres fixup no preguntan nada. Al final:

Successfully rebased and updated refs/heads/funcionalidad/exportacion-csv.
git log --oneline main..HEAD
a3f9c14 Anadir boton de exportar
d72e6b8 Añadir la exportación de tareas a CSV

Falta pulir el mensaje del segundo, pero eso lo vemos en el apartado 8. Antes, la comprobación que prometimos:

git rev-parse HEAD^{tree}
7f4b2c9e1d8a3b6f5c2e9d4a7b1f8c3e6d5a2b9f

Idéntico al de antes del rebase. El contenido de la rama no ha cambiado; solo su historia. Esa es la garantía que convierte esta operación en algo razonable y no en una temeridad.

gitGraph
   commit id: "e91d4a8"
   branch antes
   commit id: "b1c4f80"
   commit id: "3d6e9a1"
   commit id: "c8f2b47"
   commit id: "2a7f4c1"
   commit id: "9e3b8d6"
   commit id: "5c1a9f2"
   checkout main
   branch despues
   commit id: "d72e6b8"
   commit id: "a3f9c14"

  1. squash frente a fixup

Las dos unen un commit con el anterior. La única diferencia está en el mensaje, pero determina cuál usar en cada caso:

squash fixup
Contenido resultante El de los dos commits combinado El de los dos commits combinado
Mensaje resultante Los dos mensajes juntos, editables Solo el del commit anterior
¿Abre el editor? No
Cuándo usarlo Los dos commits aportan información al mensaje El segundo era un arreglo, una errata, un wip
Variante fixup -C conserva el mensaje del segundo

En la práctica: si el mensaje del commit que absorbes vale algo, squash; si es basura (arreglo, wip, ahora sí, punto y coma), fixup. Y como un squash de cinco commits te obliga a limpiar a mano cinco mensajes en el editor, en cuanto tengas tres o más suele salir a cuenta usar fixup en todos y escribir el mensaje una sola vez con reword.

  1. Reordenar y eliminar commits

Ya has visto reordenar: en la sesión de Bruno, mover 3d6e9a1 de la segunda posición a la última bastó para agrupar el trabajo por temas.

Eliminar es igual de directo: pon drop delante de la línea, o bórrala entera. Las dos formas son equivalentes; drop es preferible porque deja constancia visible de la intención y evita el borrado accidental de una línea:

pick b1c4f80 Empezar exportacion
drop 4c8f1a5 Anadir console.log para depurar
pick 3d6e9a1 Anadir boton de exportar

Dos advertencias que valen para las dos operaciones:

  • Reordenar y eliminar pueden provocar conflictos. Si el commit c8f2b47 modificaba una línea que b1c4f80 acababa de crear y los separas, el segundo ya no encuentra el contexto que esperaba. Se resuelve como cualquier conflicto de rebase (lección 05-01): resolver, git add, git rebase --continue. Recuerda que las caras ours/theirs siguen invertidas.
  • Eliminar un commit del medio deja la rama con menos cambios, no con menos código necesariamente. Si dropeas el commit que creaba una función y dejas el que la llama, la rama compila mal. --exec (apartado 10) sirve precisamente para detectarlo.

  1. Dividir un commit en dos con edit

Es la operación estrella del rebase interactivo y la que más impresiona la primera vez. Supongamos que Bruno mira d72e6b8 y decide que mezcla dos cosas: la función que genera el CSV y la que dispara la descarga. Quiere partirlo.

Paso 1: marcar el commit con edit.

git rebase -i main
edit d72e6b8 Añadir la exportación de tareas a CSV
pick a3f9c14 Anadir boton de exportar

Git aplica el commit y se detiene:

Stopped at d72e6b8...  Añadir la exportación de tareas a CSV
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

Paso 2: deshacer el commit pero conservar sus cambios.

git reset HEAD^
Unstaged changes after reset:
M	app.js

Esto es lo que hace la magia: git reset sin opciones (equivale a --mixed) mueve HEAD un commit atrás y deja todos los cambios en el directorio de trabajo, sin preparar. El commit ha desaparecido; su contenido sigue en el fichero. Es la única aparición de reset en este módulo; sus tres modos y su uso como herramienta de deshacer se estudian a fondo en la lección 09-02.

git status --short
 M app.js

Paso 3: preparar y confirmar por partes. Aquí entra git add -p, que aprendiste en la lección 02-04: preparar solo unos trozos del fichero.

git add -p app.js

Bruno acepta con y los trozos de la función tareasACSV y rechaza con n los de descargarFichero. Después:

git commit -m "Añadir la conversión de tareas a formato CSV"
[detached HEAD 6e1d9f3] Añadir la conversión de tareas a formato CSV
 1 file changed, 14 insertions(+)

Y el resto:

git add app.js
git commit -m "Descargar el CSV generado desde el navegador"
[detached HEAD b8c2a70] Descargar el CSV generado desde el navegador
 1 file changed, 9 insertions(+)

Paso 4: continuar.

git rebase --continue
Successfully rebased and updated refs/heads/funcionalidad/exportacion-csv.
git log --oneline main..HEAD
f04b7e2 Anadir boton de exportar
b8c2a70 Descargar el CSV generado desde el navegador
6e1d9f3 Añadir la conversión de tareas a formato CSV

Un commit se ha convertido en dos, sin perder una línea. Fíjate en que el commit posterior (a3f9c14f04b7e2) también ha cambiado de hash: al insertar commits antes que él, su padre es otro, y ya sabes lo que eso implica.

Truco: si al hacer git reset HEAD^ te das cuenta de que el reparto no es limpio (los cambios están entremezclados en la misma función), puedes editar los ficheros a mano antes de cada commit. No estás obligado a que cada mitad sea un subconjunto exacto de la original; solo a que el resultado final sea el mismo. Verifícalo con el truco del rev-parse HEAD^{tree}.

  1. Cambiar solo un mensaje con reword

Al commit del botón le falta la tilde y la mayúscula. Para cambiar solo el mensaje, sin tocar nada más:

git rebase -i main
pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
pick b8c2a70 Descargar el CSV generado desde el navegador
reword f04b7e2 Anadir boton de exportar

Git aplica los dos primeros, y en el tercero abre el editor con el mensaje actual. Bruno lo cambia por:

Añadir el botón de exportar a la barra de acciones

Guarda, cierra, y listo:

git log --oneline main..HEAD
9c7e5a1 Añadir el botón de exportar a la barra de acciones
b8c2a70 Descargar el CSV generado desde el navegador
6e1d9f3 Añadir la conversión de tareas a formato CSV

Tres commits, tres mensajes que explican qué hace cada uno, y una rama lista para publicar y para que la revisen.

Nota para el caso más frecuente: si el commit que quieres reescribir es el último, no hace falta rebase interactivo. git commit --amend (lección 02-04) hace lo mismo con menos ceremonia. reword es para los que están más atrás.

Qué es un buen mensaje de commit —el imperativo, la línea de asunto, el cuerpo, los pies— es el tema de la lección 08-01. Aquí solo nos ocupa el mecanismo de cambiarlo.

  1. --autosquash: commit --fixup y commit --squash

Hay un flujo mucho más cómodo que ir marcando fixup a mano, y consiste en decidirlo en el momento de hacer el commit, cuando todavía tienes fresco a qué commit corrige.

Imagina que Bruno está trabajando y descubre una errata en el commit 6e1d9f3, que hizo hace dos días. En lugar de crear un commit llamado arreglo:

# Corrige la errata en app.js
git add app.js
git commit --fixup 6e1d9f3
[funcionalidad/exportacion-csv 2b8f4d1] fixup! Añadir la conversión de tareas a formato CSV

Git ha creado un commit normal, pero con un mensaje especial: fixup! seguido del asunto del commit al que corrige. Ese prefijo es una marca que Git sabe interpretar.

git log --oneline main..HEAD
2b8f4d1 fixup! Añadir la conversión de tareas a formato CSV
9c7e5a1 Añadir el botón de exportar a la barra de acciones
b8c2a70 Descargar el CSV generado desde el navegador
6e1d9f3 Añadir la conversión de tareas a formato CSV

Y ahora, la parte bonita:

git rebase -i --autosquash main

La lista que aparece ya viene ordenada y con las órdenes puestas:

pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
fixup 2b8f4d1 fixup! Añadir la conversión de tareas a formato CSV
pick b8c2a70 Descargar el CSV generado desde el navegador
pick 9c7e5a1 Añadir el botón de exportar a la barra de acciones

Bruno solo tiene que guardar y cerrar sin tocar nada. Git ha movido el commit de corrección justo detrás de su destino y lo ha marcado como fixup.

Comando Prefijo del mensaje En el rebase se convierte en Efecto sobre el mensaje
git commit --fixup <sha> fixup! fixup Se descarta el del arreglo
git commit --squash <sha> squash! squash Se abre el editor para combinarlos
git commit --fixup=reword:<sha> amend! fixup -C Solo cambia el mensaje del original

Para no tener que acordarse de --autosquash cada vez:

git config --global rebase.autoSquash true

Y un aviso: --autosquash empareja los commits por el texto del asunto, no por el hash. Si dos commits de la rama tienen exactamente el mismo asunto, el emparejamiento puede ser ambiguo. En la práctica casi nunca pasa, y siempre puedes revisar la lista antes de guardar (que es justo lo que el editor te está pidiendo).

Este flujo —trabajar, corregir con --fixup, y aplanar todo con un rebase -i --autosquash justo antes de publicar— es probablemente la forma más productiva de usar Git a diario. Te libera de pensar en el historial mientras programas, y te deja limpiarlo de golpe al final.

  1. --exec: validar cada commit

Un historial limpio que no compila no vale de nada. Cuando reordenas, unes o divides commits, es fácil que alguno intermedio quede roto: el que llama a una función que aún no existe, el que importa un módulo que llega dos commits después.

--exec ejecuta un comando después de cada commit reaplicado y detiene el rebase si el comando devuelve un código de error distinto de cero:

git rebase -i --exec "npm test" main

Git inserta automáticamente una línea exec tras cada pick:

pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
exec npm test
pick b8c2a70 Descargar el CSV generado desde el navegador
exec npm test
pick 9c7e5a1 Añadir el botón de exportar a la barra de acciones
exec npm test

Si el segundo npm test falla:

Executing: npm test
...
warning: execution failed: npm test
You can fix the problem, and then run

  git rebase --continue

El rebase se queda parado en ese punto, con el repositorio exactamente en el estado de ese commit. Puedes arreglar lo que sea, hacer git commit --amend, y continuar.

También puedes escribir las líneas exec a mano donde te interese, en lugar de en todos los commits. Y no tiene por qué ser una suite de tests; cualquier comprobación rápida vale:

pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
exec node --check app.js
pick b8c2a70 Descargar el CSV generado desde el navegador
exec node --check app.js
exec grep -rn "console.log" app.js && exit 1 || exit 0

La última línea es un pequeño guardián: falla si queda algún console.log olvidado. Automatizar este tipo de comprobaciones de forma permanente, para que se ejecuten solas en cada commit, es el tema de los hooks (lección 06-01).

  1. break, label, reset y merge

Tres órdenes menos frecuentes, pero conviene reconocerlas.

break detiene el rebase en ese punto sin hacer nada más. Es una pausa deliberada:

pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
break
pick b8c2a70 Descargar el CSV generado desde el navegador

Útil para mirar el estado del proyecto en mitad de la secuencia, ejecutar algo a mano o simplemente respirar. Se sigue con git rebase --continue.

label, reset y merge solo aparecen cuando usas git rebase -i --rebase-merges, que reconstruye un historial conservando sus commits de fusión en lugar de aplanarlo. Git genera entonces una lista con este aspecto:

label onto

reset onto
pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
label exportacion

reset onto
pick 9c7e5a1 Añadir el botón de exportar a la barra de acciones
label boton

reset onto
merge -C 4a7d2f8 exportacion # Fusionar la exportación
merge -C 8e3b6c1 boton # Fusionar el botón

Se lee así: label X guarda un marcador en el punto actual, reset X vuelve a ese marcador, y merge -C <sha> X crea un commit de fusión con la rama marcada como X, reutilizando el mensaje del commit <sha>. Es un lenguaje pequeño para describir un grafo. No lo necesitarás casi nunca; basta con no asustarse si aparece.

  1. Qué hacer si te pierdes a mitad

Un rebase interactivo largo puede convertirse en un laberinto: cuatro conflictos, un edit a medias y la sensación de no saber en qué punto estás. Salidas, de menos a más drástica:

1. Preguntar dónde estás. git status durante un rebase interactivo es extraordinariamente informativo:

git status
interactive rebase in progress; onto e91d4a8
Last commands done (3 commands done):
   pick 6e1d9f3 Añadir la conversión de tareas a formato CSV
   fixup 2b8f4d1 fixup! Añadir la conversión de tareas a formato CSV
Next commands to do (2 remaining commands):
   pick b8c2a70 Descargar el CSV generado desde el navegador
   pick 9c7e5a1 Añadir el botón de exportar a la barra de acciones
You are currently rebasing branch 'funcionalidad/exportacion-csv' on 'e91d4a8'.

Te dice lo hecho y lo que falta. La lista viva está además en .git/rebase-merge/git-rebase-todo, y git rebase --edit-todo te la abre para cambiar lo que queda por hacer sin abortar. Es la salida elegante cuando te das cuenta a mitad de que planificaste mal:

git rebase --edit-todo    # cambia las órdenes pendientes
git rebase --continue

2. Ver qué está fallando ahora.

git rebase --show-current-patch

Muestra el commit que se está intentando aplicar, con su diff completo. Muy útil cuando el conflicto no tiene sentido a primera vista.

3. Abortar.

git rebase --abort

Deshace todo el rebase y devuelve la rama a su estado original, con los hashes originales. Es seguro, es instantáneo y no deja rastro. Ante la duda, aborta: replanificar la lista con la cabeza fría cuesta dos minutos; desenredar un rebase mal llevado, mucho más.

4. Si ya has terminado y el resultado es un desastre. Aquí es donde entra la red de seguridad: los commits originales siguen en la base de datos de objetos y el reflog guarda dónde estaba tu rama antes de empezar. El procedimiento completo para volver atrás es el tema de la lección 09-04. Y el reflejo barato de siempre, que evita tener que usarlo:

git branch copia-antes-del-rebase
git rebase -i main
# ¿Mal? -> git reset --hard copia-antes-del-rebase

Errores Comunes y Consejos

Error 1: leer la lista como si fuera git log. En el editor, el commit más antiguo está arriba. Marcar squash en la línea equivocada por esta inversión es el error número uno.

Error 2: poner squash o fixup en la primera línea. No hay commit anterior con el que unirse y el rebase falla antes de empezar. La primera línea siempre es pick, reword, edit o drop.

Error 3: borrar una línea sin querer. Ese commit desaparece. Usa drop explícito cuando quieras eliminar, y si borras una línea por accidente, cierra el editor sin guardar: Git aborta el rebase.

Error 4: rebasar interactivamente una rama ya publicada y compartida. La regla de oro no tiene excepciones cómodas. Si otras personas trabajan sobre esa rama, no la reescribas.

Error 5: hacer git commit en lugar de git rebase --continue tras resolver un conflicto. Salvo en el flujo de edit (donde sí confirmas tú), quien crea el commit es el rebase.

Error 6: olvidar que el rebase corre los hooks. Si tienes un pre-commit lento, un rebase de veinte commits lo ejecuta veinte veces. --no-verify lo desactiva, con la responsabilidad que eso implica.

Error 7: aplanar todo en un único commit gigante "para que quede limpio". Un commit de 900 líneas que dice "Añadir la exportación" es tan inútil como seis commits llamados wip. El objetivo es que cada commit sea un cambio completo y comprensible, no que haya uno solo.

Consejo 1: comprueba el árbol antes y después. git rev-parse HEAD^{tree} debe coincidir si tu intención era solo reorganizar. Si no coincide, has perdido o duplicado algo.

Consejo 2: adopta --fixup desde hoy. En cuanto tengas el reflejo de corregir con git commit --fixup <sha> en lugar de con un commit llamado arreglo, la limpieza final se vuelve trivial. Activa rebase.autoSquash true.

Consejo 3: haz rebases interactivos pequeños y frecuentes. Limpiar cinco commits es cómodo; limpiar cuarenta, no. Un rebase -i al final de cada jornada de trabajo en una rama es una costumbre excelente.

Consejo 4: usa --exec cuando reordenes o dividas. Es la única forma barata de saber que ningún commit intermedio ha quedado roto.

Consejo 5: revisa la lista antes de guardar, siempre. Ese editor abierto no es un trámite: es tu última oportunidad de ver el plan completo antes de que se ejecute.

Cuándo conviene limpiar y cuánto —si el equipo exige historial lineal, si se aplasta cada rama en un commit al integrarla, si se prohíbe reescribir— no es una decisión técnica sino de política de equipo, y se trata en la lección 08-02: Manteniendo un Historial Limpio.

Ejercicios

Ejercicio 1: la limpieza completa

Crea un repositorio con una rama main (un commit) y una rama funcionalidad/informe con estos cinco commits, en este orden:

  1. Empezar el informe (crea informe.js con una función)
  2. Anadir estilos del informe (crea informe.css)
  3. mas cosas (amplía informe.js)
  4. wip (toca informe.js)
  5. arreglo (toca informe.js)

Con un solo git rebase -i, deja la rama con dos commits: uno con todo lo de informe.js y un mensaje decente, y otro con los estilos. Demuestra con git rev-parse HEAD^{tree} que el contenido final no ha cambiado.

Ejercicio 2: dividir un commit

Partiendo del resultado del ejercicio 1, divide el commit de informe.js en dos: uno con la generación de datos y otro con el pintado. Usa edit, git reset HEAD^ y git add -p.

Ejercicio 3: --fixup y --exec

En una rama nueva con tres commits:

  1. Introduce a propósito un error de sintaxis en el segundo commit.
  2. Sigue trabajando y haz un tercer commit normal.
  3. Corrige el error con git commit --fixup <sha del segundo>.
  4. Aplícalo con git rebase -i --autosquash.
  5. Vuelve a lanzar un rebase con --exec "node --check fichero.js" y comprueba que ahora los tres commits pasan la validación.

Soluciones

Solución 1:

mkdir /tmp/practica-rebase-i && cd /tmp/practica-rebase-i
git init -b main
echo "# Proyecto" > README.md && git add . && git commit -m "Base"

git switch -c funcionalidad/informe
echo "function generarInforme() {}" > informe.js && git add . && git commit -m "Empezar el informe"
echo ".informe { margin: 1rem; }" > informe.css && git add . && git commit -m "Anadir estilos del informe"
echo "function datosInforme() {}" >> informe.js && git commit -am "mas cosas"
echo "// pendiente" >> informe.js && git commit -am "wip"
echo "function pintarInforme() {}" >> informe.js && git commit -am "arreglo"

git rev-parse HEAD^{tree}
3e8b1f7c4a9d25b6e8f1c3a7d942b6e5f8c1a3d7    (anótalo)
git rebase -i main

Lista editada (los commits de informe.js juntos arriba, los estilos al final):

pick 4a1c8e3 Empezar el informe
fixup 9d2f7b5 mas cosas
fixup 1e6a4c8 wip
fixup 7b3d9f2 arreglo
pick 5c8e1a4 Anadir estilos del informe

Después, un segundo pase para el mensaje (o marca reword en la primera línea desde el principio):

git rebase -i main
reword 8f2c5d1 Empezar el informe
pick 3a9e7b4 Anadir estilos del informe

Mensaje nuevo: Añadir la generación y el pintado del informe.

git log --oneline main..HEAD
git rev-parse HEAD^{tree}
c14b8e7 Anadir estilos del informe
6d3f2a9 Añadir la generación y el pintado del informe

El árbol coincide con el anotado: solo ha cambiado la historia.

Solución 2:

git rebase -i main
edit 6d3f2a9 Añadir la generación y el pintado del informe
pick c14b8e7 Anadir estilos del informe
git reset HEAD^
git status --short
?? informe.js

(El fichero era nuevo en ese commit, así que aparece sin seguimiento. Si ya existiera, saldría como M.)

# Preparar solo la parte de generación de datos
git add -p informe.js     # aceptar los trozos de generarInforme/datosInforme
git commit -m "Añadir la generación de los datos del informe"

git add informe.js
git commit -m "Añadir el pintado del informe"

git rebase --continue
git log --oneline main..HEAD
9e7c3b1 Anadir estilos del informe
2f8a5d6 Añadir el pintado del informe
b41e9c7 Añadir la generación de los datos del informe

Si git add -p no ofrece trozos separables porque el fichero es nuevo, usa git add -N informe.js primero: registra el fichero en el índice como vacío y permite trocear su contenido.

Solución 3:

mkdir /tmp/practica-autosquash && cd /tmp/practica-autosquash
git init -b main
echo "const a = 1;" > app.js && git add . && git commit -m "Base"
git switch -c pruebas

echo "const b = 2;" >> app.js && git commit -am "Añadir b"
echo "const c = ;" >> app.js && git commit -am "Añadir c"     # error de sintaxis
echo "const d = 4;" >> app.js && git commit -am "Añadir d"

git log --oneline main..HEAD
7c1a4e8 Añadir d
3f9b2d6 Añadir c
d82e5a1 Añadir b
# 3. Corregir el error apuntando al commit culpable
sed -i 's/const c = ;/const c = 3;/' app.js
git add app.js
git commit --fixup 3f9b2d6
[pruebas 4e8c1b9] fixup! Añadir c
# 4. Aplicarlo
git rebase -i --autosquash main

La lista viene ya preparada; basta con guardar:

pick d82e5a1 Añadir b
pick 3f9b2d6 Añadir c
fixup 4e8c1b9 fixup! Añadir c
pick 7c1a4e8 Añadir d
git log --oneline main..HEAD
a92f6c3 Añadir d
5d1e8b7 Añadir c
d82e5a1 Añadir b
# 5. Validar todos los commits
git rebase --exec "node --check app.js" main
Executing: node --check app.js
Executing: node --check app.js
Executing: node --check app.js
Successfully rebased and updated refs/heads/pruebas.

Si lo hubieras ejecutado antes del --autosquash, el segundo exec habría fallado y el rebase se habría detenido en el commit roto.

Conclusión

El rebase interactivo es el taller de reparación del historial. Lo aprendido:

  • git rebase -i <base> abre una lista de tareas con los commits que se van a reaplicar, del más antiguo al más reciente, y ejecuta lo que escribas en ella de arriba abajo.
  • Las cuatro órdenes que usarás siempre son pick (dejar), reword (cambiar el mensaje), edit (parar para modificar o dividir) y squash/fixup (unir con el anterior, conservando o descartando el mensaje). drop elimina y exec valida.
  • Reordenar líneas reordena commits y permite agrupar por temas lo que se hizo desordenado; borrar una línea elimina el commit.
  • Dividir un commit es edit + git reset HEAD^ + varios git add -p y git commit + git rebase --continue. Es la operación más potente del conjunto.
  • git commit --fixup <sha> con rebase -i --autosquash convierte la limpieza en un trámite: marcas las correcciones sobre la marcha y Git las coloca solo. Actívalo con rebase.autoSquash true.
  • --exec ejecuta una comprobación tras cada commit y detiene el rebase si falla: la única forma barata de garantizar que ningún commit intermedio queda roto.
  • Si te pierdes: git status dice dónde estás, git rebase --edit-todo replanifica lo que queda, --show-current-patch enseña qué falla y --abort lo deshace todo sin rastro.
  • git rev-parse HEAD^{tree} antes y después es la mejor verificación de que has reorganizado la historia sin alterar el contenido.
  • Y sobre todo: la regla de oro. Esto se hace antes de publicar.

Lo que viene

Rebase y rebase interactivo trabajan siempre con el conjunto de commits de una rama. Pero a veces lo que necesitas es mucho más quirúrgico: llevarte un commit concreto de un sitio a otro, dejando el resto donde está.

Ana lo va a necesitar mañana. Ha corregido en main un fallo que hace que el foco se pierda al borrar una tarea, y resulta que la versión 1.x que sigue desplegada en el cliente tiene el mismo fallo y vive en una rama de mantenimiento aparte. No quiere fusionar main entera ahí: quiere ese commit y nada más.

Para eso está git cherry-pick, y lo vemos en la lección 05-03: Cherry-Picking de Confirmaciones.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados