Repositories forken mit jj

“Fork” ist eines der meistgenutzten Wörter in der täglichen Arbeit mit GitHub, GitLab, Gitea, Forgejo oder Bitbucket - und es existiert weder in git noch in jj. git help kennt keinen fork-Befehl, jj help genauso wenig. Alles, was Forken wie eine natürliche Erweiterung der Versionsverwaltung wirken lässt, ist ein Feature der Hosting-Plattform, aufgesetzt auf ein Werkzeug, das nur Repositories, Commits und Refs kennt - und diese Grenze rührt jj gar nicht erst an. Die Beispiele unten nutzen durchgehend jj, im Colocated Mode.

Ein Fork ist ein Plattform-Feature, und diese Lücke erbt jj unverändert. Wenn eine Plattform ein Repository forkt, legt sie ein zweites Repository an, das einem anderen Account oder einer anderen Gruppe gehört und dessen Historie zunächst identisch zum Original ist. Aus Sicht von git - und jj widerspricht git an dieser Stelle nie - sind das schlicht zwei unabhängige Repositories, die zufällig einen gemeinsamen Vorfahren-Commit teilen. Weder .git noch etwas, das jj darüberlegt, vermerkt “das hier ist ein Fork von jenem”. Diese Zuordnung existiert ausschließlich in der Datenbank der Plattform, erreichbar über deren Web-UI oder API.

Zwei Repositories mit gemeinsamer Historie am Fork-Punkt, die sich danach unabhängig auseinanderentwickeln

jj benennt das lokale Vokabular um, nicht die Remote-Mechanik. Aus Branch wurde Bookmark, aus HEAD wurde @ - aber jj git clone, jj git remote, jj git fetch und jj git push sprechen weiterhin mit denselben git-Remotes, über dieselben Protokolle, wie ihre git-Pendants. Den eigenen Fork zu klonen und das Original als zweiten Remote zu ergänzen liest sich fast wie git mit einem vorangestellten Präfix:

jj git clone --colocate https://example.com/du/project.git
cd project
jj git remote add upstream https://example.com/original/project.git
jj git fetch --remote upstream

origin und upstream sind weiterhin reine Namen, erzwungen von nichts außer Gewohnheit.

Zwei gehostete Repositories, upstream und origin, beide mit demselben lokalen Clone verbunden

Ein Bookmark, der einem nicht folgt, ist genau das, was das Synchronisieren hier schmerzfrei macht. Ein git-Branch, der auf main ausgecheckt ist, bewegt sich bei jedem Commit mit - genau deshalb erfordert es Disziplin, das main eines Forks sauber zu halten: ein unachtsamer Commit, und es ist bereits abgedriftet. Ein jj-Bookmark bewegt sich nie von selbst, solange niemand darauf committet. Solange das gilt, ist Synchronisieren kein Merge und kein Rebase, sondern nur das Verschieben eines Zeigers dorthin, wo der Fetch bereits gelandet ist:

jj git fetch --remote upstream
jj bookmark set main -r main@upstream
jj git push --remote origin --bookmark main

Ist git.auto-local-bookmark ausgeschaltet, bleibt der Zustand von upstream nach diesem ersten Fetch nur als main@upstream erreichbar, bis jj bookmark track main@upstream ihn zu etwas befördert, das jj bookmark set unter seinem schlichten Namen ansprechen kann.

Beiträge bedeuten weiterhin, einen Bookmark zum Fork zu pushen, niemals zu upstream. Dort besteht so gut wie nie Schreibzugriff, mit oder ohne jj. Ein Bookmark wird erstellt, zu origin gepusht, und in dem Moment, in dem er auf den Remote übergeht, hört er auf, ein Bookmark zu sein - jj git push schreibt ihn als gewöhnlichen git-Branch, denn das ist das einzige Vokabular, das der Remote und die Plattform, die ihn liest, verstehen.

jj new main -m "fix timeout"
jj bookmark create fix-timeout
jj git push --remote origin --bookmark fix-timeout

Der Pull- oder Merge-Request wird anschließend über die UI oder API der Plattform geöffnet und vergleicht diesen Branch mit dem Zielbranch von upstream

  • ein Vergleich, den die Plattform vollständig in git-Begriffen anstellt, ohne zu wissen, dass jemals ein Bookmark im Spiel war.

Ein lokal erstellter Commit, gepusht zum Fork, dann als Vorschlag eingereicht und in upstream gemerged

Der Colocated Mode sorgt dafür, dass der Fallback von vor den Plattformen nie verschwunden ist. Ein bloßes Bare-Repository per SSH, ohne GitHub, GitLab oder eine vergleichbare Schicht darüber, kennt kein Fork-Konzept und keinen Pull Request, den man öffnen könnte. jj git push und jj git fetch funktionieren trotzdem problemlos dagegen - sie funktionierten immer schon gegen jeden git-Remote, Plattform hin oder her -, aber der Schritt “eine Änderung vorschlagen” fällt zurück auf das, was git schon vor all diesen Plattformen konnte. Weil der Colocated Mode ein echtes .git-Verzeichnis direkt daneben hält, sind git format-patch und git am genauso verfügbar wie eh und je. jj hat nie versucht, diese Schicht zu ersetzen, also gab es dort auch nichts zu verlieren.

Wird das Upstream-Repository umbenannt oder gelöscht, bleibt das für jj genauso unsichtbar wie für git. Ein Eintrag unter jj git remote ist weiterhin nur eine URL; jj hat nicht mehr Vokabular als git für “die Plattform hat das unter dir weggezogen”. Dieser blinde Fleck war nie lokale Buchführung - er liegt jenseits derselben Grenze, an der auch das Forken selbst lebt, genau der Grenze, die jj bewusst nicht überschritten hat.