Zum Inhalt springen

Git → Jujutsu, Schritt für Schritt

Wenn du Git gut kennst, wird sich jj an manchen Stellen vertraut anfühlen – und an anderen völlig auf den Kopf gestellt. Dieses Tutorial geht den Weg Workflow für Workflow und zeigt immer, was du in Git tun würdest, direkt neben dem, was du in jj tust – und, wo es hilft, den passenden Handgriff in Wyrm.

Du musst dir vorab keine neuen Befehle merken. Fang mit dem Umdenken an und folge dann den Schritten der Reihe nach.

Eine Handvoll Ideen erklärt fast jeden Unterschied, auf den du stoßen wirst:

  • Es gibt keine Staging Area. Jede Dateiänderung ist sofort Teil des aktuellen Commits.
  • Die Working Copy ist ein Commit – im Log mit @ markiert. Jeder jj-Befehl erstellt automatisch einen Snapshot deiner Dateien darin.
  • Commits sind billig und wegwerfbar. Du squashst nicht, um „vor dem Push aufzuräumen“; du squashst, wenn du es willst, und jj kümmert sich um die Folgen.
  • Historie umzuschreiben ist der normale Workflow, kein gefährlicher Notausgang.
  • Anonyme Branches sind normal. Gits gefürchteter „Detached HEAD“ ist in jj schlicht Alltag.
  • Bookmarks sind Versandetiketten, keine Arbeitsplätze. Du arbeitest nicht „auf einem Bookmark“ – du arbeitest an Commits und beschriftest einen davon, wenn es Zeit zum Pushen ist.

In Wyrm ist die oberste Zeile des Log-Graphs @. Während du Dateien bearbeitest, aktualisiert sich diese Zeile live – es gibt kein separates Panel für „nicht committete Änderungen“, um das du dich kümmern müsstest.

In Git legst du einen Branch an, bearbeitest, stagest und committest dann. In jj fängst du einfach an zu bearbeiten.

In Git …

