8. Versionskontrolle mit GIT

8.1. Was ist und was kann GIT?

Git ist ein kostenloses, verteiltes Versionskontrollsystem für Softwareprojekte. Das Programm ermöglicht es mehreren Entwicklern, unabhängig von ihrem Aufenthaltsort gleichzeitig an einem Projekt zu arbeiten.

Die Versionskontrolle macht es einfach, Änderungen eigenständig und von überall aus dem Projekt hinzuzufügen, diese Änderungen zu protokollieren und nachzuvollziehen sowie zu einem späteren Zeitpunkt auf ältere Stände des Projekts zuzugreifen. Git ist plattformunabhängig und lässt sich somit in nahezu jeder Umgebung nutzen.

In folgender Abbildung ist dargestellt, wie 3 Entwickler an einem gemeinsamen Softwareprojekt arbeiten könnten. Der Programmcode wird zentral in einem Remote Repository gespeichert. Dieses wird mit lokalen Repositories auf dem Rechner jedes Programmierers synchronisiert, welcher wiederrum die Daten mit einem Ordner im Dateisystem synchronisiert:

GIT-Repositories

In diesem Abschnitt wollen wir genauer erfahren, wie dies funktioniert. Dazu installieren wir uns zunächst GIT auf dem Computer:

sudo apt-get install git

bzw. je nach Wahl der Linux-Distribution mit dem entsprechenden Paketmanager. Windows-Nutzer können sich GIT hier herunterladen.

8.2. Erste Schritte

8.2.1. Synchronisation mit dem lokalen Repository

Wir wollen uns hier mit den grundlegenden Schritten bei der Arbeit mit GIT auseinandersetzen. Die wichtigsten Befehle, die wir in diesem Kapitel lernen sind:

Befehl

Bedeutung

git init

Lokales Repository initialisieren

git add [files]

Dateien zur Versionskontrolle hinzufügen bzw. für den nächsten Commit vormerken

git commit -m "[message]"

Änderungen der vorgemerkten Dateien ins lokale Repository laden

git log

Historie der Commits anzeigen

git status

Statusbericht anzeigen

Wir legen zunächst einen neuen Ordner für unser Programmierprojekt an und erzeugen uns ein lokales GIT-Repository mit

git init
Leeres Git-Repository in /home/maxwin/Test/.git/ initialisiert

Dies erzeugt einen versteckten Ordner namens .git in dem die Konfigurationen unserer Repositories sowie die eigentlichen Daten gespeichert werden. Diesen Ordner müssen wir allerdings nie öffnen.

Wir können nun beginnen unseren Programmcode zu schreiben. Mit einem Editor unserer Wahl erstellen wir die Datei calender.py und füllen sie mit folgendem Inhalt:

class appointment:
    pass
    
class calender:
    pass

Wir fügen diese zur Versionskontrolle hinzu und übergeben diese an unser lokales Repository:

git add calender.py
git commit -m "Created file for empty calender and appointment class"
[master (Root-Commit) ce7d6d2] Created empty calender and appointment class
 1 file changed, 6 insertions(+)
 create mode 100644 calender.py

Wir können unser Programm nun erweitern, beispielsweise die appointment-Klasse:

class appointment:
   
    def __init__(self, date, title):
        self.date = date
        self.title = title
    def __str__(self):
        return self.date + ": " + self.title

Mit dem folgenden Befehl können wir überprüfen ob wir noch synchron mit unserem lokalen Repository sind:

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 restore <Datei>...", um die Änderungen im Arbeitsverzeichnis zu verwerfen)
	geändert:       calender.py

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

Wir wollen nun die Änderungen an der Datei calender.py in das lokale Repository hochladen:

git add calender.py
git commit -m "Implemented constructor and string method for appointment class"

Im nächsten Arbeitsschritt erweitern wir unsere calender-Klasse

class calender:
    
    def __init__(self, owner):
        self.owner = owner
        self.appointments = []

    def add_appointment(self, appointment):
        self.appointments.append(appointment)

und laden die Änderungen wieder in unser lokales Repository:

git add calender.py
git commit -m "Implemented constructor and add_appointment method for calender class"

Nun haben wir bereits 3 Commits eingepflegt. Wir erhalten eine Historie mit

