Git


DatumÄnderung
13.04.2012Draft
17.05.2012Version 1
18.05.2012Version 2: Weiterer Bezug auf TortoiseGit, Kommandos mit Hyperlinks auf die Manpage versorgt, kleinere Korrekturen
28.05.2012Version 3: Textsatzkorrektur, neues verlinktes Tutorial
08.06.2012Version 4: Installationshinweise zu TortoiseGit, kleine Korrekturen
18.12.2012Version 5: Git add -p und Verlinkung zu Patch Tutorial; Erklärung zum Umfang von Commits in "Was in ein Repository gehört"

1 Einleitung

Git ist das Versionverwaltungs-System, welches Linus Torvalds für den Linux-Kernel geschrieben hat. Es ist zur Zeit das wohl mächtigste und beliebteste Versionierungswerkzeug der Open Source Gemeinde und hat mit GitHub eine sehr große Community.

Falls ihr Fehlerkorrekturen, Vorschläge für Kommandos/Szenarien, die hier auch reingehören, oder allgemein Verbesserungsvorschläge habt, schreibt uns einfach eine Mail.

1.1 Inhalt und Ziel dieses Tutorials

Ziel dieses Tutorials ist einerseits ein grundlegendes Verständnis für die Arbeitsweise unter Git zu schaffen, als auch eine praktische Anleitung für die üblicherweise genutzten und auch viele fortgeschrittene Befehle zu geben. Dabei wird davon ausgegangen, dass ein Git-Repository des Projekteservers auf der einen und TortoiseGit bzw. das Git-Kommandozeilentool auf der anderen Seite genutzt wird. Da letzteres die Referenzimplementierung eines Git-Clients ist, wird die Benutzung direkt auch für dieses Tool erklärt. Jedoch befinden sich alle Befehle mit quasi gleichem Namen auch im Kontextmenü des Windows-Explorers bei installiertem TortoiseGit, sodass auf dieses nur explizit eingegangen wird, wenn sich etwas gravierend unterscheidet.

1.2 Allgemeine Tips für Git

  • Wenn ihr Git oder eine fortgeschrittene Funktion Gits das erste Mal nutzt: Probiert sie zuerst woanders aus! Kopiert den Repositoryordner oder legt euch gleich ein Spielwiesen-Git an.
  • Alle hier vorgestellten Befehle haben noch eine teilweise sehr große Anzahl von optionalen Argumenten. Hierzu kann unter Linux man git oder man git <befehl> genutzt werden. Diese Manpages sind auch zum Beispiel hier online abrufbar.
  • Ein sehr nützlicher Parameter ist -p, mit dem ihr bei vielen Befehlen, die etwas mit einer Änderung zu tun haben, diese Änderungen im Form eines Patch-Files einsehen könnt.
  • Da dieses Tutorial mitnichten das gesamte Spektrum von Gits Leistungsfähigkeit abdecken kann, seien an dieser Stelle weitere gute Tutorials und Ressourcen empfohlen:
    • Die Git-Homepage
    • Git-About: Eine kurze, optisch gefällige Zusammenfassung der Fähigkeiten und Vorteile Gits.
    • Git-Videos: Gute Videos, die Git erklären.
    • Git Ready: Sehr gute Anlaufstelle zum Erlernen von Git, unterteilt in verschiedene kurze und prägnante Abschnitte zu einzelnen Aufgaben. In Deutsch verfügbar.
    • A successful Git branching model: Sehr netter Artikel zum Thema richtiges Branching.
    • How to create and apply a patch with Git: Artikel über das Erstellen und Anwenden von Patches, also Dateien, die wie ein Commit in Git Änderungen beinhalten. Dieses Thema führt über unser Tutorial hinaus und wird hier nicht behandelt.

1.3 Begriffsklärung

Die folgenden Fachbegriffe werden im Laufe des Tutorials fallen und auch erklärt werden:

  • remote: Entferntes Repository, zum Beispiel auf dem Projekteserver (ähnlich der SVN-Location)
  • clone: Das Kopieren eines Repositories eines remotes (ähnlich svn checkout)
  • commit: Das Übertragen von Änderungen der Arbeitskopie ans lokale Repository (ähnlich svn commit, nur mit Zwischenschritt über das lokale Repository)
  • push: Das Übertragen der comitteten lokalen Änderungen ans remote Repository
  • pull: Das Übertragen von Änderungen am remote Repository ins lokale Repository mit anschließendem mergen der Änderungen (ähnlich svn update)
  • checkout: Das Holen einer Arbeitskopie aus einem Branch oder eines bestimmten Commits
  • rebase: Nachträgliches Verändern der Commit-Historie, zum Beispiel durch Eingliedern von Branches in andere Branches oder Zusammenfügen von Commits (squash)

