== Git für Fortgeschrittene ==

Mittlerweile solltest Du Dich in den *git help* Seiten zurechtfinden und das
meiste verstanden haben. Trotzdem kann es langwierig sein, den exakten
Befehl zur Lösung einer bestimmten Aufgabe herauszufinden. Vielleicht kann
ich Dir etwas Zeit sparen: Nachfolgend findest Du ein paar Rezepte, die ich
in der Vergangenheit gebraucht habe.

=== Quellcode veröffentlichen ===

Bei meinen Projekten verwaltet Git genau die Dateien, die ich archivieren
und für andere Benutzer veröffentlichen will. Um ein tarball-Archiv des
Quellcodes zu erzeugen, verwende ich den Befehl:

 $ git archive --format=tar --prefix=proj-1.2.3/ HEAD

=== 'Commite' Änderungen ===

Git mitzuteilen, welche Dateien man hinzugefügt, gelöscht und umbenannt hat,
ist für manche Projekte sehr mühsam. Stattdessen kann man folgendes
eingeben:

 $ git add .
 $ git add -u

Git wird sich die Dateien im aktuellen Verzeichnis ansehen und sich die
Details selbst erarbeiten. Anstelle des zweiten Befehl kann man auch `git
commit -a` ausführen, falls man an dieser Stelle ohnehin 'comitten'
möchte. Siehe *git help ignore* um zu sehen, wie man Dateien definiert, die
ignoriert werden sollen.

Man kann das aber auch in einem einzigen Schritt ausführen mit:

 $ git ls-files -d -m -o -z | xargs -0 git update-index --add --remove

Die *-z* und *-0* Optionen verhindern unerwünschte Nebeneffekte durch
Dateinamen mit ungewöhnlichen Zeichen. Da diese Anweisung aber auch zu
ignorierende Dateien hinzufügt, kann man noch die `-x` oder `-X` Option
hinzufügen.

=== Mein 'Commit' ist zu groß! ===

Hast Du es zu lange versäumt zu 'comitten'? Hast Du so versessen
programmiert, dass Du darüber die Quellcodeverwaltung vergessen hast? Machst
Du eine Serie von unabhängigen Änderungen, weil es Dein Stil ist?

Keine Sorge, gib ein:

 $ git add -p

Für jede Änderung, die Du gemacht hast, zeigt Git Dir die Codepassagen, die
sich geändert haben und fragt, ob sie Teil des nächsten 'Commit' sein
sollen. Antworte mit "y" für Ja oder "n" für Nein. Du hast auch noch andere
Optionen, z.B. den Aufschub der Entscheidung; drücke "?" um mehr zu
erfahren.

Wenn Du zufrieden bist, gib

 $ git commit

ein, um exakt die ausgewählten Änderungen zu 'comitten' (die "inszenierten"
Änderungen). Achte darauf, nicht die Option *-a* einzusetzen, anderenfalls
wird Git alle Änderungen 'comitten'.

Was ist, wenn Du viele Dateien an verschiedenen Orten bearbeitet hast? Jede
Datei einzeln nachzuprüfen, ist frustrierend und ermüdend. In diesem Fall
verwende *git add -i*, dessen Bedienung ist nicht ganz einfach, dafür aber
sehr flexibel. Mit ein paar Tastendrücken kannst Du mehrere geänderte
Dateien für den 'Commit' hinzufügen ('stage') oder entfernen ('unstage')
oder Änderungen einzelner Dateien nachprüfen und hinzufügen. Alternativ
kannst Du *git commit \--interactive* verwenden, was dann automatisch die
ausgewählten Änderungen 'commited', nachdem Du fertig bist.

=== Der Index: Git's Bereitstellungsraum ===

Bis jetzt haben wir Git's berühmten 'Index' gemieden, aber nun müssen wir
uns mit ihm auseinandersetzen, um das bisherige zu erklären. Der Index ist
ein temporärer Bereitstellungsraum. Git tauscht selten Daten direkt zwischen
Deinem Projekt und seiner Versionsgeschichte aus. Vielmehr schreibt Git die
Daten zuerst in den Index, danach kopiert es die Daten aus dem Index an
ihren eigentlichen Bestimmungsort.

Zum Beispiel ist *commit -a* eigentlich ein zweistufiger Prozess. Der erste
Schritt erstellt einen Schnappschuss des aktuellen Status jeder überwachten
Datei im Index. Der zweite Schritt speichert dauerhaft den Schnappschuss, der
sich nun im Index befindet. Ein 'Commit' ohne die *-a* Option führt nur den
zweiten Schritt aus und macht nur wirklich Sinn, wenn zuvor eine Anweisung
angewendet wurde, welche den Index verändert, wie zum Beispiel *git add*.

Normalerweise können wir den Index ignorieren und so tun, als würden wir
direkt aus der Versionsgeschichte lesen oder in sie schreiben. In diesem
Fall wollen wir aber mehr Kontrolle, also manipulieren wir den Index. Wir
erstellen einen Schnappschuss einiger, aber nicht aller unser Änderungen im
Index und speichern dann diesen sorgfältig zusammengestellten Schnappschuss
permanent.

