Einführung in Git (Teil 2): Branching und Merging

Einführung in Git (Teil 2): Branching und Merging

Letztens hatte ich bereits ein wenig zu den Grundlagen der Versionsverwaltung mit Git erzählt. In diesem Artikel möchte ich das Theme ein wenig fortsetzen, und mich dabei vor allem auf das Arbeiten mit Zweigen konzentrieren.

  1. Was ist eigentlich ein Commit?
  2. Und was ist jetzt ein Zweig?
  3. Wie führe ich Zweige wieder zusammen?
  4. Was gibt es sonst noch?
  5. Was tun bei Konflikten?
  6. Zusammenfassung

Ein interessanter Fakt am Rande: In Büchern zu „klassischen“ SCM-Systemen wie CVS oder Subversion findet sich der Abschnitt zum Thema „Branching und Merging“ in der Regel – wenn überhaupt – erst ganz am Ende. Bei den meisten Git-Büchern hingegen findet sich der entsprechende Abschnitt zum selben Thema ganz am Anfang bei den Grundlagen. Man sieht: Das Arbeiten mit Zweigen nimmt bei Git einen deutlich höheren Stellenwert ein als bei anderen SCM-Systemen.

Was ist eigentlich ein Commit?

Bevor es ans Eingemachte geht, vorher noch ein paar notwendige Grundlagen. Zunächst möchte ich noch ein wenig näher darauf eingehen, was ein Git-Commit überhaupt ist. Wie schon erläutert, ist ein Git-Commit ein Schnappschuss eines bestimmten Versionsstandes eures Projekts.

Die nebenstehende Abbildung zeigt den genauen Aufbau eines Git-Commits. Zunächst einmal hat jeder Commit einen Verfasser (engl. Author) und einen Committer (im Unterschied zu SVN unterscheidet Git zwischen dem Urheber eines Commits und demjenigen, der den Commit erstellt hat).

Verzeichnisse und Dateien werden von Git als sogenannte Trees und Blobs (kurz für Binary Large Objects) dargestellt. Jeder Blob wird über die SHA1-Prüfsumme seiner Inhalte identifiziert und im Repository auch unter dieser Prüfsumme gespeichert. Dies hat einen angenehmen Nebeneffekt: Taucht in vielen Commits dieselbe Datei in unveränderter Form auf, speichert Git diese Datei trotzdem nur als einen einzelnen Blob, da die Prüfsumme immer gleich bleibt.

Ein Tree-Objekt wiederum ist für Git einfach nur eine Liste von anderen Tree-Objekten und Blobs. Auch ein Tree wird über eine SHA1-Prüfsumme identifiziert. Ein Commit enthält schließlich wiederum einen Zeiger auf ein Tree-Objekt, welches aus der obersten Verzeichnisebene des Repositories gebildet wird.

Falls ihr Lust habt, könnt ihr das mit einigen Git-Konsolenkommandos sogar einmal ausprobieren. git hash-object liefert euch die SHA1-ID einer Datei, und git show kann diese Datei dann wieder ausgeben:

> git hash-object README.md
bb67827a89afb7ce5110521f92d0662f0478fb53

> ls
 .git/objects/bb/67827a89afb7ce5110521f92d0662f0478fb53
 .git/objects/bb/67827a89afb7ce5110521f92d0662f0478fb53

> git show bb67827a89afb7ce5110521f92d0662f0478fb53
# Inhalt von README.md


Wenn das Repository bereits einen Commit enthält, und ihr anschließend weitere Commits erstellt, enthält jeder Commit zusätzlich einen Zeiger auf seinen Vorgänger-Commit. Grundsätzlich hat jeder Commit erstmal genau einen Vorgänger-Commit. Ausnahmen bilden der erste Commit (der logischerweise keinen Vorgänger haben kann) und Merge-Commits (die mehrere Vorgänger haben können; dazu später mehr).

Eine Git-History ist also letzten Endes nichts anderes als eine Reihe von miteinander verzeigerten Commits (der Informatiker beschreibt diese Struktur als gerichteten azyklischen Graphen, kurz DAG).

Und was ist jetzt ein Zweig?

