Вступление
Если ты здесь, ты наверняка знаешь, что такое git. И да, не спорю - это офигенная штука. Деды знали, что писали.
Но я долгое время работал над небольшими проектами и был там единственным разработчиком. Когда перешёл в большую команду, пришлось глубже вникнуть в git.
И тут началось: я стал тратить кучу ресурсов на постоянные вопросы:
Нужна ли отдельная ветка или нет?
Merge или Rebase?
Какой
revert
использовать?В каком статусе сейчас файл?
Где вообще находится header?
А ещё каждый коммитил как хотел. В итоге история проекта - это каша: понять, что, когда и зачем сделал человек, просто невозможно.
Я начал гуглить best practices, читать про git flow, пытался навести порядок. Но всё равно слишком много времени уходило не на код, а на борьбу с системой контроля версий.
И вот я наткнулся на Jujutsu (jj). И хочу рассказать тебе, чем он меня зацепил.
О сути jj
Основная идея jj - "У нас нет веток. У нас есть изменения".
Погоди, сейчас поясню.
Сразу скажу: всё это полностью совместимо с git. Так что можно просто взять и начать использовать jj прямо сейчас.
Пример боли
Предположим, мы работаем по классике: feature-branch.
Надо её как-то назвать. Начинаю в ней работать.
После каждого осмысленного шага - коммит. Ещё один. И ещё.
# Работаем над фичей 'new-feature'
git checkout -b new-feature
# Первый коммит
# ... код ...
git add .
git commit -m "feat: Add initial user authentication"
# Второй коммит
# ... код ...
git add .
git commit -m "refactor: Improve auth validation"
# Третий коммит
# ... код ...
git add .
git commit -m "fix: Fix minor typo in error message"
А потом замечаем: ошибка была в самом первом коммите. В feat: Add initial user authentication
.
Что делать?
Создавать новый? Делать rebase -i
?
Переключаться, править, переносить руками.
# Как исправить ошибку в первом коммите, не затрагивая последующие?
# Вариант 1: rebase -i
git rebase -i HEAD~3 # Ищем коммит, меняем 'pick' на 'edit', правим, continue
# Вариант 2: revert первого коммита, потом новый коммит с исправлением
git revert <hash_первого_коммита> # Создает новый коммит, отменяющий изменения
# ... потом fix ...
git commit -m "fix: Actually fix initial user auth"
# Вариант 3: reset до первого коммита, теряя все последующие
git reset --soft <hash_первого_коммита>
# ... потом все изменения из второго и третьего коммита в staging ...
# ... а потом перекоммичивать все заново ...
О, а тут ещё начальник прибегает с криком: "hotfix срочно!"
А я не могу у меня лапки конфликт.
Не знаю, как у вас, у меня такое регулярно.
Вследствие чего ты тратишь кучу сил на то, чтобы угомонить git. Какая версионность? Какие правильные коммиты? Какой качество код? Вы вообще о чем?
А теперь jj
Пишешь код, коммит за коммитом.
# Никаких веток
# Первый коммит
# ... код ...
# никаких `add`
jj commit -m "feat: Add initial user authentication"
# Второй коммит
# ... код ...
jj commit -m "refactor: Improve auth validation"
# Третий коммит
# ... код ...
jj commit -m "fix: Fix minor typo in error message"
Ой, ошибка в первом? Просто переходишь к нему, правишь, возвращаешься - готово.
jj edit <rev> # перходим на нужный commit
# ... правим ...
jj edit <rev> # возвращаемся к работе обратно
История переписалась, текущие изменения не потерялись.
Всё.
Я реально сижу и думаю: "А точно всё? Я что-то, слишком мало команд ввёл?"
Ты не думаешь:
"А какую merge-стратегию выбрать?.."
Ты думаешь:
"Как сделать код лучше?"
Конфликт? Hotfix? - без паники
Окей, допустим, случился конфликт.
Начальник снова кричит: "Фикс срочно!"
Без проблем - просто переключаюсь на старую версию, делаю hotfix.
jj edit <rev> # перходим на нужный commit
# ... hotfix ...
jj edit <rev> # возвращаемся к работе обратно
Конфликты? Потом разберёмся. jj позволяет не тормозить разработку.
Никаких stash
, rebase
, checkout -b
, "а где HEAD?".
Где же ветки?
В jj вместо веток - закладки (bookmarks).
Представь, что ты ведёшь дневник.
Вот ты пишешь, тебе нравится, как получилось - ставишь закладку prod
.
Пишешь дальше. Получилось неплохо, но не уверен - ставишь dev
.
Думаю, суть вы уловили.
# Создаем закладку на текущем коммите
jj bookmark prod
# Работаем дальше, создаем новые коммиты
jj commit -m "feat: More features"
# Создаем еще одну закладку
jj bookmark dev
# Смотрим закладки
jj bookmark list
# Переносим закладку на последний commit
jj bookmark move prod --to=@-
Основные понятия
Модель репозитория
Репозиторий Jujutsu - это направленный ацикличный граф (DAG), в котором каждый узел - это изменение(change), содержащее:
Снимок файловой системы в директории репозитория.
Конфликты файлов (локальны, не блокируют работу, в отличие от Git).
Один или несколько родительских изменений (корневое не имеет родителей).
Описание изменения (commit message).
Дополнительно:
"Изменение" в JJ - это аналог "коммита" в Git (но с более стабильным ID).
Одно из изменений - рабочее (
@
), аналогичноHEAD
в Git.Закладки (bookmarks) - уникальные строки, ссылающиеся на изменения, для Git это
branch
.Поддержка удалённого репозитория - закладки синхронизируются как
BOOKMARK@REMOTE
.
Основные правила
При перемещении
@
рабочая директория обновляется.Если удалить изменение, на которое указывает
@
, создаётся новое пустое изменение.Изменения без файлов, описания и ссылок исчезают.
Изменение - это diff. Перемещение может вызвать конфликты.
Почти все команды действуют на
@
по умолчанию, но могут принимать--revision
.
Конфликты файлов
Чтобы разрешить конфликт, достаточно отредактировать файл и убрать маркеры (
<<<<<<<
,=======
,>>>>>>>
).Для бинарных файлов - замените файл нужной версией.
Используйте
jj restore
, если нужно откатить изменения.
Работа с удалённым репозиторием
jj git fetch
Получает изменения из удалённого репозитория.
Несовместимые закладки создают конфликт закладок (аналог merge conflict).
Как разрешить:
Слить изменения:
jj new CHANGE-ID-1 CHANGE-ID-2
, затемjj bookmark move BOOKMARK-NAME
.Выбрать одно:
jj bookmark move BOOKMARK -r CHANGE-ID
.Сделать rebase:
jj rebase -b CHANGE-ID-2 -d CHANGE-ID-1
, затемjj bookmark move
.
jj git push
Отправляет изменения в удалённый репозиторий.
Изменённые изменения создаются заново (аналог
--force
).Основная ветка защищена - изменения нужно явно пушить с
--ignore-immutable
.jj git push -c @
создает новую временную закладку (git автоматически предложит создать MR)
Про закладки:
Локальные закладки копируются в удалённый репозиторий.
Если закладка не синхронизирована -
push
выдаёт ошибку, нужно сначала сделатьjj git fetch
.
jj bookmark track
Связывает локальную закладку с удалённой веткой.
Отслеживать изменения из удалённого репозитория при
jj git fetch
, упрощатьpush
и автоматически разрешать конфликты закладок.Синтаксис:
jj bookmark track <локальная_закладка> <удалённая_ветка@удалённый_репозиторий>
jj bookmark track <имя_ветки>
(если локальная и удалённая ветки совпадают)
Пример:
jj bookmark track develop develop@origin
Просмотр:
jj bookmark list --tracked
(показать только отслеживаемые)
Команды настройки
jj config set --user user.name МОЁ_ИМЯ
jj config set --user user.email МОЙ_EMAIL
jj config set --user ui.editor МОЙ_РЕДАКТОР
jj config edit --user # Открыть конфиг
Вместо
--user
можно использовать--repo
для конфигурации внутри конкретного репозитория.
Команды репозитория
jj git init # Инициализация репозитория
jj git clone URL [DEST] # Клонирование репозитория
jj git init --colocate # Добавление JJ в существующий git-репозиторий
Редактирование локального репозитория
Команда | Описание |
---|---|
| Показать важные изменения |
| Статус рабочего изменения, родитель, изменённые файлы |
| Отменить последнюю команду |
| Создать новое изменение |
| Задать описание |
| Показать описание изменения |
| Показать все закладки |
| Подключиться к удаленной ветке |
| Создать закладку |
| Удалить закладку |
| Переместить закладку |
| Переместить закладк на указанное изменение |
| Переименовать закладку |
| Отредактировать указанное изменение (перенсти @ на q) |
| Восстановить файлы из другого изменения |
| Создать обратное изменение |
| Отказаться от изменения |
| Показать разницу между изменениями |
| Объединить изменения |
Более подробно читаем в документации
Примеры живой работы с jj
Давайте посмотрим, как jj
справляется с типичными задачами, используя только свои команды и концепции.
Тут я решил показать возможности revsets
jj log -r "@ | bookmarks() & author('Ads')"
:

