Eliminando archivos grandes del historial de Git

Eliminando archivos grandes del historial de Git

Como desarrollador, he aprendido que los errores son inevitables, aun teniendo experiencia. A veces, esos errores pueden llevar a situaciones que parecen complicadas, pero que, con el enfoque adecuado, pueden solucionarse con éxito. Es exactamente lo que quiero contarles hoy.

El problema: archivos grandes subidos por error

Todo comenzó de manera bastante mundana. Al trabajar en un proyecto, olvidé añadir una entrada crucial en mi archivo .gitignore. Como resultado, subí accidentalmente una carpeta con archivos grandes que no deberían haber estado en el repositorio. No me di cuenta del problema hasta que intenté hacer un push a mi repositorio en GitHub, y entonces el caos se desató con un mensaje de error bastante claro:

remote: error: File assets/stores/App.fig is 66.23 MB; this is larger than GitHub's recommended maximum file size of 50.00 MB
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.

Me encontré en una situación bastante incómoda: esos archivos grandes seguían presentes en el historial del repositorio, incluso después de haber eliminado la carpeta que los contenía. Ingenuamente, creí que al eliminar la carpeta, los problemas se resolverían, pero no fue así. Aunque la carpeta se había eliminado del directorio de trabajo, los archivos ya estaban registrados en el historial de Git, lo que significaba que aún formaban parte del repositorio y estaban afectando los push a GitHub.

Aunque la solución fue relativamente sencilla para mi caso específico, ya que mi repositorio es utilizado únicamente por mí, hubiera sido una historia diferente si hubiera trabajado en equipo.

La solución: reescribir la historia

Aquí es donde entra el trabajo de limpiar el historial de Git. Para abordar el problema, opté por reescribir la historia del repositorio. Aunque suena aterrador, se puede manejar de manera segura si sigues los pasos adecuados. Aquí les cuento cómo lo hice.

Paso 1: Usar git filter-branch

El primer comando que utilicé fue (creando un script):

#!/bin/bash

git filter-branch --force --index-filter \
'git rm -rf --cached --ignore-unmatch assets/stores/App.fig' \
--prune-empty --tag-name-filter cat -- --all

Intentaré explicar lo que significa todo esto:

  • git filter-branch: Permite reescribir el historial de commits del repositorio. Es una herramienta poderosa para realizar cambios en el historial de Git, como eliminar archivos grandes de todos los commits.

  • --force: Permite sobrescribir cualquier respaldo de ramas existentes que git filter-branch crea durante el proceso. Esto asegura que el comando pueda proceder incluso si hay respaldos previos.

  • --index-filter: Modifica el índice (staging area) de cada commit. Es una opción eficiente para realizar cambios en los archivos incluidos en los commits sin afectar el contenido del árbol de trabajo.

  • 'git rm -rf --cached --ignore-unmatch ...': Este comando se ejecuta en cada commit para eliminar los archivos especificados del índice (sin eliminar los archivos de tu sistema de archivos local) usando --cached. --ignore-unmatch evita errores si el archivo no está presente en algunos commits.

  • --prune-empty: Elimina cualquier commit que quede vacío después de la eliminación de archivos. Esto ayuda a limpiar el historial eliminando commits que no contienen cambios útiles después de aplicar el filtro.

  • --tag-name-filter cat: Actualiza los nombres de las etiquetas para que coincidan con el nuevo historial reescrito. cat significa que las etiquetas se mantienen igual pero se ajustan para que apunten a los nuevos commits.

  • -- --all: Aplica los cambios a todas las ramas y etiquetas del repositorio. Esto asegura que el filtro se aplique de manera global, no solo a la rama activa.

Este comando recorre todo el historial y elimina los archivos grandes de cada commit en el que aparecieron. Aunque fue un proceso intenso, lo manejé sin problemas debido a que trabajaba solo en el repositorio.

Paso 2: Limpieza post-operación

Después de reescribir el historial del repositorio con git filter-branch, es crucial realizar una serie de tareas de limpieza para asegurarte de que el repositorio esté en un estado limpio y optimizado.

1. Eliminar referencias antiguas

git for-each-ref --format="delete %(refname)" refs/original | git update-ref --stdin

¿Por qué es necesario?

