Git
Chapters ▾ 2nd Edition

10.7 Git Interna - Wartung und Datenwiederherstellung

Wartung und Datenwiederherstellung

Möglicherweise müssen sie hin und wieder Bereinigungen durchführen. Bspw. müssen sie ein Repository komprimieren, ein importiertes Repository bereinigen oder verlorene Arbeit wiederherstellen. In diesem Abschnitt werden einige dieser Szenarien behandelt.

Wartung

Gelegentlich führt Git automatisch einen Befehl namens „auto gc“ aus. Meistens macht dieser Befehl gar nichts. Wenn sich jedoch zu viele lose Objekte (Objekte, die nicht in einem Packfile enthalten sind) oder zu viele Packfiles gibt, startet Git einen vollständigen git gc Befehl. Das „gc“ steht für Garbage Collect und der Befehl führt eine Reihe von Aktionen aus: Er sammelt alle losen Objekte und legt sie in Packfiles ab. Er fasst Packfiles zu einem großen Packfile zusammen. Außerdem entfernt er Objekte, die nicht von einem Commit erreichbar und ein paar Monate alt sind.

Sie können auto gc folgendermaßen manuell ausführen:

$ git gc --auto

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

Die andere Aktion, die gc durchführt, ist Ihre Referenzen in eine einzige 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, befinden sich diese Dateien nicht mehr im Verzeichnis refs. Git verschiebt sie aus Gründen der Effizienz in eine Datei mit dem Namen .git/packed-refs, die so 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 eine neue Datei in refs/heads. Um das passende SHA-1 für eine angegebene Referenz zu erhalten, prüft Git diese Referenz im refs-Verzeichnis und prüft dann die packed-refs-Datei als Fallback. Wenn Sie jedoch keine Referenz im refs-Verzeichnis finden können, befindet sich diese wahrscheinlich in Ihrer packed-refs-Datei.

Beachten Sie die letzte Zeile der Datei, die mit einem ^ beginnt. Dies bedeutet, dass dar darüberliegende Tag ein kommentierter Tag ist und dass diese Zeile das Commit ist, auf das der kommentierte Tag verweist.

Datenwiederherstellung

Es wird der Punkt auf Ihrer Git-Reise kommen, an dem Sie versehentlich einen oder mehrere Commits verlieren. Im Allgemeinen geschieht dies, weil Sie einen Branch mittels force Option löschen und es sich herausstellt, dass Sie den Branch doch noch benötigten. Oder aber es passiert, dass Sie einen Branch hart zurückgesetzt haben und noch benötigte Commits verworfen haben. Was können sie tun, um ihre Commits zurückzuerhalten?

In diesem Beispiel wird der Hauptbranch in Ihrem Test-Repository auf einen älteren Commit zurückgesetzt und die verlorenen Commits wiederhergestellt. Lassen Sie uns zunächst überprüfen, wo sich Ihr Repository zu diesem Zeitpunkt befindet:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Verschieben Sie nun 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 die beiden obersten Commits verloren. Sie haben jetzt keinen Branch, von dem aus diese Commits erreichbar sind. Sie müssen das letzte Commit-SHA-1 finden und anschließend einen Branch hinzufügen, der darauf verweist. Der Trick ist, das letzte Commit SHA-1 zu finden. Leider ist es ist nicht so, als hätten Sie es auswendig gelernt, oder?

Oft ist der schnellste Weg die Nutzung eines Tools namens git reflog. Während sie arbeiten, zeichnet Git lautlos im Hintergrund auf, was ihr HEAD ist und worauf es zeigt. Jedes Mal, wenn Sie Branches commiten oder ändern, wird das Reflog aktualisiert. Das reflog wird auch durch den Befehl git update-ref aktualisiert. Dies ist ein weiterer Grund, warum Sie diesen Befehl verwenden sollten, anstatt nur den SHA-1-Wert in Ihre ref-Dateien zu schreiben, wie in Git Referenzen beschrieben. 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: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb

