Im ersten Teil des Git Tutoriums wurden die ersten Schritte mit Git getätigt: Zunächst das Anlegen eines Repositories, dann das Hinzufügen und Committen von Dateien und das Anschauen des Logs. Im zweiten Teil wird nur ein Thema behandelt und zwar das Branching-Modell von Git.
Dieses Tutorium besteht aus vier Teilen. Wem das Tutorium gefällt und mehr über Git lernen möchte, der kann sich das Buch „Versionsverwaltung mit Git“ für 29,99€ bestellen. In diesem Buch, was ich geschrieben habe, werden die Themen rund um Git noch deutlich ausführlicher behandelt.
Allgemeines zum Branching
Ein wichtiges Element von Git und auch anderen Versionsverwaltungsprogrammen ist das Branchen. Das Wort „Branch“ lässt sich in diesem Fall am Besten mit „Zweig“ übersetzen. Es ist möglich, den aktuellen Entwicklungsstand „abzuzweigen“ und daran weiter zu entwickeln. Konkret bedeutet dies, dass quasi eine Kopie vom aktuellen Arbeitsstand erzeugt wird und man dort weitere Commits tätigen kann, ohne die Hauptentwicklungslinie zu berühren. Die Nutzung von Branches ist eine zentrale Eigenschaft von Git, insbesondere in der Software-Entwicklung. In der Praxis sieht das dann meistens so aus, dass einzelne Features in einzelnen Branches entwickelt werden und dann nach und nach in den Haupt-Entwicklungszweig gemergt werden. Häufig ist es allerdings so, dass man auch noch ältere Versionen pflegt, die etwa noch mit Sicherheitsaktualisierungen versorgt werden müssen. So kann man recht einfach von einem Entwicklungszweig auf einen anderen Branch wechseln und dort noch schnell einen Fehler korrigieren. Anschließend kann man wieder zurück wechseln und an seinem Feature weiterarbeiten. Das ganze Vorgehen hilft den Programmierern zwischen verschiedenen Versionen und Entwicklungslinien zu springen, ohne großen Aufwand betreiben zu müssen.
Im ersten Teil des Tutoriums wurden bereits drei Commits getätigt. Da kein spezieller Branch angegeben worden ist, geschah dies automatisch auf dem Master-Branch. Der Master-Branch ist der Haupt-Zweig, der in vielen Git-Repositories existiert. Dieser wird automatisch angelegt, wenn man in einem leeren Git-Repository den ersten Commit tätigt.
Die ersten drei Commits wurden, wie oben bereits geschrieben, auf dem Branch “master” committed. Die Entwicklung verlief bislang geradlinig, sodass keine Abzweigung erstellt wurde.
Beim Arbeiten mit Git, bietet es sich je nach Entwicklungsart häufig an, für jedes Feature, welches man implementieren möchte, einen eigenen Branch zu erstellen. Insbesondere deshalb, da oft Features zeitgleich von verschiedenen Entwicklern implementiert werden.
Branches anlegen
Die Beispiel-Webseite besitzt aktuell lediglich eine simple Überschrift. Was fehlt, wäre zum einen ein Inhalt, und zum anderen ein kleines Menü. Für beides sollen eigene Branches erstellt werden.
Um sicherzustellen, dass man auf dem richtigen Branch ist, kann man folgenden Befehl ausführen:
$ git branch
* master
Da nur ein Branch aktuell vorhanden ist, wird auch nur der Branch “master” angezeigt. Das “*” vor dem Branchnamen signalisiert, dass es sich um den Branch handelt, auf dem sich gerade befindet.
$ git branch menu
Der oben aufgeführte Befehl erzeugt den neuen Branch “menu”. Wenn man einen Branch mit dem “git branch” Befehl erzeugt, wird der Branch zwar angelegt, aber man wechselt nicht automatisch auf diesen Branch. Dies macht ein erneutes Ausführen von “git branch” deutlich:
$ git branch
* master
menu
Jetzt werden beide vorhandenen Branches angezeigt. Man befindet sich allerdings immer noch auf dem master-Branch. Zum Wechseln des Branches nutzt man den Befehl “git checkout”.
$ git checkout menu
Gewechselt zu Branch 'menu'
Beim häufigen erzeugen und wechseln zu einem Branch, wären die obigen Befehle auf Dauer zu lästig, weil man häufig sofort auf dem neu erstellten Branch wechseln will. Dafür gibt es den kombinierten Befehl:
$ git checkout -b menu
Gewechselt zu einem neuem Branch 'menu'
Dieser Befehl legt nicht nur den Branch “menu” neu an, sondern wechselt auch direkt auf diesen Branch. Es ist wichtig zu wissen, auf welchem Branch man sich befindet, wenn man den neuen Branch anlegt. Dies ist zwar in diesem Beispiel irrelevant, da nur ein Branch existiert, man sollte es aber stets beachten.
Als Basis für den neuen Branch wird nämlich immer der aktuelle Commit des aktuellen Branches genommen. Wenn man sich also auf dem Branch “menu” befindet und von dort aus den Branch “content” erstellt, dann nimmt er als Basis den aktuellsten Commit von “menu” und nicht “master”. Um das Beispiel fortzuführen, muss daher der Branch “content” erzeugt werden.
$ git checkout -b content
Gewechselt zu Branch 'content'
Zum aktuellen Zeitpunkt existieren drei Branches. Alle fußen auf dem selben Commit. In diesem Branch, wird nun ein kleiner Inhalt hinzugefügt, dazu reicht es den Lorem-Ipsum Generator zu nutzen, um einen Fülltext zu erzeugen.
Unterhalb der <h1> Überschrift in der Datei “index.html” sollte dann folgendes eingefügt werden:
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
Diese Änderung muss dann aus dem Arbeitsverzeichnis heraus, wie gewohnt committed werden:
$ git add index.html
$ git commit -m "Lorem-Ipsum Fülltext hinzugefügt"
[content 395dd48] Lorem-Ipsum Fülltext hinzugefügt
1 file changed, 3 insertions(+)
Jetzt lohnt es sich, das Log mit dem Befehl “git log” anzusehen. Auf dem aktuellen Branch “content” sind vier Commits vorhanden. Es sind nicht nur die ersten drei Commits vor dem Abzweigen vorhanden, sondern auch der zuletzt hinzugefügte Commit.
Wechselt man mit “git checkout master” zurück auf “master” und schaut sich dort das Log an, dann sind dort nur drei Commits vorhanden. Dies hängt damit zusammen, dass Git den Commit nur auf “content” ausgeführt hat und eben nicht auf “master”. Die Änderungen aus “content” können in “master” übernommen werden. Dieser Schritt folgt allerdings noch nicht an dieser Stelle.
Es gilt noch die ein oder andere Änderung im Branch “menu” durchzuführen. Dazu muss wieder auf den Branch “menu” wechseln:
$ git checkout menu
Gewechselt zu Branch 'menu'
Wenn man nun die Datei “index.html” Datei zum Bearbeiten öffnet, dann sind dort die Änderungen mit dem Fülltext nicht enthalten. Das macht auch Sinn, da die Änderungen auf dem Branch “content” durchgeführt wurden.
Die “index.html”-Datei bekommt nun ein Menü spendiert. Hierfür muss folgender Code vor der <h1>-Überschrift hinzugefügt werden:
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Link</a></li>
<li><a href="#">Link</a></li>
</ul>
</div>
</div>
</nav>
Diese Änderung kann dann ebenfalls wie gewohnt committed werden:
$ git add index.html
$ git commit -m "Bootstrap-Beispiel-Menü hinzugefügt"
Jetzt fällt aber auf, dass zwar ein Menü vorhanden ist, in beiden Menü Punkten steht allerdings nur “Link”. Der Einfachheit halber, reicht es, wenn man an dieser Stelle den ersten “Link” mit “Home” und den zweiten “Link” mit “About” ersetzt. Diese Änderung muss dann ebenfalls committed werden.
Branches mergen
Bis jetzt wurden einige Arbeiten am Repository durchgeführt. Dieser Abschnitt soll noch kurz zusammenfassen, was alles geschah. Zunächst wurden zwei neue Branches mit den Namen “content” und “menu” erstellt. Beide basieren auf den Branch “master”. Im Anschluss wurde dann ein Commit in “content” und zwei Commits in “menu” erzeugt.
Diese Änderungen können nun zusammengeführt werden. Dafür existiert der Befehl “git merge”. Dieser Befehl muss dort ausgeführt werden, wohin die Änderungen aus dem anderen Branch eingefügt werden sollen. In diesem Beispiel sollen die Änderungen aus den Branches “content” und “menu” in “master” übernommen werden. Dazu muss man auf den Branch “master” wechseln:
$ git checkout master
Anschließend kann der erste Branch gemerged werden.
$ git merge menu
Aktualisiere 24e65af..c3cf413
Fast-forward
index.html | 10 ++++++++++
1 file changed, 10 insertions(+)
Git führt hier ein sogenannten „Fast-forward“ merge durch. Dies geschieht immer genau dann, wenn seit dem Abzweigen des Branches auf dem ursprünglichen Branch keine Änderungen geschehen sind. Das ist genau bei diesem Merge der Fall. Anders sieht es hingegen aus, wenn man den Branch “content” nach “master” mergen möchte.
$ git merge content
Der Befehl öffnet den in Git konfigurierten Editor, etwa vim, mit folgendem Inhalt:
Merge branch 'content'
# Bitte geben Sie eine Commit-Beschreibung ein um zu erklären, warum dieser
# Merge erforderlich ist, insbesondere wenn es einen aktualisierten
# Upstream-Branch mit einem Thema-Branch zusammenführt.
#
# Zeilen beginnend mit '#' werden ignoriert, und eine leere Beschreibung
# bricht den Commit ab.
In der Regel belässt man den Commit-Text bei dem vorgegebenen Inhalt. Gegebenenfalls kann man allerdings, wie die Nachricht bereits aussagt, einen Grund angeben, warum der Merge nötig war. Als Ausgabe erscheint nach dem Abspeichern dann folgendes:
automatischer Merge von index.html
Merge made by the 'recursive' strategy.
index.html | 3 +++
1 file changed, 3 insertions(+)
Im Gegensatz zum ersten Merge war hier ein “recursive” Merge notwendig. Dies geschieht zwar in diesem Fall auch vollkommen automatisch, die Commit-Historie sieht allerdings anders aus. Dies hängt damit zusammen, dass durch das Mergen vom Branch “menu”, nun Änderungen auf dem Branch “master” passiert sind, seitdem der Branch “content” abgezweigt wurde. Die beiden Branches sind dadurch divergiert. Das heißt, die beiden Commits auf dem Branch “content” fußen nicht direkt auf dem neuen Commit aus “content”, welches in “master” überführt worden ist.
Wenn man nun das Git Log anschaut, dann sind mittlerweile alle Commits aus allen Branches in “master” enthalten. Zusätzlich wurde durch den letzten Merge ein weiterer Merge-Commit hinzugefügt.
Merge-Konflikte
Mergen von Branches ist nicht immer ganz einfach. Git selbst verfolgt verschiedenen Strategien, um Branches zu mergen. Das klappt bei einigen kleineren Änderungen zwar ohne Probleme, wenn allerdings größere Änderungen in den Branches stattgefunden haben, passiert es häufig, dass dann Merge-Konflikte auftreten. Merge-Konflikte sind Probleme, die auftreten, wenn der gleiche Code-Abschnitt von beiden Branches verändert wurde. Darunter fällt auch, wenn Zeilen auf einem Branch gelöscht worden sind, aber auf dem anderen noch vorhanden sind.
Das Verhalten lässt sich auch ganz einfach nachbilden. Zunächst wechselt man zurück auf den Branch “master”, falls man sich noch nicht drauf befindet.
$ git checkout master
Bereits auf 'master'
Anschließend erzeugt man einen neuen Branch:
$ git checkout -b titel
Auf diesem Branch ändert man anschließend den Titel in der <h1>-Überschrift von “Hallo Git!” in “Hallo Merge-Konflikt!”. Nachdem abspeichern, kann man die Datei wieder wie gewohnt zum Index hinzufügen und schlussendlich committen:
$ git add index.html
$ git commit -m "Titel für den Merge-Konflikt"
[titel 420e0ae] Titel für den Merge-Konflikt
1 file changed, 1 insertion(+), 1 deletion(-)
Anschließend geht es zurück auf “master”.
$ git checkout master
Dort ändert man den Titel in der <h1>-Überschrift von “Hallo Git!” auf “Hallo!”. Auch hier committed man die Änderungen.
$ git add index.html
$ git commit -m "Neuer Titel"
[master 9cb085b] Neuer Titel
1 file changed, 1 insertion(+), 1 deletion(-)
Die Voraussetzung für einen simplen Merge-Konflikt wurden somit geschaffen. Wenn man nun die beiden Branches “master” und “titel” mergen möchte, geschieht folgendes:
$ git merge titel
automatischer Merge von index.html
KONFLIKT (Inhalt): Merge-Konflikt in index.html
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie dann das Ergebnis.
Wie gewünscht, trat der Merge-Konflikt auf. Bevor man hingeht und den Konflikt behebt, lohnt sich ein Blick auf die Ausgabe von “git status”:
$ git status
Auf Branch master
Sie haben nicht zusammengeführte Pfade.
(beheben Sie die Konflikte und führen Sie "git commit" aus)
Nicht zusammengeführte Pfade:
(benutzen Sie "git add/rm <Datei>..." um die Auflösung zu markieren)
von beiden geändert: index.html
keine Änderungen zum Commit vorgemerkt (benutzen Sie "git add" und/oder "git commit -a")
Der Befehl “git status” gibt bei fehlgeschlagenen automatischen Merges immer die Information aus, dass Dateien vorhanden sind, die noch zusammengeführt werden müssen.
Schaut man sich nun die Datei “index.html” an, dann findet man dort folgende Zeilen:
<<<<<<< HEAD
<h1>Hallo!</h1>
=======
<h1>Hallo Merge-Konflikt!</h1>
>>>>>>> titel
Der Merge-Konflikt wird direkt in der Quell-Datei eingefügt. Git nutzt Marker um aufzuzeigen, welcher Teil des Codes aus welchem Branch bzw. Commit kommt. In der ersten Zeile des Konflikt ist der Marker folgender: “«««< HEAD”. HEAD ist jeweils ein Zeiger auf den aktuellen Commit auf dem Branch, auf dem man sich vor dem Merge befand. HEAD gibt es nicht nur bei Merge-Konflikten, sondern auch an allen anderen Stellen in einem Git-Repository. In diesem Fall ist das der letzte Commit im Branch “master”. Getrennt wird dies durch den weiteren Marker “=======”. Alles was zwischen “«««< HEAD” und “=======” befindet, stammt vom aktuellen Branch ab, in diesem Fall also “master”. Der zweite Teil nutzt ebenfalls das “=======” als Trennzeichen und endet mit “»»»> titel”. In diesem Teil sind dann alle Änderungen aus dem Branch “titel” enthalten.
Der Konflikt kann nun relativ einfach aufgelöst werden. Es müssen alle Marker entfernt und nur der gewünschte Teil eingefügt werden. In diesem Falle ist gewollt die Änderungen aus dem HEAD beizubehalten, weshalb man Zeile 1 und Zeile 3-5 löschen kann. Im Anschluss muss man “index.html” wieder dem Index hinzufügen.
$ git add index.html
Wenn man nun erneut “git status” ausführt, dann meldet Git, dass die Konflikte behoben worden sind.
$ git status
Auf Branch master
Alle Konflikte sind behoben, aber Sie sind immer noch beim Merge.
(benutzen Sie "git commit" um den Merge abzuschließen)
nichts zu committen, Arbeitsverzeichnis unverändert
Beim darauffolgenden Ausführen von “git commit” öffnet sich erneut der Editor mit folgender Commit-Nachricht:
$ git commit
Merge branch 'titel'
Conflicts:
index.html
#
# Es sieht so aus, als committen Sie einen Merge.
# Falls das nicht korrekt ist, löschen Sie bitte die Datei
# .git/MERGE_HEAD
# und versuchen Sie es erneut.
#
# Bitte geben Sie eine Commit-Beschreibung für Ihre Änderungen ein. Zeilen,
# die mit '#' beginnen, werden ignoriert, und eine leere Beschreibung
# bricht den Commit ab.
# Auf Branch master
# Alle Konflikte sind behoben, aber Sie sind immer noch beim Merge.
Hier kann man, wenn gewünscht, die Commit-Nachricht des Merges verändern. Nachdem dies erledigt ist, ist auch der Merge-Konflikt erfolgreich behoben.
Zum Schluss können die nicht mehr benötigten Branches aufgeräumt werden. Der Befehl “git branch” kennt hierfür den Parameter “-d” für “delete”:
$ git branch -d titel
Branch titel entfernt (war 420e0ae).
Ausblick
Der nächste Teil rundet den Einstieg in die grundlegendsten Funktionen von Git ab. Thematisiert wird zum einen wie man mit Remote-Repositories arbeitet und zum anderen wie man Branches “rebased”.