Learn Git

Was ist ein Branch?

Um wirklich zu verstehen wie Git Branching durchführt, müssen wir einen Schritt zurück gehen und untersuchen wie Git die Daten speichert. Wie Du Dich vielleicht noch an Kapitel 1 erinnerst, speichert Git seine Daten nicht als Serie von Änderungen oder Unterschieden, sondern als Serie von Schnappschüssen.

Wenn Du in Git committest, speichert Git ein sogenanntes Commit-Objekt. Dieses enthält einen Zeiger zu dem Schnappschuss mit den Objekten der Staging-Area, dem Autor, den Commit-Metadaten und einem Zeiger zu den direkten Eltern des Commits. Ein initialer Commit hat keine Eltern-Commits, ein normaler Commit stammt von einem Eltern-Commit ab und ein Merge-Commit, welcher aus einer Zusammenführung von zwei oder mehr Branches resultiert, besitzt ebenso viele Eltern-Commits.

Um das zu verdeutlichen, lass uns annehmen, Du hast ein Verzeichnis mit drei Dateien, die Du alle zu der Staging-Area hinzufügst und in einem Commit verpackst. Durch das Stagen der Dateien erzeugt Git für jede Datei eine Prüfsumme (der SHA-1 Hash, den wir in Kapitel 1 erwähnt haben), speichert diese Version der Datei im Git-Repository (Git referenziert auf diese als Blobs) und fügt die Prüfsumme der Staging-Area hinzu:

$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'

Wenn Du einen Commit mit dem Kommando git commit erstellst, erzeugt Git für jedes Projektverzeichnis eine Prüfsumme und speichert diese als sogenanntes tree-Objekt im Git Repository. Git erzeugt dann ein Commit Objekt, das die Metadaten und den Zeiger zum tree-Objekt des Wurzelverzeichnis enthält, um bei Bedarf den Snapshot erneut erzeugen zu können.

Dein Git-Repository enthält nun fünf Objekte: einen Blob für den Inhalt jeder der drei Dateien, einen Baum, der den Inhalt des Verzeichnisses auflistet und spezifiziert welcher Dateiname zu welchem Blob gehört, sowie einen Zeiger, der auf die Wurzel des Projektbaumes und die Metadaten des Commits verweist. Prinzipiell sehen Deine Daten im Git Repository wie in Abbildung 3-1 aus.

Abbildung 3-1. Repository-Daten eines einzelnen Commits.

Wenn Du erneut etwas änderst und wieder einen Commit machst, wird dieser einen Zeiger enthalten, der auf den Vorhergehenden verweist. Nach zwei weiteren Commits könnte die Historie wie in Abbildung 3-2 aussehen.

Abbildung 3-2. Git Objektdaten für mehrere Commits.

Ein Branch in Git ist nichts anderes als ein simpler Zeiger auf einen dieser Commits. Der Standardname eines Git-Branches lautet master. Mit dem initialen Commit erhältst Du einen master-Branch, der auf Deinen letzten Commit zeigt. Mit jedem Commit bewegt er sich automatisch vorwärts.

Abbildung 3-3. Branch, der auf einen Commit in der Historie zeigt.

Was passiert, wenn Du einen neuen Branch erstellst? Nun, zunächst wird ein neuer Zeiger erstellt. Sagen wir, Du erstellst einen neuen Branch mit dem Namen testing. Das machst Du mit dem git branch Befehl:

$ git branch testing

Dies erzeugt einen neuen Zeiger, der auf den gleichen Commit zeigt, auf dem Du gerade arbeitest (siehe Abbildung 3-4).

Abbildung 3-4. Mehrere Branches zeigen in den Commit-Verlauf

Woher weiß Git, welchen Branch Du momentan verwendest? Dafür gibt es einen speziellen Zeiger mit dem Namen HEAD. Berücksichtige, dass dieses Konzept sich grundsätzlich von anderen HEAD-Konzepten anderer VCS, wie Subversion oder CVS, unterscheidet. Bei Git handelt es sich bei HEAD um einen Zeiger, der auf Deinen aktuellen lokalen Branch zeigt. In dem Fall bist Du aber immer noch auf dem master-Branch. Das git branch Kommando hat nur einen neuen Branch erstellt, aber nicht zu diesem gewechselt (siehe Abbildung 3-5).