git log
commit a95560371b9984f57fff4dcbd028bb757a0918cc (HEAD -> master)
Author: Max Winkler <max.winkler@mathematik.tu-chemnitz.de>
Date:   Thu Mar 17 16:04:37 2022 +0100

    Implemented constructor and add_appointment method for calender class

commit ed29a89b272ab66e95cb7d014c90fadccb9cacc1
Author: Max Winkler <max.winkler@mathematik.tu-chemnitz.de>
Date:   Thu Mar 17 16:00:40 2022 +0100

    Implemented constructor and string method for appointment class

commit ce7d6d246e3b0042b70f2b0104ef45139f9de381
Author: Max Winkler <max.winkler@mathematik.tu-chemnitz.de>
Date:   Thu Mar 17 15:50:38 2022 +0100

    Created empty calender class

Wir finden hier unsere Commit-Nachrichten wieder, sehen, wann der Commit getätigt wurde, und können hier auch den Namen des Commits ablesen (der kryptische Code hinter dem Wort “commit”).

8.2.2. Mit einem Remote-Repository verbinden

Wir wollen nun unser lokales Repository mit einem Remote-Repository verbinden. Sinnvoll wird dies erst, wenn das Remote-Repository im Internet oder Intranet für die anderen Entwickler auffindbar ist. Es gibt bereits einige kostenfreie Anbieter für Git-Repositories:

Wir können uns bei einem dieser Anbieter registrieren und können auf der Webseite ein neues Repository einrichten. Die wichtigsten Befehle, die wir zur Synchronisierung mit dem Remote-Repository benötigen sind:

Befehl

Bedeutung

git pull

Änderungen aus dem Remote-Repository herunterladen

git push

Änderungen im lokalen Repository auf das Remote-Repository laden

git remote [...]

Verbindung zum Remote-Repository konfigurieren

git clone <url>

Klone das Remote-Repository in ein lokales

Beim ersten Versuch unseren Code zu “pushen” wird uns eine Warnung angezeigt:

git push
fatal: Kein Ziel für "push" konfiguriert.
Entweder spezifizieren Sie die URL von der Befehlszeile oder konfigurieren ein Remote-Repository unter Benutzung von

    git remote add <Name> <URL>

und führen "push" dann unter Benutzung dieses Namens aus

    git push <Name>

Dies ist nicht verwunderlich, da wir Git noch nicht mitgeteilt haben wo unser Remote-Repository liegt. Wie wir das tun steht aber schon im Fehlertext. Die URL unseres Remote-Repositories finden wir im Gitlab, wenn wir den Button “Clone” anklicken:

Remote-Repository

Dabei benutzen wir stets die SSH-Url. Mit dieser können wir nun die Repositories wie folgt verlinken:

git remote add Gitlab git@gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git
git push Gitlab
Objekte aufzählen: 9, fertig.
Zähle Objekte: 100% (9/9), fertig.
Delta-Kompression verwendet bis zu 4 Threads.
Komprimiere Objekte: 100% (6/6), fertig.
Schreibe Objekte: 100% (9/9), 983 Bytes | 327.00 KiB/s, fertig.
Gesamt 9 (Delta 1), Wiederverwendet 0 (Delta 0), Pack wiederverwendet 0
To gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git
 * [new branch]      master -> master

Auf der Gitlab-Webseite unseres Projektes sollte die Datei calender.py ebenfalls angekommen sein. Beachte, dass die Webseite nur die auf das Remote-Repository gepushten Dateien einsehen lässt, nicht aber die Änderungen in unserem lokalen Repository.

Ist das Remote-Repository erst einmal eingerichtet, können andere Programmierer dieses herunterladen und mit der Arbeit am Projekt beginnen. Dazu verwendet man:

git clone git@gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git

Anschließend darf der Programmierer Dateien verändern, die Änderungen in sein lokales Repository committen und mit

git push

in das Remote-Repository laden. Ein weiterer Programmierer kann sich diese Änderungen wiederrum mit

git pull

in das lokale Repository laden

8.3. Im Team arbeiten

8.3.1. Merges und Mergekonflikte

Wir haben bereits gelernt wie wir die Daten unseres lokalen Repositories mit dem Remote-Repository synchronisieren können (push und pull). Weitere Programmierer können dieses Repository ebenfalls klonen (clone) und uns bei der Programmierung unterstützen. Doch was passiert eigentlich, wenn mehrere Benutzer gleichzeitig Änderungen einpflegen? Wir testen dies aus und lassen unser Repository von 2 Programmierern Programmierer A und Programmierer B klonen. Diese schreiben nun an der calender-Klasse bzw. an der appointments-Klasse weiter:

Programmierer A:

from datetime import datetime

class appointment:

    def __init__(self, date, title):
        try:
            self.date = datetime.strptime(date, '%d.%m.%y %H:%M:%S')
        except:
            print("Fehler:", date, "ist kein gültiges Datumsformat.")

        self.title = title
    
    def __str__(self):
        return str(self.date) + ": " + self.title
        
    def __lt__(self, other):
        return self.date <= self.other

Programmierer B:

class calender:

    def __init__(self, owner):
        self.owner = owner
        self.appointments = []

    def add_appointment(self, appointment):
        self.appointments.append(appointment)

    def __str__(self):

        res = "Calender of "+self.owner+":\n"
        
        if len(res) == 0:
            print("<keine Einträge vorhanden>")
        else:
            for appointment in self.appointments:
                res += str(appointment) + "\n"
        return res

Beide Programmierer können während ihrer Arbeit beliebig mit git add und git commit den Programmcode mit ihren lokalen Repositories synchronisieren. Kritisch wird es allerdings beim git push. Die Programmiererin, die zuerst ihre Änderungen ins Remote-Repository läd (git push) kann dies ohne Konflikte tun. Der zweite Programmierer allerdings erhält folgenden Fehler:

git push
To gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Durch die Änderungen von Programmiererin A ist also ein Konflikt mit den lokalen Daten des Programmierers B entstanden. Programmierer B muss zunächst diese Änderungen herunterladen (git pull) und eventuell entstandene Konflikte im Programmcode beheben, bevor er die Änderungen wieder selbst in das Remote-Repository einpflegen darf:

Gibt Programmierer B nun

git pull

ein, so versucht Git automatisch bie Änderungen beider Programmiererinnen zu verschmelzen (merge). Der Merge an sich ist wieder ein Commit und man wird nach einer Commit-Message gefragt. Es öffnet sich ein Texteditor mit dem Inhalt

Merge branch 'master' of gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture

welchen man (beispielsweise in Vim mit :wq) speichern und schließen kann. Im Optimalfall wird auf der Konsole eine Erfolgsmeldung ausgegeben:

remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture
   a955603..5872b6e  master     -> origin/master
Auto-merging calender.py
Merge made by the 'recursive' strategy.
 calender.py | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

Dies bestätigt, dass der Merge erfolgreich war. Wir können nun unsere Datei calender.py öffnen, und sehen, dass die Änderungen der beiden Programmierer enthalten sind.

Programmierer B kann nun anschließend mit

git push

die aktuellste Variante in das Remote-Repository laden.

Wir überprüfen nochmal mit git log, versehen mit einigen Zusatzoptionen, was eigentlich eben geschehen ist:

git log --oneline --graph
*   224c7c9 (HEAD -> master, origin/master, origin/HEAD) 
    Merge branch 'master' of gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture
|\  
| * 5872b6e Modified appointment class
* | 209c906 Modified calender class
|/  
* a955603 Implemented constructor and add_appointment method for calender class
* ed29a89 Implemented constructor and string method for appointment class
* ce7d6d2 Created empty calender class

Links ist eine Baumstruktur zu erkennen. Nach dem dritten Commit gehen 2 Zweige (Branches) ab, jeweils für die getätigten Commits der beiden Programmiererinnen, welche zu dem Zeitpunkt nicht mehr synchron waren. Programmierer B hat beim git pull allerdings beide Branches wieder zusammengeführt.

Das eben war der schöne Fall. Was passiert aber, wenn der automatische Merge fehlschlägt? Beispielsweise wenn beide Programmierer die gleiche Zeile verändern. Nehmen wir an beide Programmierer fügen ein Kommentar zur Klasse appointment hinzu:

Programmierer A:

# Class that stores date and title of an appointment
class appointment:

Programmierer B:

# Class represents an appointment of the calender owner
class appointment:

Beide committen und pushen ihre Änderungen. Bei Programmierer B schlägt der Push fehl und er muss zunächst mit git pull die Änderungen von Programmierer herunterladen:

git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture
   224c7c9..c7bebfa  master     -> origin/master
Auto-merging calender.py
CONFLICT (content): Merge conflict in calender.py
Automatic merge failed; fix conflicts and then commit the result.

