Kapitel ▾ 2. Auflage

10.7 Git Internals - Wartung und Datenwiederherstellung

Wartung und Datenwiederherstellung

Gelegentlich müssen Sie möglicherweise einige Bereinigungsarbeiten durchführen – ein Repository kompakter machen, ein importiertes Repository bereinigen oder verlorene Arbeit wiederherstellen. Dieser Abschnitt behandelt einige dieser Szenarien.

Wartung

Gelegentlich führt Git automatisch einen Befehl namens „auto gc“ aus. Die meiste Zeit tut dieser Befehl nichts. Wenn jedoch zu viele lose Objekte (Objekte, die sich nicht in einer Packdatei befinden) oder zu viele Packdateien vorhanden sind, startet Git einen vollwertigen git gc-Befehl. „gc“ steht für Garbage Collect (Speicherbereinigung) und der Befehl tut eine Reihe von Dingen: Er sammelt alle losen Objekte und platziert sie in Packdateien, er konsolidiert Packdateien zu einer großen Packdatei und er entfernt Objekte, die von keinem Commit erreichbar sind und einige Monate alt sind.

Sie können auto gc manuell wie folgt ausführen

$ git gc --auto

Auch dies tut im Allgemeinen nichts. Sie müssen etwa 7.000 lose Objekte oder mehr als 50 Packdateien haben, damit Git einen echten gc-Befehl auslöst. Sie können diese Grenzwerte mit den Konfigurationseinstellungen gc.auto und gc.autopacklimit ändern.

Das andere, was gc tut, ist, Ihre Referenzen in einer einzigen Datei zu packen. Angenommen, Ihr Repository enthält die folgenden Branches und Tags

$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1

Wenn Sie git gc ausführen, haben Sie diese Dateien nicht mehr im Verzeichnis refs. Git verschiebt sie aus Effizienzgründen in eine Datei namens .git/packed-refs, die wie folgt aussieht

$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9

Wenn Sie eine Referenz aktualisieren, bearbeitet Git diese Datei nicht, sondern schreibt stattdessen eine neue Datei nach refs/heads. Um die entsprechende SHA-1 für eine gegebene Referenz zu erhalten, prüft Git diese Referenz im Verzeichnis refs und prüft dann die Datei packed-refs als Fallback. Wenn Sie also eine Referenz im Verzeichnis refs nicht finden können, befindet sie sich wahrscheinlich in Ihrer Datei packed-refs.

Beachten Sie die letzte Zeile der Datei, die mit einem ^ beginnt. Dies bedeutet, dass der direkt darüber liegende Tag ein annotierter Tag ist und diese Zeile der Commit ist, auf den der annotierte Tag zeigt.

Datenwiederherstellung

Irgendwann auf Ihrer Git-Reise verlieren Sie möglicherweise versehentlich einen Commit. Im Allgemeinen geschieht dies, weil Sie einen Branch mit Arbeit darauf erzwingen und sich herausstellt, dass Sie den Branch doch haben wollten; oder Sie setzen einen Branch hart zurück und geben so Commits auf, von denen Sie etwas wollten. Angenommen, dies passiert, wie können Sie Ihre Commits zurückbekommen?

Hier ist ein Beispiel, das den master-Branch in Ihrem Test-Repository auf einen älteren Commit zurücksetzt und dann die verlorenen Commits wiederherstellt. Zuerst überprüfen wir, wo sich Ihr Repository zu diesem Zeitpunkt befindet

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

Nun verschieben Sie den master-Branch zurück zum mittleren Commit

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef Third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

Sie haben effektiv die beiden oberen Commits verloren – es gibt keinen Branch, von dem diese Commits erreichbar sind. Sie müssen die neueste Commit-SHA-1 finden und dann einen Branch hinzufügen, der darauf zeigt. Der Trick besteht darin, diese neueste Commit-SHA-1 zu finden – Sie haben sie nicht auswendig gelernt, oder?

Oft ist der schnellste Weg, ein Tool namens git reflog zu verwenden. Während Sie arbeiten, zeichnet Git leise auf, was Ihr HEAD jedes Mal ist, wenn Sie es ändern. Jedes Mal, wenn Sie einen Commit machen oder den Branch wechseln, wird das Reflog aktualisiert. Das Reflog wird auch durch den Befehl git update-ref aktualisiert, was ein weiterer Grund ist, es anstelle des reinen Schreibens des SHA-1-Werts in Ihre Ref-Dateien zu verwenden, wie wir in Git-Referenzen behandelt haben. Sie können jederzeit sehen, wo Sie waren, indem Sie git reflog ausführen

$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create repo.rb

