Przejdź do głównej zawartości

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.

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ć.

W gicie tworzysz gałąź, edytujesz, dodajesz do stage’a i robisz commit. W jj po prostu zaczynasz edytować.

W gicie…

Okno terminala
# 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"

W jj…

Okno terminala
# 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

Twoja 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 @.

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):

Okno terminala
# 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:

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

W 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.

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:

Okno terminala
jj new immutable_change_id

Twoje 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
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ą.

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):

Okno terminala
# 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ę):

Okno terminala
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

Pliki mieszane (na poziomie hunków) — gdy jeden plik ma 10 prawdziwych linii i 5 testowych hacków:

Okno terminala
jj split -i

To 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:

Okno terminala
# Option A — squash everything, then split once:
jj squash --from "commit1::commit5" --into commit1
jj split -i -r that_commit
jj abandon junk_commit
Okno terminala
# 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 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.

W gicie…

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

W jj…

Okno terminala
# 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

Potomkowie są automatycznie rebase’owani po każdym squashu. Bez edytora, bez --continue, bez ceremonii.

W gicie…

Okno terminala
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.

W jj…

Okno terminala
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.

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

Rozwiązanie konfliktu to po prostu edycja pliku:

Okno terminala
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.

Kompletna funkcja, od początku do końca:

  1. Zacznij pracę — jj describe -m "feat: new navigation UI"
  2. Zapisuj kamienie milowe na bieżąco — jj commit -m "wip: nav routing done"
  3. Posprzątaj do jednego commita — jj squash --from "first::last" --into first
  4. Wyrzuć testowe śmieci — jj restore test-setup.ts (albo jj split -i)
  5. Zrób rebase na najnowszy develop — jj git fetch && jj rebase -r feature -d develop
  6. Nadaj etykietę i wypchnij — jj bookmark create feature/x && jj git push --bookmark feature/x
  7. Otwórz PR i poczekaj na review
  8. 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.

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ą):

Okno terminala
jj bookmark set feature/nav-redesign -r commit3
jj git push --bookmark feature/nav-redesign # safe force-push

B — zachowaj starą pracę i buduj na niej:

Okno terminala
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 — połącz starą i nową pracę (różne pliki, chcesz obie):

Okno terminala
jj squash --from old_work_change_id --into commit1
jj bookmark set feature/nav-redesign -r commit3
jj 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.

Git ma kilka wariantów pusha (-u za pierwszym razem, zwykły push, --force-with-lease, --delete). jj sprowadza je wszystkie do jednego polecenia:

Okno terminala
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

Nie 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.

Okno terminala
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.

Poprawki z review wprowadzasz, edytując commit w miejscu i ponownie robiąc push — bez commitów typu fixup:

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

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.

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

Dla 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.

Szef mówi: „rzuć wszystko, pilny hotfix”. W gicie to git stash / gałąź / poprawka / git stash pop i modlitwa.

Okno terminala
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

Nie ma stasha, bo nie ma czego chować — Twoja praca nigdy nie była niezacommitowana.

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
◆ develop

W 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:

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

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
  • 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.