Git
Chapters ▾ 2nd Edition

7.13 Git Tools - Replace (Ersetzen)

Replace (Ersetzen)

Wie wir bereits betont haben, sind die Objekte in der Objektdatenbank von Git unveränderbar, aber Git bietet 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 Ihnen, 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 gebraucht zum Ersetzen eines Commits in Ihrem Verlauf durch einen anderen, ohne dass Sie die gesamte Historie neu aufbauen müssen, wie z.B. mit git filter-branch.

Nehmen wir zum Beispiel an, Sie haben einen riesigen Code-Verlauf und möchten Ihr Repository aufsplitten in einen kurzen Verlauf für neue Entwickler und einen viel längeren und ausführlicheren Verlauf für Leute, die sich für Data Mining interessieren. Sie können eine Historie auf eine andere aufpfropfen, indem Sie den frühesten Commit in der neuen Zeile durch den neuesten Commit in der älteren Zeile „ersetzen“. Das ist angenehm, weil es bedeutet, dass Sie nicht wirklich jeden Commit in der neuen Geschichte neu erstellen müssen, wie Sie es normalerweise tun müssten, um sie zusammenzufügen (weil die Elterngeneration die SHA-1s beeinflussen).

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 one“ bis „commit four“ – 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.

replace1

Nun, 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
replace2

Jetzt 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

OK, damit ist unsere Historie veröffentlicht. Nun ist der schwierigere Teil, unsere jüngste 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 rebasieren.

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 in der SHA-Sprechweise. 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
Note

Das Kommando commit-tree gehört zu einer Reihe von Befehlen, die allgemein als „Basis“-Befehle (engl. plumbing commands) bezeichnet werden. Diese Befehle sind im Allgemeinen nicht für den direkten Einsatz gedacht, sondern werden, eingebettet in andere Git-Befehle, für kleinere Aufgaben verwendet. Wenn wir bei derartigen Gelegenheiten etwas Ungewöhnliches durchführen müssen, dann erlauben sie uns, echte low-level Aufgaben zu erledigen, sind aber nicht für den täglichen Gebrauch gedacht. Sie können mehr über Basisbefehle in Basisbefehle und Standardbefehle (Plumbing and Porcelain) lesen.

replace3

OK, jetzt, wo wir einen Basis-Commit haben, können wir den Rest unseres Verlaufs mit git rebase --onto auf die Basis zurückführen. Das Argument --onto wird der SHA-1 sein, den wir gerade von commit-tree zurückbekommen haben und der Rebase-Punkt wird der dritte Commit sein (der 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
replace4

Gut, 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 und wenn die Anwender jetzt dieses Repository klonen, sehen sie nur die beiden letzten Commits und dann einen Basis-Commit mit Instruktionen.

Lassen Sie uns nun die Rolle tauschen zu jemand, 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 der Mitarbeitende seine 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 vereinigen, können Sie einfach git replace mit dem Commit, den Sie ersetzen wollen und dann den Commit, mit dem Sie ihn ersetzen wollen, 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 Branchs 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, waren wir in der Lage, einen Commit in unserer Geschichte durch einen ganz anderen zu ersetzen und alle normalen Werkzeuge (bisect, blame, usw.) werden so funktionieren, wie wir es erwarten dürfen.

replace5

Interessanterweise zeigt das Log immer noch 81a708d als SHA-1, obwohl es tatsächlich die c6e1e95 Commit-Daten verwendet, durch die wir es ersetzt haben. Selbst wenn Sie einen Befehl wie cat-file ausführen, zeigt er Ihnen 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

Vergessen Sie nicht, dass der eigentliche Elternteil von 81a708d unser Platzhalter-Commit (622e88e) war, nicht 9c68fdce, wie es hier steht.

Eine weitere wichtige Sache 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.