Versionskontrolle mit Git

Vorbemerkungen

Bei der Entwicklung von Programmen ist es sinnvoll, ein Versionskontrollsystem zu verwenden, das es erlaubt, alte Programmversionen systematisch aufzubewahren. Damit wird es beispielsweise möglich, auf definierte ältere Programmstände zurückzugehen. Es kann auch sinnvoll sein, die Versionsnummer in vom Programm erzeugten Daten abzuspeichern. Sollte sich später herausstellen, dass ein Programm fehlerhaft war, lässt sich auf diese Weise entscheiden, ob Daten von diesem Fehler betroffen sind oder nicht.

Das erste Versionskontrollsystem war das 1972 entwickelte SCCS. Später folgten Systeme wie RCS, CVS, Subversion, Git und Mercurial. Bei den aktuellen Versionskontrollsysteme lassen sich zwei Arten unterscheiden, solche die die Programmversionen zentral auf einem Server speichern und solche, bei denen die Programmversionen auf verschiedenen Rechnern verteilt vorliegen können. Die zweite Variante schließt den Fall mit ein, bei dem die Programmversionen ausschließlich lokal auf einem Rechner vorgehalten werden. Während bei einem zentralen Versionskontrollsystem eine Internetverbindung zum Server zwingend notwendig ist, lassen sich bei einem dezentralen Versionskontrollsystem Versionierungen auch ohne Internetanbindung vornehmen.

Ein Beispiel für ein modernes zentrales Versionskontrollsystem ist Subversion, während es sich bei Git und Mercurial um dezentrale Versionskontrollsysteme handelt. Obwohl Mercurial in Python geschrieben ist, wollen wir uns im Folgenden mit Git beschäftigen, das sich bei der Entwicklung freier Software großer Beliebtheit erfreut. Auch wenn es im Detail Unterschiede zwischen Mercurial und Git gibt, sind die beiden Versionskontrollsysteme einander sehr ähnlich.

Die Entwicklung von Git [1] wurde 2005 von Linus Torvalds begonnen, um ein geeignetes Versionskontrollsystem zur Entwicklung des Betriebssystemkerns von Linux zur Verfügung zu haben. Die Anforderungen ergaben sich vor allem daraus, dass Linux von einer sehr großen Zahl von Programmierern entwickelt wird, und somit die Übertragung von Code möglichst effizient, aber auch sicher vonstatten gehen muss. Im Hinblick auf den ersten Aspekt ist ein dezentrales System wesentlich besser geeignet als ein zentrales System. Detaillierte Informationen über Git findet man im Internet unter git-scm.com im Dokumentationsbereich.

Grundlegende Arbeitsschritte

Um für ein Verzeichnis sowie die darunterliegenden Verzeichnisse eine Versionierung zu ermöglichen, muss man zunächst die von Git benötigte Verzeichnisstruktur einrichten. Wir nehmen an, dass in unserem Benutzerverzeichnis, hier /home/gert, ein Verzeichnis wd für »working directory« existiert, in dem wir unsere Programmentwicklung durchführen wollen. Dieses Verzeichnis kann im Prinzip jeden beliebigen geeigneten Namen haben. Wir gehen zunächst in dieses Verzeichnis und initialisieren es für die Benutzung mit Git:

$ cd ~/wd
$ git init
Initialisierte leeres Git-Repository in /home/gert/wd/.git/

Im Unterverzeichnis .git werden alle relevanten Daten des Archivs liegen. So lange dieses Verzeichnis nicht modifiziert wird, was man ohnehin nicht tun sollte, oder gar gelöscht wird, sind die dort abgelegten Daten und damit alle Versionen noch verfügbar auch wenn alle anderen Dateien im Arbeitsverzeichnis gelöscht wurden.

Zu diesem Zeitpunkt ist es sinnvoll, Git auch den vollständigen Namen des Benutzers und eine zugehörige E-Mail-Adresse mitzuteilen:

$ git config --global user.name "Gert-Ludwig Ingold"
$ git config --global user.email "gert.ingold@physik.uni-augsburg.de"

Diese Informationen legt Git im Hauptverzeichnis des Benutzers in der Datei .gitconfig ab und verwendet sie bei der Übernahme von Dateien in das Versionsarchiv. Lässt man die Option --global weg, so wird die Information im lokalen Git-Verzeichnis abgelegt und gilt auch nur dort.

Bei Bedarf lassen sich noch weitere Parameter einstellen, beispielsweise der Editor, den Git aufrufen soll, um dem Benutzer beim Abspeichern einer neuen Version die Möglichkeit zu geben, einen Kommentar abzuspeichern. Da es immer wieder vorkommt, dass defaultmäßig ein Editor verwendet wird, mit dessen Bedienung man nicht vertraut ist, empfiehlt es sich, die gewünschte Einstellung vorzunehmen, zum Beispiel