In Git ist ein Zweig nun nichts anderes als ein „Zeiger“, der auf einen bestimmten Commit in diesem Graphen zeigt. Bereits beim ersten Commit erstellt Git automatisch einen neuen Zweig: den „master“-Zweig. Der „master“-Zweig wandert automatisch mit, wenn neue Commits erstellt werden (in den nachfolgenden Beispielen verwende ich einfache Buchstaben, um die Commit-IDs zu beschreiben; in der Praxis sind diese natürlich länger).

         (master)                                 (master)
          |                                        |
A -- B -- C   --[ git commit ]-->   A -- B -- C -- D

Mit dem Befehl git branch kann nun ein neuer Zweig erstellt werden. Git legt hierzu intern einfach nur einen neuen Zeiger an, der auf den aktuellen Commit zeigt:

         (master)                                    (master)
          |                                           |
A -- B -- C   --[ git branch testing ]-->   A -- B -- C
                                                      |
                                                     (testing)

Woher weiß Git nun, mit welchem Zweig aktuell gearbeitet werden soll? Git verwaltet hierzu einen weiteren Zeiger: den sogenannten HEAD-Zeiger. Der HEAD-Zeiger zeigt auf einen bestimmten Zweig und markiert den Zweig, mit dem aktuell gearbeitet wird. Anfangs wird der HEAD immer auf den master-Zweig zeigen (und deshalb zeigt der mit git branch erstellte Zweig auch auf denselben Commit wie der master-Zweig).

Auf einen anderen Zweig kann nun mit dem Befehl git checkout gewechselt werden:

         (HEAD)
          |
         (master)                                      (master)
          |                                             |
A -- B -- C   --[ git checkout testing ]-->   A -- B -- C
          |                                             |
         (testing)                                     (testing)
                                                        |
                                                       (HEAD)

Was das Ganze soll, seht ihr recht gut, wenn ihr jetzt noch einen neuen Commit im testing-Zweig erstellt:

         (master)                             (master)
          |                                   |
A -- B -- C   --[ git commit ]-->   A -- B -- C -- D
          |                                        |
         (testing)                                (testing)
          |                                        |
         (HEAD)                                   (HEAD)

Wie ihr sehen könnt, ist der testing-Zeiger nun mit dem neuen Commit weitergewandert, während der master-Zeiger immer noch auf denselben Commit zeigt.

Machen wir die Sache noch ein wenig spannender: Was passiert, wenn ihr jetzt per git checkout master wieder auf den master-Zweig zurückwechselt und noch einen weiteren Commit erstellt?

                                                    (HEAD)
                                                     |
         (HEAD)                                     (master)
          |                                          |
         (master)                                    E
          |                                         /
A -- B -- C -- D   --[ git commit ]-->   A -- B -- C -- D
               |                                        |
              (testing)                                (testing)

An diesem Punkt ist jetzt tatsächlich eine echte Verzweigung entstanden (im Git-Sprech heißt es hier, dass die beiden Zweige divergiert sind).

Wie führe ich Zweige wieder zusammen?

Um beide Zweige wieder zusammenzuführen, kann nun der Befehl git merge verwendet werden:

           (HEAD)                                                (HEAD)
            |                                                     |
           (master)                                              (master)
            |                                                     |
            E                                                E -- F
           /                                                /    /
A -- B -- C -- D   --[ git merge testing ]-->    A -- B -- C -- D
               |                                                |
              (testing)                                        (testing)

Hoppla, was ist nun passiert? Git hat beim Zusammenführen der Zweige einen neuen Commit erstellt (in diesem Beispiel den Commit F), der sowohl Commit E (dort zeigte vorher der master-Zeiger hin) als auch Commit D (der testing-Zeiger) als Vorgänger-Commit hat.

Beim Zusammenführen geht Git wie folgt vor: Git betrachtet zunächst den Commit, auf den der aktuelle HEAD zeigt (hier Commit E) und den Commit, auf den der zu mergende Zweig zeigt (hier Commit D). Anschließend sucht Git den letzten gemeinsamen Vorfahren dieser beiden Commits (also von den Zweigspitzen aus gesehen, den ersten Commit, der von beiden Zweigen aus erreichbar ist; hier ist das Commit C).

Aus diesen drei Commits versucht Git nun, die Änderungen jedes Zweiges zu ermitteln, und fasst diese Änderungen in einem neuen Commit — einem sogenannten Merge-Commit — zusammen. Anschließend wurde der gerade ausgecheckte Zweig auf den neuen Merge-Commit vorgesetzt. Weil Git für diesen Merge genau drei Commits berücksichtigt, wird dieses Verfahren auch als Dreiwege-Merge bezeichnet.