Der Merge ist offensichtlich fehlgeschlagen, was auch nicht verwunderlich ist. Git kann schließlich nicht erahnen welche Variante die bessere ist. Programmierer B muss nun die Datei calender.py im Editor öffnen und findet folgendes vor:

<<<<<<< HEAD
# Class represents an appointment of the calender owner
=======
# Class that stores date and title of an appointment
>>>>>>> c7bebfacf5aa664e1f3ed705794856828970b787
class appointment:

Programmierer B muss hier nun die Konflikte manuell beheben, also sich für eine Version entscheiden und die andere aus der Datei löschen. Anschließend mit

git commit -am "Solved merge conflict"
git push

die Änderungen in das lokale und Remote-Repository hochladen. Programmiererin A bekommt diese Änderungen beim nächsten git pull ebenfalls.

Beachte

Vor dem Beginn der Arbeit sollte man immer ein git pull ausführen, um nicht versehentlich mit einer zu alten Version zu arbeiten. Andernfalls sind kompliziertere Merge-Konflikte vorprogrammiert.

8.3.2. Auf Branches arbeiten

Um Konflikte, welche mehrere Programmierer zwangsläufig erzeugen, zu reduzieren lohnt es sich für verschiedene Features unseres Projekts einzelne Zweige (Branches) zu erzeugen und gezielt auf diesen zu Arbeiten. Ist das Feature fertig implementiert, kann dieses in den Haupt-Branch, genannt master (auch main oder trunk) eingearbeitet (gemerget) werden. Die grobe Funktionsweise ist in folgender Abbildung dargestellt:

GIT-Branches

Wir lernen hier folgende Befehle kennen:

Befehl

Bedeutung

git branch ...

Branch erzeugen/löschen/verwalten

git checkout <branch>

Den Branch wechseln

git merge <branch>

Branch mit dem aktuellen verschmelzen

Einen neuen Branch erstellen

Wir stellen uns nun vor, dass Programmiererin A und B getrennt voneinander weiter arbeiten. Während Programmiererin A weiter auf dem master-Branch arbeitet und vielleicht ein Paar Testskripte für die Verwendung unserer calender-Klasse programmiert, arbeitet Programmierer B an neuen Features für unseren Kalender. Um die Arbeit von Programmiererin A nicht zu stören erzeugt Programmierer B einen neuen Branch:

git branch calender_features
git checkout calender_features
Switched to branch 'calender_features'

Wir können uns mit

git branch
  master
* calender_features

auch nochmal die verfügbaren Branches anschauen. Der Stern gibt an auf welchem wir uns aktuell befinden.

Wir können nun die Klasse calender erweitern, beispielsweise fügen wir folgende Funktion hinzu:

    def remove_old_appointments(self):
        today = datetime.today()
        upcoming_appointments = []

        for appointment in self.appointments:
            if appointment.date > today:
                upcoming_appointments.append(appointment)

        self.appointments = upcoming_appointments

Wir schauen uns nochmal die Ausgabe von git status an:

git status
On branch calender_features
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   calender.py

no changes added to commit (use "git add" and/or "git commit -a")

Die erste Zeile verrät uns, dass wir auf dem richtigen Branch, nämlich calender_features sind. Wir können nun committen, pushen und erhalten eine Fehlermeldung:

git commit -am "Implemented method to remove old appointments"
git push
fatal: The current branch calender_features has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin calender_features

Das liegt daran, dass wir mit git branch calender_features einen Branch in unserem lokalen Repository erzeugt haben. Das hat aber noch keine Auswirkungen auf unser Remote-Repository. Wir müssen also unseren lokalen Branch mit einem neuen Branch auf dem Remote-Repository verknüpfen. Dies erledigen wir mit:

git push --set-upstream origin calender_features
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 484 bytes | 161.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: 
remote: To create a merge request for calender_features, visit:
remote:   https://gitlab.hrz.tu-chemnitz.de/maxwin--tu-chemnitz.de/python-lecture/-/merge_requests/new?merge_request%5Bsource_branch%5D=calender_features
remote: 
To gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git
 * [new branch]      calender_features -> calender_features
Branch 'calender_features' set up to track remote branch 'calender_features' from 'origin'.

Auf der Gitlab-Webseite sehen wir in der Rubrik “Branches” nun auch diesen neuen Branch. Mit git log können wir nochmals überprüfen auf welchem Branch wir uns lokal und remote befinden:

git log
commit 2671386641522f6d2ceeffdec51263830f76d86d 
    (HEAD -> calender_features, origin/calender_features)

Der HEAD ist dabei immer der Zeiger auf die Spitze des aktuellen lokalen Branches, bei uns also calender_features, welcher mit dem Remote-Branch origin/calender_features verknüpft ist. Dabei ist origin im Prinzip nur die URL unseres Remote-Repositories. Vergleiche dazu folgende Ausgabe:

git remote -v
origin	git@gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git (fetch)
origin	git@gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git (push)

Was hat Programmiererin A eigentlich in der Zwischenzeit gemacht? Sie war weiterhin auf dem master-Branch unterwegs und hat ein nettes Test-Skript test.py geschrieben, committed und gepusht. Da beide auf verschiedenen Branches unterwegs waren gab es beim Pushen nie Probleme.

Branches mergen

Programmierer B hat mittlerweile seine Arbeit beendet, sein neues Feature ausgiebig getestet und möchte dieses nun stolz in den master-Branch einbringen (mergen). Dazu wechselt er zunächst auf den master-Branch mit

git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

und zieht sich mit

git pull

die Änderungen, die andere Programmiererinnen in der Zeit getätigt haben.

Nun kann Programmierer B (oder auch Programmiererin A) den Merge vornehmen. Zunächst nutzt man

git merge calender_features

und speichert die sich öffnende Datei mit einer aussagekräftigen Commit-Nachricht ab und erhalten als Konsolenausgabe

Merge made by the 'recursive' strategy.
 calender.py | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

Der automatische Merge war hier erfolgreich, es kann aber unter Umständen dazu kommen, dass man manuell nachhelfen muss, beispielsweise wenn auf den verschmelzten Branches die gleichen Code-Zeilen geändert wurden. Zuletzt wird der Merge noch mit

git push

in das Remote-Repository geladen. Anschließend kann ganz normal auf dem master-Branch weitergearbeitet werden. Beispielsweise könnte Programmierer B sein neues Feature in das Skript test.py einbauen.

Branch schließen

Nach dem Merge wird der Branch calender_features nicht mehr benötigt. Das bedeutet nicht, dass wir die Commits, die den Branch ausmachten wegwerfen, sondern lediglich den Zeiger auf den Branch entfernen wollen.

Lokal können wir dies mit

git branch -d calender_features
Deleted branch calender_features (was 2671386).

machen. Der Befehl git branch zeigt uns diesen Branch jetzt nicht mehr an, er existiert aber noch im Remote-Repository, wie ein Blick auf die Gitlab-Webseite verrät. Dies erledigen wir mit

git push --delete origin calender_features
To gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture.git
 - [deleted]         calender_features

Zusammenfassung

Wir können uns nun nochmal die Commit-Historie anschauen:

git log --oneline --graph
* 79912a3 (HEAD -> master, origin/master, origin/HEAD) Extended test script test.py
*   e501a9e Merge branch 'calender_features'
|\  
| * 2671386 Implemented method to remove old appointments
* | 17a8414 Implemented test script
|/  
*   b53b4d2 Resolved merge conflict
|\  
| * c7bebfa Added comment to appointment class
* | 4e73b1c Added comment to appointment class
|/  
*   224c7c9 Merge branch 'master' of gitlab.hrz.tu-chemnitz.de:maxwin--tu-chemnitz.de/python-lecture
|\  
| * 5872b6e Modified appointment class
* | 209c906 Modified calender class
|/  
* a955603 Implemented constructor and add_appointment method for calender class
* ed29a89 Implemented constructor and string method for appointment class
* ce7d6d2 Created empty calender class

Hier sehen wir alle Commits, auch die auf unserem gelöschten calender_features-Branch. Diese wurden nicht verworfen, denn sie sind fester Bestandteil der aktuellsten Version auf dem master-Branch. Auch auf dem Remote-Repository ergibt sich ein ähnliches Bild. Unter Repository \(\Rightarrow\) Graph sehen wir folgenden Graphen:

GIT-Repositories

Auch unsere Branches sind zu sehen, und zwar die, die zwangsläufig durch das gleichzeitige Arbeiten auf dem master-Branch entstanden sind, als auch unseren manuell erzeugten calender_features-Branch.