$ git config --global core.editor vim

wenn man den Editor vim benutzen möchte.

Um das Arbeiten mit Git zu illustrieren, legen wir anschließend eine erste Version eines Skripts hello.py mit folgendem Inhalt

print('Hello world')

in unserem Verzeichnis an. Nun, aber auch zu jeder anderen Zeit, kann man den Zustand des Arbeitsverzeichnisses abfragen:

$ git status
Auf Branch master

Initialer Commit

Unbeobachtete Dateien:
  (benutzen Sie "git add <Datei>...", um die Änderungen zum Commit vorzumerken)

        hello.py

nichts zum Commit vorgemerkt, aber es gibt unbeobachtete Dateien (benutzen Sie
"git add" zum Beobachten)

Git gibt hier eine ganze Menge an Informationen einschließlich eines Vorschlags, was wir als Nächstes tun könnten. Doch gehen wir der Reihe nach vor. Wir befinden uns laut der ersten Zeile der Statusausgabe auf dem master-Zweig. Weiter unten werden wir sehen, dass wir unter Git weitere Zweige anlegen können, in denen wir beispielsweise bestimmte Aspekte eines Programms weiterentwickeln wollen. Solche Zweige können später auch wieder zusammengeführt werden. Ferner weist uns Git darauf hin, dass noch keine Dateien versioniert wurden. Dem unteren Teil der Ausgabe können wir entnehmen, dass die Versionierung in zwei Stufen vor sich geht und es demzufolge zwei verschiedene Arten von Dateien gibt.

Git hat sehr wohl bemerkt, dass es eine neue Datei hello.py gibt, beachtet diese jedoch zunächst nicht weiter. Es wird aber am Ende darauf hingewiesen, dass sich Dateien mit Hilfe von git add für einen commit, also eine Versionierung, vormerken lassen. Diese Dateien werden dabei in eine so genannte staging area gebracht. Wir führen diesen Schritt nun aus und sehen uns den neuen Status an:

$ git add hello.py
$ git status
Auf Branch master

Initialer Commit

zum Commit vorgemerkte Änderungen:
  (benutzen Sie "git rm --cached <Datei>..." zum Entfernen aus der Staging-Area)

        neue Datei:     hello.py

Damit ist unsere Datei nun für einen commit vorgemerkt. Gleichzeitig gibt uns Git einen Hinweis, wie wir die Datei wieder aus der staging area entfernen können, falls wir doch keine Versionierung durchführen möchten. Bevor wir mit einem commit fortfahren, wollen wir zunächst erkunden, was es damit auf sich hat, wenn eine Datei in die staging area gebracht wird. Dazu sehen wir uns etwas im .git-Unterverzeichnis um:

$ ls .git
branches  config  description  HEAD  hooks  index  info  objects  refs
$ ls .git/objects
75  info  pack
$ ls .git/objects/75
d9766db981cf4e8c59be50ff01e574581d43fc

Im Unterverzeichnis .git/objects/75 liegt nun eine Datei mit der etwas merkwürdigen Bezeichnung d9766db981cf4e8c59be50ff01e574581d43fc. Stellt man noch die 75 aus dem Verzeichnisnamen voran, so handelt es sich hierbei um den so genannten SHA1-Hashwert [2] des Objekts, wie wir folgendermaßen überprüfen können [3]:

from hashlib import sha1
def githash(data):
    s = sha1()
    s.update(("blob %u\0" % len(data)).encode('utf8'))
    s.update(data)
    return s.hexdigest()

content = "print('hello world')\n"
print(githash(content))

SHA1-Hashwerte bestehen aus 40 Hexadezimalzahlen und charakterisieren den Inhalt eines Objekts eindeutig. Immerhin gibt es etwa \(10^{48}\) verschiedene Hashwerte. Git benutzt diesen Hashwert, um schnell Objekte identifizieren und auf Gleichheit testen zu können. Meistens genügen die ersten sechs oder sieben Hexadezimalzahlen, um ein Objekt eindeutig auszuwählen. Wir können uns den Inhalt des erzeugten Objekts mit Hilfe von Git folgendermaßen ansehen:

$ git cat-file -p 75d9766
print('hello world')

Gemäß der obigen Statusanzeige müssen wir in einem zweiten Schritt noch einen commit ausführen:

$ git commit -m "ein erstes Skript"
[master (Basis-Commit) f442b34] ein erstes Skript
 1 file changed, 1 insertion(+)
  create mode 100644 hello.py

