Git → Jujutsu, krok po kroku
Jeśli dobrze znasz gita, jj w niektórych miejscach wyda Ci się znajome, a w innych — postawione na głowie. Ten samouczek prowadzi Cię krok po kroku przez kolejne procesy pracy, zawsze pokazując co robisz w gicie obok tego, co robisz w jj — a tam, gdzie to pomaga, także odpowiedni ruch w aplikacji Wyrm.
Nie musisz z góry uczyć się nowych poleceń na pamięć. Zacznij od zmiany sposobu myślenia, a potem przechodź przez kolejne etapy po kolei.
Najważniejsza zmiana w myśleniu
Dział zatytułowany „Najważniejsza zmiana w myśleniu”Kilka idei wyjaśnia niemal każdą różnicę, na jaką trafisz:
- Nie ma staging area. Każda zmiana w pliku od razu jest częścią bieżącego commita.
- Kopia robocza jest commitem — oznaczonym w logu jako
@. Każde polecenie jj automatycznie robi migawkę Twoich plików do tego commita. - Commity są tanie i jednorazowe. Nie robisz squasha, żeby „posprzątać przed pushem”; robisz go wtedy, gdy chcesz, a jj sam zajmuje się kaskadą zmian.
- Przepisywanie historii to normalny sposób pracy, a nie niebezpieczne wyjście awaryjne.
- Anonimowe gałęzie są czymś normalnym. Straszny w gicie „detached HEAD” to w jj zwykła codzienność.
- Bookmarki to etykiety wysyłkowe, a nie miejsca pracy. Nie pracujesz „na bookmarku” — pracujesz na commitach, a gdy przychodzi czas na push, oznaczasz jeden z nich.
W aplikacji Wyrm górny wiersz grafu logu to @. Gdy edytujesz pliki, ten wiersz aktualizuje się na żywo — nie ma osobnego panelu „niezatwierdzonych zmian”, którym trzeba by zarządzać.
Krok 1 — Rozpoczęcie pracy
Dział zatytułowany „Krok 1 — Rozpoczęcie pracy”W gicie tworzysz gałąź, edytujesz, dodajesz do stage’a i robisz commit. W jj po prostu zaczynasz edytować.
W gicie…
# 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"W 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-uiTwoja praca jest bezpieczna w chwili, gdy zapiszesz plik. Gałąź nie jest potrzebna, dopóki nie robisz pusha, i nie ma żadnego „stage’owania” — wszystko żyje w @.
Krok 2 — Powrót do starej pracy
Dział zatytułowany „Krok 2 — Powrót do starej pracy”Masz stary commit z dobrymi zmianami, ale develop uciekł o 20 commitów do przodu. Jak to nadrobić?
Sztuczka: kolejność nie ma znaczenia. Najpierw rebase albo najpierw edycja — jj poradzi sobie w obu przypadkach, a konflikty Cię nie zablokują.
Opcja A — najpierw rebase, potem edycja (najczęstsza):
# 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.Opcja B — najpierw edycja, rebase później:
jj edit old_change_id # jump to it right now# ...work...jj rebase -r @ -d develop # rebase when you're readyW gicie wygląda to tak: „checkout → rebase → rozwiązywanie konfliktów → continue”, a konflikt zamraża Cię w połowie rebase’u. W jj możesz zrobić rebase, zobaczyć konflikt, zająć się czymś innym i wrócić, żeby go rozwiązać później — konflikt to po prostu stan, w którym jest commit.
Krok 3 — Niemutowalne commity
Dział zatytułowany „Krok 3 — Niemutowalne commity”Kliknąłeś dwukrotnie stary commit i dostałeś komunikat „Revision is immutable”? W gicie możesz zrobić checkout gdziekolwiek, więc wygląda to tak, jakby jj wchodził Ci w drogę. Tak nie jest.
jj chroni commity, które są już publiczne — udostępnione światu:
| Niemutowalne (◆) | Mutowalne (●) |
|---|---|
Commity na main@origin |
Twoja kopia robocza @ |
Commity na dowolnym *@origin |
Commity istniejące tylko lokalnie |
| Otagowane commity | Wszystko, czego jeszcze nie wypchnąłeś |
Dlaczego to dobra rzecz: w gicie możesz zrobić git push --force na main albo poprawić udostępniony commit i zniszczyć historię, na której polegają inni — nic Cię nie powstrzyma. jj mówi: „to już jest publiczne; nie możesz przepisać tego, na czym polega zespół”. To eliminuje całą kategorię momentów „o nie”.
Co zrobić zamiast tego — nie edytuj niemutowalnego commita, tylko zbuduj na nim nowy:
jj new immutable_change_idTwoje nowe @ zawiera wszystkie pliki z tamtego commita; edytujesz swobodnie, a oryginał pozostaje nietknięty.
| Commit mutowalny (●) | Commit niemutowalny (◆) |
|---|---|
jj edit change_id |
jj new change_id |
@ przesuwa się na ten commit |
Tworzy nowy commit potomny |
| Edycje poprawiają go w miejscu | @ to nowy commit potomny |
Ściąga: new vs edit vs describe vs commit
Dział zatytułowany „Ściąga: new vs edit vs describe vs commit”| Polecenie | Co robi | Najbliższy odpowiednik w gicie | Kiedy używać |
|---|---|---|---|
jj new |
Nowy pusty commit potomny; przenosi tam @ |
git checkout -b |
„Skończone, przechodzę do następnej rzeczy”. |
jj edit |
Przenosi @ na istniejący mutowalny commit |
git rebase -i → edit |
Wróć, żeby poprawić wcześniejszy commit. |
jj describe |
Ustawia/zmienia opis @ |
git commit --amend -m |
Nazwij to, nad czym pracujesz. |
jj commit |
describe + new w jednym |
git commit -m |
Zakończ tę pracę i zacznij następną. |
Krok 4 — Częściowe commity i porządki
Dział zatytułowany „Krok 4 — Częściowe commity i porządki”Edytowałeś 10 plików. Tylko 7 należy do tego commita; 3 to testowe śmieci. Niektóre pliki mieszają nawet prawdziwe zmiany z liniami do wyrzucenia.
Git działa na zasadzie opt-in: dodajesz do stage’a to, co chcesz, a resztę odrzucasz. jj działa na zasadzie opt-out: wszystko już jest w @, więc usuwasz to, czego nie chcesz. Ten sam cel, odwrotny kierunek.
Metoda 1 — jj restore (odrzucenie całych plików):
# 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 to odpowiednik „Discard Changes” — przywraca plik do wersji z rodzica.
Metoda 2 — jj split (zatrzymaj część, oddziel resztę):
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 commitPliki mieszane (na poziomie hunków) — gdy jeden plik ma 10 prawdziwych linii i 5 testowych hacków:
jj split -iTo otwiera interaktywny diff każdego zmienionego hunka; wybierasz, które hunki trafią do pierwszego commita, a które zostaną w drugim. To jjowy odpowiednik git add -p.
Testowe śmieci rozrzucone po kilku commitach? Nie potrzebujesz osobnego splitu dla każdego commita:
# 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 analizuje każdy cofający hunk w @, znajduje commit przodka, który dodał te linie, i wkleja poprawkę do tego commita — bez interaktywnego rebase’u, jednym poleceniem. Więcej na ten temat w kroku 8.
Krok 5 — Squash, rebase i konflikty
Dział zatytułowany „Krok 5 — Squash, rebase i konflikty”W gicie…
git rebase -i HEAD~5# Editor opens; change "pick" → "squash" on 4 lines,# write the combined message, save.W 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 commit1Potomkowie są automatycznie rebase’owani po każdym squashu. Bez edytora, bez --continue, bez ceremonii.
W gicie…
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.W 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.To, które commity się przesuną, zależy od flagi:
| Flaga | Co przesuwa | Odpowiednik w gicie |
|---|---|---|
-r (revision) |
Tylko ten jeden commit; dzieci zostają na miejscu | (brak bezpośredniego odpowiednika) |
-s (source) |
Ten commit oraz wszystkich jego potomków | git rebase --onto |
-b (branch) |
Wszystko od miejsca, w którym rozeszło się z celem | git rebase develop |
Konflikty — największa różnica
Dział zatytułowany „Konflikty — największa różnica”Rozwiązanie konfliktu to po prostu edycja pliku:
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.Ponieważ znaczniki opisują intencję (bazę + diff każdej strony), a nie dwie gotowe wersje pliku, konflikty zwykle łatwiej zrozumieć. Opis pracy z konfliktami w aplikacji Wyrm znajdziesz w przewodniku Rozwiązywanie konfliktów.
Kroki 1–5 w praktyce
Dział zatytułowany „Kroki 1–5 w praktyce”Kompletna funkcja, od początku do końca:
- Zacznij pracę —
jj describe -m "feat: new navigation UI" - Zapisuj kamienie milowe na bieżąco —
jj commit -m "wip: nav routing done" - Posprzątaj do jednego commita —
jj squash --from "first::last" --into first - Wyrzuć testowe śmieci —
jj restore test-setup.ts(albojj split -i) - Zrób rebase na najnowszy develop —
jj git fetch && jj rebase -r feature -d develop - Nadaj etykietę i wypchnij —
jj bookmark create feature/x && jj git push --bookmark feature/x - Otwórz PR i poczekaj na review
- Wprowadź poprawki z review —
jj edit feature_change→ wprowadź zmiany →jj git push --bookmark feature/x
Bez commitów typu fixup, bez drugiego squasha. Edytujesz commit, robisz push — jj sam bezpiecznie wykona force-push.
Krok 6 — Bookmarki
Dział zatytułowany „Krok 6 — Bookmarki”Bookmark w jj to nie gałąź gita. Gałąź gita podąża za HEAD: robisz commit, a ona przesuwa się razem z Tobą — zawsze jesteś „na” jakiejś gałęzi. Bookmark w jj to karteczka przyklejona do konkretnego commita. Nie przesuwa się, gdy tworzysz nowe commity, i nigdy nie jesteś „na” nim. Zawsze jesteś na @.
Git: wypisz adres na pudełku przed jego spakowaniem. jj: spakuj pudełko, a etykietę wysyłkową naklej dopiero wtedy, gdy jesteś gotów je nadać.
Najpierw więc pracujesz, a etykietę dodajesz później. Trzy typowe sytuacje, gdy stary bookmark już istnieje:
A — stara praca jest nieaktualna (Twoje nowe commity ją zastępują):
jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign # safe force-pushB — zachowaj starą pracę i buduj na niej:
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 — połącz starą i nową pracę (różne pliki, chcesz obie):
jj squash --from old_work_change_id --into commit1jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign| Zachowanie | Gałąź gita | Bookmark jj |
|---|---|---|
| Przesuwa się przy commicie? | Tak — automatycznie podąża za HEAD | Nie — zostaje na miejscu; przesuwasz go ręcznie |
| Potrzebny do pracy? | Tak — zawsze jesteś „na” jakiejś | Nie — możesz pracować bez żadnego |
| Potrzebny do pusha? | Tak | Tak — push wymaga nazwy |
| Jest „bieżący”? | Tak — wskazuje na niego HEAD | Nie ma takiego pojęcia; bieżący jest zawsze @ |
| Podąża za rebase’em? | Czasami | Tak — śledzi change ID przez kolejne przepisania |
| Strach przed detached HEAD? | Tak | Żaden — to zupełnie normalne |
| Force push | Ręczny, stresujący | Automatyczny, bezpieczny (with-lease) |
Ponieważ bookmarki śledzą change ID (a nie SHA commita), przetrwają przepisywanie historii: po rebasie po prostu robisz push, bez ręcznego force-pusha, żeby zaktualizować remote.
Krok 7 — Push, fetch i cykl PR
Dział zatytułowany „Krok 7 — Push, fetch i cykl PR”Push — jedno polecenie zamiast czterech
Dział zatytułowany „Push — jedno polecenie zamiast czterech”Git ma kilka wariantów pusha (-u za pierwszym razem, zwykły push, --force-with-lease, --delete). jj sprowadza je wszystkie do jednego polecenia:
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 bookmarkNie ma flagi --force. jj zawsze zachowuje się jak --force-with-lease: jeśli od Twojego ostatniego fetcha ktoś wypchnął coś na ten bookmark, jj odmówi i poprosi, żebyś najpierw zrobił fetch. Po prostu nie da się przypadkiem nadpisać pracy kolegi z zespołu.
Fetch — bez niespodzianek
Dział zatytułowany „Fetch — bez niespodzianek”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.Nie ma jj pull. Fetch i rebase są zawsze rozdzielone, więc nigdy nie dostaniesz niespodziewanego merge commita.
Pętla review
Dział zatytułowany „Pętla review”Poprawki z review wprowadzasz, edytując commit w miejscu i ponownie robiąc push — bez commitów typu fixup:
jj edit feature_change# ...make the requested changes...jj git push --bookmark feature/x # auto force-push, safelyKrok 8 — Zaawansowane ruchy
Dział zatytułowany „Krok 8 — Zaawansowane ruchy”Kilka możliwości jj, które nie mają czystego odpowiednika w gicie.
jj absorb — automatyczne kierowanie poprawek do właściwych commitów
Dział zatytułowany „jj absorb — automatyczne kierowanie poprawek do właściwych commitów”Recenzent pisze: „linia 42 w commicie 2 jest błędna, a w linii 88 w commicie 4 jest literówka”. W gicie oznacza to git commit --fixup dla każdego miejsca 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 absorbDla każdej zmienionej linii jj znajduje najbliższego przodka, który jej dotykał, przenosi tam poprawkę i robi rebase potomków. Obie poprawki trafiają do właściwych commitów, a @ znów jest puste.
Bez stasha — każdy stan już jest commitem
Dział zatytułowany „Bez stasha — każdy stan już jest commitem”Szef mówi: „rzuć wszystko, pilny hotfix”. W gicie to git stash / gałąź / poprawka / git stash pop i modlitwa.
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 offNie ma stasha, bo nie ma czego chować — Twoja praca nigdy nie była niezacommitowana.
Stacked PR — automatyczne przestawianie stosu
Dział zatytułowany „Stacked PR — automatyczne przestawianie stosu”Zamiast jednego olbrzymiego PR wyślij cztery małe, łatwe do przejrzenia PR, które budują jeden na drugim:
● commit 4 ← bookmark feature/tests● commit 3 ← bookmark feature/ui● commit 2 ← bookmark feature/api● commit 1 ← bookmark feature/auth-refactor◆ developW gicie poprawka błędu w bazowym PR oznacza rebase i force-push każdego PR nad nim. W jj edytujesz bazowy commit, wszyscy potomkowie są automatycznie rebase’owani, a wszystkie PR aktualizujesz za jednym razem:
jj edit feature/auth-refactor # fix the base# ...descendants auto-rebase...jj git push --all # all four PRs update togetherŚciąga jj ↔ git
Dział zatytułowany „Ściąga jj ↔ git”Cykl pracy
| Cel | jj | git |
|---|---|---|
| Status | jj status |
git status |
| Log | jj log |
git log --graph |
| Diff bieżącej pracy | jj diff |
git diff HEAD |
| Nowy commit | jj new |
git checkout -b |
| Ustaw opis commita | jj describe -m "…" |
git commit --amend -m |
| Zacommituj i idź dalej | jj commit -m "…" |
git commit -m |
Edycja commitów
| Cel | jj | git |
|---|---|---|
| Edytuj stary mutowalny commit | jj edit <rev> |
git rebase -i (edit) |
| Potomek dowolnego commita | jj new <rev> |
git checkout -b |
| Wcielenie do rodzica | jj squash |
fixup + autosquash |
| Interaktywne wcielenie | jj squash -i |
git add -p + amend |
| Podział commita | jj split |
git rebase -i (split) |
| Automatyczne kierowanie poprawek | jj absorb |
(brak) |
| Porzucenie commita | jj abandon |
git reset --hard HEAD~1 |
| Przywrócenie pliku | jj restore <file> |
git checkout HEAD -- <file> |
Operacje zdalne
| Cel | jj | git |
|---|---|---|
| Fetch | jj git fetch |
git fetch |
| Push | jj git push --bookmark <name> |
git push |
| Pierwszy push | jj git push --bookmark <name> |
git push -u |
| Force push (automatyczny) | jj git push --bookmark <name> |
git push --force-with-lease |
| Push wszystkiego | jj git push --all |
git push --all |
| Usunięcie zdalnego | jj git push --delete <name> |
git push --delete |
Bookmarki
| Cel | jj | git |
|---|---|---|
| Utwórz | jj bookmark create <name> |
git branch <name> |
| Przesuń | jj bookmark set <name> -r <rev> |
git branch -f <name> |
| Usuń | jj bookmark delete <name> |
git branch -d <name> |
| Wyświetl listę | jj bookmark list |
git branch -a |
Bezpieczeństwo i selektory
| Cel | jj | git |
|---|---|---|
| Cofnij cokolwiek | jj undo |
git reflog + reset |
| Historia operacji | jj op log |
git reflog |
| „Stash” pracy | jj new @-, potem jj edit z powrotem |
git stash |
| Revset | Znaczenie |
|---|---|
@ |
Kopia robocza |
@- |
Rodzic kopii roboczej |
trunk() |
Główna gałąź na origin |
trunk()..@ |
Twoja lokalna praca, której jeszcze nie ma w trunku |
mine() |
Twoje własne commity |
conflicts() |
Wszystkie commity z konfliktami |
bookmarks() |
Wszystkie commity z bookmarkami |
description("fix") |
Commity ze słowem „fix” w opisie |
Kolejne kroki
Dział zatytułowany „Kolejne kroki”- Podstawy jj — change ID, kopia robocza i log operacji w szczegółach.
- Graf logu — nawigowanie po historii i jej filtrowanie w aplikacji Wyrm.
- Rozwiązywanie konfliktów — wizualne rozwiązywanie konfliktów we wbudowanym narzędziu.
- Podgląd push — sprawdź dokładnie, co trafi na zdalne repozytorium, zanim zrobisz push.