Suche
Erste Schritte

Git ist einfach – wenn man weiß, wie

Ben Straub
©shutterstock.com/Krasimira Nevenova

Die Art, wie wir Computersysteme benutzen, ähnelt in gewisser Weise der Verwendung von Metaphern. Wenn man eine Textverarbeitung nutzt, denkt man nicht darüber nach, wie die einzelnen Buchstaben Byte für Byte repräsentiert werden. Genauso wenig, wie man sich die Prozesse vor Augen hält, die bestimmen, dass ein Wort kursiv oder fett dargestellt wird. Die Software abstrahiert all diese Vorgänge und bietet dem User stattdessen ein klar verständliches Interface, eine Metapher: Buchstaben auf einer weißen Seite. Man schreibt die Buchstaben, entscheidet sich für Schriftart und Schriftsatz, und wenn man soweit ist, druckt man das Ganze auf echtem Papier aus.

Ähnlich sieht die Lage aus, wenn man die Belichtung bei einem computergenerierten Foto einstellt: Man denkt nicht über die Zahlenakrobatik nach, die man braucht, um die R, G, und B Bytes jedes einzelnen Pixels zu ändern oder über den Algorithmus, der die Größe und Rotation einstellt. Die Metapher ist in diesem Fall eine Dunkelkammer, in der man Helligkeit und Belichtung bearbeitet, den Rotstich aus den Augen entfernt und den Weinfleck vom Hochzeitskleid wegretouchiert.

Versionskontrollsysteme verhalten sich ähnlich: Man will nicht wissen, wie genau die Daten gespeichert und geladen werden oder wie die Bytes während einer Netzwerkübertragung geordnet werden. Was man will, ist, seinen Code zu schreiben und bei Zeiten einen Schnappschuss an einem sicheren Ort abzuspeichern. Das zugrundeliegende Datenmodell ist kompliziert, und man muss so gut wie nie (wenn überhaupt) wissen, wie das Ganze funktioniert, da das User Interface die Details recht effektiv wegabstrahiert.

Mit Git sieht die Sache nun ein bisschen anders aus. Die Metapher, die man hier zu sehen bekommt, ist ein „directed acyclic graph“ (DAG) aus commit nodes – mit anderen Worten: es gibt keine Metapher. Der DAG ist das Modell, das für die Daten steht. Wenn man die Metapher, die man von anderen Systemen kennt verwendet, gerät man in Schwierigkeiten.

Die gute Neuigkeit: das Datenmodell ist einfach zu verstehen. Wenn man es einmal begriffen hat, wird der Umgang mit Git definitiv leichter.

Objekte

Fast alles in Git ist entweder ein object oder ref.

Mit Objects speichert Git Inhalte. Sie werden in .git/objects gespeichert, die manchmal auch objects database oder ODB genannt wird. Objects in der ODB sind unveränderlich. Hat man einmal eines erstellt, kann man es nicht mehr verändern. Das liegt daran, dass Git das SHA-1 hash eines objects verwendet, um es zu identifizieren und zu finden. Wenn man den Inhalt eines Objects ändern würde, würde sich auch das hash ändern.

Objects gibt es in vier Ausführungen: blobs, trees, commits und tag annotations. Blobs sind unstrukturierte Datenfragmente, die von Git nicht interpretiert sondern einfach gespeichert werden. Objects kann man prinzipiell recht einfach überprüfen (Listing 1).

# Print the object's type
$ git cat-file -t d7abd6
blob

# Print the first 5 lines of the object's content
$ git cat-file -p d7abd6 | head -n 5

End

Aufgepasst: Es gibt hier keinen Dateinamen. Git erwartet, dass man Dateien recht häufig umbenennt; wenn man den Dateinamen mit dem Inhalt einbettet, müsste man eine Vielzahl von Kopien des objects aufbewahren, deren einziger Unterschied der Dateiname wäre.

Man nutzt hier git cat-file, weil Git das Speichern von Objects optimiert hat. Die Objects sind mit gzip komprimiert, manchmal eine ganze Menge davon in großen pack-files. Deshalb kann es passieren, dass man nicht unbedingt etwas findet was einem object ähnelt, wenn man in .git/objects nachschaut.

Die zweite Sorte von objects nennt man tree, das ist die Art wie Git Verzeichnisstrukturen speichert (Listing 2).

$ git cat-file -t 8f5b65
tree

$ git cat-file -p 8f5b65 | head -n 5
100644 blob 08b8e...493c0 after_footer.html
100644 blob 11517...fce19 archive_post.html
100644 blob 8ad5a...5b988 article.html
040000 tree 5c216...c8810 asides
040000 tree 52deb...e3dad custom

End 

Nur die blobs sind unstrukturiert; Git erwartet ein spezifisches Format für alle anderen. Jede Zeile eines tree objects beinhaltet die permission flag der Datei, ihren Typ (blobs sind Dateien, trees sind Unterverzeichnisse), den SHA-1 hash eines objects und einen Dateinamen. Der tree ist also für die Namen und die Lage von Sachen verantwortlich, der blob für deren Inhalte.