Mit Hilfe des Arguments -m haben wir noch einen Kommentar angegeben. Ohne dieses Argument hätte Git einen Editor geöffnet, um die Eingabe eines Kommentars zu ermöglichen. Es empfiehlt sich im Hinblick auf die Übersichtlichkeit von späteren längeren Ausgaben, Kommentare auf nicht zu lange Einzeiler zu beschränken.

Was hat sich durch den commit im Verzeichnis der Objekte getan? Wir stellen fest, dass unser altes Objekt noch vorhanden ist und zwei Objekte hinzugekommen sind:

$ ls -R .git/objects
.git/objects:
75  ed  f4  info  pack

.git/objects/75:
d9766db981cf4e8c59be50ff01e574581d43fc

.git/objects/ed:
868ae92a213b64de2ad627b27458537539bcdc

.git/objects/f4:
42b34f6400811648a3c94a8ddd5bfb417e1cf5

.git/objects/info:

.git/objects/pack:

Sehen wir uns die neuen Objekte an:

$ git cat-file -p f442b34
tree ed868ae92a213b64de2ad627b27458537539bcdc
author Gert-Ludwig Ingold <gert.ingold@physik.uni-augsburg.de> 1420469345 +0100
committer Gert-Ludwig Ingold <gert.ingold@physik.uni-augsburg.de> 1420469345 +0100

ein erstes Skript
$ git cat-file -p ed868ae
100644 blob 75d9766db981cf4e8c59be50ff01e574581d43fc    hello.py

Bei dem ersten Objekt handelt es sich um ein so genanntes commit-Objekt, das neben den Angaben zur Person und dem Kommentar einen Verweis auf ein tree-Objekt enthält. Das zweite neue Objekt ist genau dieses tree-Objekt. Es enthält Informationen über die Objekte, die zu dem betreffenden commit gehören. In unserem Fall ist dies das uns bereits bekannte blob-Objekt, das den Inhalt unseres Skripts hello.py enthält.

Nun ist es Zeit, unser Skript zu überarbeiten. Im Wort »hello« ersetzen wir das kleine h durch ein großes H. Git meldet dann den folgenden Status:

$ git status
Auf Branch master
Änderungen, die nicht zum Commit vorgemerkt sind:
  (benutzen Sie "git add <Datei>...", um die Änderungen zum Commit vorzumerken)
  (benutzen Sie "git checkout -- <Datei>...", um die Änderungen im Arbeitsverzeichnis
   zu verwerfen)

        geändert:       hello.py

keine Änderungen zum Commit vorgemerkt (benutzen Sie "git add" und/oder
                                        "git commit -a")

Git hat erkannt, dass wir unser Skript modifiziert haben, führt aber keinerlei Schritte im Hinblick auf eine Versionierung aus. Diese sind uns überlassen, wobei uns Git wieder Hilfestellung gibt. Nehmen wir an, dass wir die Änderungen wieder rückgängig machen wollen. Dies geht wie folgt:

$ git checkout -- hello.py
$ git status
Auf Branch master
nichts zu committen, Arbeitsverzeichnis unverändert
$ cat hello.py
print('hello world')

Tatsächlich liegt jetzt wieder die ursprüngliche Fassung des Skripts vor. Da wir die neue Fassung nicht zur staging area hinzugefügt haben, sind unsere Änderungen verloren gegangen. Sie können somit nicht wiederhergestellt werden, wie dies bei einer erfolgten Versionierung der Fall gewesen wäre. Man sollte daher mit dem beschriebenen Vorgehen besonders vorsichtig sein.

Wir wiederholen nun zur Wiederherstellung der geänderten Version die Umwandlung des h in einen Großbuchstaben. Anschließend könnten wir wieder die beiden Schritte git add hello.py und git commit ausführen. Alternativ lässt sich dies in unserem Fall in einem einzigen Schritt bewältigen:

$ git commit -a -m "fange mit Großbuchstabe an"
[master 79ff614] fange mit Großbuchstabe an
 1 file changed, 1 insertion(+), 1 deletion(-)

Zu beachten ist dabei allerdings, dass auf diese Weise alle Dateien, von denen Git weiß, dem commit unterzogen werden auch wenn dies vielleicht nicht gewünscht ist. Es ist daher oft sinnvoll, zunächst explizit mit git add die Dateien für einen commit festzulegen. Damit lassen sich gezielt thematisch zusammenhängende Änderungen auswählen.

