Als Umsteiger von Subversion auf GIT stolpert man am Anfang über einige Hindernisse, da der Workflow sich ein wenig unterscheidet und dieser nicht sofort ersichtlich sein kann. Der große Unterschied ist, dass es keine zentrale Repository für den die Dateiablage gibt, sondern unendlich viele auf den unterschiedlichsten Maschinen.

Ein Repository forken

"Fork me on GitHub" - Was ist ein Fork?

Oft sieht man den Banner "Fork me on GitHub", doch was ist das und wie macht man das? Eine Fork kann man als komplett eigenständige Kopie eines Repositorys (Dateiablage) bezeichnen. In diesen kann man Änderungen vornehmen ohne dass die ursprüngliche Quelle (häufig upstream genannt) verändert wird. Jeder Entwickler forkt sein eigenes Repository in dem die eigenen Änderungen vorgenommen werden. Sollen diese mit anderen geteilt werden, können diese auch wieder in den upstream zurückgespielt werden.

Die eigene Fork eines Repositorys

Über die Schaltfläche Fork auf der GitHub Seite erstellt man zunächst eine Kopie des Repositorys.

Link zum forken eines Repositories

Nachdem man ein Repository auf GitHub geforkt hat, kopiert man diese Kopie via Terminal bzw. Windows Eingabeaufforderung auf die Festplatte.

Die genaue URL des Repositorys findet man unter "clone URL" auf der Seite.

Link zum GitHub Repositories

git clone https://github.com/octocat/Spoon-Knife.git

Durch das Klonen des Repositorys existieren nun bereits drei unabhängige Kopien: den upstream, die Fork auf GitHub und die lokale Kopie auf dem Rechner. Von dem lokalen Rechner aus gesehen, ist der Ursprung die Fork des entfernten Repository im Internet und wird daher origin genannt.

Den "upstream" festlegen

Durch das Klonen kennt die lokale Kopie nur die direkte origin Quelle:

git remote -v

Es fehlt aber noch die Referenz zum ursprünglichen Repository, damit Änderungen auch in der Kopie einfließen können. Daher muss der upstream händisch hinzugefügt werden:

git remote add upstream https://github.com/my-user/Spoon-Knife.git

Anschließend sollte der remote Befehl den upstream und den origin zurückliefern.

Natürlich lassen sich auch weitere entfernte Repositorys hinzufügen, falls man in komplexeren Projekten mitarbeiten möchte oder weitere Anbieter neben GitHub verwendet.

Branches verwalten

Was ist ein Branch?

Ein Branch ist übersetzt ein Entwicklungszweig auf dem parallel entwickelt werden kann. Insbesondere wenn man eine Anwendung produktiv warten muss, gleichzeitig aber auch an neuen Features bzw. Bugfix' arbeiten möchte, sind Branches sehr hilfreich.

Wenn man mit GIT arbeitet und Änderungen vornimmt, landen diese standardmäßig im master Branch (unter Subversion wäre dieses der Trunk) des Repositorys. Dieses ist nicht zu immer empfehlen, da es häufig zu Konflikten führen kann. Daher sollte der master Branch nur als Eingang für alle einkommenden Änderungen (z.B. des upstream oder des origin) dienen.

Wenn man ein atomares Feature entwickeln möchte, erstellt man für gewöhnlich einen eigenen Branch aus dem master. Sind in diesem allerdings Änderungen vorhanden, die nicht im upstream vorhanden sind, haben wir ungewollte Abhängigkeiten. GitHub merkt sich immer eine fortlaufende History, sodass jeder Commit genau einen Vorgänger hat. Eine lokale Änderung im master Branch würde eine andere History erzeugen als sie in den anderen Repositorys vorhanden ist, und somit zu Inkonsistenzen führen.

Feature Branch anlegen

Feature Branch anlegen

Arbeit man an mehreren Features, sollte man diese auf mehrere Branches aufteilen. Dieses hat den Vorteil, dass der master immer "sauber" bleibt, sodass man aus diesen beliebig viele Branches erstellen kann, ohne das es zu Konflikten kommt. Hat man seine Änderungen in den upstream zurück gespielt und der/die Besitzer des Repositorys diese akzeptiert, landen diese automatisch wieder im master Branch.

Technisch kann man auch auf den master Branch seines Repositorys arbeiten, es ist aber problematisch wenn man seine Änderungen zurückziehen möchte oder sich der master des ursprünglichen Repositorys verändert hat. Außerdem kann man mit Feature Branches eine bessere Unterscheidung zwischen Eigenentwicklung und entfernten Änderung behalten.

Damit der neue Branch auch in der lokalen Kopie bekannt ist, muss die Kopie aktualisiert werden:

git fetch 

Um auf den erstellen Feature Branch zu wechseln reicht folgender Befehl:

git checkout feature-abc

Repository synchronisieren

