Nettoyer l’historique d’une branche Git pour supprimer les fichiers indésirables

J’ai récemment eu à travailler sur un dépôt Git qui contenait des modifications à reporter sur un autre dépôt. Malheureusement, ce dépôt n’avait pas de fichier .gitignore au départ, si bien que de nombreux fichiers inutiles (répertoires bin/obj/packages…) avaient été archivés. Cela rendait l’historique très difficile à lire, puisque chaque commit contenait des centaines de fichiers modifiés.

Heureusement, Git permet assez facilement de "nettoyer" une branche, en recréant les mêmes commits sans les fichiers qui n’auraient pas dû se trouver là. Voyons donc pas-à-pas comment arriver à ce résultat.

Mise en garde

L’opération à réaliser ici consiste en une réécriture de l’historique, une mise en garde s’impose donc : il ne faut jamais réécrire l’historique d’une branche publiée partagée avec d’autres personnes. En effet, si quelqu’un d’autre crée des commits à partir de la version actuelle de la branche, et que celle-ci est réécrite, il deviendra beaucoup plus compliqué d’intégrer ces commits à la branche réécrite.

Dans mon cas, je n’avais pas besoin de publier la branche réécrite mais seulement de l’examiner en local, donc le problème ne se posait pas. Mais n’appliquez pas cette solution à une branche sur laquelle vos collègues travaillent, si vous tenez à conserver de bonnes relations avec eux 😉.

Créer une branche de travail

On va faire des modifications assez lourdes et potentiellement risquées sur le dépôt, il convient donc de prendre quelques précautions. Le plus simple dans ce genre de situation est tout bêtement de travailler sur une autre branche, pour ne pas risquer de faire des dégâts sur la branche originale. Par exemple, si la branche à nettoyer est master, on va créer une nouvelle branche master2 à partir de master :

git checkout -b master2 master

Identifier les fichiers à supprimer

Avant de lancer le nettoyage, il faut d’abord identifier les fichiers à supprimer. Dans le cas d’un projet .NET, il s’agit bien souvent du contenu des répertoires bin et obj (où qu’ils se trouvent) et packages (généralement à la racine de la solution), on va donc partir sur cette hypothèse pour l’instant. Les patterns des fichiers à supprimer sont donc les suivants :

  • **/bin/**
  • **/obj/**
  • packages/**

Nettoyer la branche : la commande git filter-branch

La commande Git qui va nous permettre de supprimer les fichiers indésirables s’appelle filter-branch. Elle est décrite dans le livre Pro Git comme "l’option nucléaire", car elle est très puissante et potentiellement dévastatrice… elle est donc à manipuler avec précaution.

Le principe de cette commande est de reprendre chaque commit de la branche, lui appliquer un filtre, et le recommiter avec les modifications causées par le filtre. Il existe plusieurs types de filtre, par exemple :

  • --msg-filter : permet de réécrire les messages des commits de la branche.
  • --tree-filter : permet de filtrer les fichiers au niveau de la copie de travail du dépôt (effectue un checkout de chaque commit, ce qui peut être assez long sur un gros dépôt)
  • --index-filter : permet de filtrer les fichiers au niveau de l’index (ne nécessite pas un checkout de chaque commit, donc plus rapide).

Dans notre scénario, --index-filter est parfaitement indiqué, vu qu’on souhaite simplement filtrer les fichiers par rapport à leur chemin. La commande filter-branch avec ce type de filtre s’utilise comme ceci :

git filter-branch --index-filter '<command>'

<command> désigne une commande bash qui sera exécutée pour chaque commit de la branche à réécrire. Dans le cas qui nous intéresse, ce sera simplement un appel à git rm pour supprimer de l’index les fichiers indésirables :

git filter-branch --index-filter 'git rm --cached --ignore-unmatch **/bin/** **/obj/** packages/**'

Le paramètre --cached indique qu’on travaille sur l’index et non sur la copie de travail; --ignore-unmatch permet d’ignorer les cas où aucun fichier ne correspond au pattern spécifié. Par défaut, la commande s’applique uniquement à la branche courante.

Pour peu que la branche ait beaucoup d’historique, la commande peut prendre assez longtemps à s’exécuter, il faudra donc s’armer de patience… Une fois terminé, vous devriez avoir une branche contenant des commits identiques à ceux de la branche d’origine, mais sans les fichiers indésirables.

Cas plus complexes

Dans l’exemple ci-dessus, il n’y avait que 3 patterns de fichiers à supprimer, donc la commande était assez courte pour être écrite "inline". Mais s’il y en a beaucoup plus, ou si la logique à appliquer pour supprimer les fichiers est plus complexe, ça ne tient plus vraiment la route… le plus simple est donc d’écrire un script (bash) qui contient toutes les commandes nécessaires, et de passer ce script en paramètre de git filter-branch --index-filter.

Nettoyer uniquement à partir d’un commit spécifique

Dans l’exemple précédent, on applique filter-branch à la totalité de la branche. Mais il est également possible de ne l’appliquer qu’à partir d’un commit spécifique, en spécifiant une plage de commits :

git filter-branch --index-filter '<command>' <ref>..HEAD

Ici, <ref> désigne une référence de commit (SHA1, branche ou tag). Notez que la fin de la plage de commits doit forcément être HEAD : on ne peut pas réécrire le début ou le milieu d’une branche sans toucher aux commits suivants, puisque le SHA1 de chaque commit dépend du commit précédent.

3 thoughts on “Nettoyer l’historique d’une branche Git pour supprimer les fichiers indésirables”

  1. Bonjour,

    Merci pour cet article qui je pense va beaucoup m’aider. J’ai cependant quelques questions concernant la partie nettoyage de branche. Si notre repo est composé de plusieurs branch (feature, release, …) et de tag l’opération doit-elle être réalisée sur toutes les branches? Ou, on réalise cette opération seulement sur la branche master et on réaliser un pull –rebase par la suite sur les autre branche. Reste la question des tags.

    Par avance merci de votre réponse.

    Cordialement

    1. Bonjour Arnaud,

      Il faut bien comprendre que cette opération réécrit l’historique d’une branche en créant de nouveaux commits à la place des anciens. Si vous nettoyez “master”, une branche “test” qui partait du commit X de master partira toujours de ce commit, et ne sera donc plus liée à master puisque le commit X ne fera plus partie de master. Idem pour les tags : un tag correspond à un SHA1 de commit, donc si le commit en question est remplacé, le tag ne sera pas mis à jour pour pointer vers le nouveau.

      Au final, il me semble donc assez peu réaliste de vouloir nettoyer l’historique d’un repo complet avec toutes ses branches et ses tags. Il vaut mieux limiter cela à une branche pas encore publiée, ou alors un cas comme le mien ou je voulais juste pouvoir examiner l’historique en local en me débarrassant de ce qui “polluait” l’historique.

  2. Merci pour la précision de la réponse.

    Je voulais réaliser cette opération pour supprimer des fichiers volumineux qui ont été archivés ( vidéos) et retrouver un repo avec des caractéristiques qui conviennent à une migration vers gitHub.
    Et comme j’ai vu plusieurs articles comparant filter-branch de git et BFG Repo-Cleaner je voulais essayer de bien comprendre les avantages inconvénients des 2.

Leave a Reply

Your email address will not be published. Required fields are marked *