Git

Aus Das Sopra Wiki

Git ist ein Versionsverwaltungssystem. Es wird verwendet um

  • Änderungen am Projekt zu protokollieren und diese zu archivieren.
  • Ältere Versionen wiederherzustellen.
  • Gemeinsames Arbeiten auf der selben Datenbasis zu ermöglichen.

Wir verwenden Git im Softwarepraktikum um genau diese Punkte zu ermöglichen. Jede Gruppe erhält ihr eigenes Repository auf Gitea (einer Plattform ähnlich zu GitHub), um ihr Projekt zu verwalten.


Git in einer Nussschale

Um mit Git arbeiten zu können, ist es wichtig die prinzipielle Arbeitsweise von Git zu verstehen. Hat man die technische Umsetzung im Hinterkopf, werden die Befehle und Arbeitsweisen um Git zu bedienen klarer.

Git protokolliert und verwaltet ein Dateiverzeichnis und alle Änderungen die an den verwalteten Dateien gemacht werden. Jede Änderung produziert dabei einen neuen "Schnappschuss" (Commit) - den aktuellen Zustand des Verzeichnisses und der Dateien denen ein HASH (SHA-1) zugeordnet wird. Jeder Schnappschuss kennt seine Vorgänger, so dass mit der Zeit ein großer gerichteter azyklischer Graph entsteht. Git kümmert sich darum, dass dies speichereffizient abläuft und verwendet dazu ein spezielles Verzeichnis das .git heißt und im verwalteten Dateiverzeichnis liegt. Dieses die gesamte Historie beinhaltende .git Verzeichnis ist ein Git Repository. Meistens wird aber das verwaltete Verzeichnis synonym als Repository bezeichnet, was wir ab jetzt auch tun. Ein einfaches Repository mit nur einer Datei "README.md" sieht demnach so aus:

.
├── .git
└── README.md

Die gesamte Historie des Repository ist also (meistens) lokal vorhanden. Um mit mehreren Personen an dem Repository zu arbeiten, müssen die Teilnehmer es mit einem Remote-Repository synchronisieren.

Bearbeitungen an einem Git versionierten Verzeichnis - ändern von Dateien, hinzufügen/wechseln von Branches etc. - sind zunächst immer lokal. Die Synchronisierung mit einem Remote Repository erfolgt erst durch die entsprechenden Befehle. Insbesondere kann der aktuelle Zustand des Remote Repository beliebig von der lokal vorhandenen Kopie abweichen.

Git arbeitet mit 3 Zuständen. Jede versionierte Datei kann in einem der Zustände sein, wobei es nicht sein muss, dass zu einem Zeitpunkt alle Dateien den gleichen Zustand haben. Die 3 Zustände sind:

  • committed -> Die Datei ist, so wie sie ist, im Repository gespeichert.
  • modified -> Die Datei ist zum letzten gespeicherten Zustand verändert.
  • staged -> Die Datei (vorher im "Modified" Zustand oder eine neue Datei) wurde von dem Git Benutzer markiert, sodass die Änderungen gespeichert werden sollen.

Daneben gibt es auch noch `untracked files` dies sind Dateien, die noch nicht in die Versionskontrolle aufgenommen wurden.

Zentrale Begriffe

Commit

Ein Commit repräsentiert einen Schnappschuss des Reposity - den Zustand aller Dateien - zusammen mit einer Commitnachricht und Informationen über den Autor (Name und Email Adresse). Ändert ein Nutzer eine oder mehrere Dateien kann der Nutzer diese Änderungen mit einem neuen Commit festhalten. Jedes mal wenn der git commit Befehl ausgeführt wird, speichert Git den aktuellen Zustand und weist ihm eine eindeutige ID (dem SHA-1 Hash) zu. Eine Serie an Commits erzeugt eine verkettete Liste an Commits wobei ein Commit immer seinen Vorgänger kennt. Benutzt man auch Branches im Repository, entsteht so ein gerichteter Graph.

Branch

Ein Branch ist ein unabhängiger Abzweig des Repository. Ausgehend von einem Commit kann mittels des git branch Befehls ein neuer Branch erzeugt werden. Ein Branch funktioniert wie ein eigenes Repository mit der Besonderheit, dass der Branch ausgehend von einem Commit (Schnappschuss) des Repository erstellt wurde - technisch ist ein Branch einfach ein Pointer auf einen Commit - und mit anderen Branches des Repository wieder vereinigt werden kann (Merge). Es können praktisch beliebig viele Branches existieren und diese können beliebig stark voneinander abweichen.

