Перейти до вмісту

Git → Jujutsu, крок за кроком

Якщо ти добре знаєш Git, jj в одних місцях здаватиметься знайомим, а в інших — перевернутим догори дриґом. Цей посібник проводить тебе шляхом по одному робочому процесу за раз і завжди показує що ти робив би в Git поруч із тим, що робиш у jj, а там, де це корисно, — ще й відповідну дію у Wyrm.

Не треба заздалегідь завчати нові команди. Почни зі зміни мислення, а потім проходь етапи по черзі.

Головна зміна мислення

Section titled “Головна зміна мислення”

Кілька ідей пояснюють майже кожну відмінність, на яку ти натрапиш:

  • Staging area немає. Кожна зміна файлу одразу стає частиною поточного коміта.
  • Робоча копія — це і є коміт, позначений у журналі як @. Кожна команда jj автоматично робить знімок твоїх файлів у нього.
  • Коміти дешеві й одноразові. Ти не робиш squash, щоб «прибрати перед push»; ти робиш squash, коли хочеш, а jj сам подбає про каскадні зміни.
  • Переписування історії — звичайний робочий процес, а не небезпечний аварійний вихід.
  • Анонімні гілки — це нормально. Страшний «detached HEAD» із Git — просто буденність у jj.
  • Bookmarks — це транспортні етикетки, а не робоче місце. Ти не працюєш «на bookmark» — ти працюєш із комітами, а потім чіпляєш мітку на один із них, коли настає час push.

У Wyrm верхній рядок графа журналу — це @. Коли ти редагуєш файли, цей рядок оновлюється наживо — жодної окремої панелі «незакомічених змін», якою треба керувати.

Крок 1 — Початок роботи

Section titled “Крок 1 — Початок роботи”

У Git ти створюєш гілку, редагуєш, додаєш до staging, а потім комітиш. У jj ти просто починаєш редагувати.

У Git ти…

Terminal window
# 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"

У jj ти…

Terminal window
# 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

Твоя робота в безпеці з моменту, коли ти зберіг файл. Гілка не потрібна аж до push, і «staging» немає — усе живе в @.

Крок 2 — Повернення до старої роботи

Section titled “Крок 2 — Повернення до старої роботи”

У тебе є старий коміт із хорошими змінами, але develop пішов уперед на 20 комітів. Як наздогнати?

Секрет: порядок не має значення. Спершу rebase чи спершу правки — jj підходить будь-який варіант, а конфлікти тебе не заблокують.

Варіант A — спершу rebase, потім правки (найпоширеніший):

Terminal window
# 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.

Варіант B — спершу правки, rebase згодом:

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

У Git це «checkout → rebase → вирішити конфлікти → continue», і конфлікт заморожує тебе посеред rebase. У jj ти можеш зробити rebase, побачити конфлікт, піти зайнятися чимось іншим і повернутися до нього пізніше — конфлікт це просто стан, у якому перебуває коміт.

Крок 3 — Незмінні коміти

Section titled “Крок 3 — Незмінні коміти”

Двічі клацнув старий коміт і отримав «Revision is immutable»? У Git можна переключитися куди завгодно, тож здається, ніби jj стає тобі на заваді. Це не так.

jj захищає коміти, які вже публічні — тобто поширені для всіх:

Незмінні (◆) Змінні (●)
Коміти в main@origin Твоя робоча копія @
Коміти в будь-якому *@origin Лише локальні коміти
Коміти з тегами Усе, що ти ще не запушив

Чому це добре: у Git можна зробити git push --force поверх main або виправити спільний коміт і зламати історію, від якої залежать інші, — ніщо тебе не зупинить. jj каже: «це вже публічне; не можна переписувати те, на що покладається команда». Це прибирає цілу категорію моментів «ой, ні».

Що робити натомість — не редагуй незмінний коміт, а збудуй новий поверх нього:

Terminal window
jj new immutable_change_id

Твій новий @ містить усі файли з того коміта; ти вільно редагуєш, а оригінал лишається недоторканим.

Змінний коміт (●) Незмінний коміт (◆)
jj edit change_id jj new change_id
@ переходить на цей коміт Створює новий дочірній коміт
Правки змінюють його на місці @ — це новий дочірній коміт

Коротка довідка: new проти edit проти describe проти commit

Section titled “Коротка довідка: new проти edit проти describe проти commit”
Команда Що робить Найближчий аналог у Git Коли використовувати
jj new Новий порожній дочірній коміт; @ переходить туди git checkout -b «З цим готово, переходжу до наступного».
jj edit Переносить @ на наявний змінний коміт git rebase -i → edit Повернутися й виправити попередній коміт.
jj describe Задає/змінює повідомлення @ git commit --amend -m Назвати те, над чим працюєш.
jj commit describe + new за один раз git commit -m Завершити цю роботу і почати наступну.

Крок 4 — Часткові коміти й прибирання