Abbildung 3-5. Der HEAD-Zeiger verweist auf Deinen aktuellen Branch.

Um zu einem anderen Branch zu wechseln, benutze das Kommando git checkout. Lass uns nun zu unserem neuen Branch testing wechseln:

$ git checkout testing

Das lässt HEAD neuerdings auf den Branch „testing“ verweisen (siehe Abbildung 3-6).

Abbildung 3-6. Wenn Du den Branch wechselst, zeigt HEAD auf einen neuen Zweig.

Und was bedeutet das? Ok, lass uns noch einen weiteren Commit machen:

$ vim test.rb
$ git commit -a -m 'made a change'

Abbildung 3-7 verdeutlicht das Ergebnis.

Abbildung 3-7. Der HEAD-Zeiger schreitet mit jedem weiteren Commit voran.

Das ist interessant, denn Dein Branch testing hat sich jetzt voranbewegt und Dein master-Branch zeigt immer noch auf seinen letzten Commit. Den Commit, den Du zuletzt bearbeitet hattest, bevor Du mit git checkout den aktuellen Zweig gewechselt hast. Lass uns zurück zu dem master-Branch wechseln:

$ git checkout master

Abbildung 3-8 zeigt das Ergebnis.

Abbildung 3-8. HEAD zeigt nach einem Checkout auf einen anderen Branch.

Das Kommando hat zwei Dinge veranlasst. Zum einen bewegt es den HEAD-Zeiger zurück zum master-Branch, zum anderen setzt es alle Dateien im Arbeitsverzeichnis auf den Bearbeitungsstand des letzte Commits in diesem Zweig zurück. Das bedeutet aber auch, dass nun alle Änderungen am Projekt vollkommen unabhängig von älteren Projektversionen erfolgen. Kurz gesagt, werden alle Änderungen aus dem testing-Zweig vorübergehend rückgängig gemacht und Du hast die Möglichkeit einen vollkommen neuen Weg in der Entwicklung einzuschlagen.

Lass uns ein paar Änderungen machen und mit einem Commit festhalten:

$ vim test.rb
$ git commit -a -m 'made other changes'

Nun verzweigen sich die Projektverläufe (siehe Abbildung 3-9). Du hast einen Branch erstellt und zu ihm gewechselt, hast ein bisschen gearbeitet, bist zu Deinem Haupt-Zweig zurückgekehrt und hast da was ganz anderes gemacht. Beide Arbeiten existieren vollständig unabhängig voneinander in zwei unterschiedlichen Branches. Du kannst beliebig zwischen den beiden Zweigen wechseln und sie zusammenführen, wenn Du meinst es wäre soweit. Und das alles hast Du mit simplen branch und checkout-Befehlen vollbracht.

Abbildung 3-9. Die Historie läuft auseinander.

Branches können in Git spielend erstellt und entfernt werden, da sie nur kleine Dateien sind, die eine 40 Zeichen lange SHA-1 Prüfsumme der Commits enthalten, auf die sie verweisen. Einen neuen Zweig zu erstellen erzeugt ebenso viel Aufwand wie das Schreiben einer 41 Byte großen Datei (40 Zeichen und einen Zeilenumbruch).

Das steht im krassen Gegensatz zu dem Weg, den die meisten andere VCS Tools beim Thema Branching einschlagen. Diese kopieren oftmals jeden neuen Entwicklungszweig in ein weiteres Verzeichnis, was – je nach Projektgröße – mehrere Minuten in Anspruch nehmen kann, wohingegen Git diese Aufgabe sofort erledigt. Da wir außerdem immer den Ursprungs-Commit festhalten, lässt sich problemlos eine gemeinsame Basis für eine Zusammenführung finden und umsetzen. Diese Eigenschaft soll Entwickler ermutigen Entwicklungszweige häufig zu erstellen und zu nutzen.

Lass uns mal sehen, warum Du das machen solltest.