Hier können wir die beiden Commits sehen, die wir ausgecheckt hatten, aber hier gibt es nicht viele Informationen. Um dieselben Informationen auf eine viel nützlichere Weise anzuzeigen, können wir git log -g ausführen, was Ihnen eine normale Log-Ausgabe für Ihr Reflog liefert.

$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:22:37 2009 -0700

		Third commit

commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

       Modify repo.rb a bit

Es sieht so aus, als ob der untere Commit derjenige ist, den Sie verloren haben. Sie können ihn also wiederherstellen, indem Sie einen neuen Branch an diesem Commit erstellen. Sie können beispielsweise einen Branch namens recover-branch an diesem Commit (ab1afef) starten

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

Cool – jetzt haben Sie einen Branch namens recover-branch, der sich dort befindet, wo Ihr master-Branch früher war, wodurch die ersten beiden Commits wieder erreichbar sind. Als Nächstes nehmen wir an, Ihr Verlust war aus irgendeinem Grund nicht im Reflog – Sie können das simulieren, indem Sie recover-branch entfernen und das Reflog löschen. Jetzt sind die ersten beiden Commits von nichts erreichbar

$ git branch -D recover-branch
$ rm -Rf .git/logs/

Da die Reflog-Daten im Verzeichnis .git/logs/ gespeichert sind, haben Sie effektiv kein Reflog. Wie können Sie diesen Commit zu diesem Zeitpunkt wiederherstellen? Eine Möglichkeit ist die Verwendung des Dienstprogramms git fsck, das Ihre Datenbank auf Integrität prüft. Wenn Sie es mit der Option --full ausführen, zeigt es Ihnen alle Objekte an, auf die kein anderes Objekt zeigt

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

In diesem Fall können Sie Ihren fehlenden Commit nach der Zeichenkette „dangling commit“ sehen. Sie können ihn auf die gleiche Weise wiederherstellen, indem Sie einen Branch hinzufügen, der auf diese SHA-1 zeigt.

Entfernen von Objekten

Es gibt viele großartige Dinge an Git, aber eine Funktion, die Probleme verursachen kann, ist die Tatsache, dass ein git clone den gesamten Verlauf des Projekts herunterlädt, einschließlich jeder Version jeder Datei. Das ist in Ordnung, wenn das Ganze Quellcode ist, da Git hochgradig für die effiziente Komprimierung dieser Daten optimiert ist. Wenn jedoch jemand zu irgendeinem Zeitpunkt in der Geschichte Ihres Projekts eine einzige riesige Datei hinzugefügt hat, muss jeder Klon für immer diese große Datei herunterladen, selbst wenn sie im nächsten Commit aus dem Projekt entfernt wurde. Da sie vom Verlauf erreichbar ist, wird sie immer da sein.

Dies kann ein großes Problem sein, wenn Sie Subversion- oder Perforce-Repositories in Git konvertieren. Da Sie in diesen Systemen nicht den gesamten Verlauf herunterladen, hat diese Art von Hinzufügung wenige Konsequenzen. Wenn Sie einen Import aus einem anderen System durchführen oder feststellen, dass Ihr Repository viel größer ist als es sein sollte, finden Sie hier, wie Sie große Objekte finden und entfernen können.

Warnung: Diese Technik ist destruktiv für Ihren Commit-Verlauf. Sie schreibt jeden Commit-Objekt seit dem frühesten Baum, den Sie ändern müssen, um einen Verweis auf eine große Datei zu entfernen, neu. Wenn Sie dies unmittelbar nach einem Import tun, bevor jemand begonnen hat, Arbeit auf den Commit zu stützen, ist es in Ordnung – andernfalls müssen Sie alle Mitwirkenden benachrichtigen, dass sie ihre Arbeit auf Ihre neuen Commits neu basieren müssen.

Um dies zu demonstrieren, fügen Sie eine große Datei in Ihr Test-Repository ein, entfernen Sie sie im nächsten Commit, finden Sie sie und entfernen Sie sie dauerhaft aus dem Repository. Fügen Sie zuerst ein großes Objekt zu Ihrem Verlauf hinzu

$ curl -L https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'Add git tarball'
[master 7b30847] Add git tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 git.tgz

Ups – Sie wollten keinen riesigen Tarball zu Ihrem Projekt hinzufügen. Besser, Sie werden ihn los

$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'Oops - remove large tarball'
[master dadf725] Oops - remove large tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 git.tgz

Nun gc Ihre Datenbank und sehen Sie, wie viel Platz Sie verbrauchen

$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)

Sie können den Befehl count-objects ausführen, um schnell zu sehen, wie viel Speicherplatz Sie verbrauchen

$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0