Master-Branch

Der Master-Branch ist der erste Branch der standardmäßig von Git erzeugt wird. Lässt man alle Branch Befehle unberührt, spielt sich also jede Änderung am Repository nur auf dem Master-Branch ab.

Remote-Branch

Ist das Repository nicht nur lokal vorhanden, sondern wird mit einem Remote-Repository synchronisiert, beschreibt der Remote-Branch den Zustand des Branches im Remote-Repository. Zum Beispiel ist gewöhnlich origin/master der Remote-Branch des "master" Branch. Der Remote-Branch kann nicht direkt verändert werden sondern wird mit den Befehlen push, fetch (pull) entsprechend beeinflusst. Siehe dazu auch Remote-Branches im Git Buch

Soll ein Branch geteilt werden und ist nicht in dem Remote-Repository vorhanden muss er mit git push <remote> <branch> veröffentlicht werden.

Merge

Ein Merge ist das einfügen von Änderungen aus einem fremden (Source-) Branch in den Aktuellen (Target-) Branch.

Merge-Konflikt

Ein Merge-Konflikt entsteht wenn im Source- und Target-Branch Änderungen an der selben Datei vorhanden sind, die nicht von git aufgelöst werden können und manuelles beseitigen des Konfliktes erfordern.

Clone

Ein Clone ist eine Kopie eines schon bestehenden Git Repository. Dabei möchte man meistens die Quelle als Remote behalten um Änderungen mit dieser Synchronisieren zu können.

Remote

Remote ist die Bezeichnung für ein Repository, das zur Synchronisierung verwendet wird. Meistens ist dies auf einem externen Server und benötigt eine Authentifizierungsmethode um von diesem Änderungen zu holen (fetch) oder Änderungen hochzuladen (push). Im Softwarepraktikum verwendet jede Gruppe dazu ein eigenes von uns mit Gitea gehostetes Repository.

HEAD

HEAD ist ein Zeiger auf einen Commit. Git markiert den aktuellen (letzten) Commit in dem aktuell ausgecheckten Branch als "HEAD". Faktisch gibt es also bei mehreren Branches mehrere mögliche HEAD Commits. Praktisch zeigt HEAD aber zu jedem Zeitpunkt eindeutig auf einen Commit, da ein Checkout auf einen Branch auch immer eindeutig ist. Wird von "HEAD des dev Branch" gesprochen, meint dies den Commit auf den HEAD zeigt, wenn der dev Branch ausgecheckt wäre. Technisch ist "HEAD des dev Branch" identisch mit "dev Branch", da in der Commit Graph Struktur von Git ein Zeiger auf einen einzelnen Commit ausreichend ist um einen Branch - den Weg rückwärts im Graph - zu beschreiben. Es ergibt aber Sinn hier sprachlich zu unterscheiden, da mit "der dev Branch" meistens die Menge aller beteiligten Commits gemeint ist.

Um die jeweils aktuellen Zustände der Branches zu sehen, kann der git log Befehl mit dem --decorate verwendet werden:

git log --decorate --oneline --graph
*   c311082 (HEAD, master) Merge dev "foo bar"
|\
| * 1c34c42 (develop) Implemented bar
| * 238fb57 (develop) Implemented foo
|/
* 5d992aa (origin/master, origin/develop) Init

In Klammern sieht man jeweils den letzten Zustand des entsprechenden Branches. Außerdem (HEAD, master) lässt erkennen, dass der aktive Branch "master" ist und der Commit mit der kurz-ID "c311082" in diesem der aktuelle Commit.

Arbeiten mit Git

Wir beschreiben hier kurz und sehr abstrakt die einzelnen Funktionen, die ein Git-Client generell zur Verfügung stellt. Wie die Befehle in einzelnen GUI Programmen implementiert sind ist unterschiedlich.

Name und Email einstellen

Jeder Commit wird mit Namen und Email versehen. Diese muss man (entweder per Repository oder global) einstellen.

git config --global user.email "jane.doe@example.com"
git config --global user.name "Jane Doe"

Hinweis: Seiten wie GitHub oder auch unsere Gitea Instanz verwenden die Emailadresse um den Commit einem Benutzer zuzuordnen, die eingestellten Emailadressen sollten also übereinstimmen.

