Tipps & Tricks rund um Apple, Mac, iPhone, iPod, iPad und Apple Watch für Umsteiger, Einsteiger und Neugierige

Sei (k)ein Schwachkopf: Versionsverwaltung mit Git unter Mac OS X

Das Wort »Git« ist zunächst nichts weiter als Britischer Jargon für »Schwachkopf« oder auch »Blödmann«. Das »Git«, mit dem wir uns im Rahmen dieses Artikels befassen, ist allerdings weder schwach noch blöd — ganz im Gegenteil. Git ist ein Open-Source-System zur Versionsverwaltung, welches im April 2005 von Linus Torvalds "erschaffen" wurde, um es zur Verwaltung des Linux-Kernel-Quellcodes einzusetzen. Torvalds begründete die groteske Namensgebung damit, dass er seine Projekte stets nach sich selbst benennen würde.

»I'm an egotistical bastard, and I name all myprojects after myself. First ›Linux‹, now ›git‹.«
— Linus Torvalds

Einige Monate später — im Juli 2005 — wurde die Betreuung des Git-Projekts dem Japaner Junio Hamano anvertraut, der das Projekt seitdem betreut. Obwohl das System ursprünglich nur zur Verwaltung des Kernel-Quellcodes von Linux gedacht war, breitete sich das Projekt schnell aus und wurde von zahlreichen anderen Linux-Projekten zur Quellcode-Verwaltung adaptiert. Seit einiger Zeit erlebt Git zudem auch außerhalb der Linux-Welt einen Aufwind und wurde durch grafische Benutzeroberflächen und entsprechenden Hosting-Diensten in gewissem Maße "gesellschaftsfähig".

Heute ist Git die "State of the Art"-Lösung zur Versionsverwaltung in der Softwareentwicklung (aber auch anderen Branchen) und wird daher auch zunehmend in Unternehmen eingesetzt. Dies ist mit großer Wahrscheinlichkeit auch der Grund, warum Sie diesen Artikel gerade lesen.

Was ist »Versionsverwaltung«?

Die klassische Versionsverwaltung ist primär aus der Softwareentwicklung bekannt, wo damit unterschiedliche Entwicklungsstände von Projekten gesichert und im Notfall wiederhergestellt werden können. Allerdings dient die Versionierung hierbei nicht nur der reinen Erstellung von Sicherungskopien der einzelnen Fortschritte, sondern auch, um dezentral und parallel in Teams an gemeinsamen Projekten entwickeln zu können. Hierbei fungiert die Versionsverwaltung dann also als zentrales Lager für den Quellcode (»Repository«), aus dem die einzelnen Entwickler den aktuellen Stand des Projekts beziehen können und in welches dann später auch die vorgenommenen Änderungen wieder eingespielt werden können.

Wo parallel und dezentral gearbeitet wird, kommt es natürlich zwangsweise zu Konflikten und Überschneidungen von Quellcode-Dateien und Teilen des Quellcodes in der selben Datei. Hierfür findet bei Systemen zur Versionsverwaltung eine Zusammenführung (»Merging«) der einzelnen Teile statt. Arbeiten also mehrere Entwickler an ein und derselben Datei, so sind Versionsverwaltungssysteme meist fähig, die jeweiligen Änderungen zu erkennen und im Repository zusammenzuführen — und sollte die automatische Zusammenführung nicht funktionieren (etwa weil identische Code-Zeilen bearbeitet wurden), kann der aufgetretene Konflikt von Hand aufgelöst werden.

Weitere wichtige Features in der Versionsverwaltung sind darüber hinaus Tags und Branches. Mit Tags können Entwicklungsstände wie bspw. Release-Versionen (v0.1, v0.2, … , v1.0, v1.1, …) gekennzeichnet werden, um später einfach auf diese zurückspringen zu können oder eine bestimmte Version wiederherzustellen. Branches ermöglichen dabei die Isolierung der dezentralen Entwicklung an ein und demselben Repository in verschiedene Zweige. Sie werden daher benutzt, um verschiedene Funktionen voneinander isoliert zu entwickeln. Der Master-Branch ist dabei der "Standard"-Branch und repräsentiert oftmals die laufende, stabile Live-Version (oft auch »Production-Version« genannt) und für Änderungen zweigen die Entwickler von diesem in Form eines eigenen Branches ab, entwickeln in diesem Branch und führen nach Abschluss der Arbeiten diesen eigenen Branch und den Master-Branch wieder zusammen.

Unter dem Punkt 'Best Practices' wird im Laufe dieses Artikels nochmals detaillierter auf das Branching eingegangen.