Hier sehen wir die beiden Commits, die wir ausgecheckt haben, jedoch gibt es hier nicht viele Informationen. Um die gleichen Informationen auf eine viel nützlichere Weise anzuzeigen, können wir git log -g ausführen, wodurch Sie eine normale Logausgabe für Ihr Reflog erhalten.

$ 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

       modified repo.rb a bit

Es sieht so aus, als ob Sie das unterste Commit verloren haben. Sie können es also wiederherstellen, indem Sie bei diesem Commit einen neuen Branch erstellen. Beispielsweise können Sie einen Branch mit dem Namen recover-branch bei diesem Commit (ab1afef) starten:

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Sehr gut - jetzt haben Sie einen Branch namens recover-branch, in dem sich früher Ihr master-Branch befand, wodurch die ersten beiden Commits wieder erreichbar sind. Angenommen, Ihr Verlust war aus irgendeinem Grund nicht im Reflog - Sie können dies simulieren, indem Sie recover-branch entfernen und das Reflog löschen. Jetzt sind die ersten beiden Commits nicht mehr erreichbar:

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

Da sich die Reflog-Daten im Verzeichnis .git/logs/ befinden, haben Sie praktisch kein Reflog. Wie können Sie dieses Commit zu diesem Zeitpunkt wiederherstellen? Eine Möglichkeit ist die Verwendung des Hilfsprogramms git fsck, mit dem Ihre Datenbank auf Integrität überprüft wird. Wenn Sie es mit der Option --full ausführen, werden alle Objekte angezeigt, 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 Ihr fehlendes Commit nach dem String „Dangling Commit“ sehen. Sie können es auf die gleiche Weise wiederherstellen, indem Sie einen Branch hinzufügen, der auf diesen SHA-1 verweist.

Objekte löschen

Es gibt viele großartige Dinge an Git. Eine Funktion, die jedoch Probleme verursachen kann, ist die Tatsache, dass ein Git-Klon den gesamten Verlauf des Projekts herunterlädt, einschließlich jeder Version jeder Datei. Dies ist in Ordnung, wenn das Ganze Quellcode ist. Git ist stark darin, diese Daten effizient zu komprimieren. Wenn jedoch zu einem beliebigen Zeitpunkt im Verlauf Ihres Projekts eine einzelne große Datei hinzugefügt wird, muss jeder Klon für alle Zeiten diese große Datei herunterladen, auch wenn sie beim nächsten Commit aus dem Projekt entfernt wurde. Weil sie von der Historie aus erreichbar ist, wird sie immer da sein.

Dies kann ein großes Problem sein, wenn Sie Subversion- oder Perforce-Repositorys nach Git konvertieren. Da Sie in diesen Systemen nicht den gesamten Verlauf herunterladen, hat dieses Model des Hinzufügens nur wenige Konsequenzen. Wenn Sie einen Import von einem anderen System durchgeführt haben oder auf andere Weise feststellen, dass Ihr Repository viel größer ist, als es sein sollte, können Sie große Objekte folgendermaßen finden und entfernen.

Seien Sie gewarnt: Diese Technik wirkt sich zerstörerisch auf Ihren Commit-Verlauf aus. Es schreibt jedes Commitobjekt neu, seit dem frühesten Baum, den Sie ändern müssen, um einen Verweis auf eine große Datei zu entfernen. Wenn Sie dies unmittelbar nach einem Import tun, bevor jemand damit begonnen hat, sich auf das Commit zu stützen, ist alles in Ordnung. Andernfalls müssen Sie alle Mitwirkenden benachrichtigen, dass sie ihre Arbeit auf Ihre neuen Commits rebasen müssen.

Zur Veranschaulichung fügen Sie Ihrem Test-Repository eine große Datei hinzu, entfernen sie sie beim nächsten Commit. Anschließend suchen sie sie und entfernen sie dauerhaft aus dem Repository. Fügen Sie Ihrer Historie zunächst ein großes Objekt hinzu:

$ curl 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

Hoppla - Sie wollten Ihrem Projekt keinen riesigen Tarball hinzufügen. Besser wäre, es loszuwerden:

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