Während der Hashwert des ersten commit-Objekts mit f442b34 begann, fängt der Hashwert des neuesten commit-Objekts mit 79ff614 an. Git bezieht sich auf Versionen mit Hilfe dieser Hashwerte und nicht mit zeitlich ansteigenden Versionsnummern. Letzteres ist für ein dezentral organisiertes Versionskontrollsystem nicht möglich, da im Allgemeinen nicht bekannt sein kann, ob andere Entwickler in der Zwischenzeit Änderungen am gleichen Projekt durchgeführt haben.

Einen Überblick über die verschiedenen vorhandenen Versionen kann man sich folgendermaßen verschaffen:

$ git log
commit 79ff6141783ca76a5424271d2cede769ff45fb28
Author: Gert-Ludwig Ingold <gert.ingold@physik.uni-augsburg.de>
Date:   Mon Jan 5 16:30:22 2015 +0100

    fange mit Großbuchstabe an

commit f442b34f6400811648a3c94a8ddd5bfb417e1cf5
Author: Gert-Ludwig Ingold <gert.ingold@physik.uni-augsburg.de>
Date:   Mon Jan 5 15:49:05 2015 +0100

    ein erstes Skript

Die Ausgabe kann mit Optionen sehr detailliert beeinflusst werden. Wir geben hier nur ein Beispiel:

$ git log --pretty=oneline
79ff6141783ca76a5424271d2cede769ff45fb28 fange mit Großbuchstabe an
f442b34f6400811648a3c94a8ddd5bfb417e1cf5 ein erstes Skript

Diese einzeilige Ausgabe funktioniert dann besonders gut, wenn man sich wie weiter oben bereits empfohlen bei der Beschreibung der Version auf eine einzige, möglichst informative Zeile beschränkt. Informationen über weitere Optionen von Git-Befehlen erhält man grundsätzlich mit git help und der anschließenden Angabe des gewünschten Befehls, in unserem Falle also git help log.

Details zu einer Version, im Folgenden die Version 79ff614, erhält man folgendermaßen:

$ git show 79ff614
commit 79ff6141783ca76a5424271d2cede769ff45fb28
Author: Gert-Ludwig Ingold <gert.ingold@physik.uni-augsburg.de>
Date:   Mon Jan 5 16:30:22 2015 +0100

    fange mit Großbuchstabe an

diff --git a/hello.py b/hello.py
index 75d9766..f7d1785 100644
--- a/hello.py
+++ b/hello.py
@@ -1 +1 @@
-print('hello world')
+print('Hello world')

Dieser Ausgabe kann man entnehmen, dass das Objekt 75d9766... in das Objekt f7d1785... umgewandelt wurde. Aus den letzten Zeilen kann man die Details der Änderung ersehen.

Wir hatten weiter oben darauf hingewiesen, dass man im Detail beeinflussen kann, welche Dateien beim nächsten commit berücksichtigt werden. Dazu werden die betreffenden Dateien mit einem git add in die staging area aufgenommen. In diesem Zusammenhang kann es passieren, dass man eine Datei versehentlich zu diesem Index hinzufügt. Im folgenden Beispiel sei dies eine Datei namens spam.py:

$ git add spam.py
$ git status
Auf Branch master
zum Commit vorgemerkte Änderungen:
  (benutzen Sie "git reset HEAD <Datei>..." zum Entfernen aus der Staging-Area)

        neue Datei:     spam.py

Diese Datei lässt sich nun wie angegeben wieder aus der staging area entfernen:

$ git reset HEAD spam.py
$ git status
Auf Branch master
Unbeobachtete Dateien:
  (benutzen Sie "git add <Datei>...", um die Änderungen zum Commit vorzumerken)

        spam.py

nichts zum Commit vorgemerkt, aber es gibt unbeobachtete Dateien (benutzen Sie
"git add" zum Beobachten)

Im Arbeitsverzeichnis ist die Datei spam.py weiterhin vorhanden. Im reset-Befehl verweist HEAD hier auf die Arbeitsversion im aktuellen Zweig, deren Hashwert wir somit nicht explizit kennen müssen.

Verzweigen und Zusammenführen

Bei der Entwicklung von Software ist es häufig sinnvoll, gewisse Weiterentwicklungen vom Hauptentwicklungsstrang zumindest zeitweise abzukoppeln. Dies erreicht man durch Verzweigungen. Ein typischer Fall wäre ein öffentliches Release, das im Hauptzweig zum nächsten Release weiterentwickelt wird. Daneben kann es aber noch einen Zweig geben, in dem ausschließlich Fehler des Releases korrigiert und dann wieder veröffentlicht werden. In einem anderen Szenario behinhaltet der Hauptzweig, der in Git unter dem Namen master läuft, immer eine lauffähige Version, während zur Entwicklung gewisser Programmaspekte separate Zweige benutzt werden. Um ein auf diese Weise entwickeltes Feature in die Version des Hauptzweiges einfließen zu lassen, muss man Zweige auch wieder zusammenführen können. Das Verzweigen und Zusammenführen geht in Git sehr einfach, da lediglich Markierungen gesetzt werden. Daher gehört das Verzweigen und Zusammenführen bei der Arbeit mit Git zu den Standardverfahren, die regelmäßig zum Einsatz kommen.