1.4 Was in ein Repository gehört

Zu allererst: Unsere Repositories sind nicht dafür gedacht, euren Festplattenspeicher beim Fachbereich 4 für lau zu vergrößern. Wenn ihr riesige Dateien comitted, werden wir das bemerken!

  • gut: Quelldateien, Programmcode, Textdateien im Allgemeinen
  • noch gut: Grafiken, kleine PDFs, Word-Dokumente: Alles, was an sich keine Textdatei ist, aber eben auch nicht groß (<5MB). Wir übertreiben es nicht, eure Dokumentationen zum Beispiel könnt ihr ruhig Comitten.
  • schlecht: Binärdateien (erstellte Binaries (.exe), Java-Classfiles (.class), etc.) und allgemein alles, das aus den Quelldateien wieder erzeugt werden kann, gehört logisch nicht in ein Repository.
  • sehr schlecht: Große Videodateien, Installationsdateien eurer Tools etc.

Des weiteren sollte jeder commit ein Feature, eine abgeschlossene Sache darstellen. Dies lässt sich damit erklären, dass in Git einzelne Commits sehr leicht zurückgenommen (revert) oder herausgepickt und in andere Branches transportiert (cherry-pick) werden können. Beispiel: Ihr comittet ein neues Feature und ein paar Bugfixes im selben Commit. Soll das Feature nun zurückgenommen werden, da es z.B. nicht mehr gewünscht wird oder fehlerhaft ist, würden nun die Bugfixes gleichzeitig wieder entfernt werden.

Zu guter letzt ist es für eine gute Arbeit mit Git unerlässlich, dass nur funktionierende Arbeitsstände comitted werden. Stellt euch vor, ein Projektpartner übermittelt einen Programmcode, der nicht kompiliert werden kann - schon könnt ihr nicht mehr weiterentwickeln!

2 Git installieren/einrichten

2.1 Installation

Linux: Hier gibt es sicher für jede Distribution ein entsprechendes Paket, zum Beispiel für Debian/Ubuntu: apt-get install git.

Windows: Nach der Installation von TortoiseGit wird auch noch das eigentliche Git for Windows benötigt. In beiden Fällen sollte man sich von Begriffen wie beta oder testing im Namen des Downloads nicht verängstigen lassen und einfach die neueste Version nehmen, welche mit featured bezeichnet ist.

2.2 Konfiguration

Zu allererst solltest du Git deinen Namen und Mailadresse mitteilen, damit immer klar ist, wer einen entsprechenden Commit getätigt hat:

git config --global user.name "Firstname Lastname"
git config --global user.email "your_email@youremail.com"

TortoiseGit: Hier geht das, wie zu erwarten, bequem über die GUI: Rechtsklick im Explorer -> TortoiseGit -> Settings -> Git

3 Ein Git-Repository erstellen

Das Anlegen eines Repositories ist denkbar einfach: Auf der entsprechenden Management-Seite wählt ihr Neues Git-Repository erstellen, gebt einen gültigen Namen ein und bestätigt das Ganze. Nach einer kleinen Wartezeit hat der Server das Repository in seine Konfiguration aufgenommen.

Abb.1: Anlegen eines neuen Git-Repositories
Abb.1: Anlegen eines neuen Git-Repositories

Nun hat jedoch niemand anderes außer dir Zugriff auf dieses Repository! Das kann geändert werden, indem auf den Balken, der mit dem Namen des Repositories beschriftet ist, geklickt wird und in das entsprechende Eingabefeld ein oder mehrere gültige Benutzernamen angegeben werden. Alle eingetragenen Benutzer haben dabei die selben Rechte wie du - also Lesen und Schreiben.

Abb.2: Hinzufügen eines oder mehrerer Benutzer zu einem Git-Repository
Abb.2: Hinzufügen eines oder mehrerer Benutzer zu einem Git-Repository

4 Die Architektur

Im Vergleich zu anderen VCS (Version Control System) setzt Git nicht auf ein zentrales Repository auf irgendeinem Server, denn jeder Benutzer besitzt grundsätzlich eine vollständige Kopie des Repositories im .git-Unterordner.

Unser Projekteserver ist dabei nichts anderes als ein besonderer Benutzer, welcher keine Arbeitskopie eines Repositories, sehrwohl aber den .git-Ordner in seinem Dateisystem bereit hält. Dass wir ihn als zentrales Repository benutzen, ist also lediglich ein logisches, aber keine physisches Paradigma.

Der Server dient als sogenanntes remote, also ein Ort, an den Änderungen aus dem lokalen Repository zurückübertragen und synchronisiert werden können. Aufgrund der eben genannten Tatsache kann übrigens jeder Benutzer ein Remote sein! Das Benutzen von mehreren Remotes ist jedoch eine fortgeschrittene Technik und wird in diesem Tutorial nicht direkt behandelt werden.

