Kapitel ▾ 2. Auflage

3.1 Git-Branching - Branches im Überblick

Fast jedes VCS hat eine Form der Branching-Unterstützung. Branching bedeutet, dass Sie von der Hauptentwicklungslinie abweichen und Ihre Arbeit fortsetzen, ohne die Hauptlinie zu beeinträchtigen. In vielen VCS-Tools ist dies ein eher kostspieliger Prozess, der oft das Erstellen einer neuen Kopie Ihres Quellcodeverzeichnisses erfordert, was bei großen Projekten lange dauern kann.

Manche Leute bezeichnen das Branching-Modell von Git als sein „Killer-Feature", und es unterscheidet Git sicherlich von anderen VCS-Tools. Warum ist es so besonders? Die Art und Weise, wie Git Branches erstellt, ist unglaublich leichtgewichtig, was Branching-Operationen fast augenblicklich macht und das Hin- und Herwechseln zwischen Branches generell ebenso schnell ermöglicht. Im Gegensatz zu vielen anderen VCSs fördert Git Workflows, bei denen häufig gebrancht und zusammengeführt wird, sogar mehrmals am Tag. Das Verständnis und die Beherrschung dieser Funktion geben Ihnen ein leistungsfähiges und einzigartiges Werkzeug an die Hand und können die Art und Weise, wie Sie entwickeln, grundlegend verändern.

Branches im Überblick

Um die Funktionsweise von Git-Branching wirklich zu verstehen, müssen wir einen Schritt zurücktreten und untersuchen, wie Git seine Daten speichert.

Wie Sie sich vielleicht aus Was ist Git? erinnern, speichert Git Daten nicht als eine Reihe von Changesets oder Unterschieden, sondern stattdessen als eine Reihe von Snapshots.

Wenn Sie einen Commit erstellen, speichert Git ein Commit-Objekt, das einen Zeiger auf den Snapshot des Inhalts enthält, den Sie vorbereitet haben. Dieses Objekt enthält auch den Namen und die E-Mail-Adresse des Autors, die von Ihnen eingegebene Nachricht und Zeiger auf den Commit oder die Commits, die direkt vor diesem Commit kamen (sein Elternteil oder seine Eltern): Null Eltern für den initialen Commit, ein Elternteil für einen normalen Commit und mehrere Eltern für einen Commit, der aus dem Zusammenführen von zwei oder mehr Branches resultiert.

Um dies zu visualisieren, nehmen wir an, Sie haben ein Verzeichnis mit drei Dateien, und Sie bereiten sie alle vor und committen sie. Das Vorbereiten der Dateien berechnet einen Checksummenwert für jede Datei (den SHA-1-Hash, den wir in Was ist Git? erwähnt haben), speichert diese Version der Datei im Git-Repository (Git nennt sie Blobs) und fügt diesen Checksummenwert dem Staging-Bereich hinzu.

$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'

Wenn Sie den Commit erstellen, indem Sie git commit ausführen, berechnet Git die Checksumme für jedes Unterverzeichnis (in diesem Fall nur das Hauptprojektverzeichnis) und speichert diese als Baumobjekt im Git-Repository. Git erstellt dann ein Commit-Objekt, das die Metadaten und einen Zeiger auf den Hauptprojektbaum enthält, damit es diesen Snapshot bei Bedarf wiederherstellen kann.

Ihr Git-Repository enthält nun fünf Objekte: drei Blobs (jeder repräsentiert den Inhalt einer der drei Dateien), einen Baum, der die Inhalte des Verzeichnisses auflistet und angibt, welche Dateinamen als welche Blobs gespeichert sind, und einen Commit mit dem Zeiger auf diesen Hauptbaum und allen Commit-Metadaten.

A commit and its tree
Abbildung 9. Ein Commit und sein Baum

Wenn Sie einige Änderungen vornehmen und erneut committen, speichert der nächste Commit einen Zeiger auf den Commit, der unmittelbar davor kam.

Commits and their parents
Abbildung 10. Commits und ihre Eltern

