Los capítulos anteriores mostraron cómo modificar el commit
más reciente (commit --amend,
see Deshacer cambios). Este capítulo cubre cómo modificar
commits que están más atrás en el historial, no solo el último.
git rebase -i HEAD~5
Abre un editor con una lista de los últimos cinco commits, uno por línea, en orden cronológico (el más antiguo primero), cada uno precedido por una palabra clave que indica qué hacer con él:
pick a1b2c3d Agrega validación de formulario pick e4f5g6h Corrige typo pick i7j8k9l Refactoriza el módulo de pagos pick m1n2o3p Agrega pruebas pick q4r5s6t Corrige el cálculo de totales
Cambiar la palabra clave de un commit determina qué le ocurre al reaplicarlo:
pickAplica el commit sin cambios.
rewordAplica el commit, pero se detiene para permitir editar su mensaje.
editAplica el commit y detiene el proceso justo después, dejando ese
commit como HEAD, para poder modificar su contenido
libremente antes de continuar.
squashCombina el commit con el anterior en la lista, fusionando ambos mensajes para editarlos juntos.
fixupIgual que squash, pero descarta el mensaje de este commit,
conservando solo el del anterior.
dropElimina el commit por completo del historial resultante.
También se puede reordenar el historial simplemente cambiando el orden de las líneas antes de guardar: los commits se reaplican en el orden en que aparecen en el archivo.
Con edit, el proceso se detiene justo después de aplicar ese
commit:
pick a1b2c3d Agrega validación de formulario edit e4f5g6h Corrige typo pick i7j8k9l Refactoriza el módulo de pagos
En ese punto, HEAD está en el commit e4f5g6h y se
puede modificar el contenido libremente: editar archivos, hacer
git add, y luego:
git commit --amend git rebase --continue
Esto reemplaza ese commit específico por una versión corregida, y
Git continúa reaplicando automáticamente todos los commits
posteriores (i7j8k9l y los que sigan) sobre la nueva versión.
Como cada commit incluye el hash de su padre
(see El modelo de objetos), modificar un commit intermedio cambia
inevitablemente el hash de todos los commits que vienen después,
aunque su contenido no se haya tocado: son objetos nuevos, aunque
representen los mismos cambios.
Al igual que con commit --amend, reescribir commits que ya
fueron compartidos (subidos con push a un repositorio que
otras personas usan) invalida las copias que esas personas tengan
descargadas: sus ramas locales quedarán basadas en commits que ya no
existen en el remoto, generando conflictos confusos la próxima vez
que intenten sincronizar. La convención general
(see Rebase frente a merge) es reescribir libremente el
historial de ramas propias no compartidas, y evitarlo por completo
en ramas ya compartidas —o coordinarlo explícitamente con el equipo
si es estrictamente necesario, usando push --force-with-lease
(see Repositorios remotos).
git filter-repo ¶El rebase interactivo es adecuado para reescribir un puñado de
commits recientes. Para operaciones masivas sobre todo el
historial —eliminar un archivo grande que se subió por error hace
años, purgar una credencial filtrada de todos los commits, o
extraer una carpeta a su propio repositorio— la herramienta
recomendada es git filter-repo, un programa externo (no
incluido en Git, se instala aparte) que reemplazó al antiguo y más
lento git filter-branch.
# Eliminar un archivo de todo el historial git filter-repo --path secretos.env --invert-paths # Conservar solo una carpeta, convirtiéndola en la raíz del repositorio git filter-repo --path src/modulo --path-rename src/modulo/:
filter-repo reescribe todos los commits afectados
por el cambio, desde el primero hasta el último: es la forma más
drástica de reescritura de historia, y produce un repositorio con
hashes de commit completamente distintos a partir del primer commit
afectado. Después de una operación así, cualquier copia clonada
anteriormente queda irremediablemente desincronizada y debe
reclonarse desde cero; no hay forma de "sincronizar" con
pull después de una reescritura de esta magnitud.
Importante: eliminar un archivo del historial con
filter-repo no revoca una credencial que ya fue expuesta
públicamente (por ejemplo, si el repositorio ya fue clonado por
terceros, o si el archivo llegó a un remoto público). Reescribir el
historial es una limpieza necesaria, pero no sustituye a rotar
cualquier secreto que se haya filtrado.