Git avanzado I: reescribir la historia
Primera entrega de la serie Git avanzado. Si vienes saltado desde el nivel intermedio o básico, aquí entramos en el terreno donde git deja de ser sistema de control de versiones y empieza a ser editor de historia. Tiempo de lectura estimado: 5 minutos.
Los tres comandos de hoy permiten modificar commits que ya existen: combinarlos, reordenarlos, cambiarles el mensaje, sacarlos de una rama y meterlos en otra. Son potentes y razonablemente peligrosos: si la historia que estás reescribiendo ya la han visto otros, les vas a desordenar el mundo. La regla general, antes de empezar: reescribe historia local todo lo que quieras, reescribe historia publicada solo con acuerdo explícito del equipo.
git commit --amend: editar el último commit
--amend no crea un commit nuevo: modifica el anterior. Se usa principalmente para dos cosas.
Cambiar el mensaje del último commit:
git commit --amend -m "Mensaje corregido"
O sin -m, que te abre el editor con el mensaje actual para que lo edites.
Añadir algo que se te olvidó:
git add fichero-que-olvide.go
git commit --amend --no-edit
--no-edit conserva el mensaje existente. El fichero se añade al commit anterior como si siempre hubiera estado ahí. Muy cómodo cuando haces commit y te das cuenta de que se te olvidó algo trivial (un archivo generado, una línea de docs, un test).
Cuidado: --amend crea un commit nuevo por debajo (con nuevo hash), aunque conserva el mismo mensaje y la misma fecha del autor. Si el commit anterior ya estaba pusheado, tras el amend tu rama y la remota divergen. Tendrás que git push --force-with-lease. En una rama solo tuya (feature branch abierta solo por ti), no pasa nada. En main o en una rama compartida, problema.
git rebase: mover commits a otra base
git rebase toma un conjunto de commits y los reaplica sobre otro punto. Dicho de otro modo: cambia la "base" desde la que parte una rama.
El caso más típico: estás en una rama de feature, main ha avanzado mientras trabajabas, y quieres traer tu rama al día sin un merge commit.
git switch feature/login
git fetch origin
git rebase origin/main
Resultado: tus commits se despegan, git aplica los commits nuevos de origin/main y luego vuelve a aplicar los tuyos encima. La historia queda lineal, como si hubieras partido de origin/main actual desde el principio.
Si hay conflictos durante el rebase, git se para en cada commit que conflicte, te deja marcar los conflictos, y esperas: tras resolver, git add y git rebase --continue. Si te arrepientes: git rebase --abort y vuelves al estado anterior.
La otra modalidad es el rebase interactivo, que es donde rebase se convierte en editor de historia:
git rebase -i HEAD~5
Te abre un editor con los últimos 5 commits y un verbo al lado de cada uno:
pick abc1234 Añadir formulario de login
pick def5678 Arreglar typo
pick 9012ghi Validación de email
pick 345jklm WIP trabajando
pick nop6789 Test del formulario
Cambias los verbos:
pick: mantener tal cualreword(r): mantener pero editar el mensajesquash(s): fusionar con el commit anterior, combinando mensajesfixup(f): fusionar con el anterior, descartando el mensajedrop(d): borrar el commitedit(e): parar en ese commit para modificarlo
Y puedes reordenar las líneas para reordenar los commits. Al guardar y cerrar el editor, git aplica los cambios.
El resultado típico tras un rebase interactivo es una historia limpia: en vez de cinco commits con "WIP", "otra cosa", "ahora sí", tienes dos commits bien titulados que cuentan lo que pasó. Esto es oro cuando alguien hace code review: lee los commits uno a uno y entiende el razonamiento.
Rebase en ramas compartidas: solo si has acordado con el equipo que esa rama se reescribe (es común en branches de PR con historial limpio requerido). Para mergear tras rebase propio: git push --force-with-lease, nunca --force a secas.
git cherry-pick: traer un commit de otra rama
cherry-pick coge un commit concreto y lo aplica sobre la rama actual, creando un commit nuevo (mismo contenido, hash distinto).
git cherry-pick abc123 # un commit concreto
git cherry-pick abc123 def456 # varios
git cherry-pick abc123..def456 # un rango (exclusivo el primero)
git cherry-pick abc123^..def456 # un rango (inclusivo)
Cuándo se usa:
Un fix que hiciste en develop y te hace falta en main ya. En vez de esperar al próximo merge completo: git switch main && git cherry-pick abc123.
Backport de un fix a una rama de release antigua. Si mantienes release/v1 y arreglaste un bug en main, cherry-pick ese commit a la rama de release.
Rescatar commits de una rama que vas a tirar. Si una rama de experimento tiene dos commits que merecen la pena pero el resto es basura, cherry-pick los dos buenos a otra rama y tira la experimental.
Si hay conflictos, igual que con rebase: resolver, git add, git cherry-pick --continue. O --abort para cancelar.
Flags útiles:
git cherry-pick -n abc123 # aplica los cambios pero no hace el commit
git cherry-pick -x abc123 # añade al mensaje "(cherry picked from abc123)"
El -x es útil cuando mantienes varias ramas de release: en el mensaje queda trazabilidad explícita de dónde salió el fix originalmente.
Peligros de cherry-pick:
- Si el commit original modifica luego (rebase, amend), tendrás dos versiones ligeramente distintas del mismo cambio. Trazar eso a mano es un dolor.
- Cherry-pick en serie (traer cincuenta commits uno a uno) es mejor hacerlo con merge o rebase. Cherry-pick es para puntuales.
Un ejemplo real del día a día
Tienes una rama feature/carrito. Llevas seis commits:
pick a1 Añadir modelo de Carrito
pick a2 WIP
pick a3 arreglar bug lint
pick a4 Añadir servicio de cálculo
pick a5 Tests servicio cálculo
pick a6 typo
Antes del PR, haces rebase interactivo (git rebase -i main):
pick a1 Añadir modelo de Carrito
fixup a2
fixup a3
pick a4 Añadir servicio de cálculo
fixup a6
pick a5 Tests servicio cálculo
Guardas, cierras. Resultado: tres commits limpios:
Añadir modelo de Carrito
Añadir servicio de cálculo
Tests servicio cálculo
Cada commit es una unidad autocontenida que hace una cosa, testeada, revisable. El reviewer te lo agradece; tu yo de dentro de tres meses buscando en git log, también.
Errores típicos reescribiendo historia
Hacer force push en main. Borras trabajo de todos. --force-with-lease al menos detecta que había algo nuevo, pero aun así, reescribir main es casi nunca una buena idea.
Rebase interactivo sin antes hacer backup de la rama. Si el rebase se te va de las manos, git reflog te salva, pero es más tranquilizador hacer antes git branch backup-feature-carrito y, si todo explota, git reset --hard backup-feature-carrito.
Cherry-pick sin marcar el origen. En tres semanas nadie se acordará de por qué existe ese commit. Si vas a cherry-pick entre ramas de release, usa -x.
Amend + force push sin avisar. Si alguien más tiene esa rama pulleada, al hacer force push les rompes su copia local. Coordinación mínima.
Lo que viene
Con amend, rebase y cherry-pick puedes dejar la historia de un repo como quieras. Pero tener herramientas para cambiar la historia no sirve de mucho si luego no sabes leerla cuando algo falla. En la siguiente entrega, los comandos de investigación: git bisect, git blame y git reflog. Las tres armas para responder a "¿qué demonios pasó aquí?".