Ein Branch in Git ist einfach ein leichtgewichtiger, beweglicher Zeiger auf einen dieser Commits. Der Standard-Branchname in Git ist master. Wenn Sie beginnen, Commits zu erstellen, erhalten Sie einen master-Branch, der auf den letzten von Ihnen erstellten Commit zeigt. Jedes Mal, wenn Sie committen, bewegt sich der master-Branch-Zeiger automatisch vorwärts.

Hinweis

Der „master“-Branch in Git ist kein spezieller Branch. Er ist genau wie jeder andere Branch. Der einzige Grund, warum fast jedes Repository einen hat, ist, dass der Befehl git init ihn standardmäßig erstellt und die meisten Leute sich nicht die Mühe machen, ihn zu ändern.

A branch and its commit history
Abbildung 11. Ein Branch und seine Commit-Historie

Einen neuen Branch erstellen

Was passiert, wenn Sie einen neuen Branch erstellen? Nun, das Erstellen eines neuen Branches erstellt einen neuen Zeiger, den Sie bewegen können. Nehmen wir an, Sie möchten einen neuen Branch namens testing erstellen. Dies tun Sie mit dem Befehl git branch.

$ git branch testing

Dies erstellt einen neuen Zeiger auf denselben Commit, auf dem Sie sich gerade befinden.

Two branches pointing into the same series of commits
Abbildung 12. Zwei Branches, die auf dieselbe Reihe von Commits zeigen

Woher weiß Git, auf welchem Branch Sie sich gerade befinden? Es verwaltet einen speziellen Zeiger namens HEAD. Beachten Sie, dass dies sich stark von dem Konzept von HEAD in anderen VCSs unterscheidet, die Sie vielleicht gewohnt sind, wie Subversion oder CVS. In Git ist dies ein Zeiger auf den lokalen Branch, auf dem Sie sich gerade befinden. In diesem Fall sind Sie immer noch auf master. Der Befehl git branch hat nur einen neuen Branch erstellt – er hat nicht zu diesem Branch gewechselt.

HEAD pointing to a branch
Abbildung 13. HEAD zeigt auf einen Branch

Sie können dies leicht sehen, indem Sie einen einfachen Befehl git log ausführen, der Ihnen zeigt, worauf die Branch-Zeiger zeigen. Diese Option heißt --decorate.

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit

Sie sehen die Branches master und testing, die sich direkt neben dem Commit f30ab befinden.

Branches wechseln

Um zu einem vorhandenen Branch zu wechseln, führen Sie den Befehl git checkout aus. Wechseln wir zum neuen testing-Branch.

$ git checkout testing

Dies verschiebt HEAD, sodass es auf den testing-Branch zeigt.

HEAD points to the current branch
Abbildung 14. HEAD zeigt auf den aktuellen Branch

Was ist die Bedeutung davon? Nun, machen wir einen weiteren Commit.

$ vim test.rb
$ git commit -a -m 'Make a change'
The HEAD branch moves forward when a commit is made
Abbildung 15. Der HEAD-Branch bewegt sich bei einem Commit vorwärts

Das ist interessant, denn jetzt hat sich Ihr testing-Branch vorwärts bewegt, aber Ihr master-Branch zeigt immer noch auf den Commit, auf dem Sie sich befanden, als Sie git checkout ausgeführt haben, um den Branch zu wechseln. Wechseln wir zurück zum master-Branch.

$ git checkout master
Hinweis
git log zeigt nicht immer alle Branches an

Wenn Sie jetzt git log ausführen würden, fragen Sie sich vielleicht, wohin der von Ihnen gerade erstellte "testing"-Branch verschwunden ist, da er nicht in der Ausgabe erscheint.

Der Branch ist nicht verschwunden; Git weiß einfach nicht, dass Sie an diesem Branch interessiert sind, und versucht, Ihnen zu zeigen, wofür es hält, dass Sie sich interessieren. Mit anderen Worten, standardmäßig zeigt git log nur die Commit-Historie unterhalb des von Ihnen ausgecheckten Branches an.

Um die Commit-Historie für den gewünschten Branch anzuzeigen, müssen Sie ihn explizit angeben: git log testing. Um alle Branches anzuzeigen, fügen Sie --all zu Ihrem Befehl git log hinzu.

HEAD moves when you checkout
Abbildung 16. HEAD bewegt sich beim Auschecken