Terminal-Fenster
# 1. You're on develop
git checkout -b feature/new-ui # branch BEFORE you work
# 2. Edit files (now in a dirty, unsaved state)
# 3. Stage and commit
git add src/ui/*.ts && git commit -m "feat: new ui"

In jj …

Terminal-Fenster
# 1. You're on develop (same)
# 2. Just start editing. That's it.
# No branch. No staging. Every save is captured in @.
# 3. Name the work when you're ready:
jj describe -m "feat: new ui"
# 4. (Optional, only when pushing) add a bookmark:
jj bookmark create feature/new-ui

Deine Arbeit ist in dem Moment sicher, in dem du eine Datei speicherst. Einen Branch brauchst du erst zum Pushen, und es gibt kein „Staging“ – alles lebt in @.

Du hast einen alten Commit mit guten Änderungen, aber develop ist inzwischen 20 Commits weiter. Wie holst du auf?

Der Trick: Die Reihenfolge spielt keine Rolle. Erst rebasen oder erst bearbeiten – jj ist beides recht, und Konflikte blockieren dich nicht.

Option A – erst rebasen, dann bearbeiten (am häufigsten):

Terminal-Fenster
# Move your old commit onto the latest develop:
jj rebase -r old_change_id -d develop
# Then continue working on it:
jj edit old_change_id
# Now @ IS that commit; edits amend it directly.
# The change ID stays the same even after the rebase.

Option B – erst bearbeiten, später rebasen:

Terminal-Fenster
jj edit old_change_id # jump to it right now
# ...work...
jj rebase -r @ -d develop # rebase when you're ready

In Git heißt das „checkout → rebase → Konflikte lösen → continue“, und ein Konflikt lässt dich mitten im Rebase erstarren. In jj kannst du rebasen, einen Konflikt sehen, dich etwas anderem widmen und später zurückkommen, um ihn zu lösen – der Konflikt ist einfach ein Zustand, in dem sich der Commit befindet.

Du hast auf einen alten Commit doppelgeklickt und „Revision is immutable“ bekommen? In Git kannst du überall auschecken, also fühlt sich das an, als stünde dir jj im Weg. Tut es aber nicht.

jj schützt Commits, die bereits öffentlich sind – also mit der Welt geteilt:

Unveränderlich (◆) Veränderlich (●)
Commits auf main@origin Deine Working Copy @
Commits auf beliebigen *@origin Rein lokale Commits
Getaggte Commits Alles, was du noch nicht gepusht hast

Warum das gut ist: In Git kannst du per git push --force über main schreiben oder einen geteilten Commit amenden und damit Historie zerstören, auf die andere angewiesen sind – nichts hält dich auf. jj sagt: „Das ist schon öffentlich; was das Team nutzt, kannst du nicht umschreiben.“ Damit verschwindet eine ganze Kategorie von „Oh nein“-Momenten.

Was du stattdessen tust – bearbeite nicht den unveränderlichen Commit, sondern baue einen neuen darauf auf:

Terminal-Fenster
jj new immutable_change_id

Dein neues @ enthält alle Dateien aus diesem Commit; du bearbeitest frei, und das Original bleibt unberührt.

Veränderlicher Commit (●) Unveränderlicher Commit (◆)
jj edit change_id jj new change_id
@ wandert zu diesem Commit Erstellt einen neuen Child-Commit
Änderungen amenden ihn direkt @ ist der neue Child-Commit

Kurzübersicht: new vs. edit vs. describe vs. commit

Abschnitt betitelt „Kurzübersicht: new vs. edit vs. describe vs. commit“
Befehl Was er tut Nächster Git-Handgriff Wann verwenden
jj new Neuer leerer Child-Commit; @ wandert dorthin git checkout -b „Fertig damit, weiter zum Nächsten.“
jj edit @ auf einen bestehenden veränderlichen Commit setzen git rebase -i → edit Zurückgehen, um einen früheren Commit zu korrigieren.
jj describe Nachricht von @ setzen/ändern git commit --amend -m Benennen, woran du gerade arbeitest.
jj commit describe + new in einem git commit -m Diese Arbeit abschließen und die nächste beginnen.

Du hast 10 Dateien bearbeitet. Nur 7 gehören in diesen Commit; 3 sind Test-Müll. Manche Dateien mischen sogar echte Änderungen mit Wegwerfzeilen.

Git ist Opt-in: Du stagest, was du willst, und verwirfst den Rest. jj ist Opt-out: Alles steckt schon in @, also entfernst du, was du nicht willst. Gleiches Ziel, entgegengesetzte Richtung.

Methode 1 – jj restore (ganze Dateien verwerfen):

Terminal-Fenster
# Revert these files to the parent's version:
jj restore test-setup.ts dev-nav-hack.svelte .env.test
# @ now has only the 7 real files. Name it:
jj describe -m "fix: navigation and auth"

jj restore entspricht „Discard Changes“ – es setzt eine Datei auf den Stand ihres Parents zurück.

Methode 2 – jj split (einiges behalten, den Rest abtrennen):

Terminal-Fenster
jj split # pick what goes into the FIRST commit
# Result:
# ○ @ — leftover (3 test files)
# ● fix: navigation and auth (7 real files)
# ◆ develop
jj bookmark create nav-fix -r @-
jj git push --bookmark nav-fix
jj abandon @ # throw away the test-junk commit

Gemischte Dateien (auf Hunk-Ebene) – wenn eine Datei 10 echte Zeilen und 5 Test-Hacks enthält:

Terminal-Fenster
jj split -i

Das öffnet einen interaktiven Diff aller geänderten Hunks; du entscheidest, welche Hunks im ersten Commit landen und welche im zweiten bleiben. Das ist jjs git add -p.

Test-Müll über mehrere Commits verteilt? Du brauchst nicht einen Split pro Commit:

Terminal-Fenster
# Option A — squash everything, then split once:
jj squash --from "commit1::commit5" --into commit1
jj split -i -r that_commit
jj abandon junk_commit
Terminal-Fenster
# Option B — jj absorb:
# 1. In @, simply UNDO the test changes (delete the files,
# remove the test lines). jj snapshots those "undo" edits.
# 2. Route them home automatically:
jj absorb

jj absorb schaut sich jeden Rückgängig-Hunk in @ an, findet den Vorfahren-Commit, der diese Zeilen hinzugefügt hat, und faltet die Korrektur in genau diesen Commit – kein interaktiver Rebase, ein einziger Befehl. Mehr dazu in Schritt 8.

In Git …

Terminal-Fenster
git rebase -i HEAD~5
# Editor opens; change "pick" → "squash" on 4 lines,
# write the combined message, save.

In jj …

Terminal-Fenster
# One at a time, into the parent:
jj squash -r commit5
jj squash -r commit4
# …or a whole range in one command:
jj squash --from "commit1::commit5" --into commit1

Nachfahren werden nach jedem Squash automatisch rebased. Kein Editor, kein --continue, kein Zeremoniell.

In Git …

Terminal-Fenster
git checkout feature/new-ui
git fetch origin
git rebase origin/develop
# On conflict: STOP, fix files, git add ., git rebase --continue
# …repeated for EACH conflicting commit.

In jj …

Terminal-Fenster
jj git fetch
jj rebase -r your_change -d develop
# On conflict: nothing stops. The commit is just marked CONFLICT.
jj edit your_change # fix the markers when you're ready — that's it.

Welche Commits verschoben werden, hängt vom Flag ab:

Flag Was verschoben wird Git-Entsprechung
-r (revision) Nur dieser eine Commit; Children bleiben, wo sie sind (keine direkte Entsprechung)
-s (source) Dieser Commit und alle seine Nachfahren git rebase --onto
-b (branch) Alles, seit es vom Ziel abgezweigt ist git rebase develop

Einen Konflikt zu lösen heißt einfach, eine Datei zu bearbeiten:

Terminal-Fenster
jj edit conflicted_change
# Open the file. jj's markers show the base plus what each
# side intended, not two opaque full versions:
#
# <<<<<<< Conflict 1 of 1
# %%%%%%% diff from base to side #1
# context
# -old line
# +your change
# +++++++ side #2
# their change
# >>>>>>> Conflict 1 of 1 ends
#
# Fix it, save. jj snapshots. Resolved. No git add. No --continue.

Weil die Marker die Absicht beschreiben (Basis + jeder Diff) statt zweier fertiger Blobs, lassen sich Konflikte meist leichter nachvollziehen. Den Workflow in Wyrm findest du unter Konfliktlösung.

Ein komplettes Feature, von Anfang bis Ende:

  1. Mit der Arbeit beginnen – jj describe -m "feat: new navigation UI"
  2. Zwischenstände unterwegs festhalten – jj commit -m "wip: nav routing done"
  3. Zu einem Commit zusammenfassen – jj squash --from "first::last" --into first
  4. Test-Müll entfernen – jj restore test-setup.ts (oder jj split -i)
  5. Auf das neueste develop rebasen – jj git fetch && jj rebase -r feature -d develop
  6. Beschriften und pushen – jj bookmark create feature/x && jj git push --bookmark feature/x
  7. Den PR öffnen, Review einholen
  8. Feedback einarbeiten – jj edit feature_change → Änderungen vornehmen → jj git push --bookmark feature/x

Keine Fixup-Commits, kein zweiter Squash. Commit bearbeiten, pushen – jj macht den Force-Push von selbst und sicher.

Ein jj-Bookmark ist kein Git-Branch. Ein Git-Branch folgt HEAD: Du committest, und er bewegt sich mit – du bist immer „auf“ einem Branch. Ein jj-Bookmark ist ein Haftzettel an einem bestimmten Commit. Er bewegt sich nicht, wenn du neue Commits erstellst, und du bist nie „auf“ ihm. Du bist immer auf @.

Git: die Adresse auf den Karton schreiben, bevor du ihn packst. jj: den Karton packen und das Versandetikett aufkleben, wenn er bereit zum Verschicken ist.

Du arbeitest also zuerst und beschriftest später. Drei typische Situationen, wenn schon ein altes Bookmark existiert:

A – die alte Arbeit ist überholt (deine neuen Commits ersetzen sie):

Terminal-Fenster
jj bookmark set feature/nav-redesign -r commit3
jj git push --bookmark feature/nav-redesign # safe force-push

B – die alte Arbeit behalten und darauf aufbauen:

Terminal-Fenster
jj rebase -s commit1 -d feature/nav-redesign # your commits on top
jj bookmark set feature/nav-redesign -r commit3
jj git push --bookmark feature/nav-redesign

C – alte und neue Arbeit zusammenführen (unterschiedliche Dateien, beides soll rein):

Terminal-Fenster
jj squash --from old_work_change_id --into commit1
jj bookmark set feature/nav-redesign -r commit3
jj git push --bookmark feature/nav-redesign
Verhalten Git-Branch jj-Bookmark
Bewegt sich beim Commit? Ja – folgt HEAD automatisch Nein – bleibt stehen; manuell verschieben
Zum Arbeiten nötig? Ja – immer „auf“ einem Nein – Arbeit ganz ohne möglich
Zum Pushen nötig? Ja Ja – ein Push braucht einen Namen
Gibt es einen „aktuellen“? Ja – HEAD zeigt darauf Kein solches Konzept; @ ist immer aktuell
Folgt einem Rebase? Manchmal Ja – verfolgt die Change-ID durch Umschreibungen
Angst vor Detached HEAD? Ja Keine – das ist ganz normal
Force-Push Manuell, beängstigend Automatisch, sicher (with-lease)

Weil Bookmarks die Change-ID verfolgen (nicht den Commit-SHA), überstehen sie Umschreibungen: Nach einem Rebase pushst du einfach – ein manueller Force-Push zum Aktualisieren des Remotes ist nicht nötig.

Git kennt mehrere Push-Varianten (-u beim ersten Mal, normaler Push, --force-with-lease, --delete). jj bündelt sie in einem Befehl:

Terminal-Fenster
jj git push --bookmark my-thing # first push
jj git push --bookmark my-thing # every push after
jj git push --all # all local bookmarks
jj git push --delete old-bookmark # delete a remote bookmark

Es gibt kein --force-Flag. jj verhält sich immer wie --force-with-lease: Hat seit deinem letzten Fetch jemand auf dieses Bookmark gepusht, verweigert jj den Push und fordert dich auf, erst zu fetchen. Du kannst die Arbeit einer Kollegin oder eines Kollegen also schlicht nicht versehentlich überschreiben.

Terminal-Fenster
jj git fetch # downloads remote state; does NOT merge,
# rebase, or touch your working copy.
# Then YOU decide:
jj rebase -d main@origin # rebase your work onto the update — or don't.

Es gibt kein jj pull. Fetchen und Rebasen sind immer getrennt, deshalb bekommst du nie einen unerwarteten Merge-Commit.

Feedback arbeitest du ein, indem du den Commit direkt bearbeitest und erneut pushst – ganz ohne Fixup-Commits:

Terminal-Fenster
jj edit feature_change
# ...make the requested changes...
jj git push --bookmark feature/x # auto force-push, safely

Ein paar Fähigkeiten von jj, für die es in Git keine saubere Entsprechung gibt.

jj absorb – Korrekturen automatisch in die richtigen Commits leiten

Abschnitt betitelt „jj absorb – Korrekturen automatisch in die richtigen Commits leiten“

Die Reviewerin sagt: „Zeile 42 in Commit 2 ist falsch, und Zeile 88 in Commit 4 hat einen Tippfehler.“ In Git bedeutet das ein git commit --fixup pro Stelle plus git rebase -i --autosquash.

Terminal-Fenster
# 1. In @, just fix BOTH lines wherever they live now.
# jj snapshots; @ holds both fixes.
# 2. Route them home:
jj absorb

jj findet für jede geänderte Zeile den jüngsten Vorfahren, der sie berührt hat, verschiebt die Korrektur dorthin und rebased die Nachfahren. Beide Fehler landen in den richtigen Commits, und @ ist wieder leer.

Kein Stash – jeder Zustand ist bereits ein Commit

Abschnitt betitelt „Kein Stash – jeder Zustand ist bereits ein Commit“

Die Chefin sagt: „Lass alles stehen und liegen, Hotfix, sofort.“ In Git heißt das git stash / Branch / Fix / git stash pop und ein Stoßgebet.

Terminal-Fenster
jj new develop -m "hotfix: prod is down"
# Your previous work is still there as a sibling commit.
# Fix, push, then return:
jj edit feature_a_id # right back where you left off

Es gibt keinen Stash, weil es nichts zu stashen gibt – deine Arbeit war nie uncommittet.

Liefere vier kleine, gut reviewbare PRs, die aufeinander aufbauen, statt eines riesigen:

● commit 4 ← bookmark feature/tests
● commit 3 ← bookmark feature/ui
● commit 2 ← bookmark feature/api
● commit 1 ← bookmark feature/auth-refactor
◆ develop

Wenn du in Git einen Fehler im untersten PR behebst, musst du jeden PR darüber rebasen und force-pushen. In jj bearbeitest du den Basis-Commit, lässt alle Nachfahren automatisch rebasen und aktualisierst alle PRs auf einmal:

Terminal-Fenster
jj edit feature/auth-refactor # fix the base
# ...descendants auto-rebase...
jj git push --all # all four PRs update together

Arbeitsablauf

Ziel jj git
Status jj status git status
Log jj log git log --graph
Diff der aktuellen Arbeit jj diff git diff HEAD
Neuen Commit beginnen jj new git checkout -b
Commit-Nachricht setzen jj describe -m "…" git commit --amend -m
Committen und weitermachen jj commit -m "…" git commit -m

Commits bearbeiten

Ziel jj git
Alten veränderlichen Commit bearbeiten jj edit <rev> git rebase -i (edit)
Child eines beliebigen Commits jj new <rev> git checkout -b
In den Parent falten jj squash fixup + autosquash
Interaktiv falten jj squash -i git add -p + amend
Commit aufteilen jj split git rebase -i (split)
Korrekturen automatisch zuordnen jj absorb (keine)
Commit verwerfen jj abandon git reset --hard HEAD~1
Datei wiederherstellen jj restore <file> git checkout HEAD -- <file>

Remote-Operationen

Ziel jj git
Fetch jj git fetch git fetch
Push jj git push --bookmark <name> git push
Erster Push jj git push --bookmark <name> git push -u
Force-Push (automatisch) jj git push --bookmark <name> git push --force-with-lease
Alles pushen jj git push --all git push --all
Auf dem Remote löschen jj git push --delete <name> git push --delete

Bookmarks

Ziel jj git
Erstellen jj bookmark create <name> git branch <name>
Verschieben jj bookmark set <name> -r <rev> git branch -f <name>
Löschen jj bookmark delete <name> git branch -d <name>
Auflisten jj bookmark list git branch -a

Sicherheit & Selektoren

Ziel jj git
Alles rückgängig machen jj undo git reflog + reset
Operationshistorie jj op log git reflog
Arbeit „stashen“ jj new @-, dann jj edit zurück git stash
Revset Bedeutung
@ Working Copy
@- Parent der Working Copy
trunk() Haupt-Branch auf origin
trunk()..@ Deine lokale Arbeit, die noch nicht in trunk ist
mine() Deine eigenen Commits
conflicts() Alle Commits mit Konflikten
bookmarks() Alle Commits mit Bookmark
description("fix") Commits mit „fix“ in der Nachricht
  • jj-Grundlagen – Change-IDs, die Working Copy und das Operations-Log im Detail.
  • Log-Graph – Historie in Wyrm navigieren und filtern.
  • Konfliktlösung – Konflikte visuell mit dem integrierten Drei-Wege-Resolver lösen.
  • Push-Vorschau – genau prüfen, was auf dem Remote landet, bevor du pushst.