In regelmäßigen Abständen sollten die entfernten Änderungen in der lokalen Kopie eingespielt werden.

git fetch upstream

Die Änderungen werden dann lokalen zwischengespeichert.

Im master Branch werden die Änderungen des upstream master anschließend übernommen

git checkout master
git merge upstream/master

Die Befehle fetch und merge lassen sich auch über einen kombinierten Befehl ausführen. Diese landen dann in dem aktiven Branch.

git pull upstream master

GitHub Fork synchronisieren

Wenn das lokale Repository synchron mit dem upstream ist, können die Änderungen ebenso in die origin Fork zurückspielt werden. Dafür genügt folgender Befehl:

git push origin master

Das gleiche funktioniert ebenso für andere Branches oder Ziel Repositorys, hierbei muss das Wort master (bzw. das Wort origin) durch den entsprechenden Namen des Branches ersetzt werden.

Änderungen im Fork vornehmen

Lokale Änderungen erfassen

Nachdem Änderungen in den Dateien vorgenommen wurden, müssen diese am Ende im (lokalen) Repository aufgenommen werden. Hierfür werden Commits verwendet. Werden die Änderungen nicht lokal commited, werden diese im Falle eines cleanups zurückgesetzt und die Änderungen gehen verloren.

git add file.html
git commit -am "Beschreibung der Änderungen"
Über den add Befehl wird eine Datei (bzw. mehrere Dateien) hinzugefügt und mit commit wird diese final aufgenommen.

Änderungen zu GitHub zurückspielen

Die Änderungen sollen anschließend zurück in GitHub Repository (origin) landen. Dafür wird der push Befehl mit dem Namen des Ziels und dem dazugehörigen Branch (hier: feature-abc) verwendet.

git push origin feature-abc

Der Name des Ziel Branches muss nicht zwingend der gleiche sein. Man könnte genauso gut auf den lokalen master Branch arbeiten und diese in den feature-abc Branch übertragen. Zu beachten ist nur, dass man den gleichen Stand bzw. die gleiche Revision der Dateien auf dem lokalen Rechner besitzt. Dieses kann durch ein vorheriges fetch bzw. pull durchgeführt werden. Um diese Arbeit und Fehler zu vermeiden, empfiehlt es sich daher auf den gleichen Branch zu arbeiten.

Lokale Änderungen verwerfen

Falls man ausversehen doch einmal Änderungen im master Branch vorgenommen hat, kann man diese mit folgenden Befehlen in einen neuen Branch mit "git branch" verschieben und den master mit "git reset" zurücksetzen.

git branch feature-xyz
git reset --hard upstream/master
git checkout feature-xyz

Hierbei wird zuerst ein neuer Branch feature-xyz aus dem master erstellt und anschließend der master Branch auf den Stand des upstream Repositorys zurückgesetzt. Nach dem checkout Befehl arbeitet man am selben Stand in dem neuen Branch.

Änderungen mit anderen Teilen

Damit die Änderungen auch bei allen anderen Entwicklern landen muss auf der GitHub Seite ein pull request gestellt werden. Unter pull request > create a pull request werden alle Änderungen des aktiven Branches zusammengefasst.

Pull Request Dialog auf GitHub

Hier sollte eine eindeutige Beschreibung für die Änderungen gewählt werden und ein Kommentar der die genauen Änderungen beschreibt oder welche neue Funktionen durch den pull request hinzugekommen sind. Der Besitzer des Ursprungs muss diesen dann akzeptiert, damit dieser und anschließend im master (bzw. den entsprechenden Branch) des upstreams landet.

Änderungen überarbeiten mit "rebase" und "squash"

Oft passiert es, dass der Besitzer des upstream mit den Änderungen nicht zufrieden ist und man den pull request überarbeiten muss. Die einfachste Variante wäre es den pull request mit weiteren Commits zu ergänzen, dadurch wird die History allerdings mit vielen kleinen Commits unübersichtlich. Eine saubere, aber auch mit vorsichtig zu genießende Variante ist das Arbeiten mit rebase.

Mit rebase besteht die Möglichkeit die komplette Commit History zu verändern. Insbesondere bei öffentlichen und gemeinschaftlichen Repositorys ist hiervon aber abzuraten, da es zu Problemen bei anderen Personen führen kann, wenn Entwickler einen nicht mehr existenten Commit als Revision verwenden. Daher sollte man diese Funktion nur in eigenen Repositorys bzw. Branches verwenden oder diese vorher mit den Entwicklern absprechen.

Bevor die History umgeschrieben werden kann, wird zunächst der letzte Stand des entfernten Repositorys benötigt:

git fetch upstream

Der nächste Befehl gibt den Hash des Commits zurück an dem die Fork erstellt wurde.

git merge-base my-branch master

Den erhaltenen Hash Wert (hier: abcd123) wird für den nächsten Befehl benötigt um den interactive rebase Dialog zu erhalten.

git rebase --interactive abcd123