Git ist dabei bei Weitem nicht das einzige System zur Versionskontrolle: Weitere bekannte und populäre Systeme sind »CVS« sowie dessen Nachfolger »SVN«. Zwei große Besonderheiten, die Git mit sich bringt, sind dessen Effizienz und Geschwindigkeit — auch bei großen Projekten mit langen Projekt-Geschichten. Zudem benötigt Git dabei keinen zentralen Projekt-Server, auf dem gearbeitet wird. Oft existiert jedoch ein zentraler Server (»Remote«), auf den alle Entwickler nach Abschluss ihrer Arbeiten pushen. Dabei lädt jeder Entwickler das vollständige Projekt (inkl. seiner vollständigen Historie) vom Remote-Repository in seine lokale Umgebung (»Working Copy«) — durch diesen dezentralen Ansatz ist eine Ausfallsicherheit des Entwicklungsverlaufs gewährleistet, da jeder Entwickler auch bei Ausfall des zentralen Repositories stets über das gesamte Projekt verfügt und keine Unterscheidung zwischen lokalen und entfernten Entwicklungszweigen stattfindet. Dieser fehlende Zwang einer zentralen Instanz ermöglicht es zudem, ausschließlich für sich selbst lokal auf dem eigenen Mac zu arbeiten. Der initiale Installations- und Konfigurationsaufwand, um Git zu nutzen, ist dabei kurz und schmerzlos.

Installation

Git ist bereits Teil jeder Installation von Mac OS X und muss daher nicht gesondert nachinstalliert werden. Welche Git-Version installiert ist, kann mit folgendem Befehl im Terminal (befindet sich im Programm-Ordner "Dienstprogramme") ausfindig gemacht werden.

git --version

Konfiguration

Zunächst müssen Name und E-Mail-Adresse des Entwicklers angegeben werden. Diese Angaben werden als Autor-Angaben für Commits verwendet und sollen dazu dienen, den Entwickler zu identifizieren.

git config --global user.name "Moritz Macintosh"
git config --global user.email moritz@icloud.com

Die Konfiguration wird dabei global in der Datei .gitconfig im Home-Verzeichnis des Benutzers abgelegt. Dort kann sie überprüft und/oder manuell editiert werden.

Befehle

Die folgenden Befehle repräsentieren das grundlegende Werkzeug für den Einstieg in Git. Da sich dieser Artikel mit dem Einstieg in die Versionsverwaltung mit Git beschäftigt, wird nur auf die für den Start relevanten Befehle und Parameter eingegangen.

Erstellen / Klonen

Neues Repository anlegen

Erstellt ein neues Repository in dem Ordner, in dem der Befehl aufgerufen wurde.

git init

Lokales Repository klonen

git clone /pfad/zum/repository

Remote-Repository klonen

git clone username@host:/pfad/zum/repository

Hinzufügen / Entfernen

Ein Repository besteht prinzipiell aus 3 "Instanzen". Die erste ist die eigene Arbeitskopie (»Working Copy«), die die "echten" Dateien enthält. Die zweite ist der Index, der als eine Art Zwischenstufe (»Staging«) agiert. Die dritte ist der HEAD, der auf den letzten Commit zeigt. Änderungen wandern dabei schrittweise von der ersten bis zur dritten Instanz: Sie werden zunächst in den Dateien in der Arbeitskopie vorgenommen, werden dann über die folgenden Befehle dem Index hinzugefügt und schließlich in Form eines Commits zum HEAD der Projekt-Geschichte.

Änderungen an einzelner Datei dem Index hinzufügen

git add <dateiname>

Alle Änderungen an Dateien dem Index hinzufügen

git add .

Entfernen von Dateien aus dem Index

git rm --cached <dateiname>

Status anzeigen

Zeigt veränderte Dateien im Working-Tree an.

git status

Commit & Synchronisieren

Sind die gewünschten Änderungen dem Index hinzugefügt, werden diese in Form von Commits bestätigend freigeschaltet — also "fix" gemacht.

Änderungen commiten

git commit -am "Commit message"

Commits auf Remote-Repository pushen

Nach dem Commit befinden sich die Änderungen im HEAD, aber noch nicht im entfernten Repository. Daher müssen Commits dann noch gepusht werden.

git push origin master

master ist hierbei der Branch, der gepusht werden soll.

Remote-Repositories anzeigen

git remote -v

Remote-Repository zum lokalen Repository hinzufügen

Wurde ein Repository nicht von einem Remote geklont, soll später aber dorthin gepusht werden, können beliebig viele Remote-Repositories hinzugefügt werden.

git remote add origin <server>

Remote-Repository ersetzen

git remote set-url origin <server>

Branches

Neuen Branch anlegen und dorthin wechseln