5 Einfache Nutzung

5.1 Clone: Repository vom Server holen

Nachdem wir nun die Architektur kennen, wird auch klar, warum das erstmalige Herunterladen eines Repositories auch nicht checkout, sondern clone heißt: Das entfernte (remote) Repository wird einfach auf die lokale Festplatte kopiert und automatisch eine lokale Arbeitskopie des master-Entwicklungszweiges ausgecheckt.

git clone https://studi.f4.htw-berlin.de/git/f4studi/tutorial.git
Cloning into tutorial...
Username: 
Password: 
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.

Wird kein Ziel angegeben, wird in den Ordner mit dem Namen des Repositories - hier tutorial - geklont, welcher danach folgendermaßen aussieht:

$ ls -la
drwxr-xr-x  8 devel devel 4096  7. Mai 21:24 .git
-rw-r--r--  1 devel devel 2587  7. Mai 21:24 readme.txt

Zwei Dinge sind zu erkennen:

  1. Der .git-Ordner ist das Repository und eine exakte Kopie dessen, was auf dem Projekteserver liegt. Mit diesem Ordner muss zu keinem Zeitpunkt interagiert werden!
  2. Die readme.txt wurde von uns hinzugefügt, um eine kleine Anfangs-Hilfestellung zu geben. Sie ist eine Arbeitskopie des Entwicklungszweiges (branch) master, welcher die Spitze der Entwicklung, ähnlich dem trunk bei SVN, darstellt.

Übrigens kann die doch etwas nervige Abfrage des Benutzernamens und Passwortes auch bei Git automatisiert werden.

5.2 Staging und Commit: Änderungen lokal übertragen

Wir nehmen nun ein paar Änderungen an dieser readme.txt vor und wollen diese natürlich ins Repository übertragen, wobei es wichtig ist, zwei Schritte voneinander zu unterscheiden: staging und commit. Zunächst sieht Git unsere Änderungen, jedoch sind diese nicht bereit zum comitten, wie im status zu sehen:

$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

Nun kommt die Staging-Area ins Spiel. Diese ist ein eigener kleiner Index, in den alles kommt, was comitted werden soll. Dies geschieht auf der Kommandozeile mit gitadd<dateiname> oder auch z.B. gitadd. für den aktuellen Ordner und seinen gesamten Inhalt.

TortoiseGit: Hier geschieht das staging entweder explizit über das Kontextmenü->add... (nur bei neuen Dateien) oder implizit durch Anhaken der Datei im Commit-Dialog.

$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   readme.txt
#

Git weiß jetzt also, dass wir die Änderungen an der readme.txt übertragen wollen. Den Inhalt der Staging-Area an das lokale Repository zu committen, ist nun ganz einfach:

$ git commit -m "Unser erster Commit"
[master 90de6d4] Unser erster Commit
 1 files changed, 2 insertions(+), 0 deletions(-)

Anmerkung1: Wie in den oberen Beispielen bereits zu sehen ist, gibt Git auch zur Laufzeit Tipps, damit man sich nicht ständig alle Kommandos merken muss. Zum Beispiel ist es möglich, mitgit checkout -- <datei>die Änderungen an der Arbeitskopie rückgängig zu machen, was dem svn revert entspricht.

Anmerkung2: Um eine Datei wieder aus dem Staging-Bereich zu entfernen, kanngit reset HEAD <datei>verwendet werden. HEAD ist ein Zeiger auf den letzten ausgecheckten Commit - in diesem Fall also dem letzten Commit im master-Entwicklungszweig.

5.3 Push: Lokale Änderungen an den Server übertragen

Der grade übertragene Commit ist zur Zeit noch nur lokal gespeichert. Um ihn an den Projekteserver zu übertragen und es damit anderen Programmierern zu ermöglichen, die Änderungen am lokalen Repository zu nutzen, müssen diese an den Projekteserver zurückübertragen (gepusht)werden. Außerdem erhöht dies natürlich durch Speicherung auf einen entfernten Server die Datensicherheit.

$ git push
Counting objects: 5, done.
Delta compression using up to 3 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 320 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://studi.f4.htw-berlin.de/git/f4studi/tutorial.git
   61df014..90de6d4  master -> master

Was sagt die Meldung aus? Interessant ist eigentlich nur die letzte Zeile, die besagt, dass unser Commit 61df014 zum Eltern-Commit 90de6d4 vom lokalen master-Zweig in den entfernten master-Zweig übertragen wurde. Es muss übrigens nicht nach jedem commit gepusht werden - das kann zu einem beliebigen Zeitpunkt geschehen.