Repository erstellen

Um einen Ordner unter Git Versionskontrolle zu stellen führt man im entsprechenden Ordner den init Befehl aus:

git init

Git antwortet darauf mit

Initialized empty Git repository in <pfad zum ordner>/.git/

Repository klonen

 
In der Repositoryansicht von Gitea sieht man die genauen URLs für das eigene Gruppenrepository in der roten Box.

Um ein bestehendes Repository in einen Ordner zu klonen benutzt man den clone Befehl. Dabei kann entweder HTTPS oder SSH als Protokoll verwendet werden. Beachten Sie dazu auch die Anleitungen zum Hinzufügen eines SSH Keys zu Gitea und Setzen eines HTTPS Kennworts für Gitea.

Für HTTPS verwendet man folgenden Aufruf:

git clone https://git.sopranium.de/<semester>/<gruppe>.git

Für SSH ist der äquivalente Befehl

git clone gitea@git.sopranium.de:<gruppe>/<gruppe>.git

Der Befehl clone legt eine exakte Kopie der gesamten Repositorydaten auf der lokalen Maschine ab.

Am Repository arbeiten

Im Folgenden zeigen wir die grundlegenden Handgriffe, die zur Benutzung von Git notwendig sind. Um tiefer in die Materie einzutauchen empfehlen wir Git Pro Book.

Neue Datei

Eine neue Datei ist zunächst untracked. Um die Datei committen zu können muss sie erst in den Staging Zustand gebracht werden

git add <datei>

Jetzt können die Dateien committet werden:

git commit -m "Added <datei>"

Änderungen an einer Datei

Gibt es Änderungen an einer Datei, befindet sich diese im modified Zustand. Dies sieht man mittels git status. Die jeweiligen Änderungen können mit git diff <datei> angezeigt werden.

Änderungen an einzelnen Dateien können direkt committed werden:

git commit <datei_1> <datei_2> -m "Fix gun sound delay."

Um alle Dateien, die sich im Staging Zustand befinden (also solche die mit git add als im Staging Zustand markiert wurden), können auch committed werden. Dateien die geändert wurden müssen auch mit git add gestaged werden.

git commit -m "My changes regarding that thing."

Remote Änderungen synchronisieren

Änderungen die im remote Repository gemacht wurden können mit

git fetch

in das lokale Repository geladen werden. Achtung: git fetch merged die Änderungen der lokalen branches nicht automatisch in die aktualisierten remote Branches. Ein manueller Merge kann nun mit git merge <remote/branch> <branch> ausgeführt werden.

Ist der aktuelle Branch ein einfacher Klon des Remotebranch ohne eigene spezielle Einstellungen, wird automatisch der local Branch den remote Branch tracken. Damit kann einfacher der pull Befehl verwendet werden

git pull

der automatisch die Änderungen in den lokalen Branch merged. Siehe auch die Dokumentation dazu. Um unschöne Merges zu vermeiden, kann dem Befehl auch ein --rebase angehängt werden. Anstatt einen neuen Commit zu erstellen, in dem der lokale und der remote Branch gemerged werden, und diesen dann zu committen, werden alle Änderungen die am lokalen Branch gemacht wurden an den remote Branch angehängt, als wären sie erst nach dem fetch entstanden.

Änderungen in das remote Repository laden (pushen)

Sind eigene Änderungen (Commits) vorhanden, was mit dem git status Befehl überprüft werden kann. Beispielsweise

$ git status
On branch develop
Your branch is ahead of 'origin/develop' by 1 commit.
  (use "git push" to publish your local commits)

Zeigt an, dass 1 lokaler Commit noch nicht veröffentlicht wurde. Um den Commit in das remote Repository zu laden wird der push befehl verwendet:

git push

Es kann passieren, dass git push mit einer Fehlermeldung abbricht, die bessagt, dass es Änderungen auf dem remote Branch gibt, die es auf der lokalen Kopie nocht nicht gibt. In diesem Fall müssen zuerst die Änderungen von remote geladen werden.

To <remote url>
! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to '<remote url>'
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.

Änderungen Rückgängig machen

Wichtig ist zu unterscheiden, ob Änderungen gemacht werden sollen an Dingen, die bereits remote vorhanden sind oder nur local (also noch nicht synchronisiert). Im ersten Fall sollte die Änderung immer durch einen neuen Commit erfolgen. Im letzten Fall können auch brachialere Methoden angewendet werden, die Commits tatsächlich löschen.