Was gibt es sonst noch?

Es sei einmal angenommen, ihr wollt zwei Zweige zusammenführen, die noch gar nicht divergiert sind. Nehmen wir dazu eines der vorherigen Beispiele noch einmal her:

         (HEAD)                                               (HEAD)
          |                                                    |
         (master)                                             (master)
          |                                                    |
A -- B -- C -- D   --[ git merge testing ]-->   A -- B -- C -- D
               |                                               |
              (testing)                                       (testing)

In diesem Fall hat Git offenbar ebenfalls den master-Zweig verschoben, jedoch keinen neuen Commit erstellt. Dies liegt in diesem Fall daran, dass der Commit, auf den der Ziel-Zweig zeigt (hier der master) bereits ein direkter Vorgänger des zu mergenden Zweiges ist (in diesem Beispiel ist Commit C ein direkter Vorgänger von Commit D).

In diesem Fall begnügt Git sich damit, den master-Zeiger einfach ein paar Commits „vorzurücken“. Dieses Verhalten wird als Fastforward-Merge bezeichnet.

Was tun bei Konflikten?

Was passiert nun, wenn zwei Zweige Änderungen enthalten, die miteinander kollidieren? In der Regel passiert dies nur, wenn in zwei Commits exakt dieselben Zeilen oder nah beieinander liegende Zeilen bearbeitet wurden. Zunächst einmal wird Git euch beim Mergen auf diese Konflikte hinweisen. Das sieht dann ungefähr so aus:

> git merge testing
Auto-merging some/file.php 
CONFLICT (content): Merge conflict in some/file.php
Automatic merge failed; fix conflicts and then commit the result.

Den aktuellen Konflikt-Zustand sieht man auch in der git status-Ausgabe:

> git status
# On branch master
# Changes to be committed:
#
# modified: some/other/file.php
#
# Unmerged paths:
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: some/file.php
#

In diesem Fall muss der Konflikt von Hand aufgelöst werden. Der Befehl git mergetool startet eine grafische Oberfläche, über die ihr den Konflikt auflösen könnt (normalerweise wird hierzu das Tool vimdiff gestartet; das lässt sich aber über die Git-Konfiguration ändern). Alternativ könnt ihr die Datei auch einfach in einem Texteditor bearbeiten. Innerhalb der Datei wird der Konflikt wie folgt dargestellt:

<<<<<<< HEAD
echo "Hallo Welt!\n";
=======
echo "Lebwohl Welt!\n";
>>>>>>> branch-a

Nachdem ihr den Konflikt behoben habt, könnt ihr die Datei per git add zum Index hinzufügen. Den Merge-Commit müsst ihr anschließend noch von Hand erstellen; die nötigen Änderungen sind aber bereits alle in der Staging Area, sodass ihr nur noch git commit aufrufen müsst.

Zusammenfassung

„Ui, das klingt jetzt aber alles mächtig kompliziert“, mag jetzt der ein oder andere denken. Aus eigener Erfahrung kann ich jedoch sagen, dass Arbeitsweise mit häufigem Branching und Merging — wenn auch ungewohnt — schneller in Fleisch und Blut übergeht, als man glaubt. In einem späteren Artikel werde ich dann noch einmal genau darauf eingehen, wie ihr die Arbeit mit Git-Zweigen am besten in euren Entwicklungs-Workflow integrieren könnt.

Viel Spaß beim Mergen! ;)

Versionsverwaltung: Einführung in Git (Teil 1)

Kommentare

  1. Gravatar
    Ugur.P am
    Ja leider sind keine Bilder ersichtlich. Der Artikel ist super und würde für alle noch wünschen, dass die Bilder dargestellt werden. Vielen Dank für die Mühe!
    Antworten
  2. Gravatar
    Reiner am
    Die in dem Artikel gehörenden Bilder sind nciht zu sehen.
    Antworten
    1. Kristina Kiebe am
      Hallo Reiner, vielen Dank für deinen Hinweis. Anscheinend sind die Bilder bei der Umstellung von WordPress auf Neos leider verloren gegangen. Wir versuchen sie aber schnellstmöglich wieder zu finden.

      Viele Grüße
      Kristina
      Antworten

Kommentar hinzufügen