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.
Das zentrale Umdenken
Abschnitt betitelt „Das zentrale Umdenken“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.
Schritt 1 – Mit der Arbeit beginnen
Abschnitt betitelt „Schritt 1 – Mit der Arbeit beginnen“In Git legst du einen Branch an, bearbeitest, stagest und committest dann. In jj fängst du einfach an zu bearbeiten.
In Git …
# 1. You're on developgit checkout -b feature/new-ui # branch BEFORE you work# 2. Edit files (now in a dirty, unsaved state)# 3. Stage and commitgit add src/ui/*.ts && git commit -m "feat: new ui"In jj …
# 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-uiDeine 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 @.
Schritt 2 – Alte Arbeit wieder aufnehmen
Abschnitt betitelt „Schritt 2 – Alte Arbeit wieder aufnehmen“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):
# 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:
jj edit old_change_id # jump to it right now# ...work...jj rebase -r @ -d develop # rebase when you're readyIn 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.
Schritt 3 – Unveränderliche Commits
Abschnitt betitelt „Schritt 3 – Unveränderliche Commits“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:
jj new immutable_change_idDein 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. |
Schritt 4 – Teil-Commits & Aufräumen
Abschnitt betitelt „Schritt 4 – Teil-Commits & Aufräumen“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):
# 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):
jj split # pick what goes into the FIRST commit# Result:# ○ @ — leftover (3 test files)# ● fix: navigation and auth (7 real files)# ◆ developjj bookmark create nav-fix -r @-jj git push --bookmark nav-fixjj abandon @ # throw away the test-junk commitGemischte Dateien (auf Hunk-Ebene) – wenn eine Datei 10 echte Zeilen und 5 Test-Hacks enthält:
jj split -iDas ö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:
# Option A — squash everything, then split once:jj squash --from "commit1::commit5" --into commit1jj split -i -r that_commitjj abandon junk_commit# 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 absorbjj 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.
Schritt 5 – Squash, Rebase & Konflikte
Abschnitt betitelt „Schritt 5 – Squash, Rebase & Konflikte“In Git …
git rebase -i HEAD~5# Editor opens; change "pick" → "squash" on 4 lines,# write the combined message, save.In jj …
# One at a time, into the parent:jj squash -r commit5jj squash -r commit4
# …or a whole range in one command:jj squash --from "commit1::commit5" --into commit1Nachfahren werden nach jedem Squash automatisch rebased. Kein Editor, kein --continue, kein Zeremoniell.
In Git …
git checkout feature/new-uigit fetch origingit rebase origin/develop# On conflict: STOP, fix files, git add ., git rebase --continue# …repeated for EACH conflicting commit.In jj …
jj git fetchjj 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 |
Konflikte – der größte Unterschied
Abschnitt betitelt „Konflikte – der größte Unterschied“Einen Konflikt zu lösen heißt einfach, eine Datei zu bearbeiten:
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.
Schritte 1–5 im Zusammenspiel
Abschnitt betitelt „Schritte 1–5 im Zusammenspiel“Ein komplettes Feature, von Anfang bis Ende:
- Mit der Arbeit beginnen –
jj describe -m "feat: new navigation UI" - Zwischenstände unterwegs festhalten –
jj commit -m "wip: nav routing done" - Zu einem Commit zusammenfassen –
jj squash --from "first::last" --into first - Test-Müll entfernen –
jj restore test-setup.ts(oderjj split -i) - Auf das neueste develop rebasen –
jj git fetch && jj rebase -r feature -d develop - Beschriften und pushen –
jj bookmark create feature/x && jj git push --bookmark feature/x - Den PR öffnen, Review einholen
- 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.
Schritt 6 – Bookmarks
Abschnitt betitelt „Schritt 6 – Bookmarks“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):
jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign # safe force-pushB – die alte Arbeit behalten und darauf aufbauen:
jj rebase -s commit1 -d feature/nav-redesign # your commits on topjj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesignC – alte und neue Arbeit zusammenführen (unterschiedliche Dateien, beides soll rein):
jj squash --from old_work_change_id --into commit1jj bookmark set feature/nav-redesign -r commit3jj 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.
Schritt 7 – Push, Fetch & der PR-Zyklus
Abschnitt betitelt „Schritt 7 – Push, Fetch & der PR-Zyklus“Push – ein Verb statt vier
Abschnitt betitelt „Push – ein Verb statt vier“Git kennt mehrere Push-Varianten (-u beim ersten Mal, normaler Push, --force-with-lease, --delete). jj bündelt sie in einem Befehl:
jj git push --bookmark my-thing # first pushjj git push --bookmark my-thing # every push afterjj git push --all # all local bookmarksjj git push --delete old-bookmark # delete a remote bookmarkEs 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.
Fetch – nie eine Überraschung
Abschnitt betitelt „Fetch – nie eine Überraschung“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.
Die Review-Schleife
Abschnitt betitelt „Die Review-Schleife“Feedback arbeitest du ein, indem du den Commit direkt bearbeitest und erneut pushst – ganz ohne Fixup-Commits:
jj edit feature_change# ...make the requested changes...jj git push --bookmark feature/x # auto force-push, safelySchritt 8 – Power-Moves
Abschnitt betitelt „Schritt 8 – Power-Moves“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.
# 1. In @, just fix BOTH lines wherever they live now.# jj snapshots; @ holds both fixes.# 2. Route them home:jj absorbjj 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.
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 offEs gibt keinen Stash, weil es nichts zu stashen gibt – deine Arbeit war nie uncommittet.
Gestapelte PRs – automatisches Restacking
Abschnitt betitelt „Gestapelte PRs – automatisches Restacking“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◆ developWenn 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:
jj edit feature/auth-refactor # fix the base# ...descendants auto-rebase...jj git push --all # all four PRs update togetherSpickzettel jj ↔ git
Abschnitt betitelt „Spickzettel jj ↔ git“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 |
Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- 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.