Nun wenden sie gc auf Ihre Datenbank an und sehen sie, wie viel Speicherplatz Sie verwenden:

$ 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 verwenden:

$ 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 size-pack-Eintrag gibt die Größe Ihrer Packdateien in Kilobyte an. Somit verwenden Sie fast 5 MB an Speicherplatz. Vor dem letzten Commit haben Sie einen Wert von ungefähr 2 KB belegt. Wenn Sie die Datei aus dem vorherigen Commit entfernen, würde sie offensichtlich nicht aus Ihrem Verlauf entfernt. Jedes Mal, wenn jemand dieses Repository klont, muss er 5 MB klonen, um dieses winzige Projekt zu erhalten, da Sie versehentlich eine große Datei hinzugefügt haben. Lass sie es uns loswerden.

Als erstes müssen wir es finden. In diesem Fall wissen Sie bereits, um welche Datei es sich handelt. Angenommen, Sie wissen es nicht. Wie würden Sie feststellen, welche Datei oder welche Dateien so viel Speicherplatz beanspruchen? Wenn Sie git gc ausführen, befinden sich alle Objekte in einer Packdatei. Sie können die großen Objekte identifizieren, indem Sie einen anderen Installationsbefehl namens git verify-pack ausführen und die Ausgabe nach dem dritte Feld sortieren, das die Dateigröße ist. Sie können es auch über den Befehl tail pipen, da Sie nur an den letzten, großen 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 Ein bestimmtes Commit-Message-Format erzwingen verwendet haben. Wenn Sie --objects an rev-list übergeben, werden alle festgeschriebenen SHA-1s und auch die BLOB-SHA-1s mit den ihnen zugeordneten Dateipfaden aufgelistet. Sie können dies verwenden, um den Namen Ihres Blobs zu finden:

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

Jetzt müssen Sie diese Datei von allen Bäumen in Ihrer Historie entfernen. Sie können leicht sehen, welche Commits diese Datei geändert haben:

$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball

Sie müssen alle Commits hinter 7b30847 neu schreiben, um diese Datei vollständig aus Ihrem Git-Verlauf zu entfernen. Verwenden Sie dazu filter-branch, den Sie in Rewriting History 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 ähnelt der Option --tree-filter, die in Rewriting History verwendet wurde. Jedoch ändern sie jedes Mal Ihren Staging-Bereich oder Index anstatt daß sie einen Befehl zum Modifizieren von ausgecheckten Dateien auf ihrer Platte übergeben.

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 dafür ist die Geschwindigkeit - da Git nicht jede Revision auf der Festplatte auschecken muss, bevor der Filter ausgeführt wird, kann der Prozess sehr viel schneller sein. Sie können die gleiche Aufgabe mit --tree-filter ausführen, wenn Sie möchten. Die Option --ignore-unmatch für git rm weist an, dass keine Fehler auftreten, wenn das zu entfernende Muster nicht vorhanden ist. Schließlich fordern Sie filter-branch auf, Ihre Historie erst ab dem` 7b30847`-Commit neu zu schreiben, da Sie wissen, wo dieses Problem begann. Andernfalls wird es von vorne beginnen und unnötig länger dauern.

Ihr Verlauf enthält nun keinen Verweis mehr auf diese Datei. Ihr Reflog und eine neue Gruppe von Refs, die Git hinzugefügt hat, als Sie den filter-branch unter .git/refs/original ausgeführt haben, enthält jedoch weiterhin Verweise, sodass Sie sie entfernen und die Datenbank neu packen müssen. Sie müssen alles loswerden, das einen Zeiger auf diese alten Commits enthält, 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)

Mal sehen, 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 beträgt nur noch 8 KB, was viel besser als 5 MB ist. Sie können anhand der Größe erkennen, dass sich das große Objekt noch in Ihren losen Objekten befindet, sodass es nicht verschwunden ist. Es wird jedoch nicht auf einen Push oder nachfolgenden Klon übertragen, was wichtig ist. Wenn Sie es wirklich wollten, können 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