=== Verliere nicht Deinen KOPF ===

Der HEAD Bezeichner ist wie ein Cursor, der normalerweise auf den jüngsten
'Commit' zeigt und mit jedem neuen 'Commit' voranschreitet. Einige Git
Anweisungen lassen Dich ihn manipulieren. Zum Beispiel:

 $ git reset HEAD~3

bewegt den HEAD Bezeichner drei 'Commits' zurück. Dadurch agieren nun alle
Git Anweisungen, als hätte es die drei letzten 'Commits' nicht gegeben,
während Deine Dateien unverändert erhalten bleiben. Siehe auf der Git
Hilfeseite für einige Anwendungsbeispiele.

Aber wie kannst Du zurück in die Zukunft? Die vergangenen 'Commits' wissen
nichts von der Zukunft.

Wenn Du den SHA1 Schlüssel vom originalen HEAD hast, dann:

 $ git reset 1b6d

Aber stell Dir vor, Du hast ihn niemals notiert? Keine Sorge: Für solche
Anweisungen sichert Git den original HEAD als Bezeichner mit dem Namen
ORIG_HEAD und Du kannst gesund und munter zurückkehren mit:

 $ git reset ORIG_HEAD

=== KOPF-Jagd ===

Möglicherweise reicht ORIG_HEAD nicht aus. Vielleicht hast Du gerade
bemerkt, dass Du einen kapitalen Fehler gemacht hast, und nun musst Du zu
einem uralten 'Commit' in einem länst vergessenen 'Branch' zurück.

Standardmäßig behält Git einen 'Commit' für mindesten zwei Wochen, sogar
wenn Du Git anweist, den 'Branch' zu zerstören, in dem er enthalten ist. Das
Problem ist, den entsprechenden SHA1-Wert zu finden. Du kannst Dir alle
SHA1-Werte in `.git/objects` vornehmen und ausprobieren ob Du den gesuchten
'Commit' findest. Aber es gibt einen viel einfacheren Weg.

Git speichert jeden errechneten SHA1-Wert eines 'Commits' in
`.git/logs`. Das Unterverzeichnis `refs` enthält den Verlauf aller
Aktivitäten auf allen 'Branches', während `HEAD` alle SHA1-Werte enthält,
die jemals diese Bezeichnung hatten. Die letztere kann verwendet werden, um
SHA1-Werte von 'Commits' zu finden, die sich in einem 'Branch' befanden, der
versehentlich gestutzt wurde.

Die reflog Anweisung bietet eine benutzerfreundliche Schnittstelle zu diesen
Logdateien. Versuche

  $ git reflog

Anstatt SHA1-Werte aus dem reflog zu kopieren und einzufügen, versuche:

 $ git checkout "@{10 minutes ago}"

Oder rufe den fünftletzten 'Commit' ab, mit:

 $ git checkout "@{5}"

Siehe in der ``Specifying Revisions'' Sektion von *git help rev-parse* für
mehr.

Vielleicht möchtest Du eine längere Gnadenfrist für todgeweihte 'Commits'
konfigurieren. Zum Beispiel:

  $ git config gc.pruneexpire "30 days"

bedeutet, ein gelöschter 'Commit' wird nur dann endgültig verloren sein,
nachdem 30 Tage vergangen sind und *git gc* ausgeführt wurde.

Du magst vielleicht auch das automatische Ausführen von *git gc* abstellen:

  $ git config gc.auto 0

wodurch 'Commits' nur noch gelöscht werden, wenn Du *git gc* manuell
aufrufst.

=== Auf Git bauen ===

In echter UNIX Sitte erlaubt es Git's Design, dass es auf einfache Weise als
Low-Level-Komponente von anderen Programmen benutzt werden kann, wie zum
Beispiel grafischen Benutzeroberflächen und Internetanwendungen, alternative
Kommandozeilenanwendungen, Patch-Werkzeugen, Import- und
Konvertierungswerkzeugen und so weiter. Sogar einige Git Anweisungen selbst
sind nur winzige Skripte, wie Zwerge auf den Schultern von Riesen. Mit ein
bisschen Handarbeit kannst Du Git anpassen, damit es Deinen Anforderungen
entspricht.

Ein einfacher Trick ist es, die in Git integrierte Aliasfunktion zu verwenden,
um die am häufigsten benutzten Anweisungen zu verkürzen:

  $ git config --global alias.co checkout
  $ git config --global --get-regexp alias  # display current aliases
  alias.co checkout
  $ git co foo                              # same as 'git checkout foo'

Etwas anderes ist der aktuelle 'Branch' im Prompt oder Fenstertitel. Die
Anweisung

  $ git symbolic-ref HEAD

zeigt den Namen des aktuellen 'Branch'. In der Praxis möchtest Du aber das
"refs/heads/" entfernen und Fehler ignorieren:

  $ git symbolic-ref HEAD 2> /dev/null | cut -b 12-