Commit nachricht ändern

Falls noch nicht mit dem remote synchronisiert, kann mit dem Befehl

git commit --amend

Der commit geändert werden.

Dateien löschen

Versionierte Dateien lassen sich durch den rm Befehl entfernen.

git rm <datei>

Dieser Befehl löscht die Datei und fügt die Löschung zur Stage hinzu, so dass sie committet werden kann.


Dateien Ignorieren

Man kann in Git mittels einer .gitignore Datei andere Dateien ignorieren, d.h. sie explizit nicht unter Versionskontrolle stellen. Das ist insbesondere für temporäre Dateien (die z.B. bei jedem Build neu erzeugt werden) oder benutzerspezifische Einstellungen sinnvoll und muss von jedem verwendet werden. Die folgenden Dateien und Verzeichnisse müssen auf jedenfall auf die Ignore-Datei und dürfen nicht mit eingecheckt werden:

Warum?
binDie Dateien in diesem Ordner werden automatisch beim Kompillieren erstellt, sie hochzuladen ist also unnötig. Zudem sind auch binäre Dateien dabei die sich beim kompilieren häufig ändern. Da binäre Dateien nicht gemerged werden können wird das zu häufigen Konflikten mit den Commits anderer Teammitglieder führen.
objSiehe bin.
*.suoDie Dateien mit der Endung .suo (Solution User Options) beinhalten eine Reihe von Benutzer-spezifischen Einstellungen für Visual Studio, die niemanden außer den Benutzer selber interessieren. Sie enthalten außerdem eine Reihe von absoluten Pfadangaben, die bei anderen Benutzern massive Probleme auslösen können. Sie sollen auf keinen Fall zum Repository hinzugefügt werden.
*.cachefileSiehe bin. Auch merkt sich MonoGame hier, welche Dateien es schon in ein ihm genehmes Format konvertiert hat. Wenn ein anderes Teammitglied eine neue Datei hinzufügt und diese Datei im .cachefile als bereits konvertiert markiert ist, kann es passieren daß sich XNA denkt "Hey die Datei hab ich doch schon" und sie nicht neu konvertiert.
*.thumbVon Windows generierte Datei, die Vorschaubilder für die Miniaturansicht im Explorer enthält.
thumbs.dbSiehe .thumb
.vsVon Visual Studio generiertes Verzeichnis für interne Einstellungen.

Mit mehreren Branches arbeiten

Im Softwarepraktikum verwenden wir hauptsächlich 2 Branches (siehe Git Workflow im Softwarepraktikum).

  • release => Hier ist der aktuelle Stand des Projekts in lauffähigem Zustand mit fertig implementierten Tasks. Dieser Branch ist Grundlage für die Bewertung des Spiels und Abgabe von Artefakten. Dieser Branch muss zu jeder Zeit ein kompillier- und lauffähiges Spiel darstellen.
  • master => Hier werden die Tasks entwickelt. Sie müssen nicht zwangsläufig fertig sein, aber der master Branch soll zu jeder Zeit kompillieren und laufen.
  • feature/<task> => Feature branches können als Erweiterung des master Branches gesehen werden. Hier wird ein einzelner Task implementiert bis er fertig ist und in den master Branch gemerged wird.

Wichtige branch Befehle

git status                    # Zeigt neben dem aktuellen Zustand des Repos auch auf welchem Branch man gerade ist.
git branch -v                 # Zeigt verfügbare Branches mit aktuellem HEAD.
git checkout <branch_name>    # Wechselt den Branch nach <branch_name>.
git branch <branch_name>      # Erstellt einen neuen Branch <branch_name>.
git checkout -b <branch_name> # Erstellt <branch_name> und wechselt in diesen falls er noch nicht existiert.
git branch -d <branch_name>   # Löscht den <branch_name> Branch.

Branch mergen

Möchte man einen feature Branch nach master mergen, wechselt man zunächst in den master Branch

git checkout master

Jetzt merged man den feature Branch mit

git merge feature/<task>

