Kapitel ▾ 2. Auflage

7.13 Git-Tools - Ersetzen

Ersetzen

Wie wir bereits betont haben, sind die Objekte in Git's Objektdatenbank unveränderlich, aber Git bietet eine interessante Möglichkeit, Objekte in seiner Datenbank zu simulieren und durch andere Objekte zu ersetzen.

Der Befehl replace ermöglicht es Ihnen, ein Objekt in Git anzugeben und zu sagen: "Jedes Mal, wenn Sie auf dieses Objekt verweisen, tun Sie so, als ob es ein anderes Objekt wäre". Dies ist am häufigsten nützlich, um einen Commit in Ihrer Historie durch einen anderen zu ersetzen, ohne die gesamte Historie neu erstellen zu müssen, z. B. mit git filter-branch.

Nehmen wir zum Beispiel an, Sie haben eine riesige Code-Historie und möchten Ihr Repository in zwei aufteilen: eine kurze Historie für neue Entwickler und eine viel längere und größere Historie für Leute, die sich für Data Mining interessieren. Sie können eine Historie an die andere "anheften", indem Sie den frühesten Commit in der neuen Linie durch den neuesten Commit der älteren ersetzen. Das ist praktisch, weil Sie nicht jeden Commit in der neuen Historie umschreiben müssen, wie Sie es normalerweise tun müssten, um sie zusammenzufügen (da die Elternschaft die SHA-1s beeinflusst).

Lassen Sie uns das ausprobieren. Wir nehmen ein bestehendes Repository, teilen es in zwei Repositorys auf, eines aktuell und eines historisch, und sehen dann, wie wir sie wieder zusammenführen können, ohne die SHA-1-Werte des aktuellen Repositorys über replace zu ändern.

Wir werden ein einfaches 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 möchten dies in zwei Historienstränge aufteilen. Ein Strang geht von Commit eins bis Commit vier - das wird der historische. Der zweite Strang wird nur aus den Commits vier und fünf bestehen - das wird die aktuelle Historie sein.

Example Git history
Abbildung 163. Beispiel einer Git-Historie

Nun, die Erstellung der historischen Historie ist einfach. Wir können einen Branch in der Historie platzieren und diesen Branch dann in 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
Creating a new `history` branch
Abbildung 164. Erstellen eines neuen history-Branches

Nun können wir den neuen history-Branch 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, unsere Historie ist veröffentlicht. Jetzt kommt der schwierigere Teil: das Kürzen unserer aktuellen Historie, damit sie kleiner wird. Wir brauchen eine Überlappung, damit wir einen Commit in einem durch einen äquivalenten Commit im anderen ersetzen können. Wir werden dies also auf nur die Commits vier und fünf kürzen (sodass 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

Es ist in diesem Fall 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 gekürzten Historie stoßen und mehr benötigen. Was wir also tun werden, ist, einen initialen Commit als unseren Basispunkt mit Anweisungen zu erstellen und dann die verbleibenden Commits (vier und fünf) darauf zu rebasen.

Dazu müssen wir einen Punkt zum Aufteilen wählen, der für uns der dritte Commit ist, was in SHA-Sprache 9c68fdc ist. Unser Basis-Commit wird also auf diesem Tree basieren. Wir können unseren Basis-Commit mit dem Befehl commit-tree erstellen, der einfach einen Tree nimmt und uns einen brandneuen, elternlosen Commit-Objekt-SHA-1 zurückgibt.

$ echo 'Get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Hinweis

Der Befehl commit-tree gehört zu einer Reihe von Befehlen, die üblicherweise als "Plumbing"-Befehle bezeichnet werden. Dies sind Befehle, die im Allgemeinen nicht direkt verwendet werden sollen, sondern stattdessen von **anderen** Git-Befehlen verwendet werden, um kleinere Aufgaben zu erledigen. In Fällen, in denen wir seltsamere Dinge wie diese tun, ermöglichen sie uns, sehr Low-Level-Dinge zu tun, sind aber nicht für den täglichen Gebrauch gedacht. Sie können mehr über Plumbing-Befehle in Plumbing and Porcelain lesen.

Creating a base commit using `commit-tree`
Abbildung 165. Erstellen eines Basis-Commits mit commit-tree

OK, jetzt, da wir einen Basis-Commit haben, können wir den Rest unserer Historie darauf mit git rebase --onto rebasen. Das Argument --onto wird der SHA-1 sein, den wir gerade von commit-tree erhalten 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
Rebasing the history on top of the base commit
Abbildung 166. Rebasing der Historie auf den Basis-Commit

OK, jetzt haben wir unsere aktuelle Historie auf einen Wegwerf-Basis-Commit umgeschrieben, der jetzt Anweisungen enthält, wie die gesamte Historie wiederhergestellt werden kann, wenn wir das möchten. Wir können diese neue Historie in ein neues Projekt pushen und wenn Leute dieses Repository dann klonen, sehen sie nur die letzten beiden Commits und dann einen Basis-Commit mit Anweisungen.

Wechseln wir nun die Rolle zu jemandem, der das Projekt zum ersten Mal klont und die gesamte Historie möchte. Um die Historie nach dem Klonen dieses gekürzten Repositorys abzurufen, müsste man ein zweites 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 hätte der Kollaborateur seine aktuellen Commits im master-Branch und die historischen Commits im project-history/master-Branch.

$ 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 zusammenzuführen, können Sie einfach git replace mit dem zu ersetzenden Commit und dann dem Commit aufrufen, durch das Sie ihn ersetzen möchten. Wir möchten also den "vierten" Commit im master-Branch durch den "vierten" Commit im project-history/master-Branch ersetzen

$ git replace 81a708d c6e1e95

Wenn Sie sich nun die Historie des master-Branches ansehen, scheint sie wie folgt auszusehen

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

Cool, oder? Ohne alle SHA-1s upstream ändern zu müssen, konnten wir einen Commit in unserer Historie durch einen völlig anderen Commit ersetzen, und alle normalen Werkzeuge (bisect, blame usw.) funktionieren wie erwartet.

Combining the commits with `git replace`
Abbildung 167. Zusammenführen der Commits mit git replace

Interessanterweise wird immer noch 81a708d als SHA-1 angezeigt, obwohl tatsächlich die c6e1e95-Commit-Daten verwendet werden, mit denen wir ihn ersetzt haben. Selbst wenn Sie einen Befehl wie cat-file ausführen, werden Ihnen die ersetzten Daten angezeigt

$ 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

Denken Sie daran, dass der tatsächliche Elternteil von 81a708d unser Platzhalter-Commit (622e88e) war und nicht 9c68fdce, wie hier angegeben.

Eine weitere interessante Sache ist, dass diese Daten in unseren Referenzen gespeichert werden

$ 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, unsere Ersetzungen mit anderen zu teilen, da wir sie auf unseren Server pushen können und andere sie leicht herunterladen können. Dies ist im Szenario des Graftings von Historien, das wir hier behandelt haben, nicht sehr hilfreich (da alle ohnehin beide Historien herunterladen würden, warum sie also trennen?), kann aber in anderen Situationen nützlich sein.