TortoiseGit: Push und Pull verstecken sich im Menüpunkt Git Sync...

Anmerkung: Die oben gezeigten Commit-IDs sind nur die bei Git üblichen Kurzformen der eigentlichen Hashes. Über jeden Commit wird solch ein Hash gebildet, welcher auf dem Eltern-Commit und anderen Faktoren beruht, sodass ein nachträgliches Fälschen der Historie (log) sehr schwer ist:

$ git log 
commit 90de6d4c321efaec38c45f21b1bf1641caec4179
Author: f4studi <f4studi@htw-berlin.de>
Date:   Mon May 7 22:14:42 2012 +0200

    Unser erster Commit

commit 61df01427663ac0c11aaac981f5489c2391f4f6d
Author: f4studi <f4studi@htw-berlin.de>
Date:   Fri Oct 7 00:41:22 2011 +0200

    Initial commit

5.4 Pull: Änderungen vom Server holen

Nun hat jemand anderes oder du selbst an einem entfernten Rechner etwas am Repository geändert und an den Projekteserver gepusht. Um diese Änderungen auf den lokalen Rechner zu bekommen, genügt ein

$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://studi.f4.htw-berlin.de/git/f4studi/tutorial
   90de6d4..68a2a52  master     -> origin/master
Updating 90de6d4..68a2a52
Fast-forward
 readme.txt |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)

TortoiseGit: Push und Pull verstecken sich im Menüpunkt Git Sync...

Im oben abgebildeten Fall wurden die Änderungen ins lokale Repository geholt und in die aktuelle Arbeitskopie eingefügt - pull holt übrigens den Stand aller Branches!

Solltest du parallel auch Änderungen an der selben Datei wie derjenige, dessen Änderungen du gerade gepullt hast, vorgenommen haben, muss Git diese Änderungen mergen. Meist geschieht dies vollautomatisch, da der Merging-Algorithmus von Git sehr gut arbeitet. Sollten die Änderungen an der Datei jedoch zu gravierend sein, musst du sie manuell mergen. Diesen Vorgang genau zu erklären, würde den Rahmen dieses Tutorials sprengen, jedoch seien zwei Hinweise gegeben:

  • Ein grafisches Merge-Tool ist sehr zu empfehlen. TortoiseGit-Benutzer haben das sowieso, aber auch andere Tools wie EGit für Eclipse sind verfügbar.
  • Google bietet viele gute Tipps zum Thema Merging.

Hinweis: Es könnte sein, dass du die Meldung bekommst, dass deine Arbeitskopie nicht sauber wäre und somit nicht gepullt werden kann. In diesem Falle musst du die Arbeitskopie bereinigen. Eine gute Möglichkeit dazu ist der Stash.

6 Fortgeschrittene Nutzung

6.1 Details der Architektur Gits

Kommt man aus dem Subversion-Bereich, fällt sofort auf, dass Gits Commits mit sehr langen Hashes und nicht mit fortlaufenden natürlichen Zahlen bezeichnet werden. Zwei Gründe hierfür sind:

  1. Die Hashes werden über der ID des Elterncommits, den übertragenen Daten und anderen Faktoren berechnet, was das nachträgliche Fälschen eines Commits erschwert.
  2. Da Git ein verteiltes System ist, in dem lokale Commits und Branches fest verankert sind, wäre es praktisch unmölich, eine fortlaufende Sequenz als ID zu vergeben.

Ohne noch viel weiter in die Tiefe zu gehen: Commits sind immer Änderungen am Repository, kein Abbild des gesamten Repositories. Sie haben immer einen (normaler Commit) oder mehrere (Merge) Vorgänger und bilden damit den Revisionsgraphen. Auf dieser wohl jedem Informatiker bekannten Datenstruktur interpretieren wir dann Begriffe wie Branches hinein.

So kann auch jeder Commit einzeln ausgecheckt werden: Es muss bei git checkout nicht unbedingt ein Branch angegeben werden, Commit-IDs sind ebenso möglich. Außerdem gibt es die sogenannten Referenzen, also Zeiger auf bestimmte Commits. Branches sind somit eigentlich auch nur Zeiger auf irgendeinen Commit, der berühmte HEAD ist einfach die Spitze des aktuellen Branches. Die folgende Abbildung zeigt anhand eines kleinen Beispiels, wie der origin/dev-Branch, also der dev-Branch auf dem Projekteserver, immernoch in der Mitte des Graphen hängt, obwohl dev schon lange in den master gemerged wurde.

Abb.3: Die Referenz auf den dev-Branch des Projekteservers hängt irgendwo.
Abb.3: Die Referenz auf den dev-Branch des Projekteservers hängt irgendwo.