jj log
показывает историю изменений в репозитории. Каждый блок соответствует одному изменению (коммиту) и содержит несколько ключевых элементов:
Рабочая копия - на нее указывает
@
○
(локальное изменение) - это то, что вы можете свободно менять.◆
(неизменяемое) - это коммит, который вы уже отправили на удалённый сервер и трогать его не стоит (хотяjj
позволяет и это с флагом-ignore-immutable
).ID изменения - уникальный короткий идентификатор, например
szqumyoy
,szq
- alias для данного изменения.Автор и email - кто сделал изменение.
Дата и время - когда было сделано изменение.
Закладки (
bookmarks
) и/или ветки - метки, указывающие на изменение, напримерmaster
илиmaster@origin
.Локальная закладка - те, что ещё не синхронизирована с удалённым репозиторием имеют символ
*
ID Git-коммита - хэш коммита в Git (для совместимости). Например,
59d9790f
.Сообщение коммита - описание сделанных изменений.
~ (elided revisions)
- Это пропущенные изменения.jj
иногда скрывает их в данном случае из-за "фильтров".
jj status
(alias st
):

Здесь мы видим какие изменения сейчас есть в нашей
рабочей копии
.Буквы
A
,M
означают тип изменения файла.Тут же мы видим ссылку на:
@
- это изменение и@-
- родителя.
Из скриншота видно, что тут явно 2 вида изменений. Я хочу чтобы было красиво.
jj split
:

Открывается diff-editor
- это мощный инструмент, который позволяет работать с изменениями, а не с файлами. Он помогает поддерживать чистую и понятную историю, не прибегая к сложным манипуляциям.

Выбираем нужные нам изменения и жмем c
(Это тут такое управление, если что можно все делать мышкой)

И попадаем в интерфейс jj commit
, для ввода description

jj split
автоматически разбил изменения на 2 коммита и выстроил их в линейку, посмотрим на jj
.

Перенесем закладку на новые изменения. Переносить на @ нельзя там пусто, так что --to=@-

Workflow: "Черновики" и их "уборка"
Лично мне очень нравится workflow, когда я пишу код, быстро сохраняю свои наработки и пишу дальше. В моей голове это "чек-поинты", к которым я в любой момент могу вернуться.

Затем, когда функционал реализован, я привожу историю в порядок:
jj squash
- объединяю все "черновые" коммиты в один.jj split
- разделяю одно большое изменение на несколько логичных. В итоге история становится чистой и понятной.

Можно заметить что id vxzxpzxm
, ktvwvoqs
не поменялись, а их git-hash изменился.
Push и автоматический MR
jj
умеет работать с удалёнными репозиториями очень элегантно. Например, jj git push -c @
не просто отправляет изменения, но создает отдельную новую ветку и сразу предлагает ссылку на создание Merge Request, если ваша система это поддерживает. Но можно и просто:

И вновь jj
удивляет и сразу предлагает ссылку на MR
Финал
Jujutsu
- это не замена Git, а его улучшенная оболочка. Он решает многие проблемы, которые так раздражают в классическом Git:
Грязная история и мучительный
rebase -i
.Сложности с
git add
иgit stash
.Страх "сломать" репозиторий, ведь у
jj
естьjj undo
.
Если вы хотите освободить свой мозг для написания кода, а не для борьбы с системой контроля версий, попробуйте jj
. Просто попробуйте, если что, вы всегда сможете вернуться на Git.