Réduire l'espace disque utilisé par un dépôt git avec BFG repo cleaner

Gagner de l'espace disque, mais aussi réduire le temps de téléchargement d'un dépôt en supprimant les éléments lourds et inutiles

Sur l'un de nos projets, nous utilisons intensivement la gem VCR pour enregistrer et mocker des requêtes HTTP sur des serveurs externes à l'application. Ces enregistrements se matérialisent par des fichiers YAML qui servent de cache pour rejouer des requêtes HTTP. Elles sont ajoutées au versionning du projet afin de pouvoir être utilisées dans la suite de tests, et en particulier sur l'outil d'intégration continue que nous utilisons.

Or, lorsque nous avons une évolution des serveurs externes, ces “cassettes” (jargon VCR) doivent être enregistrées à nouveau et font l'objet de nouveaux commits. L'enregistrement de ces requêtes étant potentiellement assez lourd en taille (parfois quelques Mo voir quelques dizaines), le répertoire .git de ce projet a tendance à grossir rapidement, du fait de la structure même du stockage des fichiers/blobs dans git1.

Comme l'intérêt de conserver ces anciennes requêtes HTTP est assez limité (qui essayer de faire tourner une app sur de très anciens commits ?), une des possibilités au niveau de git (via git-filter-branch) est de retracer tout l'historique et d'y supprimer les éléments indésirables. Cela est très pratique si on a laissé trainer dans un commit un mot de passe ou une variable de configuration sensible.

git-filter-branch n'est cependant pas l'outil le plus rapide ni efficace pour faire ce nettoyage, et il existe un outil bien plus pratique pour cela : BFG repo cleaner ! Bon d'accord, c'est un outil java… cela dit, il fait le boulot, plutôt bien et assez rapidement.

Le principe est relativement simple, bfg-repo-cleaner travaille sur un dépôt de type “bare”, c'est à dire uniquement sur la base du dossier .git (sans les fichiers du working-tree en gros). Pour obtenir ce type de dépôt, il suffit de le cloner à l'aide du paramètre --mirror.

$ git clone --mirror git://github.com/levups/some-big-repo.git

Cela va cloner notre dépôt dans un répertoire some-big-repo.git (l'extension .git au dossier est une convention indiquant un dépôt de type “bare”). Ensuite, il suffit d'indiquer à bfg les options de filtrage. Pour notre cas particulier, nous souhaitions supprimer tous les blobs de taille supérieure à 1M dans l'historique.

$ java -jar bfg.jar --strip-blobs-bigger-than 1M some-big-repo.git

Une fois l'opération terminée, bfg indique qu'il a réécrit les commits sélectionnés (et leurs enfants, forcément), et que l'opération de nettoyage réelle nous incombe en forçant le déclenchement du garbage collector de git au sein même du dépôt de travail.

$ cd some-big-repo.git
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive

L'inconvénient majeur de cette opération est la ré-écriture complète des commits suivant celui qui est modifié (de part la structure hiérarchique de git), ce qui n'est pas foncièrement gênant au niveau d'un dépôt git simple, mais bien plus perturbant lorsqu'on parle de référentiels comme le sont tous les dépôts sur GitHub. En effet, une fois cette opération effectuée en local, pour partager ce “ménage” il est nécessaire de pousser tous les nouveaux éléments (blobs, références, commits, etc.) sur le dépôt de référence. Cette opération annihile potentiellement toutes les références utilisées dans les différentes pages d'un projet GitHub, telles que les liens vers des commits dans une discussion, ou une référence utilisé dans un message de pull-request.

Si toutefois cette manipulation vaut le “prix” à payer au niveau du chamboulement opéré au niveau de GitHub, il suffit de pousser2 les éléments du dépôt “bare” (vu qu'on l'a cloné à partir de GitHub).

$ git push

Afin de récupérer toutes ces nouvelles références, il convient ensuite de synchroniser les dépôts locaux clonés, et re refaire une opération de garbage collecting. Le plus simple est à mon avis de supprimer le dépôt local et de le cloner à nouveau. C'est également l'occasion de constater la différence de temps de récupération du dépôt désormais nettoyé.

Attention toutefois à l'organisation dans ce genre de manipulations, informer les contributeurs, figer les développements, éventuellement fusionner les fusiodemandes en cours, etc. Une fois les nouveaux éléments poussés, pas de retour en arrière possible sauf à restaurer une archive d'un dépôt local.

À noter également que ce n'est pas parce de nouvelles références, blobs et autres commits sont poussés sur GitHub, que ce dernier va également déclencher sont propre garbage collector (si tant est que GitHub fonctionne de la sorte ?). Après avoir effectué cette opération sur un dépôt non critique, il s'avère que l'interface de GitHub continue d'être fonctionnelle, les références utilisées dans les commits/pull-requests/tickets sont toujours accessibles, bien que n'existant plus sur le dépôt mis à jour. Est-ce temporaire ? Difficile à dire. Il y a très peu de littérature sur le sujet et quasiment pas de retour en dehors des tickets du projet bfg-repo-cleaner ou quelques désespérés sur StackOverflow.

À ce jour, nous n'avons pas assez de recul sur ce genre de manipulations pour nous lancer réellement dans une telle opération sur un dépôt plus actif.


  1. se reporter à une doc des commandes git porcelain 

  2. il se peut que des erreurs de références remontent à ce moment, notamment en cas de fusiodemandes (pull-requests) provenant de dépôts externes comme c'est souvent le cas sur les projets open-source sur Github.