Cuando se utiliza git filter-branch, Git crea referencias de respaldo para cada una de las ramas que se modifican. Estas referencias se almacenan en refs/original y permiten que puedas recuperar la historia original si es necesario. Sin embargo, una vez que estás seguro de que el proceso de reescritura de la historia ha terminado y que no necesitas recuperar las versiones anteriores, es importante limpiar estas referencias para evitar confusiones y mantener el repositorio ordenado.

Función del comando:

Este comando elimina todas las referencias en refs/original. Utiliza git for-each-ref para listar las referencias que necesitan ser eliminadas y git update-ref para realizar la eliminación. Esto asegura que las referencias antiguas no permanezcan en el repositorio, evitando posibles confusiones en el futuro.

2. Eliminar entradas antiguas del reflog

git reflog expire --expire=now --all

¿Por qué es necesario?

El reflog (registro de referencias) en Git almacena un historial de las actualizaciones de referencias (como HEAD) durante un período de tiempo. Después de reescribir el historial con git filter-branch, el reflog puede contener entradas que apuntan a commits que ya no existen en el historial reescrito. Estas entradas obsoletas pueden ocupar espacio y hacer que el repositorio sea más difícil de manejar.

Función del comando:

git reflog expire --expire=now --all elimina las entradas antiguas del reflog, forzando a Git a expirar todas las entradas de referencia que ya no son necesarias. Esto ayuda a limpiar el historial de referencias obsoletas y a reducir el tamaño del repositorio.

3. Recolectar basura

git gc --prune=now

¿Por qué es necesario?

Después de reescribir el historial y eliminar referencias y entradas antiguas, es importante limpiar y optimizar el repositorio para liberar espacio y mantener su rendimiento. El recolector de basura de Git (git gc) realiza esta tarea al eliminar objetos que ya no son necesarios y optimizar el almacenamiento.

Función del comando:

git gc --prune=now ejecuta el recolector de basura de Git, que realiza varias tareas, como:

  • Eliminar objetos huérfanos: Elimina objetos (commits, árboles, blobs) que ya no están referenciados por ninguna rama o etiqueta.
  • Optimizar el repositorio: Reorganiza los objetos y compacta los archivos para mejorar el rendimiento del repositorio.

El parámetro --prune=now indica que Git debe eliminar todos los objetos que no estén referenciados en el repositorio inmediatamente, en lugar de esperar el período de retención predeterminado.

Paso 3: Push forzado

Finalmente, hice un push forzado para actualizar el repositorio remoto con el historial limpio:

git push origin --force --all

Este comando sobrescribe el historial en el repositorio remoto con la nueva versión sin los archivos grandes.

¿Qué hubiera hecho en un entorno colaborativo?

Aunque el proceso fue relativamente sencillo en mi caso, el escenario cambia si trabajas en un equipo. Reescribir el historial puede causar conflictos y problemas para otros colaboradores. En un entorno de equipo, mi estrategia hubiera sido diferente:

  1. Comunicación: Informar a todos los miembros del equipo sobre el problema y coordinar la solución.
  2. Clonación y eliminación: Una estrategia más drástica pero efectiva podría ser clonar el repositorio, eliminar los archivos grandes en el repositorio clonado, y luego subir el nuevo repositorio limpio. Esto, aunque es una medida drástica, garantiza que todos los cambios se hagan de manera coherente.
  3. Uso de Git LFS: Para evitar problemas similares en el futuro, considerar el uso de Git Large File Storage (LFS) para manejar archivos grandes sin que impacten el historial del repositorio principal.

Conclusión

Reescribir la historia de Git puede parecer intimidante, pero con el enfoque adecuado, se puede limpiar el repositorio y resolver problemas como archivos grandes que se quedan atascados en el historial.

Esta experiencia me dejó algunas lecciones valiosas que quiero compartir:

  1. Siempre revisa tu .gitignore: Asegúrate de incluir todas las carpetas y archivos que no deben estar en el repositorio desde el principio.
  2. Actúa con rapidez: Cuanto antes detectes y corrijas un error en el historial, más fácil será solucionarlo.
  3. Considera las implicaciones en equipo: En un entorno colaborativo, planifica cuidadosamente cómo abordar los cambios en el historial para evitar conflictos.
  4. Mantén tu repositorio limpio: Regularmente revisa el repositorio para evitar que se acumulen archivos grandes o innecesarios.

Happy coding! :D


Photo by bruce mars on Unsplash

Written with StackEdit.

Jack Fiallos

Jack Fiallos

Te gustó este artículo?