-
1. Los geht's
- 1.1 Wozu Versionskontrolle?
- 1.2 Die Geschichte von Git
- 1.3 Git Grundlagen
- 1.4 Git installieren
- 1.5 Git konfigurieren
- 1.6 Hilfe finden
- 1.7 Zusammenfassung
-
2. Git Grundlagen
-
3. Git Branching
- 3.1 Was ist ein Branch?
- 3.2 Einfaches Branching und Merging
- 3.3 Branch Management
- 3.4 Branching Workflows
- 3.5 Externe Branches
- 3.6 Rebasing
- 3.7 Zusammenfassung
-
4. Git auf dem Server
- 4.1 Die Protokolle
- 4.2 Git auf einen Server bekommen
- 4.3 Generiere deinen öffentlichen SSH-Schlüssel
- 4.4 Einrichten des Servers
- 4.5 Öffentlicher Zugang
- 4.6 GitWeb
- 4.7 Gitosis
- 4.8 Gitolite
- 4.9 Git Daemon
- 4.10 Git Hosting
- 4.11 Einrichten eines Benutzeraccounts
- 4.12 Zusammenfassung
-
5. Distribuierte Arbeit mit Git (xxx)
-
6. Git Tools
- 6.1 Revision Auswahl
- 6.2 Interaktives Stagen
- 6.3 Stashing
- 6.4 Änderungshistorie verändern
- 6.5 Mit Hilfe von Git debuggen
- 6.6 Submodule
- 6.7 Subtree Merging
- 6.8 Zusammenfassung
-
7. Git individuell einrichten
-
8. Git und andere Versionsverwaltungen
- 8.1 Git und Subversion
- 8.2 Zu Git umziehen
- 8.3 Zusammenfassung
-
9. Git Internas
- 9.1 Plumbing und Porcelain
- 9.2 Git Objekte
- 9.3 Git Referenzen
- 9.4 Pack-Dateien
- 9.5 Die Refspec
- 9.6 Transfer Protokolle
- 9.7 Wartung und Datenwiederherstellung
- 9.8 Zusammenfassung
9.7 Git Internas - Wartung und Datenwiederherstellung
Wartung und Datenwiederherstellung
Gelegentlich will man ein bißchen aufräumen - ein Repository verdichten, ein importiertes Repository entfernen (xxx) oder verloren gegangene Daten wieder herstellen. Dieses Kapitel wird sich mit einigen derartigen Szenarien befassen.
Wartung
Git führt den Befehl auto gc hin und wieder automatisch aus. In den meisten Fällen tut dieser Befehl nichts. Wenn allerdings zu viele lose Objekte (d.h. Objekte, die nicht in einem Packfile gepackt sind) oder zu viele einzelne Packfiles vorhanden sind, führt Git den Befehl git gc aus. gc steht für "Garbage Collection". Dieser Befehl führt eine Reihe von Aufgaben durch: er sammelt die losen Objekte und packt sie in ein Packfile, er führt einzelne Packfiles zu einem einzigen, großen Packfile zusammen, und er entfernt Objekte, die mit keinem Commit erreichbar und einige Monate alt sind.
Du kannst auto gc wie folgt manuell ausführen:
$ git gc --auto
Wie schon erwähnt tut dies normalerweise gar nichts. Es müssen sich etwa 7.000 lose Objekte oder mehr als 50 Packfiles angesammelt haben, bevor Git tatsächlich die Garbage Collection startet. Du kannst diese Werte mit Hilfe der gc.auto bzw. gc.autopacklimit Konfigurationsvariablen manuell setzen.
gc packt außerdem Referenzen in eine einzige Datei zusammen. Nehmen wir an, dein 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
Nachdem du git gc ausgeführt hast, werden diese Dateien um der Effizienz willen aus dem refs Verzeichnis entfernt und in eine Datei .git/packed-refs verschoben, die dann wie folgt aussieht:
$ cat .git/packed-refs
# pack-refs with: peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9
Wenn du eine Referenz bearbeitest, läßt Git diese Datei unverändert und schreibt statt dessen eine neue Datei nach refs/heads. Um einen SHA für eine Referenz nachzuschlagen, schaut Git zunächst im refs Verzeichnis und danach erst in der packed-refs Datei nach, falls nötig. Wenn eine Referenz also nicht im refs Verzeichnis liegt, befindet sie sich wahrscheinlich in packed-refs Datei.
Beachte, daß die letzte Zeile der Datei mit ^ anfängt. Das bedeutet, daß der Tag darüber ein annotierter Tag ist und diese Zeile zeigt den Commit, auf den der annotierte Tag zeigt.
Daten Wiederherstellung
Irgendwann wird es vielleicht mal vorkommen, daß du während der Arbeit mit Git einen Commit verlierst. Normalerweise passiert das, wenn du versehentlich einen Branch löschst, an dem du gearbeitet hattest und den du noch brauchst. Oder du führst git reset --hard aus und stellst fest, daß du einige der Commits noch brauchst. Nehmen wir an, du steckst in einer solchen Situation - wie kannst du deine Commits wieder herstellen?
Das folgende Beispiel setzt zuerst den master Branch auf einen älteren Commit zurück und stellt die verlorenen Commits dann wieder her. Zunächst schauen wir uns den gegenwärtigen Zustand des Repositories an:
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
Jetzt setzen wir den master Branch zurück auf den 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
Du hast damit die oberen beiden Commits verloren, d.h. es gibt keinen Branch mehr, von dem aus diese Commits erreichbar wären. Um sie wieder herzustellen kannst du einen neuen Branch anlegen, der auf den SHA Hash des obersten (letzten) Commits zeigt. Der Trick besteht darin, diesen letzten Commit Hash heraus zu finden. Es ist ja nicht so, daß du dir jederzeit all die Hashes merken könntest, oder?
In der Regel ist der schnellste Weg, solche Hashes zu finden, der Befehl git reflog. Während du mit Git arbeitest, macht Git im Stillen fortlaufende Notizen darüber, was HEAD ist. Jedes Mal, wenn du einen Commit anlegst oder den Branch wechselst, wird das "Reflog" aktualisiert. Das Reflog wird außerdem vom Befehl git update-ref verwendet - ein weiterer guter Grund, nicht statt dessen einfach den SHA Wert in eine Referenz Datei zu schreiben (wie wir das in der Sektion "Git Referenzen" zuvor in diesem Kapitel besprochen haben). Du kannst also jederzeit nachschlagen, woran du jeweils gearbeitet hast, indem du den Befehl git reflog verwendest:
$ git reflog
1a410ef HEAD@{0}: 1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD
ab1afef HEAD@{1}: ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD
Das zeigt also die beiden verloren gegangenen Commits an, die wir zuvor ausgecheckt hatten. Allerdings zeigt es auch nicht viel mehr Information. Um das Reflog in einer anderen, etwas nützlicheren Weise anzuzeigen, kannst du git log -g verwenden. Das gibt das Reflog im gewohnten Log Format aus:
$ 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 a bit
Es sieht also so aus, als sei der untere Commit derjenige, den du verloren hast, aber noch brauchst. Du kannst ihn jetzt wieder herstellen, indem du einen neuen Branch erstellst, der auf diesen Commit zeigt. Beispielsweise kannst du einen Branch recover-branch anlegen, der auf den Commit ab1afef zeigt:
$ 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. Du hast jetzt einen neuen Branch recover-branch, der diejenigen Commits enthält, die sich zuvor in deinem master Branch befanden. Damit hast du wieder Zugriff auf die beiden verloren gegangenen Commits. Als nächstes nehmen wir aber außerdem an, daß diese verlorenen Commits aus irgendeinem Grunde nicht im Reflog enthalten sind - du kannst das z.B. simulieren, indem du den Branch recover-branch und das Reflog löschst. Damit sind die beiden Commits jetzt von nirgendwo her mehr erreichbar:
$ git branch -D recover-branch
$ rm -Rf .git/logs/
Das Reflog wird im Verzeichnis .git/logs/ aufbewahrt, d.h. du hast nun faktisch kein Reflog mehr. Wie kann man einen Commit jetzt noch wieder herstellen? Eine Möglichkeit dazu ist der Befehl git fsck, der die Git Datenbank Integrität prüft. Wenn du den Befehl mit der Option --full ausführst, zeigt er alle Objekte an, auf die nicht von einem anderen Objekt verwiesen wird:
$ git fsck --full
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
In diesem Fall findest du den verlorenen Commit nach dem (xxx dangling xxx) Commit. Du kannst ihn dann auf die selbe Weise wieder herstellen wie zuvor, indem du einen Branch erstellst, der auf diesen Commit Hash zeigt.
Objekte entfernen
Git ist in vielerlei Hinsicht unschlagbar, aber es gibt auch Features, die Probleme verursachen können. Ein solches Problem kann darin bestehen, daß git clone die vollständige Historie eines Projektes herunter lädt, d.h. jede einzelne Version jeder einzelnen Datei. Das ist eine feine Sache, solange es sich um Quellcode handelt, den Git ist darauf optimiert, diese Art von Daten effizient zu komprimieren. Wenn allerdings irgendwann einmal eine einzelne, sehr große Datei zur Versionskontrolle hinzugefügt wurde, wird jeder Clone dieses Repositories diese Datei gezwungenermaßen herunter laden müssen - auch dann, wenn die Datei inzwischen aus dem Repository entfernt würde. Weil sie über die Historie erreichbar ist, muß die Datei vorhanden sein.
Hierin kann ein großes Problem bestehen, wenn du Subversion oder Perforce Repositories nach Git konvertierst. Weil du in diesen Systemen nicht die gesamte Historie herunter lädst, kann diese Art von (xxx addition??? hu? xxx) einige unangenehme Konsequenzen haben. Wenn du dein Repository aus einem anderen System importiert hast oder aus irgendeinem anderen Grunde findest, daß es sehr viel größer ist als es eigentlich sein sollte, kannst du große Objekte wie folgt suchen und entfernen.
Sei dir allerdings bewußt, daß diese Technik die Commit Historie zerstört. Sie schreibt angefangen beim ursprünglichen Tree jedes einzelne Commit Objekt neu, um die jeweilige, große Datei zu entfernen. Wenn du das direkt nach einem Import tust, d.h. bevor jemand angefangen hat, auf der Basis eines Commits zu arbeiten, ist das in Ordnung - andernfalls müssen alle Mitarbeiter ihre Arbeit auf deinen Commit rebasen.
Um das zu demonstrieren, werden wir eine große Datei zu deinem Test Repository hinzufügen, sie dann im nächsten Commit löschen, in der Datenbank nachschlagen und sie schließlich dauerhaft aus dem Repository entfernen. Als erstes committe also eine große Datei in dein Repository:
$ curl http://kernel.org/pub/software/scm/git/git-1.6.3.1.tar.bz2 > git.tbz2
$ git add git.tbz2
$ git commit -am 'added git tarball'
[master 6df7640] added git tarball
1 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tbz2
Oha. Du wolltest keinen dermaßen großen Tarball in deinem Projekt. Am besten löschen wir es gleich wieder:
$ git rm git.tbz2
rm 'git.tbz2'
$ git commit -m 'oops - removed large tarball'
[master da3f30d] oops - removed large tarball
1 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tbz2
Jetzt lassen wir die Garbage Collection über die Datenbank laufen und sehen, wieviel Platz sie braucht:
$ git gc
Counting objects: 21, done.
Delta compression using 2 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (21/21), done.
Total 21 (delta 3), reused 15 (delta 1)
Du kannst auch den count-objects Befehl laufen lassen, um einen schnellen Überblick darüber zu erhalten, wieviel Platz das Repository einnimmt:
$ git count-objects -v
count: 4
size: 16
in-pack: 21
packs: 1
size-pack: 2016
prune-packable: 0
garbage: 0
Der size-pack Eintrag zeigt die Größe der Packfiles in Kilobytes, d.h. dein Repository braucht 2 MB. Vor dem letzten Commit lag dieser Wert eher bei 2 KB. D.h., die Datei wurde im letzten Commit eindeutig nicht aus der History entfernt. Jedes Mal, wenn jemand künftig dieses sehr kleine Repository clont, wird er die 2 MB mit herunterladen müssen - nur weil wir versehentlich diese große Datei hinzugefügt hatten. Also versuchen wir, sie endgültig los zu werden.
Zunächst mal müssen wir sie finden. In diesem Fall wissen wir bereits, um welche Datei es sich handelt. Aber nehmen wir an, wir wüßten es nicht. Wie würdest du herausfinden, welche Datei (oder welche Dateien) so viel Platz verbrauchen? Wenn du git gc laufen gelassen hast, werden sich alle Objekte in einem Packfile befinden. Du kannst dann große Objekte identifizieren, indem du einen weiteren Plumbing Befehl, nämlich git verify-pack ausführst und nach dem dritten Feld der Ausgabe sortierst, d.h. der Dateigröße. Du kannst sie außerdem durch den tail Befehl leiten, denn du bist ja nur an den wenigen größten Objekten interessiert:
$ git verify-pack -v .git/objects/pack/pack-3f8c0...bb.idx | sort -k 3 -n | tail -3
e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4667
05408d195263d853f09dca71d55116663690c27c blob 12908 3478 1189
7a9eb2fba2b1811321254ac360970fc169ba2330 blob 2056716 2056872 5401
Das größte Objekt ist ganz klar das letzte: es ist 2 MB groß. Um herauszufinden, welche Datei das ist, kannst du den rev-list Befehl verwenden, den wir in Kapitel 7 schon einmal kurz verwendet haben. Wenn du die Optionen --objects und rev-list verwendest, werden alle Commit und Blob SHAs mit den jeweiligen Dateipfaden aufgelistet, die mit ihnen assoziiert sind. Auf diese Weise kannst du den Namen des Blobs finden:
$ git rev-list --objects --all | grep 7a9eb2fb
7a9eb2fba2b1811321254ac360970fc169ba2330 git.tbz2
Du mußt diese Datei jetzt aus allen Trees entfernen, in denen er sich befindet. Du kannst leicht herausfinden, welche Commits diese Datei verändert haben:
$ git log --pretty=oneline --branches -- git.tbz2
da3f30d019005479c99eb4c3406225613985a1db oops - removed large tarball
6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 added git tarball
Es geht also darum, alle Commits angefangen bei 6df76 neu zu schreiben, so daß der Tarball nicht mehr in deiner Git Historie enthalten ist. Um das zu erreichen, verwendest du den Befehl git filter-branch, den wir schon mal in Kapitel 6 verwendet haben:
$ git filter-branch --index-filter \
'git rm --cached --ignore-unmatch git.tbz2' -- 6df7640^..
Rewrite 6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 (1/2)rm 'git.tbz2'
Rewrite da3f30d019005479c99eb4c3406225613985a1db (2/2)
Ref 'refs/heads/master' was rewritten
Die --index-filter Option ist ähnlich der --tree-filter Option aus Kapitel 6. Allerdings übergibt man in diesem Fall nicht einen Befehl, der die Dateien verändert, die sich jeweils ausgecheckt auf der Festplatte befinden. Statt dessen verändert man jeweils die Staging Area bzw. den Index. Statt eine bestimmte Datei mit z.B: rm file zu entfernen, müssen wir also git rm --cached verwenden - denn wir wollen sie aus dem Index, nicht von der Festplatte löschen. Der Grund dafür ist einfach Geschwindigkeit: Git braucht nicht jede einzelne Revision auf die Festplatte auszuchecken, um den Filter anzuwenden. Auf diese Weise läuft der ganze Prozeß sehr viel schneller. Man kann dieselbe Aufgabe aber auch mit --tree-filter erledigen, wenn man will. Die Option --ignore-match weist Git an, nicht mit einer Fehlermeldung abzubrechen, wenn die Datei, die wir löschen wollen, nicht vorhanden ist. Außerdem teilen wir filter-branch mit, die Historie nur von dem Commit 6df7640 an umzuschreiben. Andernfalls würde der Befehl von ganz vorn beginnen und unnötig länger brauchen.
Deine Historie enthält jetzt nicht länger eine Referenz auf diese Datei. Dein Reflog und ein neues Set an Referenzen, die Git unter .git/refs/original angelegt hat, als du filter-branch ausgeführt hast, tun das allerdings immer noch - also entfernen wir sie von dort und packen das Repository erneut. Bevor du packst, mußt du zuerst alles entfernen, was noch Referenzen auf das alte Objekt enthält:
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 19, done.
Delta compression using 2 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (19/19), done.
Total 19 (delta 3), reused 16 (delta 1)
Prüfen wir also, wieviel Platz wir damit eingespart haben:
$ git count-objects -v
count: 8
size: 2040
in-pack: 19
packs: 1
size-pack: 7
prune-packable: 0
garbage: 0
Das gepackte Repository umfaßt jetzt nur noch 7K - sehr viel besser als die vorherigen 2MB. Du kannst an dem Wert size erkennen, daß sich die große Datei selbst jetzt immer noch in deinen loosen Objekten befindet. Aber sie wird bei einem git push oder anschließenden git clone nicht übermittelt werden - und das war unser Ziel. Wenn du das wirklich willst, kannst du sie jetzt vollständig und endgültig mit git prune --expire aus deinem Repository löschen.