Je nach Zustand des Repository gibt es nun mehrere Szenarios was passiert:

  • Fast-forward Merge Falls man den aktuellen HEAD des master Branch durch einfaches zurücklaufen in der History des feature/<task> Branch erreichen kann, sind offensichtlich Konflikte ausgeschlossen und Git kann einfach den HEAD von master auf den HEAD des feature/<task> Branches zeigen lassen. Effektiv wird also alles was in feature/<brach> seit dem erstellen des feature/<brach> passiert ist auch in master passieren.
  • Recursive Merge Falls in der Zwischenzeit der master Branch weiterentwickelt wurde, ist ein Fast-forward Merge nicht mehr möglich. Falls es aber keine Dateien gibt, die jetzt unterschiedliche Inhalte haben, wird bei einem recursive Merge ein neuer Commit erstellt, der die Vereinigung der Änderungen beider Branches darstellt. Git fordert den Nutzer in diesem Fall auf eine Commit Nachricht anzugeben. In der Regel sollte diese dann lauten: "Merge feature/<task> Evtl zusätzliche Info oder Zusammenfassung für neues Feature."
  • Merge Konflikt Falls in beiden Branches Änderungen an gleichen Inhalten gemacht wurden, weiß Git nicht wie diese aufzulösen sind. Wie man Merge Konflikte löst ist in Konflikte lösen beschrieben.
  • Weitere Möglichkeiten zu mergen werden hier beschrieben: https://git-scm.com/docs/merge-strategies

Konflikte lösen

Kann ein merge Befehl wegen einem Konflikt nicht ausgeführt werden, meldet sich git so

Automatic merge failed; fix conflicts and then commit the result.

Die betroffenen Dateien kann man sich mit git status anzeigen lassen. Eine Ausgabe kann dann so aussehen:

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   README.md

In diesem Fall ist die Datei README.md in beiden Branches bearbeitet worden und Git weiß nicht wie diese Datei nach dem Merge aussehen soll. Öffnet man diese Datei jetzt, kann der Inhalt beispielsweise so Aussehen:

<<<<<<< HEAD
# Setup
=======
# Installation
>>>>>>> feature/foo
1. Turn PC on.

Hier makiert alles zwischen <<<<<<< HEAD und ======= (In diesem Fall also # Setup) den vom Konflikt betroffenen Bereich im aktuellen master Zustand. Ab ======= bis >>>>>>> feature/foo (In diesem Fall also # Installation) Den vom Konflikt betroffenen Bereich im feature/foo Branch. Alle diese Bereiche (es kann mehrere geben) müssen nun manuell aufgelöst werden. Dabei ist es wichtig auch die Git Markierungen zu entfernen. Eine Lösung könnte also sein:

# Setup
1. Turn PC on.

Hat man die Konflikte einer Datei behoben, gibt man der Datei den "staged" Zustand (git add <datei>). Ist man mit allen Konflikten fertig, macht man einen Commit, dessen Nachricht in der Regel etwa lautet: "Merge feature/foo Evtl zusätzliche info oder Zusammenfassung für neues Feature"

Das rein manuelle Lösen von Konflikten kann sehr zeitintensiv und umständlich sein. Deshalb gibt es eine Reihe von Tools, die helfen. Git arbeitet mit einer Reihe davon zusammen und man startet diese mit

git mergetool

Hat man die Konflikte in dem Tool behoben, fügt Git die Dateien automatisch zur Stage hinzu, so dass man nur noch den merge Commit machen muss.

Git im Softwarepraktikum

Git für das Softwarepraktikum vorbereiten

Nachdem Sie sich für einen Git-Client entschieden haben, müssen Sie sicherstellen:

Git und Gitea

Gitea kann nicht nur die Commitmessages aus dem Repository anzeigen. In der Commitmessage kann auf ein Ticket verwiesen werden, indem man einfach die Nummer des Tickets (links neben dem Tickettitel in Gitea) mit # davor, angibt. Das sorgt auch dafür, dass der Commit in den Kommentaren des Tickets aufgelistet wird. Tickets können per Commitmessage geschlossen werden, indem davor noch closes (oder close, fix, fixes) geschrieben wird.

Beispiele

Dies ist ein einfacher Verweis #33.
Dieser Commit schließt das Ticket mit Nummer 33, closes #33.
Kombinationen sind auch möglich close #44 close #42, see #12.

Anmerkung: eine Referenz auf einen Issue darf nicht am Anfang einer Zeile stehen (dort leitet # eine Überschrift ein)

#Dies ist eine Überschrift!

Siehe auch

Links

Referenzen