Zu Beginn gibt es nur einen Zweig, der, wie wir bereits wissen, den Namen master besitzt. Im vorigen Kapitel haben wir in diesem Zweig zwei Versionen erzeugt. Eine graphische Darstellung, die hier mit dem Git-Archive-Betrachter gitg erzeugt wurde, sieht dann folgendermaßen aus:

_images/gitbranch_01.png

Die Information über die vorhandenen Zweige lässt sich auch direkt auf der Kommandzeile erhalten. In der folgenden Ausgabe ist zu erkennen, dass es nur einen Zweig, nämlich master gibt. Der Stern zeigt zudem an, dass wir uns gerade in diesem Zweig befinden.

$ git branch
* master

Die Situation wird interessanter, wenn wir einen weiteren Zweig anlegen, der von master abzweigt. Wir nennen ihn develop, da dort die Programmentwicklung erfolgen soll, während in master immer eine lauffähige Version enthalten sein soll. Damit ist es unproblematisch, wenn das Programm im develop-Zweig zeitweise nicht funktionsfähig ist.

$ git branch develop
$ git branch
  develop
* master
_images/gitbranch_02.png

Der neue Zweig develop tritt zunächst nur als weitere Bezeichnung neben master in Erscheinung. Die Verzweigung wird erst später deutlich werden, wenn wir Dateien in den jeweiligen Zweigen verändern.

Um nun in develop arbeiten zu können, müssen wir in diesen Zweig wechseln:

$ git checkout develop
Zu Branch 'develop' gewechselt
$ git branch
* develop
  master

Der Stern zeigt an, dass der Zweigwechsel tatsächlich vollzogen wurde.

Bearbeitet man nun eine Datei im develop-Zweig und führt ein commit durch, so wird die Trennung der beiden Zweige deutlich.

_images/gitbranch_03.png

Wir wechseln nun in den master-Zweig zurück und führen ein merge, also eine Vereinigung von zwei Zweigen durch. Git sucht in diesem Fall nach dem gemeinsamen Vorfahren der beiden Zweige und baut die im develop-Zweig durchgeführten Änderungen auch im master-Zweig ein:

$ git checkout master
Zu Branch 'master' gewechselt
$ git merge develop
Aktualisiere 79ff614..79f695b
Fast-forward
 hello.py | 1 +
  1 file changed, 1 insertion(+)

Da im master-Zweig in der Zwischenzeit keine Änderungen vorgenommen wurden, linearisiert Git die Vorgeschichte. Es sind aber nach wie vor beide Zweige vorhanden.

_images/gitbranch_04.png

Möchte man festhalten, dass die Entwicklung im develop-Zweig durchgeführt wurde, so kann man dieses so genannte fast forward mit der Option --no-ff beim Zusammenführen der beiden Zweige verhindern. Um dies zu zeigen, wechseln wir zunächst in den develop-Zweig.

$ git checkout develop
Zu Branch 'develop' gewechselt

Dort führen wir die gewünschten Änderungen und einen anschließenden commit durch. Nach dem Wechsel in den master-Zweig benutzen wir nun beim Zusammenführen die Option --no-ff.

$ git commit -a -m 'dreifache Ausgabe'
[develop d2bfce0] dreifache Ausgabe
 1 file changed, 3 insertions(+), 2 deletions(-)
$ git checkout master
Zu Branch 'master' gewechselt
$ git merge --no-ff develop
Merge made by the 'recursive' strategy.
 hello.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

Die folgende Abbildung zeigt, dass die Versionsgeschichte jetzt den Zweig darstellt, in dem die Änderung tatsächlich erfolgte.

_images/gitbranch_05.png

Genauso wie man Änderungen aus dem develop-Zweig in den master-Zweig übernehmen kann, kann man auch Änderungen vom master-Zweig in den develop-Zweig übernehmen. Eine typische Situation besteht darin, dass im master-Zweig ein Fehler korrigiert wird, der auch in der Entwicklungsversion vorliegt. Zunächst nehmen wir an, dass im develop-Zweig weiter gearbeitet wird. Im master-Zweig wird der Fehler korrigiert, so dass jetzt in beiden Zweigen Änderungen vorliegen.

