blog · git · desktop · images · contact & privacy · gopher


Git: Wie sah eine Datei damals aus?

Dieses Problem habe ich öfter: Ich will eine Datei aus einem Repo wieder in den Zustand bringen, in dem sie zu einem bestimmten Datum war. Daher will ich einmal sauber aufschreiben, wie das geht – und wie nicht.

Was man beim Suchen im Netz zuerst findet, ist etwas wie:

$ git checkout 'master@{2013-02-03 13:00}'              # Leider beides
$ git show 'master@{2013-02-03 13:00}:$dateiname'       # irreführend!

Klingt verlockend, ist aber leider nicht notwendigerweise korrekt. Das @-Konstrukt schaut nämlich im Reflog (siehe Git Book, Git Ready) nach, also in einem Log, das aufzeichnet, wann sich welche Referenz wie bewegt hat. Zum einen ist diese Information lokal, es geht also darum, wann sich mein master-Branch bewegt hat. Das hat nicht unbedingt etwas mit den Timestamps von Commits zu tun. Zum anderen sind die Reflogs zeitlich beschränkt, sie verfallen normalerweise nach 90 Tagen und beginnen ohnehin erst zum Zeitpunkt des Repo-Clones.

Der korrekte Weg ist, in der History einen Commit zu finden, der sich möglichst nahe am gewünschten Datum befindet. Git speichert bei jedem Commit einen vollen Snapshot des Repos. In einem Commit-Objekt ist dazu ein Verweis auf ein Tree-Objekt hinterlegt. Ein Tree ist vergleichbar mit einem Verzeichnis auf Dateisystemebene, er enthält Verweise auf Blobs (Dateien) oder weitere Trees. In einem Tree sind Verweise auf alle Dateien hinterlegt, auch jene, die sich mit diesem Commit gar nicht geändert haben. Habe ich meinen Commit in zeitlicher Nähe gefunden, kann ich also den zugehörigen Tree auslesen und dort den Verweis auf den gewünschten Blob finden.

Grafisch sieht das so aus:

Illustration der Git-Objekte

Ich will in folgendem Beispiel herausfinden, wie meine „~/.vimrc“ zum Zeitpunkt dieses Screenshots aussah:

$ pwd
/home/void/work/dotfiles

$ git log -n 1 --until='2012-06-04 16:02'
commit ee68d8a5ca8aced4933b7b390f0366c14ad4c037
Author: Ich <bla@example.invalid>
Date:   Mon Jun 4 15:39:13 2012 +0200

    mpd: Playlists.

Da ist mein Commit. Auf welchen Tree verweist er?

$ git cat-file -p ee68d8a
tree 14f538d3f751056f26b000a6b2854d6e286eff74
parent 2f3feb641bca890766cba576980367f45281de38
author Ich <bla@example.invalid> 1338817153 +0200
committer Ich <bla@example.invalid> 1338817153 +0200

mpd: Playlists.

Jetzt felt nur noch der Hash des Blobs meiner „.vimrc“:

$ git cat-file -p 14f538d | grep vimrc
100644 blob deed2a8e7312d22ca969280a57da58aaebf32ab0    .vimrc

Da ist er. Das kann ich mir nun anschauen, das ist die Datei zum gewünschten Zeitpunkt:

$ git cat-file -p deed2a8

Sich auf diese Weise durch die Commits zu hangeln, ist etwas aufwändig, allerdings durchaus interessant, wenn man verstehen will, wie Git intern arbeitet. Will ich schnell zum Ziel, dann gibt es einen Abkürzung, sobald ich den Hash des gewünschten Commits kenne:

$ git show ee68d8a:.vimrc

Und mit einer Befehlssubstitution kann man nun noch die Suche nach dem Commit-Hash hinzufügen, sodass sich die ganze Aktion auf folgenden Befehl reduzieren lässt:

$ git show $(git rev-list -n 1 --until='2012-06-04 16:02' master):.vimrc

Sollte man das täglich brauchen, lohnt sich eine Shell-Funktion.