Wie jj darunter tatsächlich gebaut ist

Teil 4 von 9.

Das mentale Modell aus dem letzten Beitrag (Arbeitskopie als Commit, Change-ID getrennt von der Commit-ID) ist die nach außen sichtbare Geschichte. Nachdem ich jjs eigene Design-Dokumente und den Quellcode-Aufbau etwas länger als unbedingt nötig durchgegangen bin, erklärt die zugrunde liegende Architektur, warum dieses Modell zusammenhält und warum sich der Colocated-Modus nicht anfühlt wie das Betreiben zweier zusammengeklebter Versionskontrollsysteme.

Der Commit-Store ist ein austauschbares Backend, und Git ist eine Implementierung davon. jj definiert seinen eigenen internen Begriff eines Commits, und ein Backend-Trait entscheidet, wie das in Bytes auf der Platte verwandelt wird. Das Git-Backend ist der Standard und dasjenige, das dieser ganze Kurs verwendet; es speichert jjs Commits zufällig als echte Git-Commit-Objekte. Das ist der eigentliche Mechanismus hinter “alles ist darunter immer noch ein normales Git-Repo”, es ist keine nachträglich angeschraubte Übersetzungsschicht, es ist der primäre Speicherpfad.

Der Colocated-Modus dupliziert die Objektspeicherung nicht. .jj hält jj-spezifischen Zustand, das Operations-Log, die aktuelle View, Tracking-Daten der Arbeitskopie, aber die Commits, Trees und Blobs selbst leben in .git/objects, denselben Objekten, die Git selbst liest. Deshalb hinterlässt das Löschen von .jj ein vollständig intaktes, gewöhnliches Git-Repository: Es gab nie eine zweite Kopie des tatsächlichen Inhalts, die abzugleichen wäre.

Change-IDs reisen innerhalb des Git-Commits mit, unsichtbar für Git. Git hat kein natives Konzept einer stabilen Identität, die ein Umschreiben übersteht, ein neuer Commit ist einfach ein neues Objekt mit einem neuen Hash, Punkt. jj bekommt seine Change-ID, indem es einen zusätzlichen opaken Identifier innerhalb des zugrunde liegenden Git-Commit-Objekts versteckt, an einer Stelle, die gewöhnliches Git-Tooling ignoriert. Git sieht einen normalen Commit; jj sieht zusätzlich die Identität, die ihn an die History der Änderung über Bearbeitungen und Rebases hinweg bindet.

Das Operations-Log ist ein DAG, keine lineare History, und das ist Absicht. Jeder jj-Befehl produziert eine neue Operation, und jede Operation hält einen vollständigen Snapshot des Repo-Zustands fest (jedes Bookmark, jeden Head) plus einen Link zurück zu der oder den Operationen, auf die sie folgte. Zwei Terminals, die jj gleichzeitig gegen dasselbe Repo laufen lassen, produzieren zwei Operationen mit demselben Elternteil, eine Verzweigung, strukturell identisch zu zwei Commits, die sich einen Elternteil teilen. Der nächste jj-Befehl, der auf das Repo schaut, merged diese verzweigten Operationen automatisch, dieselbe Drei-Wege-Merge-Maschinerie, die zum Mergen von Dateiinhalt benutzt wird, nur angewandt auf “wie sieht das Repo aus”. Das ist eine wirklich andere Antwort auf gleichzeitigen Zugriff als Gits Datei-Locking um einzelne Ref-Updates, und es ist das, was das Ausführen von jj aus zwei Shells gegen ein Repo zu einem Nicht-Ereignis macht statt zu einer Race Condition.

Nichts wird nur deshalb garbage-collected, weil es von keinem Bookmark referenziert wird. Weil Lebendigkeit über das Operations-Log verfolgt wird statt über benannte Refs, so wie Gits eigenes GC funktioniert, bleibt ein Commit, der an kein Bookmark angehängt ist, ein Experiment aus Lektion 5s “neue Arbeit an einem vergangenen Punkt”, vollständig intakt und sichtbar für jj log, bis er wirklich aufgegeben wird oder die ihn referenzierende Operations-History herausaltert. Anonyme Heads sind eine erstklassige, dauerhafte Sache, die man herumliegen haben kann, kein GC-Rennen, das darauf wartet zu passieren.

Nichts davon ändert irgendetwas an den Befehlen aus den anderen Lektionen. Es ist der Teil, der mich dem Tool vertrauen ließ statt nur seine Oberfläche zu genießen, die Sicherheitseigenschaften (Korrektheit bei gleichzeitigem Zugriff, Undo, das alles abdeckt, Colocated-Modus, der wirklich verlustfrei ist) kommen von Entscheidungen auf dieser Schicht, nicht von sorgfältiger Disziplin, die auf einen zerbrechlichen Kern aufgeschichtet wird.