_images/gitbranch_06.png

Um Änderungen aus dem master-Zweig in den develop-Zweig zu übernehmen, wechseln wir in Letzteren und führen dort ein merge des master-Zweigs durch:

$ git checkout develop
Zu Branch 'develop' gewechselt
$ git merge master
Merge made by the 'recursive' strategy.
 hello.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

Damit sieht unser Verzweigungsschema folgendermaßen aus:

_images/gitbranch_07.png

Um ein neues Feature für ein Programm zu entwickeln, wird häufig ein Zweig vom develop-Zweig abgespalten und nach der Entwicklung mit Letzterem wieder zusammengeführt. Sollte die Entwicklung nicht erfolgreich gewesen sein, so verzichtet man auf die Zusammenführung oder löscht den überflüssig gewordenen Zweig. Bei dieser Gelegenheit zeigen wir, wie man das Anlegen eines neuen Zweigs und das Wechseln in diesen Zweig mit einem Kommando erledigen kann:

$ git checkout -b feature1
Gewechselt zu einem neuen Branch 'feature1'
_images/gitbranch_08.png

Unabhängig von der Entwicklung im feature1-Zweig kann man nun Änderungen zwischen dem master- und dem develop-Zweig austauschen:

$ git branch
  develop
* feature1
  master
$ git checkout master
Zu Branch 'master' gewechselt
$ git merge develop
Aktualisiere 70f9136..5b5d1e9
Fast-forward
 foo.py | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 foo.py
_images/gitbranch_09.png

Bis jetzt gingen alle Zusammenführungen problemlos von statten. Es kann aber durchaus zu Konflikten kommen, die sich für Git nicht eindeutig auflösen lassen. Dann muss der Konflikt von Hand gelöst werden. Um dies zu illustrieren, führen wir im develop-Zweig eine Änderung ein, die beim Zusammenführen mit dem feature1-Zweig zu einem Konflikt führt.

_images/gitbranch_10.png

Die folgende Ausgabe zeigt, wie Git einen Konflikt anzeigt. In der konfliktbehafteten Datei hello.py sind die kritischen Stellen gegenübergestellt. Zunächst wird die problematische Codestelle in der Arbeitsversion des develop-Zweigs angezeigt. Getrennt von ======= folgt dann der Code aus dem feature1-Zweig, der im develop-Zweig aufgenommen werden soll.

$ git branch
* develop
  feature1
  master
$ git merge feature1
automatischer Merge von hello.py
KONFLIKT (Inhalt): Merge-Konflikt in hello.py
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie
dann das Ergebnis.
$ cat hello.py
<<<<<<< HEAD
for n in range(4):
    print('Hello world!')
    print('Hallo Welt!')
=======
def myfunc1(n):
    for _ in range(3):
        print('Hello world!')
        print('Hallo Welt!')
>>>>>>> feature1

In einer solchen Situation muss der Benutzer entscheiden, welche Version die gewünschte ist. Unter Umständen kann es erwünscht, Teile jeweils aus dem einen oder dem anderen Zweig zu entnehmen. Hat man eine zufriedenstellende Version erstellt, kann man einen commit durchführen.

$ git add hello.py
$ git commit -m'Konflikt behoben'
[develop ef71e70] Konflikt behoben

Um abschließend die drei Zweige zu zeigen, die in der Diskussion eine Rolle gespielt haben, führen wir noch je eine Änderung im master- und im feature1-Zweig durch und erhalten damit das folgende Bild:

_images/gitbranch_11.png

Der Umstand, dass wir bereits in wenigen Schritten ein relativ komplexes Verzweigungsdiagramm erhalten haben, legt es inbesondere für größere Projekte nahe, sich eine Strategie für das Anlegen von Zweigen und die darin auszuführenden Aufgaben zu überlegen. Bei Projekten mit mehreren Entwicklern ist andererseits gerade die Möglichkeit, Zweige einzurichten, nützlich, um die anderen Entwickler nicht unnötig mit Code zu belasten, der nur lokal für einen Entwickler von Bedeutung ist.

Umgang mit entfernten Archiven

Bis jetzt haben wir uns nur mit der Arbeit mit einem lokalen Archiv beschäftigt. Wenn mehrere Entwickler zusammenarbeiten, muss es jedoch die Möglichkeit des Austauschs von Code geben. Unter einem zentralen Versionskontrollsystem wie Subversion dient hierzu das Archiv auf dem zentralen Server, über den ohnehin die gesamte Versionskontrolle läuft. Auch unter Git ist es sinnvoll, ein zentrales Archiv zu haben, das jedoch vor allem für den Datenaustausch und nicht so sehr für die Versionskontrolle herangezogen wird. Somit benötigt man nur für den Datenaustausch mit dem zentralen Archiv eine funktionierende Internetanbindung, während die Versionskontrolle auch ohne sie möglich ist.