Die dritte Art object ist der commit. Er ist quasi ein Schnappschuss in einer Reihe von unterschiedlichen Versionen einer Datei (Listing 3).

$ git cat-file -t e365b1
commit

$ git cat-file -p e365b1
tree 58c796e7717809c2ca2217fc5424fdebdbc121b1
parent d4291dfddfae86cfacec789133861098cebc67d4
author Ben Straub  1380719530 -0700
committer Ben Straub  1380719530 -0700

Fix typo, remove false statement

End

Ein commit hat genau einen tree-Verweis, der auch das Ursprungsverzeichnis des commits ist. Er hat null oder mehr parents, welche Verweise auf andere Parents sind und beinhaltet außerdem noch die Metadaten zum commit – wer ihn wann gemacht hat und worum es bei dem commit geht.

Schließlich gibt es noch eine Art von object, das aber nicht sehr oft genutzt wird. Man nennt es tag annotation und verwendet es, um ein Tag mit Kommentaren zu erstellen.

$ git cat-file -t 849a5e34a
tag

$ git cat-file -p 849a5e34a
object a65fedf39aefe402d3bb6e24df4d4f5fe4547750
type commit
tag hard_tag
tagger Ben Straub  Fri May 11 11:47:58 2012 -0700

Tag on tag

End

Ich werde später noch einmal darauf zu sprechen kommen, im Moment reicht es aus, sich vor Augen zu halten, dass das object SHA dort gespeichert wird.

Das war’s! Wie Ihr seht könnt ihr die objects an einer Hand abzählen – so einfach ist das Ganze.

Refs

Verweise /(engl.: references oder refs) sind nichts weiter als Anhaltspunkte, die auf andere objects oder refs hinweisen. Sie bestehen aus zwei Informationen: dem Namen des refs und dem Ort des Verweises. Wenn ein ref auf ein object verweist, nennt man das direct ref, wenn er auf einen anderen ref verweist heißt er symbolic ref.

Die meisten refs sind direkt. Um sich im Zweifelsfall zu vergewissern checkt man einfach alle Inhalte unter .git/refs/heads; direct refs sind einfache Textdateien, deren Inhalt aus dem SHA hash des commits bestehen auf den sie verweisen.

$ cat .git/refs/heads/master

2b67270f960563c55dd6c66495517bccc4f7fb17

Git verwendet auch ein paar symbolic refs für spezielle Zwecke. Einer der am häufigsten verwendeten ist HEAD, der auf den branch verweist, den man als letztes ausgecheckt hat:

$ cat HEAD
ref: refs/heads/master

Jetzt, da wir wissen, wie refs funktionieren, können wir uns noch einmal dem tag annotation object zuwenden, das uns vorher begegnet ist. Noch einmal zur Auffrischung: refs sind im Grunde nur Namen für Locations; es sind keine Kommentare mit ihnen assoziiert, und man kann sie jederzeit ändern. Tag annotations lösen diese beiden Probleme, indem sie ref-ähnliche Informationen in die ODB einschließen (damit wird es unveränderlich und kann auch mehr Inhalte speichern) und indem sie die Informationen mittels eines regulären Tags auffindbar machen. Das Ganze sieht im Endeffekt so aus: 

tag (ref) --> tag_ann (odb) --> commit

Damit steht einem ein ganzes Universum an Möglichkeiten offen: refs müssen nicht unbedingt auf commits verweisen, sondern können als Bezugspunkt für alle möglichen Arten von objects dienen. Im Prinzip könnte man damit etwas der folgenden Art aufsetzen (wobei nicht ganz klar ist warum man das tun sollte): 

branch --> tag --> tag_ann_a --> tag_ann_b --> blob

Drei trees

Tree objects in der ODB sind nicht die einzigen trees, die von Git erkannt werden. Während der täglichen Arbeit beschäftigt man sich für gewöhnlich mit drei von ihnen: HEAD, Index und work tree. 

HEAD ist der letzte commit, den man gemacht hat und damit der parent des nächsten commits. Im Prinzip ist es ein symbolic ref, der auf den branch hinweist in dem man sich befindet, der wiederum auf den letzten commit verweist. Für unsere Zwecke vereinfachen wir das ganze ein wenig.

