-
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.13 Git Tools - Replace (Ersetzen)
Replace (Ersetzen)
Wie wir bereits betont haben, sind die Objekte in der Objektdatenbank von Git unveränderbar. Git bietet aber eine interessante Möglichkeit, so zu tun, als ob man Objekte in der Datenbank durch andere Objekte ersetzen würde.
Der Befehl replace
ermöglicht es dir, ein Objekt in Git zu bestimmen und zu sagen „jedes Mal, wenn ich auf dieses Objekt verweise, behandle es so, als wäre es ein anderes Objekt“.
Das wird am häufigsten zum Ersetzen eines Commits in deinem Verlauf durch einen anderen genutzt. Dadurch musst du nicht die gesamte Historie neu aufbauen, wie z.B. mit git filter-branch
.
Nehmen wir zum Beispiel an, du hast einen riesigen Code-Verlauf und möchtest dein Repository aufsplitten in eins mit kurzen Verlauf für neue Entwickler und eins mit viel längeren und ausführlicheren Verlauf für Leute, die sich für Data Mining interessieren. Du kannst eine Historie in eine andere einpflanzen, indem du den frühesten Commit in der neuen Zeile durch den neuesten Commit in der älteren Zeile „ersetzt“. Das ist angenehm, weil es bedeutet, dass du nicht wirklich jeden Commit in der neuen Historie neu erstellen musst, wie du es normalerweise tun müsstest, um sie zusammenzufügen (weil die Elternabstammung die SHA-1-Werte beeinflusst).
Probieren wir das einmal aus.
Nehmen wir ein vorhandenes Repository, teilen es in zwei Repositorys auf, ein aktuelles und ein altes. Dann untersuchen wir, wie wir sie rekombinieren können, ohne die aktuellen SHA-1-Werte des Repositorys durch replace
zu verändern.
Wir werden ein kleines Repository mit fünf einfachen Commits verwenden:
$ git log --oneline
ef989d8 Fifth commit
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
Wir wollen dieses in zwei unterschiedliche Historien aufteilen. Eine Linie geht von Commit eins bis Commit vier – das wird die historische Linie sein. Die zweite Linie wird nur aus den Commits vier und fünf bestehen – das wird die jüngere Historie sein.
Die Erstellung des historischen Verlaufs ist einfach. Wir können einen Branch in den Verlauf einfügen und dann diesen Branch auf den master
Branch eines neuen Remote-Repositorys pushen.
$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
history
BranchesJetzt können wir den neuen Branch history
in den master
Branch unseres neuen Repositorys pushen:
$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
* [new branch] history -> master
Damit ist unsere Historie veröffentlicht. Nun ist der schwierigere Teil, unsere jüngere Historie nach hinten zu kürzen, damit sie kleiner wird. Wir brauchen eine Überlappung, damit wir einen Commit in der einen durch einen gleichwertigen Commit in der anderen ersetzen können. Deshalb werden wir diesen Teil auf die Commits vier und fünf kürzen (so dass sich Commit vier überlappt).
$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
In diesem Fall ist es nützlich, einen Basis-Commit zu erstellen, der Anweisungen zum Erweitern der Historie enthält, damit andere Entwickler wissen, was zu tun ist, wenn sie auf den ersten Commit in der getrennten Historie treffen und mehr brauchen. Was wir also vornehmen werden, ist ein erstes Commit-Objekt als unseren Basispunkt mit Anweisungen zu erstellen und dann die restlichen Commits (vier und fünf) darauf zu rebasen.
Dazu müssen wir einen Punkt wählen, an dem wir abspalten möchten, der in unserem Beispiel der dritte Commit ist. Er lautet 9c68fdc
im SHA-Vokabular.
Unser Basis-Commit wird also auf diesem Baum basieren.
Wir können unseren Basis-Commit mit dem Befehl commit-tree
erstellen, der einfach einen Baum nimmt und uns ein brandneues, elternloses SHA-1-Commit-Objekt zurückgibt.
$ echo 'Get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Anmerkung
|
Das Kommando |
commit-tree
Jetzt, wo wir einen Basis-Commit haben, können wir den Rest unseres Verlaufs mit git rebase --onto
darauf rebasen.
Das Argument --onto
ist der SHA-1, den wir gerade von commit-tree
zurückbekommen haben. Der Rebase-Punkt wird der dritte Commit sein (das Elternteil des ersten Commits, den wir behalten wollen, 9c68fdc
):
$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
Nun haben wir also unseren jüngsten Verlauf auf einer Übergabebasis neu geschrieben, die jetzt Anweisungen enthält, wie wir die gesamte Historie rekonstruieren könnten, wenn wir es wollen. Wir können diesen neuen Verlauf auf ein neues Projekt übertragen. Wenn die Anwender jetzt dieses Repository klonen, sehen sie nur die beiden letzten Commits und dann einen Basis-Commit mit Anweisungen.
Lass uns nun die Rolle tauschen mit jemandem, der das Projekt zum ersten Mal klont und den gesamten Verlauf des Projekts haben will. Um die Verlaufsdaten nach dem Klonen dieses abgetrennten Repositorys zu erhalten, müsste man einen zweiten Remote für das historische Repository hinzufügen und fetchen:
$ git clone https://github.com/schacon/project
$ cd project
$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah
$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
* [new branch] master -> project-history/master
Nun würde die andere Person deine jüngsten Commits im master
Branch und die historischen Commits im project-history/master
Branch erhalten.
$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah
$ git log --oneline project-history/master
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
Um sie zu vereinen, kannst du einfach git replace
mit dem Commit, den du ersetzen willst, und dann den Commit, mit dem du ihn ersetzen willst, aufrufen.
Wir wollen also den „vierten“ Commit im master
Branch durch den "vierten" Commit im project-history/master
Branch ersetzen:
$ git replace 81a708d c6e1e95
Wenn man sich nun den Verlauf des master
Branch anschaut, sieht er so aus:
$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
Klasse, oder? Ohne alle SHA-1s im Upstream ändern zu müssen, konnten wir einen Commit in unserer Historie durch einen ganz anderen ersetzen, und alle normalen Werkzeuge (bisect
, blame
, usw.) werden so funktionieren, wie wir es erwarten.
git replace
Interessanterweise zeigt das Protokoll ('log') immer noch 81a708d
als SHA-1 an, obwohl es tatsächlich die c6e1e95
-Commit-Daten verwendet, durch die wir es ersetzt haben.
Selbst wenn du einen Befehl wie cat-file
ausführst, zeigt er dir die ersetzten Daten an:
$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700
fourth commit
Vergiss nicht, dass das eigentliche Elternteil von 81a708d
unser Platzhalter-Commit (622e88e
) war, nicht 9c68fdce
, wie es hier steht.
Interessant ist, dass diese Daten in unseren Referenzen gespeichert sind:
$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit refs/replace/81a708dd0e167a3f691541c7a6463343bc457040
Das bedeutet, dass es einfach ist, unseren Ersatz mit anderen zu teilen, weil wir diesen auf unseren Server pushen können und andere Anwender ihn leicht herunterladen können. Das ist in dem Szenario zur Verlaufsoptimierung, das wir hier durchgespielt haben, nicht so hilfreich (da jeder sowieso beide Historien herunterladen würde. Warum also beide trennen?). Es kann aber unter anderen Umständen sinnvoll sein.