Alternativ kann man auch nur die letzten X Commits verändern:

git rebase --interactive HEAD~3

Es öffnet sich ein Text Editor mit den Commits:

pick 1fc6c95 do something
pick 6b2481b do something else
pick dd1475d changed some things

Der Text wird folgendermaßen verändert, sodass nur noch den Commit 1fc6c95 enthalten ist und alle weiteren zu diesen hinzufügen (gesquashed) werden.

pick 1fc6c95 do something
squash 6b2481b do something else
squash dd1475d changed some things

Nach dem Speichern und Schließen des Editors erscheint ein neues Fenster mit den kombinierten Nachrichten der einzelnen Commits. Dieser kann beliebig angepasst werden und dient als Text für den kombinierten Commit.

Mit dem folgenden Befehl wird der lokale Stand zurück zu dem remote Repository übertragen, hierbei wird die History neu geschrieben.

git push origin feature -f

Die Angabe des Ziel Branches feature ist optional, im Standard ist es der gleiche auf den gearbeitet wird. Über den Parameter -f steht für "Force" und dient zur Absicherung, da diese Operation als gefährlich angesehen wird und bestätigt werden muss, da hierdurch die History verändert wird.

Einzelne Commits übertragen mit "cherry-pick"

In manchen Fällen kann es vorkommen, dass nur einzelne Commits übertragen werden sollen, z.B. falls man sein Feature nochmal in mehrere Bestandteile unterteilen möchte. Hierfür kann das Cherry Picking verwendet werden.

Zunächst kann über den log Befehl die History angesehen werden um den SHA Wert des entsprechenden Commits zu erhalten:

git log

Der einzelne Commit soll in einen neuen Branch dev übernommen werden:

git branch dev
git checkout dev

Dieser Branch wird auf den upstream Stand zurückgesetzt:

git reset --hard upstream/master

Über cherry-pick wird der Commit in den neuen Branch übernommen:

git cherry-pick c98fd12

Dieser Befehl kann für beliebige commits wiederholt werden. Anschließend kann der Branch in weitere Repositorys mittels push zurückgespielt werden.

Änderungen zurücknehmen

Werden die Änderungen im upstream nicht mehr benötigt oder führen zu anderen Problemen kann man den pull request schließen. Hierdurch ist dieser immer noch im Archiv sichtbar, er steht aber nicht mehr zur Überprüfung und mergen zur Verfügung.

Dafür muss man den pull request auf GitHub öffnen und kann diesen über den Button Close pull request schließen.

Pull request schließen auf GitHub

Sollten die Änderungen bereits akzeptiert worden sein, kann man die Änderungen auch reverten. Hierbei wird automatisch ein neuer pull request erstellt, der die Änderungen zurück nimmt. Der revert muss allerdings durch den Besitzer wieder akzeptiert werden. In der Detailansicht des pull request kann man nach dem Akzeptieren über den Revert Link ein neues pull request erstellen.

Commit auf GitHub zurücknehmen

Über die Konsole lässt sich ein Commit über den folgenden Befehl zurücknehmen. Dabei wird Hash-Wert des Commits als Parameter benötigt.

git revert c98fd12

Aufräumen

Branch aufräumen

Nachdem man die Änderungen in das upstream Repository übertragen und angenommen wurden, kann man die Fork aufgeräumt und alte Branches gelöscht werden. Dieses dient in erster Linie der Übersicht, damit nach einigen Monaten nicht dutzende alte Branches im Repository auftauchen.

Auf der GitHub Seite kann das über den Link branches erhält man eine Übersicht über die man einen Branch löschen kann.

Branch Liste auf GitHub

Auf dem lokalen Rechner reicht folgenden Befehl:

git branch -d feature-abc

Alternativ lässt sich auch ein entfernter Branch über die Konsole entfernen:

git push origin :feature-abc

Probleme

Konflikt beim Übertragen

error: remote 'refs/heads/master' is not an ancestor of
local  'refs/heads/master'.
Maybe you are not up-to-date and need to pull first?
error: failed to push to 'git://[email protected]/path/to/repo.git'

Diese Fehlermeldung erhält man, wenn man beim Übertragen der lokalen Änderungen nicht die letzte Revision aus dem Ziel Repository hat. Es genügt, wenn man diese mittels pull erneut abholt.

Hinweis: Wenn man Änderungen übertragen, muss der letzte Comment in dem Ziel Repository immer der Vorgänger des zu pushenden Commits sein.

Begriffsübersicht

upstream: Bezeichnet das ursprüngliche Repository, das geforkt wurde.
fork: Bezeichnet ein kopiertes Repository
origin: Bezeichnet das ursprüngliche Repository aus der lokalen Sicht.
branch: Bezeichnet einen Zweig in einem Repository.
commit: Bezeichnet eine zusammenhängende Änderung eines Benutzers
head: Zeigt auf den aktuellen Branch