git checkout -b <branch>

Zu Branch wechseln

git checkout <branch>

Zu Master-Branch zurückwechseln

git checkout master

Branch löschen

git branch -d <branch>

Lokalen Branch in das Remote-Repository pushen

gut push origin <branch>

Updating

In der Entwicklung in Teams ist es selbsterklärend wichtig, dass jeder Entwickler selbst seine vorgenommenen Änderungen pusht, sondern auch, dass alle Entwickler diese Änderungen untereinander abgleichen.

Lokales Repository auf Änderungen am Remote-Repository aktualisieren

git pull

Mit einem Pull werden dabei eigentlich zwei Befehle ausgeführt: Fetching, bei dem die Änderungen zunächst heruntergeladen aber noch nicht angewandt werden, und Merging, bei dem heruntergeladene Änderungen auf den lokalen Stand angewandt werden.

Merging

Änderungen von anderem Branch mergen

git merge <branch>

Änderungen/Unterschiede zwischen zwei Branches einsehen

git diff <quell_branch> <ziel_branch>

Tagging

Tag erstellen

git tag <tag> <commit_id>

Commit-IDs anzeigen

git log

Restore

Datei der Arbeitskopie durch die letzte aus dem HEAD ersetzen

git checkout -- <dateiname>

Änderungen, die bereits dem Index hinzugefügt wurden, bleiben bestehen.

Arbeitskopie vollständig zurücksetzen

Um lokale Änderungen vollständig zu entfernen, kann der letzte Stand vom Remote bezogen werden.

git fetch origin
git reset --hard origin/master

Tagging

Tag setzen

Tags werden auf Commits gesetzt, die dann den letzten Commit unter dem entsprechenden Tag (also der Version, wenn Tags für Release-Versionen verwendet werden) darstellen. Diese werden dabei über ihre Commit-ID angesprochen. Es ist möglich, die Commit-ID zu kürzen, solange diese noch eindeutig ist. In der Regel werden — sofern möglich — die ersten 10 Zeichen der Commit-ID verwendet.

git tag 1.2.3 3f09db07b3

Liste aller Tags anzeigen

git tag -l

Commits eines bestimmten Tags anzeigen

git show 1.2.3

.gitignore

In beinahe jedem Projekt existieren Dateien und Ordner, die nicht in ein Repository übernommen werden müssen oder sollen — etwa Cache-Ordner. Hierfür wird im gewünschten Repository eine Datei namens .gitignore angelegt und dort die entsprechenden Regeln eingepflegt. Meist gibt es keine allgemeingültige .gitignore-Datei, da sich die auszuschließenden Dateien von Projekt zu Projekt unterscheiden. Ein einfaches Beispiel könnte wie folgt aussehen.

# Einzelne Datei ausschließen
.DS_Store

# Ordner ausschließen
build

# Einzelne Datei in Ordner ausschließen
src/main.less

# Wildcards
*.zip

Zudem besteht die Möglichkeit, eine globale .gitignore-Datei anzulegen. Darin enthaltene Regeln werden dann für alle Git-Repositories angewandt. Diese Datei wird in der Regel im Home-Verzeichnis des aktuellen Benutzers unter ~/.gitignore_global abgespeichert und mit folgendem Befehl aktiviert.

git config --global core.excludesfile ~/.gitignore_global

Änderungen in .gitignore werden ignoriert?

Wurden Änderungen an der .gitignore-Datei getätigt, diese aber ignoriert, so muss der Git-Cache mit folgendem Befehl geleert werden. Keine Sorge: Dabei werden die Dateien nicht auf dem Dateisystem gelöscht, sondern lediglich aus dem Tracking-Cache von Git.

git rm -r --cached .

Anschließend werden wieder alle Änderungen dem Index hinzugefügt — nun wird die .gitignore-Datei allerdings korrekt angewandt — und commited.

git add .
git commit -m "Nun funktioniert die .gitignore-Datei wieder korrekt."

Übersicht

Die folgende Grafik resümiert nochmals die grundlegenden Befehle und die Instanzen Arbeitskopie, Index sowie die Projekt-Geschichte.

Zusätzlich visualisiert das folgende Zustandsdiagramm nochmals die verschiedenen Zustände von Änderungen und die grundlegenden Befehle hierbei.

Tools / GUIs

Wer sich im Terminal und mit obigen Befehlen eher unwohl fühlt, der kann auf Tools und GUIs für die Arbeit mit Git zurückgreifen. Meine persönliche Empfehlung ist hierbei »SourceTree« von Atlassian, das neben Mac OS X auch für Windows erhältlich ist.