Der Index ist ein Vorschlag für den nächsten commit. Wenn man einen Checkout macht, kopiert Git den HEAD tree in den Index. Wenn man jetzt commit –m `foo´ eingibt nimmt Git alles was im Index ist und speichert es im ODB als tree des neuen commits.

Der work tree ist eine Umgebung zum Herumexperimentieren, also eine Sandbox. Hier kann man nach Belieben Dateien erstellen, updaten und löschen, da Git Backups von Allem anfertigt. Auf diese Weise macht Git die Inhalte für andere Tools verfügbar.

Es gibt ein paar Befehle, die sich hauptsächlich mit diesen drei Sorten von trees befassen:

  • git checkout: kopiert den Inhalt vom HEAD in den Index und den work tree. Man kann auch zunächst den HEAD bewegen.
  • git add: kopiert Inhalte vom work tree in den Index
  • git commit: kopiert Inhalt vom Index in den HEAD

Reset ist jetzt einfacher

Jetzt, da wir einige Dinge wissen, fangen manche der zunächst merkwürdig erscheinenden Git-Befehle an, Sinn zu ergeben. Ein Beispiel dafür ist git reset, eines der gefürchtetsten und gehassten Befehle in Git. Im Allgemeinen beinhaltet Reset drei Schritte: 

  1. Bewege HEAD (und den Pfad auf den es verweist) um auf ein anderes commit zu verweisen
  2. Mach ein Update vom Index um an die Inhalte vom HEAD zu kommen
  3. Macht ein Update vom work tree um an die Inhalte vom Index zu kommen

 

Mit einigen etwas merkwürdig bezeichneten Kommandozeilen-Optionen kann man auswählen wann der Vorgang aufhört.

  • git reset — soft hört nach Schritt 1 auf. HEAD und der Pfad von dem man ausgecheckt hat, werden bewegt aber das ist alles.
  • git reset — mixed hört nach Schritt 2 auf. Der work tree ist überhaupt nicht betroffen, aber HEAD und Index schon. Das ist auch die Grundeinstellung von reset, das —mixed-Argument ist optional.
  • git reset — hard führt alle drei Schritte aus. Nach den ersten zwei wird der work tree mit den Inhalten aus dem Index überschritten.

Wenn man reset mit einem Pfad verwendet, überspringt Git den ersten Schritt, da das Bewegen vom HEAD eine komplette Speicheroperation ist. Die anderen Schritte werden aber ausgeführt; –mixed updatet den Index mit der HEAD-Version der Datei und –hard aktualisiert das Arbeitsverzeichnis, womit im Prinzip alle Modifikationen, die man seit dem letzten Checkout an der Datei vorgenommen hat, verworfen werden. 

Das Alltagsmuster

Mit unserer neu erworbenen Git-Röntgenbrille können wir uns jetzt ein extrem häufig vorkommendes Muster vornehmen. 

$ git checkout -b bug-2345 master

Git erstellt einen neuen Zweig namens bug-2345 und verweist auf den selben commit auf den auch der Master verweist. Anschließend bewegt es den HEAD um auf bug-2345 zu verweisen und updatet den Index und den work tree, damit diese dem HEAD entsprechen.

Man ein bisschen damit arbeiten, ändert dann die Dateien im work tree und ist anschließend bereit einen commit zu machen.

$ git add foo.txt
$ git add -p bar.html

Git aktualisiert den Index, damit er den Inhalten des work tree entspricht. Man kann ihn auch updaten, wenn er nur einige der Änderungen in der Datei beinhaltet.

$ git commit -m 'Update foo and bar'

Git konvertiert den Index in eine Reihe von verlinkten objects in der ODB. Blobs und trees, deren Inhalte übereinstimmen werden wiederverwendet, während die geänderten Dateien und Verzeichnisse neue blobs und trees bekommen. Git erstellt dann ein neues commit, das auf den neuen root tree verweist und (da HEAD auf einen Pfad verweist) bewegt wird um auf  den neuen commit zu verweisen.

Jetzt seid Ihr dran

Die meisten Versionskontrollsysteme belassen es dabei, dass der Nutzer das User Interface kennenlernt – die Details werden wegabstrahiert. Git ist anders. Das grundlegende Datenmodell ist auf einem Level, das hoch genug ist um die Sache leicht zu verstehen, während das User Interface so dünn ist, dass man die Interna versteht ob man will oder nicht. Wenn man mehr als das allernötigste mit Git machen will kommt man da auch nicht drum herum. Mit diesem Artikel konnte ich Euch hoffentlich überzeugen, dass es nicht so viel zu lernen gibt und dass man dabei viele neue Fähigkeiten erwirbt.

Euer neues Wissen macht Euch stark. Nutzt es, um Gutes zu tun!

Ben Straub war schon immer Programmierer und liebt es Dinge zum Laufen zu bringen. Er hackt verschiedene Dinge auf dem Github.

Von der JAXenter-Redaktion aus dem Englischen übersetzt.

Aufmacherbild: We Make It Easy Concept von Shutterstock / Urheberrecht: Krasimira Nevenova

Geschrieben von
Ben Straub
Kommentare

Hinterlasse einen Kommentar

1 Kommentar auf "Git ist einfach – wenn man weiß, wie"

avatar
400
  Subscribe  
Benachrichtige mich zu:
trackback

[…] Article “Git ist einfach – wenn man weiß, wie” (in German) This article describes how the data model of Git works. Understanding the data model helps me a lot with the handling with Git. […]