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. 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.

replace1
Abbildung 163. Beispiel Git Historie

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
Abbildung 164. Erstellen eines neuen history Branches

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

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 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, um 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. Du kannst mehr über Basisbefehle in Basisbefehle und Standardbefehle (Plumbing and Porcelain) nachlesen.

replace3
Abbildung 165. Erstellen eines Basis-Commits mit 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
replace4
Abbildung 166. Rebasen der Historie auf den Basis-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.

replace5
Abbildung 167. Kombinieren der Commits mit 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.

scroll-to-top