Reescribir el historial de forma segura
Cada reescritura —commit --amend, reset, rebase, filter-repo— produce confirmaciones nuevas y mueve el puntero de la rama fuera de las antiguas. Esto es inofensivo mientras nadie más tenga las confirmaciones antiguas, y destructivo en cuanto alguien las tenga: su clon aún conserva el historial antiguo, y la próxima vez que empujen (push) o traigan (pull), reaparece como duplicados o conflictos. Por lo tanto, la primera pregunta nunca es "cómo", es "¿quién más tiene estas confirmaciones?"
1. Decidir: reescribir o revertir
git fetch --prune
git status --short --branch # adelantado/retrasado, y estado sucio
git branch -r --contains <commit> # qué ramas remotas ya lo contienen
git log --oneline @{u}..HEAD # commits que existen solo localmente
| Dónde vive el commit | Haz esto |
|---|---|
| Solo local, nunca empujado | Reescribe libremente. |
| Empujado a una rama en la que solo trabaja este usuario (su rama de PR) | Reescribe, luego empuja con 'lease' (paso 5). |
En una rama compartida o protegida: main, master, develop, release/*, o cualquier otra con commits de otras personas encima | No reescribas. Usa git revert. |
| Ya liberado o desplegado | git revert. Una reescritura no puede desplegarlo. |
Cuando el usuario dice "elimina ese commit de main", normalmente quiere que su efecto desaparezca, no su SHA. git revert <sha> hace eso, mantiene el historial honesto y solo necesita un push normal. Para un commit de fusión (merge), usa git revert -m 1 <merge-sha>. Explica por qué elegiste revertir, y si el usuario insiste en borrarlo de una rama compartida, nombra a quién afectará y obtén un sí explícito primero.
Una reversión (revert) genera conflictos cuando commits posteriores editaron las mismas líneas, que es exactamente el caso donde los commits se construyeron sobre el malo. Resuélvelo como cualquier conflicto (ver git-conflict-resolution): elimina el cambio malo, conserva el trabajo posterior, luego git revert --continue.
Una trampa de la reversión: después de revertir una fusión, fusionar la misma rama de nuevo no trae nada de vuelta, porque git piensa que esos commits ya llegaron. Para volver a traer el trabajo, revierte primero la reversión.
2. Tomar un punto de recuperación antes de tocar nada
git rev-parse HEAD # anótalo
git branch backup/<rama>-pre-rewrite # una referencia real, no solo el reflog
El reflog también registra dónde estaba la rama, pero es local a este clon, expira (90 días para entradas alcanzables, 30 para las no alcanzables por defecto), y git gc después de reflog expire lo termina. Una rama de respaldo con nombre no cuesta nada. Déjala en su lugar hasta que el usuario haya confirmado el resultado.
3. Elegir el comando menos destructivo
| Objetivo | Comando | Notas |
|---|---|---|
| Corregir el mensaje del último commit o añadir un archivo olvidado | git commit --amend | -m "..." o --no-edit evita abrir un editor. |
| Deshacer el commit pero mantener los cambios en el área de preparación | git reset --soft HEAD~1 | |
| Deshacer el commit y sacar del área de preparación, mantener los archivos | git reset --mixed HEAD~1 (el predeterminado) | |
| Mover una rama hacia atrás, negándose a perder cambios locales | git reset --keep <commit> | Se aborta si sobrescribiría cambios sin confirmar. Prefiere esto a --hard. |
| Descartar también el trabajo sin confirmar | git reset --hard | El trabajo sin confirmar no está en el reflog y no se puede recuperar. Solo después de confirmar que no hay nada deseado; git stash -u primero si no estás seguro. |
| Descartar cambios en un archivo | git restore <archivo> | |
| Fusionar ('squash') o plegar arreglos (fix-ups) en commits anteriores | git commit --fixup=<sha> luego autosquash | Ver abajo. |
| Eliminar o reordenar commits | rebase -i con un 'todo' proporcionado | Ver abajo. |
Un 'pre-commit hook' fallido significa que no se hizo ninguna confirmación. Ejecutar git commit --amend para "reintentar" modifica entonces el commit anterior, mezclando los nuevos cambios en el equivocado. Después de un fallo del hook, corrige el problema, vuelve a preparar los archivos (stage) y haz un nuevo commit normal.
Rebase interactivo sin editor interactivo
git rebase -i abre un editor, que se bloquea en una terminal no interactiva. No confíes en él. Dos formas que funcionan:
# Plegar commits de arreglo (fix-up) en sus objetivos - acepta el 'todo' que git genera:
git commit --fixup=<sha-objetivo>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
# Eliminar o reordenar: escribe el 'todo' tú mismo.
GIT_SEQUENCE_EDITOR="sed -i '/^pick <sha-corto> /d'" git rebase -i <base>
<base> es el commit anterior al primero que quieres cambiar. Si un rebase se detiene en un conflicto, resuélvelo, haz git add, luego GIT_EDITOR=true git rebase --continue. Si va mal, git rebase --abort regresa a donde empezó.
4. Verificar que la reescritura cambió lo que pretendías
Una edición de mensaje, reordenación o fusión (squash) debe dejar el árbol final idéntico:
git diff backup/<rama>-pre-rewrite HEAD --stat # vacío = mismo contenido
git range-diff backup/<rama>-pre-rewrite...HEAD # comparación commit por commit
Si pretendías eliminar contenido, la diferencia debe mostrar exactamente ese contenido y nada más. Cualquier cosa inesperada significa restaurar desde la copia de seguridad y rehacerlo.
5. Publicar - solo con la aprobación del usuario
Empujar una reescritura es el paso visible hacia el exterior y difícil de revertir. Confirma primero a menos que el usuario ya haya pedido el empuje.
git push --force-with-lease=<rama>:<sha-remota-esperada> --force-if-includes origin <rama>
- Nunca
--forcedesnudo. Sobrescribe lo que sea que esté en el remoto, incluido el empuje de un compañero que no hayas visto. - Nombra la SHA esperada. Un
--force-with-leasesimple compara contra tu referencia de seguimiento remoto, y ungit fetchde fondo del IDE actualiza silenciosamente esa referencia, por lo que el lease entonces no protege nada. Usa la SHA que viste en el paso 1 (git rev-parse origin/<rama>). --force-if-includes(git 2.30+) además rechaza si el remoto se movió a algo que tu rama local nunca integró.- Nunca fuerces el empuje en
main/master/develop/release/*. Si una regla del alojamiento lo bloquea, esa regla es correcta; no la evites.
Después de empujar, informa al usuario que cualquier persona que tenga la rama antigua en su checkout debe restablecerse a la nueva en lugar de hacer pull, y que los comentarios de revisión de un PR abierto pueden perder sus anclas.
Barreras de seguridad
- Las confirmaciones firmadas pierden su firma al ser reescritas; vuelve a firmar con
-So informa al usuario. - No reescribas confirmaciones de otro autor sin preguntar, incluso en una rama de características.
- Las ramas apiladas por encima de la reescrita ahora están obsoletas. Usa
git-stacked-pr-rebasepara reordenarlas en lugar de hacer rebase de cada una manualmente. - Un secreto confirmado no se soluciona con una reescritura. Rota la credencial primero; asume que está comprometida.
- Informa claramente: cuál era el antiguo tope, cuál es el nuevo, dónde está la referencia de respaldo, y si hay algo sin empujar. Si la reversión fue la decisión correcta, di que eso es lo que hiciste.