Dieser Befehl hat zwei Dinge getan. Er hat den HEAD-Zeiger zurückbewegt, um auf den master-Branch zu zeigen, und er hat die Dateien in Ihrem Arbeitsverzeichnis auf den Snapshot zurückgesetzt, auf den master zeigt. Das bedeutet auch, dass die Änderungen, die Sie von diesem Zeitpunkt an vornehmen, von einer älteren Version des Projekts abweichen werden. Es spult im Wesentlichen die Arbeit zurück, die Sie in Ihrem testing-Branch geleistet haben, damit Sie eine andere Richtung einschlagen können.

Hinweis
Das Wechseln von Branches ändert Dateien in Ihrem Arbeitsverzeichnis

Es ist wichtig zu beachten, dass sich Dateien in Ihrem Arbeitsverzeichnis ändern, wenn Sie in Git die Branches wechseln. Wenn Sie zu einem älteren Branch wechseln, wird Ihr Arbeitsverzeichnis auf den Zustand zurückgesetzt, den es beim letzten Commit auf diesem Branch hatte. Wenn Git dies nicht sauber durchführen kann, wird es Ihnen den Wechsel überhaupt nicht erlauben.

Machen wir ein paar Änderungen und committen wir erneut.

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

Jetzt hat sich Ihre Projekt-Historie aufgespalten (siehe Aufgespaltene Historie). Sie haben einen Branch erstellt und zu ihm gewechselt, einige Arbeiten daran ausgeführt und dann zu Ihrem Hauptbranch zurückgewechselt und andere Arbeiten erledigt. Beide Änderungen sind in separaten Branches isoliert: Sie können zwischen den Branches hin- und herwechseln und sie zusammenführen, wenn Sie bereit sind. Und das alles haben Sie mit einfachen Befehlen branch, checkout und commit erledigt.

Divergent history
Abbildung 17. Aufgespaltene Historie

Sie können dies auch leicht mit dem Befehl git log sehen. Wenn Sie git log --oneline --decorate --graph --all ausführen, wird die Historie Ihrer Commits ausgegeben, wobei angezeigt wird, worauf Ihre Branch-Zeiger zeigen und wie sich Ihre Historie aufgespalten hat.

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Make other changes
| * 87ab2 (testing) Make a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 Initial commit of my project

Da ein Branch in Git tatsächlich eine einfache Datei ist, die die 40 Zeichen lange SHA-1-Prüfsumme des Commits enthält, auf den sie zeigt, sind Branches günstig zu erstellen und zu löschen. Das Erstellen eines neuen Branches ist so schnell und einfach wie das Schreiben von 41 Bytes in eine Datei (40 Zeichen und ein Zeilenumbruch).

Dies steht in scharfem Gegensatz dazu, wie die meisten älteren VCS-Tools verzweigen, was das Kopieren aller Projektdateien in ein zweites Verzeichnis beinhaltet. Dies kann je nach Projektgröße Sekunden oder sogar Minuten dauern, während es in Git immer augenblicklich ist. Da wir die Eltern beim Committen aufzeichnen, wird die Ermittlung einer geeigneten Merge-Basis zum Zusammenführen automatisch für uns erledigt und ist im Allgemeinen sehr einfach. Diese Funktionen fördern die häufige Erstellung und Nutzung von Branches durch Entwickler.

Mal sehen, warum Sie das tun sollten.

Hinweis
Einen neuen Branch erstellen und gleichzeitig zu ihm wechseln

Es ist üblich, einen neuen Branch zu erstellen und sofort zu diesem neuen Branch wechseln zu wollen – dies kann in einem einzigen Vorgang mit git checkout -b <neuerbranchname> erfolgen.

Hinweis

Ab Git Version 2.23 können Sie git switch anstelle von git checkout verwenden, um

  • Zu einem vorhandenen Branch wechseln: git switch testing-branch.

  • Einen neuen Branch erstellen und zu ihm wechseln: git switch -c new-branch. Das Flag -c steht für create, Sie können auch das vollständige Flag verwenden: --create.

  • Zurück zum zuletzt ausgecheckten Branch wechseln: git switch -.