Aus dieser Architektur Gits ergeben sich viele sehr praktische funktionen, die es von anderen Systemen abhebt. Auf diese wird im Folgenden eingegangen.

6.2 Branches

Branches - Entwicklungszweige - sind ein integraler Bestandteil Gits. Zur Erinnerung: Das, was wir auf der Festplatte als Repository sehen (den .git-Ordner ausgenommen) ist nur eine Arbeitskopie, die wir aus einem beliebigen Branch ausgecheckt haben. Dies steht im Gegensatz zu Subversion, wo ein Branch nicht mehr als ein kopierter Ordner, der per Konvention in /branches liegt, ist.

Bis jetzt haben wir immer in einem Branch gearbeitet, dem master. Das mag vielleicht in kleinen Projekten mit wenigen Mitgliedern nicht schief gehen, aber selbst dort wäre es angebracht. Stellt euch folgendes vor:

  1. Was passiert, wenn ich an einem neuen Feature arbeite, lokal schon ein paar Commits zum Sichern meines Arbeitsstandes gemacht habe und nun jemand einen Bug bemerkt, den ich beheben soll?
  2. Was, wenn ich an einem Feature arbeite, commits tätige, dann aber an einem anderen Rechner weitermachen möchte?
  3. Ist es nicht sehr unübersichtlich, wenn zehn Projektmitglieder an zehn Features arbeiten und ihre Zwischenstände in den Master comitten?

Da das hier keine Klassenarbeit ist, kommen die Antworten natürlich sofort:

  1. Hättest du das so gemacht, könntest du entweder den Bugfix mit deinen bereits gemachten Änderungen comitten und an den Server pushen - aber das wäre, keine Frage, extrem unsauber.
  2. Ohne Branch müsstest du die Änderungen im Master an den Server pushen und würdest somit in den eigentlich sauberen master deine Änderungen, die vielleicht sogar zu Kompilierungsproblemen führen, hineinpressen.
  3. Die Pflege des Revisionsgraphen ist bei Git sehr wichtig, weswegen es auch Befehle gibt, mit denen du die Historie im Nachhinein ändern kannst! Das hat unter Anderem einen sehr großen Vorteil: Ist ein neues Feature ein einziger Commit, so kann man diesen Commit und somit auch das Feature mit einem einzigen Befehl wieder rückgängig machen.

6.2.1 Einen Branch anlegen

$ git branch feature1
$ git checkout feature1
Switched to branch 'feature1'

TortoiseGit: Der Menüpunkt heißt direkt Create Branch... und der Checkout wird automatisch getätigt, falls ihr das Kästchen switch to new branch ankreuzt. Dies kann auch manuell mit Switch/Checkout... erledigt werden.

Nun, das war einfach. Wirklich! Der neue Branch wurde angelegt (branch) und wir haben ihn ausgecheckt (checkout). Da noch keine Änderungen an dem Branch getätigt wurden, befinden wir uns immernoch an der selben Stelle wie der Master.

Abb.4: Revisionsgraph nach Erstellung des Branches klzzwxh:0115.
Abb.4: Revisionsgraph nach Erstellung des Branches feature1.

Hier kann jetzt exakt so gearbeitet werden, als wäre es der master. Um das Folgende zu verdeutlichen, machen wir ein paar Änderungen in feature1 und master:

Abb.5: Revisionsgraph nach Änderungen an klzzwxh:0120 und klzzwxh:0121.
Abb.5: Revisionsgraph nach Änderungen an feature1 und master.

6.2.2 Einen Branch auf den Server pushen

Was ist zu tun, wenn auf einem anderen Rechner an einem Branch weitergearbeitet werden soll? git push überträgt feature1 nicht an den Projekteserver!

$ git push origin feature1
Counting objects: 5, done.
Delta compression using up to 3 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 326 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://studi.f4.htw-berlin.de/git/f4studi/tutorial.git
 * [new branch]      feature1 -> feature1

In den letzten beiden Zeilen der Erfolg: feature1 wurde auf dem Projekteserver erstellt.

TortoiseGit: Hier ist es noch einfacher: Im Sync/Push-Dialog kann der zu pushende lokale Branch direkt gewählt werden - oder man nutzt das Kästchen push all branches.

Abb.6: Der Branch wurde auch auf dem Projekteserver erstellt (klzzwxh:0129).
Abb.6: Der Branch wurde auch auf dem Projekteserver erstellt (origin/feature1).

Hinweis: git push arbeitet nur mit Branches, die im remote, also auf dem Projekteserver, existieren. Das heißt, dass ihr ab dem ersten git push origin <branchname> nur noch git push nutzen müsst.

6.2.3 Einen neuen Branch vom Server pullen

