-
1. Erste Schritte
-
2. Git Grundlagen
-
3. Git Branching
- 3.1 Branches auf einen Blick
- 3.2 Einfaches Branching und Merging
- 3.3 Branch-Management
- 3.4 Branching-Workflows
- 3.5 Remote-Branches
- 3.6 Rebasing
- 3.7 Zusammenfassung
-
4. Git auf dem Server
- 4.1 Die Protokolle
- 4.2 Git auf einem Server einrichten
- 4.3 Erstellung eines SSH-Public-Keys
- 4.4 Einrichten des Servers
- 4.5 Git-Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Von Drittanbietern gehostete Optionen
- 4.10 Zusammenfassung
-
5. Verteiltes Git
-
6. GitHub
-
7. Git Tools
- 7.1 Revisions-Auswahl
- 7.2 Interaktives Stagen
- 7.3 Stashen und Bereinigen
- 7.4 Deine Arbeit signieren
- 7.5 Suchen
- 7.6 Den Verlauf umschreiben
- 7.7 Reset entzaubert
- 7.8 Fortgeschrittenes Merging
- 7.9 Rerere
- 7.10 Debuggen mit Git
- 7.11 Submodule
- 7.12 Bundling
- 7.13 Replace (Ersetzen)
- 7.14 Anmeldeinformationen speichern
- 7.15 Zusammenfassung
-
8. Git einrichten
- 8.1 Git Konfiguration
- 8.2 Git-Attribute
- 8.3 Git Hooks
- 8.4 Beispiel für Git-forcierte Regeln
- 8.5 Zusammenfassung
-
9. Git und andere VCS-Systeme
- 9.1 Git als Client
- 9.2 Migration zu Git
- 9.3 Zusammenfassung
-
10. Git Interna
-
A1. Anhang A: Git in anderen Umgebungen
- A1.1 Grafische Schnittstellen
- A1.2 Git in Visual Studio
- A1.3 Git in Visual Studio Code
- A1.4 Git in IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine
- A1.5 Git in Sublime Text
- A1.6 Git in Bash
- A1.7 Git in Zsh
- A1.8 Git in PowerShell
- A1.9 Zusammenfassung
-
A2. Anhang B: Git in deine Anwendungen einbetten
- A2.1 Die Git-Kommandozeile
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. Anhang C: Git Kommandos
- A3.1 Setup und Konfiguration
- A3.2 Projekte importieren und erstellen
- A3.3 Einfache Snapshot-Funktionen
- A3.4 Branching und Merging
- A3.5 Projekte gemeinsam nutzen und aktualisieren
- A3.6 Kontrollieren und Vergleichen
- A3.7 Debugging
- A3.8 Patchen bzw. Fehlerkorrektur
- A3.9 E-mails
- A3.10 Externe Systeme
- A3.11 Administration
- A3.12 Basisbefehle
7.7 Git Tools - Reset entzaubert
Reset entzaubert
Bevor wir zu spezialisierteren Werkzeugen übergehen, sollten wir über die Befehle reset
und checkout
sprechen.
Diese Befehle sind, wenn man ihnen zum ersten Mal begegnet, die beiden verwirrendsten Teile von Git.
Sie erledigen so viele Aufgaben, dass es aussichtslos erscheint, sie wirklich zu verstehen und richtig anzuwenden.
Deshalb empfehlen wir eine einfache Metapher.
Die drei Bäume
Eine bessere Methode, um über reset
und checkout
zu reflektieren, ist der gedankliche Ansatz, dass Git ein Inhaltsmanager von drei verschiedenen Bäumen ist.
Mit „Baum“ meinen wir hier in Wahrheit eine „Sammlung von Dateien“, nicht speziell die Datenstruktur.
Es gibt ein paar Fälle, in denen sich der Inhalt nicht genau wie ein Baum verhält, aber für unsere Zwecke ist es vorerst einfacher, auf diese Weise darüber nachzudenken.
Als System verwaltet Git im regulären Modus drei Bäume:
Baum | Rolle |
---|---|
HEAD |
letzter Commit-Snapshot, nächstes Elternteil |
Index (Staging-Area) |
nächster, geplanter Commit-Snapshot |
Arbeitsverzeichnis |
Sandbox |
Der HEAD
HEAD ist der Verweis auf die aktuelle Branch-Referenz, die wiederum ein Pointer zu dem letzten Commit auf dem aktuellen Branch ist. Das bedeutet, dass HEAD das Elternteil des nächsten Commits ist, der erzeugt wird. Es ist generell am einfachsten, sich HEAD als den Schnappschuss deines letzten Commits auf dem aktuellen Branch vorzustellen.
Es ist ziemlich einfach zu erkennen, wie dieser Schnappschuss aussieht. Hier ist ein Beispiel, wie man die aktuelle Verzeichnisliste und die SHA-1-Prüfsummen für jede Datei im HEAD-Snapshot erhält:
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
Die Git-Befehle cat-file
und ls-tree
sind „Basisbefehle“, die für Aufgaben auf low-level Ebene verwendet werden und nicht wirklich in der täglichen Arbeit eingesetzt werden. Es hilft jedoch sie zu verstehen, was sie genau tun.
Der Index
Index ist dein nächster, geplanter Commit.
Wir haben diesen Konzept auch als Git’s „Staging-Area“ bezeichnet, da Git darauf schaut, wenn du git commit
ausführst.
Git füllt den Index mit allen Dateiinhalten, die du zuletzt in dein Arbeitsverzeichnis ausgecheckt hast und zeigt dir, wie sie beim letzten Auschecken ausgesehen haben.
Du tauschst dann einige dieser Dateien mit neueren Versionen aus, und git commit
konvertiert diese in den Baum für einen neuen Commit.
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
Nochmals, wir verwenden hier git ls-files
, ein Kommando, das eher ein Basisbefehl ist, welcher dir anzeigt, wie dein Index derzeit aussieht.
Der Index ist technisch gesehen keine hierarchische Struktur. Er ist eigentlich als flaches Manifest umgesetzt, aber für unsere Zwecke ist das ausreichend genau.
Das Working Directory oder Arbeitsverzeichnis
Abschließend gibt es dein Arbeitsverzeichnis (engl. „working directory“ oder „working tree“).
Die beiden anderen Bäume speichern deinen Inhalt auf effiziente, aber unpraktische Weise innerhalb des .git
Ordners.
Das Arbeitsverzeichnis entpackt sie in echte Dateien, was es wesentlich einfacher macht, sie zu bearbeiten.
Stelle dir das Arbeitsverzeichnis wie einen Sandkasten (engl. sandbox) vor, in der du Änderungen ausprobieren kannst, bevor du sie in deiner Staging-Area und dann in den Verlauf überträgst.
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
Der Workflow
Der typische Arbeitsablauf von Git sieht vor, dass du durch die Bearbeitung dieser drei Bäume nach und nach bessere Momentaufnahmen deines Projekts erzeugst.
Stellen wir uns folgenden Ablauf vor: Angenommen, du wechselst in ein neues Verzeichnis, in dem sich eine einzige Datei befindet.
Wir nennen das die v1 der Datei und kennzeichnen sie in blau.
Nun führen wir git init
aus, das ein Git-Repository mit einer HEAD-Referenz erzeugt, die auf den noch nicht existierenden master
Branch zeigt.
Zu diesem Zeitpunkt hat nur der Verzeichnisbaum (engl working tree) des Arbeitsverzeichnisses (engl. working directory) irgendeinen Inhalt.
Nun wollen wir diese Datei committen, also benutzen wir git add
, um den Inhalt im Arbeitsverzeichnis zu übernehmen und in den Index zu kopieren.
git add
auf den Index kopiertDann führen wir git commit
aus, das den Inhalt der Staging-Area (oder Index) als endgültigen Snapshot speichert, ein Commit-Objekt erzeugt, das auf diesen Snapshot zeigt, und den Branch master
aktualisiert, um auf diesen Commit zu zeigen.
git commit
SchrittWenn wir jetzt git status
ausführen, werden wir keine Änderungen sehen, weil alle drei Bäume gleich sind.
Nun wollen wir eine Änderung an dieser Datei vornehmen und sie übertragen. Wir führen den gleichen Vorgang durch. Zuerst ändern wir die Datei in unserem Arbeitsverzeichnis. Wir nennen sie v2 dieser Datei und markieren sie in rot.
Wenn wir jetzt den Befehl git status
aufrufen, sehen wir die Datei in rot als „Changes not staged for commit“ (dt. Änderungen nicht zum Commit vorgemerkt), weil sich dieser Eintrag im Index zu dem im Arbeitsverzeichnis unterscheidet.
Als nächstes führen wir git add
aus, um sie in unseren Index zu übernehmen, d.h zur Staging-Area hinzuzufügen.
Wenn wir zu diesem Zeitpunkt git status
ausführen, sehen wir die Datei in grün unter „Changes to be committed“ (dt. Änderungen zum Commit vorgemerkt), weil sich der Index und der HEAD unterscheiden – d.h. unser geplanter nächster Commit unterscheidet sich nun von unserem letzten Commit.
Schließlich führen wir git commit
aus, um die Daten zu übertragen.
git commit
Schritt mit geänderter DateiNun wird uns git status
keine Ergebnisse liefern, weil alle drei Bäume wieder gleich sind.
Das Wechseln von Branches oder das Klonen geht ähnlich vor sich. Wenn du einen Branch auscheckst, ändert er HEAD so, dass er auf den neuen Branch-Ref zeigt, füllt deine Staging-Area (bzw. Index) mit dem aktuellen Schnappschuss dieses Commits und kopiert dann den Inhalt des Index in dein Arbeitsverzeichnis.
Die Bedeutung von Reset
Der Befehl reset
macht mehr Sinn, wenn wir folgenden Fall betrachten.
Für diesen Zweck nehmen wir an, dass wir file.txt
erneut modifiziert und ein drittes Mal committet hätten.
Nun sieht unser Verlauf so aus:
Lass uns nun genau untersuchen, was reset
bewirkt, wenn du es aufrufst.
Es manipuliert die drei Bäume auf einfache und kalkulierbare Weise direkt.
Es führt bis zu drei einfache Operationen aus.
Step 1: Den HEAD verschieben
Als erstes wird reset
das verschieben, worauf HEAD zeigt.
Das ist nicht dasselbe wie HEAD selbst zu ändern (was checkout
macht). reset
verschiebt den Branch, auf den HEAD zeigt.
Das bedeutet, wenn HEAD auf den Branch master
gesetzt ist (d.h. du befindest dich gerade auf dem master
Branch), wird die Ausführung von git reset 9e5e6a4
damit starten, dass master
auf 9e5e6a4
zeigt.
Egal, mit welcher Methode du reset
bei einem Commit aufrufst. Dies ist immer die erste Aktion, die versucht wird auszuführen.
Mit reset --soft
wird es dort einfach stoppen.
Nimm dir nun eine Minute Zeit, um dir diese Abbildung anzusehen und dich zu fragen, was da passiert ist. Es hat im Wesentlichen den letzten git commit
Befehl rückgängig gemacht.
Wenn du git commit
ausführst, erzeugt Git einen neuen Commit und verschiebt den Branch, auf den HEAD zeigt, dorthin.
Wenn du auf HEAD~ (das Elternteil von HEAD) zurücksetzt, verschiebst du den Branch wieder an seine ursprüngliche Stelle, ohne den Index oder das Arbeitsverzeichnis zu ändern.
Du kannst nun den Index aktualisieren und git commit
erneut ausführen, um das zu erreichen, was git commit --amend
getan hätte (siehe auch Den letzten Commit ändern).
Step 2: Den Index aktualisieren (--mixed)
Bitte berücksichtige, dass du bei Ausführung von git status
in grün den Unterschied zwischen dem Index und dem neuen HEAD sehen wirst.
Als nächstes wird reset
den Index mit dem Inhalt des Schnappschusses aktualisieren, auf den HEAD jetzt zeigt.
Wenn du die Option --mixed
angibst, wird reset
an dieser Stelle beendet.
Das ist auch die Voreinstellung, wenn du überhaupt keine Option angibst (in diesem Fall nur git reset HEAD~
), wird der Befehl dort enden.
Nir noch noch eine Minute Zeit, um dir jetzt diese Abbildung anzuschauen und zu erkennen, was passiert ist: Es hat deinen letzten commit
rückgängig gemacht, aber auch alles auf unstaged gesetzt.
Du wurdest auf den Stand zurück versetzt, bevor du alle deine git add
und git commit
Befehle ausgeführt hast.
Step 3: Das Working Directory (Arbeitsverzeichnis) aktualisieren (--hard)
Als Drittes wird das Arbeitsverzeichnis durch reset
zurückgesetzt, damit es dem Index entspricht.
Wenn du die Option --hard
verwendest, wird es bis zu diesem Schritt fortgesetzt.
Denken wir also darüber nach, was gerade passiert ist.
Du hast deinen letzten Commit rückgängig gemacht, die Befehle git add
und git commit
und dazu noch die gesamte Arbeit, die di in deinem Arbeitsverzeichnis geleistet hast.
Es ist sehr wichtig zu wissen, dass das Flag (--hard
) die einzige Möglichkeit ist, den Befehl reset
gefährlich zu machen und einer der wenigen Fälle, in denen Git tatsächlich Daten vernichtet.
Jeder andere Aufruf von reset
kann ziemlich leicht rückgängig gemacht werden, aber nicht die Option --hard
, da sie Dateien im Arbeitsverzeichnis zwingend überschreibt.
In diesem speziellen Fall haben wir noch immer die v3 Version unserer Datei in einem Commit in unserer Git-Datenbank. Wir könnten sie durch einen Blick auf unser reflog
zurückholen. Hätten wir sie aber nicht committet, dann hätte Git die Datei überschrieben und sie wäre nicht wiederherstellbar.
Zusammenfassung
Der Befehl reset
überschreibt diese drei Bäume in einer bestimmten Reihenfolge und stoppt, wann du es willst:
-
Verschiebe den Branch-HEAD und (stoppt hier, wenn
--soft
). -
Lasse den Index wie HEAD erscheinen (hier stoppen, wenn nicht
--hard
). -
Lasse das Arbeitsverzeichnis wie den Index erscheinen.
Zurücksetzen (reset) mit Pfadangabe
Das deckt das Verhalten von reset
in seiner Basisform ab, aber du kannst ihm auch einen Pfad angeben, auf dem er aktiv werden soll.
Wenn du einen Pfad festlegst, überspringt reset
Step 1 und beschränkt die restlichen Aktionen auf eine bestimmte Datei oder eine Gruppe von Dateien.
Das macht tatsächlich Sinn. HEAD ist nur ein Pointer. Du kannst nicht auf den einen Teil eines Commits und auf einen Teil eines anderen zeigen.
Der Index und das Arbeitsverzeichnis können jedoch teilweise aktualisiert werden, so dass das Zurücksetzen mit den Schritten 2 und 3 fortgesetzt wird.
Nehmen wir also an, wir führen ein git reset file.txt
aus.
Da du hier keinen Commit-SHA-1 oder -Branch angegeben hast und auch nicht die Optionen --soft oder --hard verwendet hast, ist das die Kurzform für git reset --mixed HEAD file.txt
. Der Befehl wird Folgendes bewirken:
-
Verschiebt den Branch, HEAD zeigt auf (übersprungen).
-
Passt den Index an HEAD an (stopt hier).
Er kopiert also im Endeffekt nur file.txt
von HEAD in den Index.
Das hat den praktischen Effekt, dass die Datei aus der Staging-Area entfernt wird (engl. unstage).
Wenn wir uns die Abbildung für diesen Befehl ansehen und überlegen, was git add
macht, sind die beiden Befehle genau gegensätzlich.
Deshalb schlägt die Anzeige des Befehls git status
vor, dass du den Befehl git reset
ausführst, um eine Datei aus der Staging-Area zu entfernen. Siehe auch Kapitel 2 Eine Datei aus der Staging-Area entfernen für weitere Informationen.
Wir könnten ebenso einfach, Git nicht annehmen lassen, dass wir damit meinen, es soll „die Daten aus dem HEAD pullen“, indem wir einen bestimmten Commit angeben, aus dem diese Dateiversion gezogen werden soll.
Stattdessen würden wir einfach etwas wie git reset eb43bf file.txt
ausführen.
Das macht effektiv dasselbe, als ob wir den Inhalt der Datei im Arbeitsverzeichnis auf v1 geändert, git add
darauf ausgeführt und dann wieder auf v3 zurückgewandelt hätten (ohne wirklich alle diese Schritte zu durchlaufen).
Wenn wir jetzt git commit
aufrufen, wird er eine Modifikation registrieren, die diese Datei wieder auf v1 zurücksetzt, obwohl wir sie nie wieder in unserem Arbeitsverzeichnis hatten.
Interessant ist auch, dass der reset
Befehl wie auch git add
die Option --patch
akzeptiert, um Inhalte schrittweise zu entfernen.
Du kannst also selektiv Inhalte aufheben oder zurücksetzen.
Squashing (Zusammenfassen)
Schauen wir uns an, was wir mit dieser neu entdeckten Möglichkeit machen können – das Zusammenfassen von Commits.
Angenommen, du hast eine Reihe von Commits mit Nachrichten wie „Ups“, „WIP“ und „Diese Datei vergessen“.
Du kannst reset
verwenden, um diese schnell und einfach in einem einzigen Commit zusammenzufassen, der dich wirklich clever aussehen lässt.
Commits zusammenfassen zeigt dir eine andere Möglichkeit auf, aber in diesem Fall ist es einfacher reset
zu verwenden.
Stellen wir uns vor, du hast ein Projekt, bei dem der erste Commit eine Datei enthält, der zweite Commit eine neue Datei hinzufügt und die erste ändert, und der dritte Commit die erste Datei erneut ändert. Der zweite Commit war eine unfertige Arbeit und du willst diese zusammenfassen.
Du kannst git reset --soft HEAD~2
ausführen, um den HEAD-Branch zurück zu einem älteren Commit (dem neuesten Commit, den du behalten willst) zu verschieben:
Danach einfach erneut git commit
ausführen:
Jetzt kannst du sehen, dass dein gewünschter Verlauf, der Verlauf, den du pushen möchtest, jetzt so aussieht, als hättest du einen Commit mit file-a.txt
v1 gemacht. Anschließend hättest du einen zweiten gemacht, der sowohl file-a.txt
zu v3 modifiziert als auch file-b.txt
hinzugefügt hat.
Der Commit mit der Version v2 der Datei ist nicht mehr im Verlauf enthalten.
Auschecken (checkout)
Zum Schluss wirst du dich vielleicht fragen, was der Unterschied zwischen checkout
und reset
ist.
Wie reset
manipuliert checkout
die drei Bäume. Es ist ein bisschen unterschiedlich, je nachdem, ob du dem Befehl einen Dateipfad mitgibst oder nicht.
Ohne Pfadangabe
Das Benutzen von git checkout [branch]
ist dem Ausführen von git reset --hard [branch]
ziemlich ähnlich, da es alle drei Bäume aktualisiert, damit sie wie [branch]
aussehen, aber es gibt zwei wichtige Unterschiede.
Erstens, anders als bei reset --hard
, ist bei checkout
das Arbeitsverzeichnis sicher. Es wird geprüft, ob Dateien, die Änderungen enthalten, nicht gelöscht werden.
Eigentlich ist es noch etwas intelligenter. Es versucht, eine triviales Merge im Arbeitsverzeichnis durchzuführen, so dass alle Dateien, die du nicht geändert hast, aktualisiert werden.
reset --hard
hingegen, wird alles ohne Überprüfung einfach ersetzen.
Der zweite wichtige Unterschied ist die Frage, wie checkout
den HEAD aktualisiert.
Während reset
den Branch verschiebt, auf den HEAD zeigt, so bewegt checkout
den HEAD selbst, um auf einen anderen Branch zu zeigen.
Angenommen, wir haben master
und develop
Branches, die zu verschiedenen Commits zeigen und wir befinden uns gerade im develop
Branch (also weist HEAD dorthin).
Sollten wir git reset master
ausführen, wird develop
selbst nun auf den gleichen Commit zeigen, den master
durchführt.
Wenn wir stattdessen git checkout master
ausführen, ändert sich develop
nicht, HEAD selbst bewegt sich.
HEAD zeigt nun auf master
.
In beiden Fällen verschieben wir also HEAD, um auf Commit A zu zeigen, aber die Methode ist sehr unterschiedlich.
reset
verschiebt den Branch zum HEAD, checkout dagegen verschiebt den HEAD selbst (nicht den Branch).
git checkout
und git reset
Mit Pfadangabe
Die andere Möglichkeit, das Auschecken (checkout
) auszuführen, ist inkl. der Angabe eines Dateipfades, der, wie bei reset
, den HEAD nicht verschiebt.
Es ist genau wie bei git reset [branch] Datei
, indem es den Index mit dieser Datei beim Commit aktualisiert, aber es überschreibt auch die Datei im Arbeitsverzeichnis.
Es wäre genau wie git reset --hard [branch] Datei
(wenn reset
dich das ausführen lassen würde). Das Arbeitsverzeichnis ist nicht sicher und der Befehl verschiebt den HEAD nicht.
Ebenso wie git reset
und git add
akzeptiert checkout
die Option --patch
, die es dir erlaubt, den Inhalt von Dateien auf Basis von einzelnen Teilen selektiv zurückzusetzen.
Zusammenfassung
Wir hoffen, dass du jetzt den Befehl reset
besser kennen und anwenden kannst. Wahrscheinlich bist du aber immer noch etwas unsicher, wie genau er sich von checkout
unterscheidet. Du kannst dir vermutlich nicht alle Regeln der verschiedenen Aufrufe merken.
Hier ist eine Tabelle, die zeigt, welche Befehle sich auf welche Bäume auswirken. In der Spalte „HEAD“ bedeutet „REF“, dass dieser Befehl die Referenz (den Branch) verschiebt, auf die HEAD zeigt. „HEAD“ signalisiert, dass er HEAD selbst verschiebt. Achte besonders auf die Spalte „WD sicher?“. Wenn dort „NO“ steht, überlege dir genau, ob du diesen Befehl ausführen willst.
HEAD | Index | Workdir | WD sicher? | |
---|---|---|---|---|
Commit Level |
||||
|
REF |
NO |
NO |
YES |
|
REF |
YES |
NO |
YES |
|
REF |
YES |
YES |
NO |
|
HEAD |
YES |
YES |
YES |
File Level |
||||
|
NO |
YES |
NO |
YES |
|
NO |
YES |
YES |
NO |