Git nos ofrece una gran potencia y flexibilidad pero, como pasa en Spiderman un gran poder conlleva una gran responsabilidad. En este tutorial veremos cómo borrar ficheros del histórico de Git, y en general cómo manipular todo el histórico.
Índice de contenidos
1. Introducción
Hay dos situaciones que son bastante típicas cuando trabajamos con repositorios de código:
-
Una es subir ficheros que no corresponden al repositorio. Bien porque los subimos por equivocación o porque los subimos con información sensible, como por ejemplo claves de usuario.
-
La otra es querer dividir un repositorio en dos porque ha crecido demasiado. Llega un momento donde aparecen proyectos con suficiente entidad como para tener su repositorio propio.
En estas situaciones si simplemente hacemos un nuevo commit
quitando del repositorio los ficheros, el problema que se nos presenta es que estos siguen estando en el histórico, por lo que, si es información sensible siempre se podrá seguir recuperando, y si son ficheros que no deben estar en este repositorio (porque los pusimos por error o porque hemos dividido el repositorio en dos) seguirán ocupando espacio.
Para este tipo de situaciones Git es ideal ya que nos permite reescribir por completo todo el histórico, de forma que no dejemos rastro alguno de los ficheros en cuestión.
Si bien esto parece un poco de magia negra, en este tutorial vamos a ver cómo hacerlo.
¡¡¡Ojo porque estamos manipulando el histórico así que estas operaciones NO SON REVERSIBLES!!! Es más que recomendable que antes de hacer todo esto os hagáis una copia completa del todo el repositorio.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
-
Hardware: Portátil MacBook Pro 15» (2.5 GHz Intel i7, 16GB 1600 Mhz DDR3, 500GB Flash Storage).
-
AMD Radeon R9 M370X
-
Sistema Operativo: Mac OS X El Capitan 10.11.6
-
Git 2.9.2
-
Java v1.8.0_112
3. La herramienta: BFG Repo-Cleaner
BFG Repo-Cleaner es una herramienta de terceros (no pertenece a Git), escrita en Scala y Open-Source, que se plantea como la mejora alternativa para hacer este tipo de trabajamos, no porque no lo podamos hacer con Git directamente, sino porque es infinitamente más rápida y más fácil de usar, ya que es una herramienta pensada específicamente para hacer este trabajo.
Si también queréis ver una alternativa de cómo hacer esto mismo directamente con Git git-filter-branch
, aquí tenéis una buena referencia: GitHub Help – Remove sensitive data
Para ejecutar BFG simplemente tenemos que descargarlo de su página web. Es un fichero .jar
por lo que para poder ejecutarlo tenemos que tener instalado Java.
Para que resulte más sencillo ejecutarlo nos podemos hacer un alias:
$ alias bfg='java -jar <ruta donde esté instalado>/bfg.jar'
4. Preparando el repositorio
En primer lugar nos vamos a asegurar de que en el HEAD
de nuestro repositorio no están los ficheros que queremos borrar. Esto es porque BFG considera este commit como protegido y no nos va a dejar cambiarlo (esto se puede forzar, pero mejor hagamos las cosas por las buenas). Así que usaremos git rm
o cualquier otra técnica que usemos de forma habitual para conseguir un HEAD limpio, y nos aseguramos de haber hecho el commit
y el push
para garantizar que nuestro repositorio remoto está correctamente actualizado.
También tenemos que estar coordinados con el resto de nuestros compañeros y que no tengan trabajo pendiente ya que al cambiar el histórico no van a poder mergear nada. Mi recomendación sería que todo el mundo llegue a un punto estable, hacer la modificación del histórico y luego que todo el mundo se vuelva a clonar el repositorio. Así seguro que no tenemos problemas.
Ahora que ya tenemos nuestro repositorio remoto limpio y a nuestros compañeros coordinados, necesitamos clonarnos el repositorio en local ya que BFG trabajar en local, pero no nos vale un clone
normal, tenemos que hacer un nuevo clone con la opción --mirror
. Con esta opción conseguimos lo que se denomina bare repository, que es una copia del repositorio con todos los ficheros de administración y control, y que no podemos usar para hacer checkout local. Por ejemplo:
$ git clone --mirror git://example.com/some-big-repo.git
5. Borrando los ficheros que sobran
BFG nos permite varias opciones de borrado del histórico, por ejemplo por tamaño, o incluso convertir ficheros a Git LFS (Git Large File Storage), o simplemente cambiar texto dentro de un fichero. También podemos borrar ficheros o directorios completos (todo su contenido de ficheros y subdirectorios). Cuando hacemos borrado ficheros o directorios tenemos que tener en cuenta que lo hace por nombre no por ruta completa, así que cuidado con nombres demasiado comunes, no sea que borremos más de la cuenta!!!
Por ejemplo, para borrar directorios:
$ bfg --delete-folders "submodule-*" some-big-repo.git
En el ejemplo se ve como podemos usar wildcards en el nombre de los directorios o ficheros.
Para borrar ficheros:
$ bfg --delete-files "really-big-file.zip" some-big-repo.git
Estas operaciones lo que hacen internamente es sacar la referencia de estos ficheros de cualquier commit, pero los ficheros siguen estando dentro del repositorio. Para eliminarlos por completo tenemos que limpiar el propio repositorio de Git:
$ cd some-big-repo.git
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
6. Borrar los commits vacíos
Si hemos borrado muchos ficheros o directorios (por ejemplo si lo que hemos hecho es partir un repositorio en dos) es muy probable que en el histórico se nos hayan quedado commits vacíos, commits que se ven en el histórico pero que dentro no se hace referencia a ningún ficheros. Lo ideal sería limpiar estos commits, reescribiendo el histórico, ya que no partan ningún valor.
Para ello lo mejor que he encontrado por ahor es hacer:
$ git filter-branch -f --prune-empty --tag-name-filter cat -- --all
Esto borra los commits vacíos, pero aun así nos pueden quedar commits de merges también vacíos. Para eliminar estos commits de merges vacíos podemos hacer:
$ git filter-branch -f --prune-empty --parent-filter /tmp/rewrite_parent.rb master
Veis que estoy haciendo referencia a un script en Ruby /tmp/rewrite_parent.rb
. Este lo podéis poner en la ruta que queráis y tiene que tener el siguietne contenido:
#!/usr/bin/ruby
old_parents = gets.chomp.gsub('-p ', ' ')
if old_parents.empty? then
new_parents = []
else
new_parents = `git show-branch --independent #{old_parents}`.split
end
puts new_parents.map{|p| '-p ' + p}.join(' ')
Referencias de estas operaciones:
-
Stack Overflow – Prune empty merge commits from history in Git repository
-
Thread on the kernel mailing list – Removing useless merge commit with «filter-branch»
7. Cómo ver todos los ficheros en el histórico
Para ver como nos va quedando el histórico poodemos ejecutar el siguiente comando que sacará un listado de todos ficheros que existen dentro del histórico:
git log --pretty=format: --name-only --diff-filter=A | sort -u
La referencia:
Stack Overflow – List all the files that ever existed in a Git repository
8. Subir de nuevo los cambios a nuestro repositorio remoto
Todo el trabajo que hemos hecho está aplicado sobre nuestro repositorio local (acordaos del clone --mirror
que hicimos al principio), así que por ahora no hay peligro. Pero llegados a este punto nos toca subir los cambios a nuestro repositorio remoto para que se los puedan disfrutar todos nuestros compañeros.
¡¡¡Ojo aquí porque a partir de aquí las operaciones que hagamos no tiene vuelta atrás!!!
Ante de hacer la subida os recomiendo que borréis todos los tags que tengáis en el repositorio remoto. Esto es porque al reescribir el histórico han cambiado las referencias y al intentar hacer la subida se encuentra con un conflicto (yo por lo menos no he conseguido forma de forzar esta operación y los he borrado a mano). No hay problema en borrarlos, porque como a continuación vamos a hacer una subida del repo, estos tags se van a volver a crear, con las referencias a los commits correctos.
Y ya sin más dilación ejecutamos hacemos la subida del repositorio:
$ git push origin --force --all
9. Conclusiones
Git nos ofrece una gran potencia y flexibilidad pero, como pasa en Spiderman un gran poder conlleva una gran responsabilidad. Así que recordad que las operaciones que hemos visto aquí son destructivas e irreversibles, por lo que debemos usarlas con precaución y siempre coordinados con el resto del equipo.
Una vez repetida la advertencia, si me gustaría destacar como tenemos a nuestro alcance una potente herramienta para empezar pequeños (un único repositorio cuando todavía no tenemos claras todas las piezas que formarán parte del proyecto) y una vez las cosas van cogiendo peso, ir separándolo en otros repositorios con entidad suficiente.
10. Sobre el autor
Alejandro Pérez García (@alejandropgarci)
Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster
Socio fundador de Autentia Real Business Solutions S.L. – «Soporte a Desarrollo»
Socio fundador de ThE Audience Megaphone System, S.L. – TEAMS – «Todo el potencial de tus grupos de influencia a tu alcance»