Das geschieht automatisch, sobald sich der neue Branch auf dem Projekteserver befindet (siehe voriger Schritt).

6.2.4 Branches zusammenführen (mergen)

Nun zum wichtigsten Feature Gits, dem Mergen. Das bedeutet nichts anderes, als dass Änderungen aus verschiedenen Commits bzw. Branches wieder zusammengeführt werden. Wie im ersten Teil des Tutorials bereits erwähnt, ist Gits Algorithmus hierfür sehr fortgeschritten, sodass selten ein manuelles Eingreifen erforderlich ist.

Auf diese Fälle kann hier leider nicht im Einzelnen eingegangen werden. Es sei nur gesagt, dass ein grafisches Merge-Tool wie das in TortoiseGit oder EGit für Eclipse eingebaute dringend empfohlen werden.

Nehmen wir also unser Repository von weiter oben her und mergen die Änderungen aus feature1 in den master, da das Feature komplett implementiert wurde. Achtung: Man muss sich im Branch befinden, in den hineingemerged werden soll, nicht anders herum!

$ git status
# On branch feature1
nothing to commit (working directory clean)

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 2 commits.

$ git merge feature1
Auto-merging readme.txt
Merge made by recursive.
 readme.txt |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)

Abb.7: Der Branch klzzwxh:0142 wurde in den klzzwxh:0143 gemerged.
Abb.7: Der Branch feature1 wurde in den master gemerged.

TortoiseGit: Hier gilt das selbe: Man muss sich im Branch befinden, in den hineingemerged werden soll. Danach kann aus dem Kontextemenü Merge... aufgerufen werden.

Kurz gesagt: Es befinden sich alle Änderungen an der readme.txt aus beiden Branches im master.

6.2.5 Historie verändern: Rebase

TÖDLICHER HINWEIS: Alle Änderungen an der Historie dürfen nur erfolgen, wenn die betreffenden Commits noch nicht gepusht wurden! Denkt daran: Git ist ein verteiltes System. Sobald jemand anderes eure Änderungen gepullt hat, zieht eine Änderung am Revisionsgraphen böse Inkonsistenz und den Untergang der Welt herbei!

Eingangs dieses Abschnittes wurde erwähnt, dass die Pflege der Historie bei Git wichtig ist. Sehen wir uns das Log an, bemerken wir, dass es zwei sehr ähnliche Commits im Master gibt. Wenn diese Commits nun eigentlich ein einziges Feature sind, sollten diese beiden Commits auch zusammengefasst werden!

Achtung: Dieser Schritt sollte Dringend vor einem Merge wie im vorigen Abschnitt beschrieben geschehen, da rebase die Historie komplett neu schreibt und den Merge verwirft!

Hinweis: Wenn ihr das erste Mal rebase benutzt, solltet ihr den Repositoryordner kopieren, falls ihr aus versehen etwas kaputt macht! Schaut euch vor dem endgültigen pushen den Revisionsgraphen an, um festzustellen, ob wirklich alles OK ist!

Hier kommt das magische Kommando rebase ins Spiel. Zuerst benutzen wir seine Fähigkeit, Commits zusammenzuführen (squash):

$ git rebase -i HEAD~2
[detached HEAD 0bdf2bd] Eine gemeinsame Änderung im Master.
 1 files changed, 4 insertions(+), 0 deletions(-)
Successfully rebased and updated refs/heads/master.

Die Anweisung lässt Git einen interaktiven rebase mit den letzten zwei Commits des aktuellen Branches (Erinnerung: HEAD ist eine Referenz auf die Spitze des aktuellen Commits) machen. Da es interaktiv ist, werden zwei Abfragen erscheinen:

pick 2c00a0a Eine Änderung im Master 1.
squash 05a501a Eine Änderung im Master 2.

Zuerst soll die Aktion gewählt werden. Hier nehmen wir den ersten Commit und squashen den zweiten in ihn hinein. Die folgende Abfrage ist ebenfalls selbsterklärend und möchte eine neue Commit-Message haben. Nach dieser Aktion sieht unser Revisionsgraph dann geordneter aus:

Abb.8: Der Revisionsgraph nach zusammenfügen zweier Commits zu klzzwxh:0166.
Abb.8: Der Revisionsgraph nach zusammenfügen zweier Commits zu Eine gemeinsame Änderung im Master.

Nun steht aber unser einzelner Commit im Branch, der ja auch schon durch einen Squash entstanden sein könnte, ziemlich alleine da. Sähe doch dumm aus, wenn der Branch aus einem einzigen Commit mit einem kleinen Feature bestehen würde. Hier kommt die eigentliche Magie rebases zum Vorschein:

$ git rebase feature1
First, rewinding head to replay your work on top of it...
Applying: Eine gemeinsame Änderung im Master.

Außer dass wir rebase anstatt merge schreiben, ändert sich an diesem Aufruf nicht viel. Doch, siehe da, der Revisionsgraph ist schön übersichtlich und bereinigt:

Abb.9: Nun wurde klzzwxh:0170 auch direkt in den Master überführt.
Abb.9: Nun wurde Eine Änderung im Branch auch direkt in den Master überführt.

TortoiseGit: Hier geschieht das alles über den Menüpunkt Rebase.... UpStream ist dabei der Branch, in den der Rebase gehen soll. Allerdings ist dieser Dialog im Vergleich zur Kommandozeile wirklich hakelig, sodass auch hier die Git-Kommandozeile unter Windows empfohlen wird.

6.2.6 Einen Branch löschen (lokal und remote)

Wird ein Branch nicht mehr benötigt, kann er sowohl lokal, als auch auf dem Server gelöscht werden:

$ git branch -d feature1
Deleted branch feature1 (was dcca4c0).

Löscht den Branch lokal. Achtung: Sollten sich im Branch Änderungen befinden, die noch nicht in den master gemerged wurden, wird eine Warnung angezeigt. Diese kann - auf eigene Gefahr! - mit -D ignoriert werden.

$ git push origin :feature1
To https://studi.f4.htw-berlin.de/git/f4studi/tutorial.git
 - [deleted]         feature1

Diese Anweisung löscht den Branch auf dem Projekteserver. Die Anweisung sieht zuerst etwas verwirrend aus, haben wir doch vorhin mit push origin feature1 erst den Branch erstellt! Die vollständige Syntax dieses Befehls lautet jedoch eigentlich push <remote> <local branch>:<remote branch>. In diesem Falle ist der lokale Branch einfach leer, wir überschreiben den remote Branch also mit nichts.

Abb.10: Der Revisionsgraph nach dem Löschen des lokalen und remote Branch klzzwxh:0181.
Abb.10: Der Revisionsgraph nach dem Löschen des lokalen und remote Branch (origin/)feature1.

TortoiseGit: Im Sync-Dialog gibt es neben den Auswahlfeldern für den local und remote Branch einen Button mit drei Punkten, über den Ihr an die Verwaltung der lokalen bzw remote Branches kommt.

6.3 Lokale Änderungen zwischenspeichern

Wenn man grade konzentriert an einem neuen Feature in einem Branch arbeitet, ein anderer Programmierer aber genau jetzt einen Bug gefixt haben möchte, hat man ein Problem: Wohin mit den lokalen Änderungen? Repository woanders neu klonen, master auschecken? Das geht viel einfacher: stash ist eine Art Zwischenspeicher, mit dem ihr eure Arbeitskopie ganz einfach säubern und somit an etwas anderem weiterarbeiten könnt.

$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

Es gibt Änderungen an der readme.txt! Diese jagen wir in einen Stash:

$ git stash
Saved working directory and index state WIP on master: 14de5dd Eine gemeinsame Änderung im Master.
HEAD is now at 14de5dd Eine gemeinsame Änderung im Master.

$ git stash list
stash@{0}: WIP on master: 14de5dd Eine gemeinsame Änderung im Master.

$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 2 commits.
#
nothing to commit (working directory clean)

Und die Arbeitskopie ist wieder sauber. Nun kann der gespeicherte Zwischenstand zum Beispiel mit pop wieder herausgeholt werden.

$ git stash pop
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (06c452424c48143531fe79e99992ed34371f71fb)

stash ist ein sehr nützliches und einfach zu bedienendes Tool. Im entsprechenden Handbuch können noch weitere Fähigkeiten nachgelesen werden, wie zum Beispiel das Anwenden eines bestimmten Stashes und nicht des ersten.

TortoiseGit: Auch hier ist TortoiseGit etwas beschnitten, sodass der Stash tatsächlich nur als Stack mit save und pop benutzt werden kann.

6.4 Arbeitskopie säubern

Manchmal möchte man die Arbeitskopie von allen nicht von Git getrackten Dateien säubern (clean), zum Beispiel kompilierte Binärdateien:

$ ll
insgesamt 20
drwxr-xr-x  3 devel devel 4096 17. Mai 16:56 .
drwxr-xr-x 52 devel devel 4096 17. Mai 16:56 ..
drwxr-xr-x  8 devel devel 4096 17. Mai 16:53 .git
-rw-r--r--  1 devel devel    7 17. Mai 16:56 muelldatei
-rw-r--r--  1 devel devel 2747 17. Mai 16:53 readme.txt

$ git clean -f
Removing muelldatei