Section titled “Крок 4 — Часткові коміти й прибирання”

Ти змінив 10 файлів. Лише 7 із них належать до цього коміта; 3 — тестове сміття. Деякі файли навіть змішують справжні зміни з одноразовими рядками.

Git працює за принципом opt-in: ти додаєш до staging те, що хочеш, і відкидаєш решту. jj працює за принципом opt-out: усе вже в @, тож ти прибираєш те, що не потрібно. Мета та сама, напрямок протилежний.

Спосіб 1 — jj restore (відкинути файли цілком):

Terminal window
# 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 — це відповідник «Discard Changes»: він повертає файл до стану в батьківському коміті.

Спосіб 2 — jj split (залишити частину, решту відокремити):

Terminal window
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

Змішані файли (на рівні hunk) — коли в одному файлі 10 справжніх рядків і 5 тестових хаків:

Terminal window
jj split -i

Ця команда відкриває інтерактивний diff усіх змінених hunk; ти вибираєш, які з них потраплять у перший коміт, а які залишаться в другому. Це git add -p у світі jj.

Тестове сміття розкидане по кількох комітах? Не треба робити split для кожного коміта окремо:

Terminal window
# 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 window
# 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 переглядає кожен hunk зі скасуванням у @, знаходить коміт-предок, який додав ці рядки, і вливає виправлення в той коміт — без інтерактивного rebase, однією командою. Докладніше — у Кроці 8.

Крок 5 — Squash, rebase і конфлікти

Section titled “Крок 5 — Squash, rebase і конфлікти”

У Git ти…

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

У jj ти…

Terminal window
# 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

Нащадки автоматично перебазовуються після кожного squash. Без редактора, без --continue, без церемоній.

У Git ти…

Terminal window
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.

У jj ти…

Terminal window
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.

Які саме коміти переміщуються, залежить від прапорця:

Прапорець Що переміщує Аналог у Git
-r (revision) Лише цей один коміт; дочірні лишаються на місці (прямого відповідника немає)
-s (source) Цей коміт і всіх його нащадків git rebase --onto
-b (branch) Усе, починаючи з місця, де гілка відійшла від цілі git rebase develop

Конфлікти — найбільша відмінність

Section titled “Конфлікти — найбільша відмінність”

Вирішити конфлікт — це просто відредагувати файл:

Terminal window
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.

Оскільки маркери описують намір (базу + кожен diff), а не дві готові версії, міркувати про конфлікти зазвичай простіше. Робочий процес у Wyrm описано в розділі Вирішення конфліктів.

Збираємо кроки 1–5 докупи

Section titled “Збираємо кроки 1–5 докупи”

Повна фіча від початку до кінця:

  1. Почни працювати — jj describe -m "feat: new navigation UI"
  2. Фіксуй проміжні етапи — jj commit -m "wip: nav routing done"
  3. Злий усе в один коміт — jj squash --from "first::last" --into first
  4. Прибери тестове сміття — jj restore test-setup.ts (або jj split -i)
  5. Зроби rebase на свіжий develop — jj git fetch && jj rebase -r feature -d develop
  6. Познач і запуш — jj bookmark create feature/x && jj git push --bookmark feature/x
  7. Відкрий PR, отримай ревʼю
  8. Опрацюй зауваження — jj edit feature_change → внеси зміни → jj git push --bookmark feature/x

Жодних fixup-комітів, жодного повторного squash. Відредагуй коміт, запуш — jj сам безпечно зробить force-push.

Bookmark у jj — не гілка Git. Гілка Git іде за HEAD: робиш коміт — і вона рухається з тобою; ти завжди «на» якійсь гілці. Bookmark у jj — це стікер на конкретному коміті. Він не рухається, коли ти створюєш нові коміти, і ти ніколи не перебуваєш «на» ньому. Ти завжди на @.

Git: напиши адресу на коробці до того, як пакувати. jj: спакуй коробку, а потім наліпи транспортну етикетку, коли будеш готовий відправляти.

Тож спершу працюєш, а мітку ставиш потім. Ось три типові ситуації, коли старий bookmark уже існує:

A — стара робота застаріла (нові коміти її замінюють):

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

B — зберегти стару роботу й будувати поверх неї:

Terminal window
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 — обʼєднати стару й нову роботу (різні файли, потрібне і те, і те):

Terminal window
jj squash --from old_work_change_id --into commit1
jj bookmark set feature/nav-redesign -r commit3
jj git push --bookmark feature/nav-redesign
Поведінка Гілка Git Bookmark jj
Рухається під час коміта? Так — автоматично йде за HEAD Ні — лишається на місці; пересуваєш вручну
Потрібен для роботи? Так — ти завжди «на» якійсь Ні — можна працювати взагалі без них
Потрібен для push? Так Так — для push потрібна назва
Є «поточний»? Так — на нього вказує HEAD Такого поняття немає; поточний завжди @
Іде за rebase? Іноді Так — відстежує change ID крізь переписування
Страх detached HEAD? Так Жодного — це звичайна справа
Force push Вручну, страшно Автоматично, безпечно (with-lease)