Das +contrib+ Unterverzeichnis ist eine Fundgrube von Werkzeugen, die auf
Git aufbauen. Mit der Zeit können einige davon zu offiziellen Anweisungen
befördert werden. Auf Debian und Ubuntu, findet man dieses Verzeichnis unter
+/usr/share/doc/git-core/contrib+.

Ein beliebter Vertreter ist +workdir/git-new-workdir+. Durch cleveres
verlinken erzeugt dieses Skript ein neues Arbeitsverzeichis, das seine
Versionsgeschichte mit dem original 'Repository' teilt:

  $ git-new-workdir ein/existierendes/repo neues/verzeichnis

Das neue Verzeichnis und die Dateien darin kann man sich als 'Clone'
vorstellen mit dem Unterschied, dass durch die gemeinschaftliche
Versionsgeschichte die beiden Versionen automatisch synchron bleiben. Eine
Synchronisierung mittels 'merge', 'push' oder 'pull' ist nicht notwendig.

=== Gewagte Kunststücke ===

Heutzutage macht es Git dem Anwender schwer, versehentlich Daten zu
zerstören. Aber, wenn man weiß, was man tut, kann man die Schutzmaßnahmen der
häufigsten Anweisungen umgehen.

*Checkout*: Nicht versionierte Änderungen lassen 'checkout' scheitern. Um trotzdem die Änderungen zu zerstören und einen vorhandenen 'Commit' abzurufen, benutzen wir die 'force' Option:

  $ git checkout -f HEAD^

Auf der anderen Seite, wenn Du einen speziellen Pfad für 'checkout' angibst,
gibt es keine Sicherheitsüberprüfungen mehr. Der angegebene Pfad wird
stillschweigend überschrieben. Sei vorsichtig, wenn Du 'checkout' auf diese
Weise benutzt.

*Reset*: Reset versagt auch, wenn unversionierte Änderungen vorliegen. Um es zu erzwingen, verwende:

  $ git reset --hard 1b6d

*Branch*: 'Branches' zu löschen scheitert ebenfalls, wenn dadurch Änderungen verloren gehen. Um das Löschen zu erzwingen, gib ein:

  $ git branch -D dead_branch  # instead of -d

Ebenso scheitert der Versuch, einen 'Branch' durch ein 'move' zu
überschreiben, wenn das einen Datenverlust zur Folge hat. Um das Verschieben
zu erzwingen, gib ein:

  $ git branch -M source target  # instead of -m

Anders als bei 'checkout' und 'reset' verschieben diese beiden Anweisungen
das Zerstören der Daten. Die Änderungen bleiben im .git Unterverzeichnis
gespeichert und können wieder hergestellt werden, wenn der entsprechende
SHA1-Wert aus `.git/logs` ermittelt wird (siehe "KOPF-Jagd"
oben). Standardmäßig bleiben die Daten mindestens zwei Wochen erhalten.

*Clean*: Verschiedene git Anweisungen scheitern, weil sie Konflikte mit unversionierten Dateien vermuten. Wenn Du sicher bist, dass alle unversionierten Dateien und Verzeichnisse entbehrlich sind, dann lösche diese gnadenlos mit:

  $ git clean -f -d

Beim nächsten Mal werden diese lästigen Anweisung gehorchen!

=== Verhindere schlechte 'Commits' ===

Dumme Fehler verschmutzen meine 'Repositories'. Am schrecklichsten sind
fehlende Dateien wegen eines vergessenen *git add*. Kleinere Verfehlungen
sind Leerzeichen am Zeilenende und ungelöste 'merge'-Konflikte: obwohl sie
harmlos sind, wünschte ich, sie würden nie in der Öffentlichkeit erscheinen.

Wenn ich doch nur eine Trottelversicherung abgeschlossen hätte, durch
Verwendung eines _hook_, der mich bei solchen Problemen alarmiert.

 $ cd .git/hooks
 $ cp pre-commit.sample pre-commit  # Older Git versions: chmod +x pre-commit

Nun bricht Git einen 'Commit' ab, wenn es überflüssige Leerzeichen am
Zeilenende oder ungelöste 'merge'-Konflikte entdeckt.

Für diese Anleitung hätte ich vielleicht am Anfang des *pre-commit* 'hook'
folgendes hinzugefügt, zum Schutz vor Zerstreutheit:

 if git ls-files -o | grep '\.txt$'; then
   echo FAIL! Untracked .txt files.
   exit 1
 fi

Viele Git Operationen unterstützen 'hooks'; siehe *git help hooks*. Wir
haben den Beispiel 'hook' *post-update* aktiviert, weiter oben im Abschnitt
Git über HTTP. Dieser läuft immer, wenn der 'HEAD' sich bewegt. Das Beispiel
'post-update' Skript aktualisiert Dateien, welche Git für die Kommunikation
über 'Git-agnostic transports' wie z.B. HTTP benötigt.
