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 ти…
# 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"У 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-uiТвоя робота в безпеці з моменту, коли ти зберіг файл. Гілка не потрібна аж до push, і «staging» немає — усе живе в @.
Крок 2 — Повернення до старої роботи
Section titled “Крок 2 — Повернення до старої роботи”У тебе є старий коміт із хорошими змінами, але develop пішов уперед на 20 комітів. Як наздогнати?
Секрет: порядок не має значення. Спершу rebase чи спершу правки — jj підходить будь-який варіант, а конфлікти тебе не заблокують.
Варіант A — спершу rebase, потім правки (найпоширеніший):
# 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 згодом:
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 каже: «це вже публічне; не можна переписувати те, на що покладається команда». Це прибирає цілу категорію моментів «ой, ні».
Що робити натомість — не редагуй незмінний коміт, а збудуй новий поверх нього:
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 (відкинути файли цілком):
# 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 (залишити частину, решту відокремити):
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 commitЗмішані файли (на рівні hunk) — коли в одному файлі 10 справжніх рядків і 5 тестових хаків:
jj split -iЦя команда відкриває інтерактивний diff усіх змінених hunk; ти вибираєш, які з них потраплять у перший коміт, а які залишаться в другому. Це git add -p у світі jj.
Тестове сміття розкидане по кількох комітах? Не треба робити split для кожного коміта окремо:
# 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 переглядає кожен hunk зі скасуванням у @, знаходить коміт-предок, який додав ці рядки, і вливає виправлення в той коміт — без інтерактивного rebase, однією командою. Докладніше — у Кроці 8.
Крок 5 — Squash, rebase і конфлікти
Section titled “Крок 5 — Squash, rebase і конфлікти”Squash
Section titled “Squash”У Git ти…
git rebase -i HEAD~5# Editor opens; change "pick" → "squash" on 4 lines,# write the combined message, save.У 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 commit1Нащадки автоматично перебазовуються після кожного squash. Без редактора, без --continue, без церемоній.
Rebase
Section titled “Rebase”У 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.У 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.Які саме коміти переміщуються, залежить від прапорця:
| Прапорець | Що переміщує | Аналог у Git |
|---|---|---|
-r (revision) |
Лише цей один коміт; дочірні лишаються на місці | (прямого відповідника немає) |
-s (source) |
Цей коміт і всіх його нащадків | git rebase --onto |
-b (branch) |
Усе, починаючи з місця, де гілка відійшла від цілі | git rebase develop |
Конфлікти — найбільша відмінність
Section titled “Конфлікти — найбільша відмінність”Вирішити конфлікт — це просто відредагувати файл:
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 докупи”Повна фіча від початку до кінця:
- Почни працювати —
jj describe -m "feat: new navigation UI" - Фіксуй проміжні етапи —
jj commit -m "wip: nav routing done" - Злий усе в один коміт —
jj squash --from "first::last" --into first - Прибери тестове сміття —
jj restore test-setup.ts(абоjj split -i) - Зроби rebase на свіжий develop —
jj git fetch && jj rebase -r feature -d develop - Познач і запуш —
jj bookmark create feature/x && jj git push --bookmark feature/x - Відкрий PR, отримай ревʼю
- Опрацюй зауваження —
jj edit feature_change→ внеси зміни →jj git push --bookmark feature/x
Жодних fixup-комітів, жодного повторного squash. Відредагуй коміт, запуш — jj сам безпечно зробить force-push.
Крок 6 — Bookmarks
Section titled “Крок 6 — Bookmarks”Bookmark у jj — не гілка Git. Гілка Git іде за HEAD: робиш коміт — і вона рухається з тобою; ти завжди «на» якійсь гілці. Bookmark у jj — це стікер на конкретному коміті. Він не рухається, коли ти створюєш нові коміти, і ти ніколи не перебуваєш «на» ньому. Ти завжди на @.
Git: напиши адресу на коробці до того, як пакувати. jj: спакуй коробку, а потім наліпи транспортну етикетку, коли будеш готовий відправляти.
Тож спершу працюєш, а мітку ставиш потім. Ось три типові ситуації, коли старий bookmark уже існує:
A — стара робота застаріла (нові коміти її замінюють):
jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign # safe force-pushB — зберегти стару роботу й будувати поверх неї:
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 — обʼєднати стару й нову роботу (різні файли, потрібне і те, і те):
jj squash --from old_work_change_id --into commit1jj bookmark set feature/nav-redesign -r commit3jj 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, щоб оновити віддалений репозиторій.
Крок 7 — Push, fetch і цикл PR
Section titled “Крок 7 — Push, fetch і цикл PR”Push — одне дієслово замість чотирьох
Section titled “Push — одне дієслово замість чотирьох”У Git є кілька форм push (-u першого разу, звичайний push, --force-with-lease, --delete). jj зводить їх усі до однієї команди:
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 bookmarkПрапорця --force немає. jj завжди поводиться як --force-with-lease: якщо хтось запушив у цей bookmark після твого останнього fetch, jj відмовить і попросить спершу зробити fetch. Випадково затерти роботу колеги просто неможливо.
Fetch — жодних несподіванок
Section titled “Fetch — жодних несподіванок”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-коміт ніколи не зʼявиться.
Цикл ревʼю
Section titled “Цикл ревʼю”Опрацьовуй зауваження, редагуючи коміт на місці й знову роблячи push, — жодних fixup-комітів:
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.
# 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 — і молитва.
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 offStash немає, бо нема чого ховати — твоя робота ніколи не була незакоміченою.
Стеки 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 оновлюються разом:
jj edit feature/auth-refactor # fix the base# ...descendants auto-rebase...jj git push --all # all four PRs update togetherШпаргалка jj ↔ git
Section titled “Шпаргалка jj ↔ git”Життєвий цикл роботи
| Мета | 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» у повідомленні |
Наступні кроки
Section titled “Наступні кроки”- Основи jj — change ID, робоча копія та журнал операцій докладно.
- Граф журналу — навігація історією та її фільтрація у Wyrm.
- Вирішення конфліктів — візуальне вирішення конфліктів у вбудованому інструменті.
- Попередній перегляд push — переглянь, що саме потрапить на віддалений репозиторій, перш ніж робити push.