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:
In diesem Abschnitt wollen wir genauer erfahren, wie dies funktioniert. Dazu installieren wir uns zunächst GIT auf dem Computer:
conda install -c conda-forge git
Man kann natürlich auch unter Linux den Paketmanager benutzen, da GIT nicht nur für unsere Python-Projekte hilfreich ist. Unter Ubuntu nutzt man folgenden Befehl:
sudo apt-get install git
Bei anderen Linux-Distribution muss hier der entsprechende Paketmanager verwendet werden. 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 |
---|---|
|
Lokales Repository initialisieren |
|
Dateien zur Versionskontrolle hinzufügen bzw. für den nächsten Commit vormerken |
|
Änderungen der vorgemerkten Dateien ins lokale Repository laden |
|
Historie der Commits anzeigen |
|
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:
TU Chemnitz - Das Gitlab der TU Chemnitz
Github - Öffentliches Repository
Bitbucket - Öffentliches Repository
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 |
---|---|
|
Änderungen aus dem Remote-Repository herunterladen |
|
Änderungen im lokalen Repository auf das Remote-Repository laden |
|
Verbindung zum Remote-Repository konfigurieren |
|
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:
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:
Wir lernen hier folgende Befehle kennen:
Befehl |
Bedeutung |
---|---|
|
Branch erzeugen/löschen/verwalten |
|
Den Branch wechseln |
|
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:
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.