Nach längerer Pause zum Thema Versionsverwaltung mit Git geht es an dieser Stelle ein wenig weiter. Git-Repositorys beinhalten bekanntlich ihre vollständige Historie, so dass alle Änderungen immer in der Historie verbleiben. Problematisch wird das aber, wenn Zugangsdaten im Repository gelandet sind. Ein bloßes Entfernen funktioniert da leider nicht, da die Historie ja vollständig da ist.

Ein einfaches Beispiel sieht man etwa, wenn man bei GitHub nach „delete id_rsa“ sucht, was ein privater SSH-Key ist, der nicht eigentlich in ein Git-Repository landen sollte. Genauso findet man auf GitHub Commit-Messages mit dem Inhalt „remove password“.

Wenn man die entsprechenden Passwörter oder SSH-Private-Keys ändert bzw. ersetzt, dann muss man natürlich nicht viel mehr in dem Git-Repository ändern. Anders sieht es aus, wenn die Datei bzw. die Zeilen vollständig entfernt werden soll. Die Historie muss dafür (maximal) vollständig angepasst werden, was für jede Person, die mit dem Repository arbeitet, heißt, dass es vermutlich am einfachsten ist, das Projekt einmal neu zu klonen.

Abhilfe um gleich eine Vielzahl von Commits anzupassen bietet das Subkommando filter-branch. Dies kann nicht nur zum Entfernen von Passwörtern oder anderen Credentials dienen, sondern auch, wenn etwa große Binärdateien im Repository sind, die entfernt werden sollen.

Um das Verfahren auszuprobieren, kann man sich ein einfaches Repository anlegen, in dem zahlreiche Commits erzeugt werden, um dieses Repository dann aufzuräumen.

$ mkdir test-repo
$ for i in {1..500}; do echo "Commit $i" >> index; git add -A; git commit -m "Commit $i"; done

Diese Bash-For-Schleife legt im Test-Repository die Datei index an und schreibt in jedem Durchlauf den Durchlaufzähler in die Datei, mit dem Wort “Commit” vorne dran. Nach jeder Zeile wird dann ein Commit mit selbigen Inhalt in der Commit-Message gespeichert.

Das Repository wäre somit vorbereitet. Filter-Branch hat diverse Filter, die man anwenden kann. Eines ist etwa der tree-filter, mit dem man dateibasiert arbeitet.

Eine einfache Methode eine Datei aus allen Commits wäre es, ein rm -f index auszuführen, was in jedem Commit die Datei entfernen würde. Händisch wäre das ja zu aufwändig, weshalb man folgenden Befehl ausführen kann:

$ git filter-branch --tree-filter 'rm -f index' HEAD

Statt die Datei zu entfernen, kann man auch Inhalte der Datei verändern, etwa mit sed. Dazu muss dann der Befehl entsprechend angepasst werden. Es gibt auch noch weitere Filter, wie etwa der subdirectory-filter. Dieser kann genutzt werden, um ganze Unterverzeichnisse in ein eigenes Repository zu überführen. Die Historie des restlichen Repositorys wird dabei ignoriert, sofern sie nicht relevant ist. Wesentlicher Vorteil ist dabei, dass die vollständige Historie eines Subdirectories erhalten bleibt, sodass man auch nach dem Extrahieren frühere Änderungen nachvollziehen kann.

Alternative: BFG Repo-Cleaner

Eine Alternative zum reinen Filter-Branch ist der BFG Repo-Cleaner. Im Gegensatz zu Filter-Branch ist die Nutzung deutlich einfacher und es soll auch schneller sein. Das Tool ist in Java geschrieben und besitzt ein paar nette Features, die man sich bei Filter-Branch erstmal selbst zusammen schreiben müsste. Ein Beispiel ist etwa die automatische Entfernung von Dateien, die größer sind als eine spezifizierte Größe. Dazu besitzt bfg den Parameter --strip-blobs-bigger-than. Das vorherige Beispiel mit dem Löschen der Datei funktioniert zwar prinzipiell mit Filter-Branch auch einfach, allerdings beachtet es immer nur den aktuellen Pfad. Wenn etwa eine oder mehrere zu löschende Dateien im Repository zuvor herum geschoben wurde, dann hilft bfg mit dem Parameter --delete-files schon viel mehr, da es immer die Dateien mit den entsprechenden Namen löscht.

Für viele dürfte der BFG Repo-Cleaner ein einfaches Interface bieten, um die Historie vollständig anzupassen und trotzdem viel und auch schneller zu arbeiten. In der Regel erfolgt die Nutzung von Filter-Branch auch häufig nur einmalig, weshalb sich einige Sachen nur mit gewissen Aufwand durchführen lassen, die mit BFG einfacher und wohl auch schneller gehen. Bedacht werden sollte aber immer, dass sehr viele Commits angepasst werden. Man sollte dies nur tun, wenn es wirklich nötig ist und auch die Mitarbeiter entsprechend vorher informieren, damit kein bzw. nur kurz ein kaputtes Repository lokal vorhanden ist!

Vielen Dank an Dirk der mich auf BFG Repo-Cleaner hinwies. Mehr zu Git findet sich auf meinem Blog in der Git-Kategorie.