Der Eintrag size-pack ist die Größe Ihrer Packdateien in Kilobyte, Sie verbrauchen also fast 5 MB. Vor dem letzten Commit verbrauchten Sie näher an 2 KB – klar, das Entfernen der Datei aus dem vorherigen Commit hat sie nicht aus Ihrem Verlauf entfernt. Jedes Mal, wenn jemand dieses Repository klont, muss er alle 5 MB klonen, nur um dieses winzige Projekt zu erhalten, weil Sie versehentlich eine große Datei hinzugefügt haben. Lassen Sie sie los werden.

Zuerst müssen Sie sie finden. In diesem Fall wissen Sie bereits, welche Datei es ist. Aber nehmen wir an, Sie wüssten es nicht; wie würden Sie identifizieren, welche Datei oder Dateien so viel Platz beanspruchen? Wenn Sie git gc ausführen, sind alle Objekte in einer Packdatei; Sie können die großen Objekte identifizieren, indem Sie einen weiteren Plumbing-Befehl namens git verify-pack ausführen und nach dem dritten Feld in der Ausgabe sortieren, das die Dateigröße ist. Sie können es auch durch den Befehl tail leiten, da Sie nur an den letzten paar größten Dateien interessiert sind

$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
  | sort -k 3 -n \
  | tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob   4975916 4976258 1438

Das große Objekt befindet sich unten: 5 MB. Um herauszufinden, um welche Datei es sich handelt, verwenden Sie den Befehl rev-list, den Sie kurz in Erzwingen eines bestimmten Commit-Nachrichtenformats verwendet haben. Wenn Sie --objects an rev-list übergeben, listet er alle Commit-SHA-1s und auch die Blob-SHA-1s mit den zugehörigen Dateipfaden auf. Sie können dies verwenden, um den Namen Ihres Blobs zu finden

$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz

Nun müssen Sie diese Datei aus allen Bäumen in Ihrer Vergangenheit entfernen. Sie können leicht sehen, welche Commits diese Datei geändert haben

$ git log --oneline --branches -- git.tgz
dadf725 Oops - remove large tarball
7b30847 Add git tarball

Sie müssen alle Commits nach 7b30847 umschreiben, um diese Datei vollständig aus Ihrem Git-Verlauf zu entfernen. Dazu verwenden Sie filter-branch, das Sie in Verlauf umschreiben verwendet haben

$ git filter-branch --index-filter \
  'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten

Die Option --index-filter ist ähnlich der Option --tree-filter, die in Verlauf umschreiben verwendet wurde, außer dass Sie jedes Mal die Staging-Area oder den Index modifizieren, anstatt einen Befehl zu übergeben, der Dateien auf der Festplatte modifiziert, die ausgecheckt wurden.

Anstatt eine bestimmte Datei mit etwas wie rm file zu entfernen, müssen Sie sie mit git rm --cached entfernen – Sie müssen sie aus dem Index entfernen, nicht von der Festplatte. Der Grund für diese Vorgehensweise ist die Geschwindigkeit – da Git jede Revision nicht auf die Festplatte auschecken muss, bevor es Ihren Filter ausführt, kann der Prozess viel, viel schneller sein. Sie können die gleiche Aufgabe auch mit --tree-filter erledigen, wenn Sie möchten. Die Option --ignore-unmatch zu git rm teilt ihr mit, keinen Fehler auszugeben, wenn das Muster, das Sie entfernen möchten, nicht vorhanden ist. Schließlich bitten Sie filter-branch, Ihren Verlauf nur ab dem Commit 7b30847 nach oben neu zu schreiben, da Sie wissen, dass dort dieses Problem begann. Andernfalls beginnt er von vorne und dauert unnötig länger.

Ihr Verlauf enthält keine Referenz mehr auf diese Datei. Ihr Reflog und ein neuer Satz von Refs, die Git hinzugefügt hat, als Sie filter-branch unter .git/refs/original ausgeführt haben, enthalten sie jedoch noch. Sie müssen sie also entfernen und die Datenbank dann neu packen. Sie müssen alles loswerden, was einen Verweis auf diese alten Commits hat, bevor Sie neu packen

$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)

Sehen wir, wie viel Platz Sie gespart haben.

$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0

Die Größe des gepackten Repositorys ist auf 8 KB gesunken, was viel besser ist als 5 MB. Sie können anhand des Größenwerts sehen, dass das große Objekt immer noch in Ihren losen Objekten vorhanden ist, also ist es nicht weg; aber es wird bei einem Push oder einem nachfolgenden Klon nicht übertragen, und das ist es, was wichtig ist. Wenn Sie es wirklich wollten, könnten Sie das Objekt vollständig entfernen, indem Sie git prune mit der Option --expire ausführen

$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0