Dabei hat Atlassian das Rad nicht neu erfunden, aber die Einstiegshürde zur Arbeit zum Git stark reduziert. SourceTree bietet kein zentrales Killer-Feature, sondern diverse Kleinigkeiten, die die tägliche Arbeit mit Git vereinfachen.

Neben SourceTree existieren noch folgende Tools bzw. GUIs für die Arbeit Git unter Mac OS X.

Best Practices

Branches verwenden

Branches sind eine der mächtigsten und wichtigsten Funktionen von Git — und das nicht ohne Grund: Das schnelle und einfache Erstellen von (parallelen) Zweigen in der Entwicklung war von Anfang an eines der zentralen Anforderungen. Sie vermeiden Chaos und gegenseitiges Überschreiben von Code während der parallelen Entwicklung. Für jedes neue Feature, jeden Bugfix und jede Idee sollte daher ein neuer, eigenständiger Branch erstellt werden, in dem das Feature, der Bugfix oder die Idee ausprogrammiert wird und nach Abschluss in den Master gemergt wird.

Dabei wird zu keinem Zeitpunkt im Master-Branch selbst entwickelt und herumprobiert. Der Master-Branch bleibt sauber und stellt die Production-Version dar. Nach Abschluss eines Branches wird ein Pull-Request an den Verantwortlichen (meist der Leiter der Entwicklung eines Teams) geschickt und nur dieser merged den Branch in den Master. Jeglicher Push/Merge direkt in den Master-Branch wird kommentarlos reverted.

Verwandtes in Commits zusammenfassen

Ein Commit sollte die Hülle für verwandte und zusammengehörende Änderungen sein. Werden bspw. zwei unteschiedliche Bugs behoben, sollten die Änderungen in zwei unterschiedliche Commits, die jeweils einem Bug zugehörig sind, verpackt werden. So ist es leichter für andere Entwickler, Änderungen nachzuvollziehen und im Fehlerfall Rollbacks durchzuführen.

Änderungen vor jedem Commit testen

Ist eine Änderung ausprogrammiert, darf diese nicht blind in der Annahme, sie sei komplett, commitet werden. Jede Code-Anpassung muss zuvor gründlich getestet werden, um sicherzustellen, dass die Änderung auch wirklich vollständig abgeschlossen ist und keine unerwünschten Nebenwirkungen resultiert.

Gute Commit-Messages schreiben

Commits sollten mit einer kurzen Zusammenfassung der vorgenommenen Änderungen beginnen (~50 Zeichen als Orientierung). Zudem sollte angegeben werden, was die Motivation für die Änderung war (bspw. Angabe eines Tickets) und was sich von der bisherigen Implementierung unterscheidet. In der Formulierung sollte stets Imperativ Präsens verwendet werden (»change« statt »changed« oder »changes«), um die Konsistenz mit den vom Git-Merging-Tool erzeugten Messages zu wahren.

Commits möglichst oft erstellen

Das häufige Commiten von Änderungen hält die Commits klein sowie übersichtlich und ermöglicht es, Verwandtes zusammenzufassen. Darüber hinaus ermöglicht es, Änderungen am Code schneller und häufiger mit den anderen Entwicklern im selben Repository zu teilen. So lassen sich Änderungen schneller/besser einspielen und größere Merge-Conflicts vermeiden.

Keinen halbfertigen Code commiten

Nur abgeschlossener Code sollte commitet werden. Dies bedeutet nicht, dass ein großes Feature vollständig abgeschlossen werden muss, bevor die Änderungen commitet werden — das Gegenteil ist der Fall: Große Änderungen sollten in kleine und logische Einheiten unterteilt werden, die jeweils für sich abgeschlossen und commitet werden. Es macht aber keinen Sinn, am Ende des Tages halbfertigen Code zu commiten, nur damit der Feierabend guten Gewissens eingeleutet werden kann.

Sollte eine saubere Arbeitskopie nötig sein (etwa um einen frisch-aufgetauchten Bug zu beheben), sollten die bisherigen Arbeiten nicht einfach commitet werden, sondern durch die Stashing-Funktion von Git verwahrt werden. Ist der Zwischen-Bug behoben und commitet, können die vorherigen Änderungen wiederhergestellt werden.

Versionskontrolle ≠ Backup-System

Das eigene Repository in Form eines Remote-Repositories gesichert zu haben ist ein netter Seiteneffekt der Verwendung von Versionskontrolle während der Entwicklung. Allerdings sollte die Versionskontrolle nicht als klassisches Backup-System verstanden werden: Die Aufmerksamkeit sollte auf semantischen und durchdachten Commits liegen, nicht auf dem reinen Vollstopfen mit Dateien.