Je nachdem welches Protokoll für den Datenzugriff zugelassen ist und welche Zugriffsrechte man besitzt, kann man auf das zentrale Archiv lesend oder eventuell auch schreibend zugreifen. In den folgenden Beispielen wollen wir einen Zugriff per ssh, also secure shell, annehmen, der uns, nach entsprechender Authentifizierung, sowohl Lese- als auch Schreibzugriff ermöglicht. Das zentrale Archiv soll auf dem Rechner nonexistent liegen, der, wie der Name schon andeutet, in Wirklichkeit nicht existiert. Der Name ist also entsprechend anzupassen. Der Zugriff erfolge über einen Benutzer namens user. Auch der Benutzername muss an die tatsächlichen Gegebenheiten angepasst werden.

Als erstes erzeugen wir uns lokal ein Git-Arbeitsverzeichnis, indem wir das zentrale Archiv klonen. Zur Illustration haben wir dort zunächst wieder nur eine Version eines einfachen Skripts abgelegt.

$ git clone ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git dummy
Klone nach 'dummy'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Empfange Objekte: 100% (3/3), Fertig.
Prüfe Konnektivität... Fertig
$ cd dummy
$ git branch -va
* master                96ffbf6 hello world Skript
  remotes/origin/HEAD   -> origin/master
  remotes/origin/master 96ffbf6 hello world Skript
$ cat hello.py
print('hello world')

Nach dem Wechsel in das Arbeitsverzeichnis sehen wir, dass neben dem gewohnten master-Zweig noch zwei remote-Zweige existieren. Hierbei handelt es sich um Zweige, die auf das zentrale Archiv verweisen, das standardmäßig mit origin bezeichnet wird. Um die remote-Zweige angezeigt zu bekommen, muss die Option -a angegeben werden. Andernfalls beschränkt sich die Ausgabe auf die lokal vorhandenen Zweige. Informationen über entfernte Archive und den zugehörigen Zugriffsweg erhält man mit:

$ git remote -v
origin  ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git (fetch)
origin  ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git (push)

Nehmen wir an, dass auf dem zentralen Server eine Datei verändert wurde, so können wir diese von dort in unser Arbeitsverzeichnis holen:

$ git fetch origin
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Entpacke Objekte: 100% (3/3), Fertig.
Von ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git
   96ffbf6..26f3c10  master     -> origin/master

Dabei wird nur der origin-Zweig aktualisiert, wie am Ausrufezeichen, das in der aktuellen Version von hello.py hinzugefügt wurde, zu sehen ist:

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
$ cat hello.py
print('hello world')
$ git checkout origin
Note: checking out 'origin'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD ist jetzt bei 26f3c10... mit Ausrufezeichen
$ cat hello.py
print('hello world!')

Wie uns Git informiert, können wir im origin-Zweig keine Änderungen vornehmen. Wir können uns dort aber umsehen und uns auf diese Weise davon überzeugen, dass das Skript dort das Aufrufezeichen enthält. Die Änderung können wir wie im vorigen Kapitel beschrieben in den master-Zweig unseres lokalen Archivs übernehmen:

$ git checkout master
Vorherige Position von HEAD war 26f3c10... mit Ausrufezeichen
Zu Branch 'master' gewechselt
Ihr Branch ist zu 'origin/master' um 1 Commit hinterher, und kann vorgespult werden.
  (benutzen Sie "git pull", um Ihren lokalen Branch zu aktualisieren)
$ git merge origin
Aktualisiere 96ffbf6..26f3c10
Fast-forward
 hello.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ cat hello.py
print('hello world!')

Die Aktualisierung auf den Stand des zentralen Archivs haben wir hier in zwei Schritten durchgeführt. Es ist jedoch auch möglich, dies in einem Schritt zu erledigen. Wir nehmen an, dass ein anderer Entwickler das Skript mit einem weiteren Ausrufezeichen versehen hat, und führen dann eine so genannte pull-Operation aus:

$ git pull origin
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Entpacke Objekte: 100% (3/3), Fertig.
Von ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git
   26f3c10..10f6489  master     -> origin/master
Aktualisiere 26f3c10..10f6489
Fast-forward
 hello.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ cat hello.py
print("hello world!!")

Schreibzugriff vorausgesetzt kann man umgekehrt auch neue Dateiversionen im zentralen Archiv ablegen. Hierzu dient die push-Operation. Hierzu ändern wir den Ausgabetext unseres Skripts und legen das neue Skript in unser lokales Archiv. Anschließend kann die Übertragung in das zentrale Archiv erfolgen:

$ cat hello.py
print "Hallo Welt!!"
$ git commit hello.py -m"deutsche Variante"
[master d2b98d1] deutsche Variante
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push origin
Zähle Objekte: 3, Fertig.
Schreibe Objekte: 100% (3/3), 290 bytes | 0 bytes/s, Fertig.
Total 3 (delta 0), reused 0 (delta 0)
To ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git
   10f6489..d2b98d1  master -> master

Problematisch wird die Situation, wenn zwischen einer pull-Operation und einer push-Operation ein anderer Entwickler das zentrale Archiv verändert hat:

$ git commit hello.py -m"Ausgabe deutsch und englisch"
[master 8e5577d] Ausgabe deutsch und englisch
 1 file changed, 1 insertion(+)
$ git push origin
To ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy
 ! [rejected]        master -> master (fetch first)
error: Fehler beim Versenden einiger Referenzen nach
               'ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git'
Hinweis: Aktualisierungen wurden zurückgewiesen, weil das Remote-Repository Commits
Hinweis: enthält, die lokal nicht vorhanden sind. Das wird üblicherweise durch einen
Hinweis: "push" von Commits auf dieselbe Referenz von einem anderen Repository aus
Hinweis: verursacht. Vielleicht müssen Sie die externen Änderungen zusammenzuführen
Hinweis: (z.B. 'git pull ...') bevor Sie erneut "push" ausführen.
Hinweis: Siehe auch die Sektion 'Note about fast-forwards' in 'git push --help'
Hinweis: für weitere Details.

Wir folgen dem Hinweis und holen uns zunächst die veränderte Version:

$ git pull origin
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Entpacke Objekte: 100% (6/6), Fertig.
Von ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git
   d2b98d1..a2e308b  master     -> origin/master
automatischer Merge von hello.py
KONFLIKT (Inhalt): Merge-Konflikt in hello.py
Automatischer Merge fehlgeschlagen; beheben Sie die Konflikte und committen Sie dann
das Ergebnis.

Dabei kommt es zu einem Konflikt, da das gleiche Skript in verschiedener Weise verändert wurde. Zunächst muss nun dieser Konflikt beseitigt werden, damit anschließend die gewünschte Fassung der Datei versioniert werden kann:

$ git add hello.py
$ git commit
[master c189281] Merge branch 'master' of
   ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git

Anschließend kann diese Version erfolgreich im zentralen Archiv abgelegt werden:

$ git push origin
Zähle Objekte: 4, Fertig.
Delta compression using up to 4 threads.
Komprimiere Objekte: 100% (2/2), Fertig.
Schreibe Objekte: 100% (4/4), 515 bytes | 0 bytes/s, Fertig.
Total 4 (delta 1), reused 0 (delta 0)
To ssh://user@nonexistent.physik.uni-augsburg.de/home/user/dummy.git
   a2e308b..c189281  master -> master

An diesem Beispiel wird deutlich, dass es durchaus problematisch sein kann, wenn viele Entwickler Schreibzugriff auf ein zentrales Archiv haben. Daher wird häufig einem breiteren Personenkreis lediglich Lesezugriff gewährt. Nur ein einzelner Entwickler oder eine kleine Gruppe hat Schreibzugriff auf das zentrale Archiv und kann so neuen Code dort ablegen. Dies geschieht mit einer pull-Operation von einem Archiv des Entwicklers, der den Code zur Verfügung stellt. Hierzu ist wiederum eine Leseberechtigung nötig. Möchte ein Entwickler bei diesem Verfahren Code für das zentrale Archive zur Verfügung stellen, so stellt er eine pull-Anfrage (pull request). Eventuell nach einer Diskussion und Prüfung entscheidet der Verantwortliche für das zentrale Archiv über die Aufnahme in das zentrale Archiv und führt die pull-Operation durch (oder auch nicht). Eine häufig verwendete Infrastruktur, die in dieser Weise insbesondere auch für die Entwicklung freier Software benutzt wird, ist GitHub.

[1]Zur Namensgebung sagte Linus Torvalds unter anderem „I’m an egoistical bastard, and I name all my projects after myself. First Linux, now git.“ Die Ironie dieses Satzes wird deutlich wenn man bedenkt, dass git im Englischen so viel wie Blödmann oder Depp bedeutet.
[2]Siehe zum Beispiel en.wikipedia.org/wiki/SHA-1.
[3]Der folgende Code basiert auf einem Vorschlag auf stackoverflow.com/questions/552659/assigning-git-sha1s-without-git.