$ ll
insgesamt 16
drwxr-xr-x  3 devel devel 4096 17. Mai 16:56 .
drwxr-xr-x 52 devel devel 4096 17. Mai 16:56 ..
drwxr-xr-x  8 devel devel 4096 17. Mai 16:53 .git
-rw-r--r--  1 devel devel 2747 17. Mai 16:53 readme.txt

Hinweis: Bei aktivierter Git-Variable clean.requireForce (default: ja) ist -f ist nötig.

6.5 Einen bestimmten Commit zurücknehmen

Wenn man nun später feststellt, dass ein Feature doch nicht so toll war, man aber brav die Historie gepflegt hat und das Feature ein einziger Commit war, so kann man das ganz einfach wieder reverten:

$ git revert dcca4c0ead53a27f3404f40ccbc0b58ace5ec7d3
Finished one revert.
[master f95ac16] Revert "Eine Änderung im Branch"
 1 files changed, 0 insertions(+), 2 deletions(-)

Nach einer kurzen Abfrage für eine Commit-Message des Commits, der die Änderungen rückgängig macht, ist das Feature weg:

Abb.11: Der Revisionsgraph nach dem Zurücknehmen von klzzwxh:0201.
Abb.11: Der Revisionsgraph nach dem Zurücknehmen von Eine Änderung im Branch.

TortoiseGit: Hier kommt ihr über das Log an die Liste der Commits heran, von wo aus wieder alles schön per Rechtsklick auf einen Commit funktioniert.

6.6 Die Arbeitskopie auf einen Commit zurücksetzen

Alles Mist, ihr wollt wieder eine saubere Arbeitskopie und ihr braucht eure Änderungen nicht mehr? Das, was bei Subversion verwirrenderweise revert heißt, nennt sich in Git reset:

$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

$ git reset --hard
HEAD is now at f95ac16 Revert "Eine Änderung im Branch"

$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
nothing to commit (working directory clean)

Bitte beachtet, dass dieses Kommando sich auf alle von Git getrackten Dateien bezieht, also in dieser Hinsicht irgendwo das Gegenstück zu clean ist. --hard bedeutet hier, dass ohne Rücksicht auf Verluste alles auf den HEAD zurückgesetzt wird. Natürlich kann als optionales Argument der HEAD oder jede andere Commit-ID explizit angegeben werden.

TortoiseGit: Auch hier funktioniert das alles über das Log.

6.7 Nicht gepushte Commits verwerfen

Ganz einfach: git reset --hard HEAD~x, wobei x die Anzahl der Commits ist, die verworfen werden soll.

Achtung: Auch hier sollte das nur getan werden, wenn noch nicht gepusht wurde!

TortoiseGit: Log! (siehe die beiden Punkte zuvor)

6.8 Einen bestimmten Commit in einen Branch mergen

Stellt euch vor, ihr arbeitet gerade an einem neuen Feature in einem Branch, während ein Kollege ein anderes Feature, ein besseres Installationsskript oder Ähnliches in den Master comitted, welches ihr aber gerne auch in eurem Branch haben möchtet.

In Subversion könnte man vielleicht nur die entsprechende Datei updaten, was jedoch bei Git nicht geht, da die feingranularste Änderung in Git der Commit und nicht die Datei ist. Hier kommt das Kommando cherry-pick zum Einsatz. Wie es schon sagt, kann man sich einen Commit herauspicken und in den eigenen Branch einpflegen:

Abb.12: klzzwxh:0221 möchten wir in klzzwxh:0222 sehen...
Abb.12: Ein anderes Feature möchten wir in feature2 sehen...

$ git cherry-pick 92969541c9e0ddf8972ef3c408f8ddef45f56064
Finished one cherry-pick.
[feature2 b8df4bf] Ein anderes Feature
 1 files changed, 2 insertions(+), 0 deletions(-)

Und schon haben wir das neue Feature auch in unserem Branch:

Abb.13: Nun befindet sich klzzwxh:0224 auch in klzzwxh:0225
Abb.13: Nun befindet sich Ein anderes Feature auch in feature2

TortoiseGit: Wieder Log! (Siehe die drei Punkte zuvor)

6.9 Nur bestimmte Änderungen einer Datei comitten

Nicht immer ist der Alltag so linear, wie Git es möchte: Sehr oft findet man während der Implementierung eines Features einen Bug und fixt ihn nebenbei. Was nun, wenn man das in der selben Datei, die auch Änderungen des neuen Features beinhaltet, getan hat? Hier kommt der eingangs erwähnte -p Parameter zum Einsatz:

$ git add -p [dateiname]

Es werden nun nach und nach alle Änderungen der Datei interaktiv abgefragt. Durch drücken der h Taste kann eine Hilfe zu allen Befehlen angezeigt werden.