Оскільки bookmarks відстежують change ID (а не SHA коміта), вони переживають переписування: після rebase ти просто робиш push — жодного ручного force-push, щоб оновити віддалений репозиторій.

Push — одне дієслово замість чотирьох

Section titled “Push — одне дієслово замість чотирьох”

У Git є кілька форм push (-u першого разу, звичайний push, --force-with-lease, --delete). jj зводить їх усі до однієї команди:

Terminal window
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

Прапорця --force немає. jj завжди поводиться як --force-with-lease: якщо хтось запушив у цей bookmark після твого останнього fetch, jj відмовить і попросить спершу зробити fetch. Випадково затерти роботу колеги просто неможливо.

Fetch — жодних несподіванок

Section titled “Fetch — жодних несподіванок”
Terminal window
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.

jj pull немає. Fetch і rebase завжди розділені, тож несподіваний merge-коміт ніколи не зʼявиться.

Опрацьовуй зауваження, редагуючи коміт на місці й знову роблячи push, — жодних fixup-комітів:

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

Крок 8 — Потужні прийоми

Section titled “Крок 8 — Потужні прийоми”

Кілька можливостей jj, які не мають чистого відповідника в Git.

jj absorb — автоматично спрямовує виправлення у правильні коміти

Section titled “jj absorb — автоматично спрямовує виправлення у правильні коміти”

Рецензент пише: «рядок 42 у коміті 2 неправильний, а в рядку 88 коміта 4 одруківка». У Git це означає git commit --fixup для кожного місця плюс git rebase -i --autosquash.

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

Для кожного зміненого рядка jj знаходить найближчого предка, який його торкався, переносить виправлення туди й перебазовує нащадків. Обидва виправлення потрапляють у правильні коміти, а @ знову порожній.

Без stash — кожен стан уже є комітом

Section titled “Без stash — кожен стан уже є комітом”

Бос каже: «Кидай усе, терміново виправ це». У Git це git stash / гілка / виправлення / git stash pop — і молитва.

Terminal window
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

Stash немає, бо нема чого ховати — твоя робота ніколи не була незакоміченою.

Стеки PR — автоматичне перебудування

Section titled “Стеки PR — автоматичне перебудування”

Випускай чотири невеликі PR, зручні для ревʼю, які будуються один на одному, замість одного гігантського:

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

У Git виправлення бага в базовому PR означає rebase і force-push кожного PR над ним. У jj ти редагуєш базовий коміт, усі нащадки автоматично перебазовуються, і всі PR оновлюються разом:

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

Життєвий цикл роботи

Мета jj git
Статус jj status git status
Журнал jj log git log --graph
Diff поточної роботи jj diff git diff HEAD
Почати новий коміт jj new git checkout -b
Задати повідомлення коміта jj describe -m "…" git commit --amend -m
Закомітити й рухатися далі jj commit -m "…" git commit -m

Редагування комітів

Мета jj git
Редагувати старий змінний коміт jj edit <rev> git rebase -i (edit)
Дочірній коміт від будь-якого коміта jj new <rev> git checkout -b
Злити в батьківський jj squash fixup + autosquash
Інтерактивне злиття jj squash -i git add -p + amend
Розділити коміт jj split git rebase -i (split)
Автоматично розподілити виправлення jj absorb (немає)
Відкинути коміт jj abandon git reset --hard HEAD~1
Відновити файл jj restore <file> git checkout HEAD -- <file>

Операції з віддаленими репозиторіями

Мета jj git
Fetch jj git fetch git fetch
Push jj git push --bookmark <name> git push
Перший push jj git push --bookmark <name> git push -u
Force push (автоматично) jj git push --bookmark <name> git push --force-with-lease
Push усього jj git push --all git push --all
Видалити на віддаленому jj git push --delete <name> git push --delete

Bookmarks

Мета jj git
Створити jj bookmark create <name> git branch <name>
Пересунути jj bookmark set <name> -r <rev> git branch -f <name>
Видалити jj bookmark delete <name> git branch -d <name>
Список jj bookmark list git branch -a

Безпека й селектори

Мета jj git
Скасувати будь-що jj undo git reflog + reset
Історія операцій jj op log git reflog
«Stash» роботи jj new @-, потім jj edit назад git stash
Revset Значення
@ Робоча копія
@- Батьківський коміт робочої копії
trunk() Головна гілка в origin
trunk()..@ Твоя локальна робота, якої ще немає в trunk
mine() Твої власні коміти
conflicts() Усі конфліктні коміти
bookmarks() Усі коміти з bookmarks
description("fix") Коміти зі словом «fix» у повідомленні