Вступление

Если ты здесь, ты наверняка знаешь, что такое 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).

Как разрешить:

  1. Слить изменения: jj new CHANGE-ID-1 CHANGE-ID-2, затем jj bookmark move BOOKMARK-NAME.

  2. Выбрать одно: jj bookmark move BOOKMARK -r CHANGE-ID.

  3. Сделать 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-репозиторий

Редактирование локального репозитория

Команда

Описание

jj или jj log

Показать важные изменения

jj status

Статус рабочего изменения, родитель, изменённые файлы

jj undo

Отменить последнюю команду

jj new

Создать новое изменение

jj describe -m "edit foo"

Задать описание

jj show

Показать описание изменения

jj bookmark list

Показать все закладки

jj bookmark track develop@origin

Подключиться к удаленной ветке

jj bookmark create feat/ui

Создать закладку

jj bookmark delete feat/ui

Удалить закладку

jj bookmark move feat/ui

Переместить закладку

jj bookmark move feat/ui --to r

Переместить закладк на указанное изменение

jj bookmark rename feat/ui feat/ux

Переименовать закладку

jj edit q

Отредактировать указанное изменение (перенсти @ на q)

jj restore --from q (paths..)

Восстановить файлы из другого изменения

jj backout

Создать обратное изменение

jj abandon q

Отказаться от изменения

jj diff (paths..)

Показать разницу между изменениями

jj squash

Объединить изменения

Более подробно читаем в документации


Примеры живой работы с 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, когда я пишу код, быстро сохраняю свои наработки и пишу дальше. В моей голове это "чек-поинты", к которым я в любой момент могу вернуться.

Затем, когда функционал реализован, я привожу историю в порядок:

  1. jj squash - объединяю все "черновые" коммиты в один.

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

Комментарии (25)


  1. Mayurifag
    18.08.2025 11:26

    Спасибо за статью и отдельное за понятный пример, зачем читателю эта штука. А то когда узнал впервые про jj и зашёл на сайт, сходу не понял, да и закрыл за ненадобностью кажущейся. Но тут дело скорее во мне, уже с гитом привык работать, в зубодробительных ситуациях переключаюсь на GUI какой-нибудь, опыта достаточно набил за годы.

    Мне ещё понравился их log, — чуть репрезентативнее сравнение можно прямо в репозитории проекта посмотреть, — в сравнении с тем, что стандартно в git.

    Ну и чтобы пользу комментарий какую-никакую нёс, вот что спрошу. Тем кто гитхуками пользуется, тяжело будет пересаживаться? Из коробки есть поддержка, не проверяли на своих задачах? Надеюсь бесшовно работают. Без гитхуков очень многим ненужно будет (да и с ними, хе-хе).


    1. Ads_2s Автор
      18.08.2025 11:26

      Поддержка нативных Git-хуков в Jujutsu находится в разработке.

      Сложность в том, что философия jj сильно отличается от Git. jj не использует классическую staging area, а также имеет другой подход к коммитам (рабочая копия - это всегда коммит). Из-за этого стандартные Git-хуки, такие как pre-commit и post-commit, которые завязаны на состояние HEAD и staging area, просто не имеют смысла в рабочем процессе jj.
      Разработчики jj планируют внедрить собственную систему native hooks в будущем, которая будет работать с концепциями jj (например, на уровне транзакций).

      Пока что многие обходятся через alias: делают commit-with-checks и push-with-checks, комбинируя pre-commit с jj - это рабочий временный вариант. Я лично использую просто pre-commit run --all-files.

      Также есть jj fix, который, по заверению пользователей, намного круче, но совсем другое. Тут я не пробовал, комментировать не буду.


  1. artptr86
    18.08.2025 11:26

    Для начала JJ работает поверх бекенда Git, а это уже опасно в плане инкапсуляции особенностей поведения JJ, поскольку другие сотрудники и утилиты ничего о JJ не знают.

    Без проблем - просто переключаюсь на старую версию, делаю hotfix.

    Изменение старого коммита перепишет вам историю в Git. Под капотом там скорее всего что-то вроде

    git checkout <commit>
    git reset --soft
    <ожидание коммита>
    git rebase --onto ...

    Даже если JJ позволяет вам забить на конфликты, они неизбежно возникнут у других сотрудников и на серверах контроля версий просто из-за изменения истории.

    jj git push -c @ не просто отправляет изменения, но создает отдельную новую ветку

    git push -u origin <название> сделает то же самое.

    и сразу предлагает ссылку на создание Merge Request, если ваша система это поддерживает

    Это фича сервера контроля версий, а не JJ.


    1. Ads_2s Автор
      18.08.2025 11:26

      Спасибо вам большое за ваш комментарий.

      Для начала JJ работает поверх бэкенда Git, а это уже опасно в плане инкапсуляции особенностей поведения JJ, поскольку другие сотрудники и утилиты ничего о JJ не знают.

      Да, вы правы, jj работает поверх git и сохраняет совместимость. Это как раз одна из его целей — не ломать экосистему. А в чем конкретно вы видите проблему?

      Изменение старого коммита перепишет вам историю в Git. Под капотом там, скорее всего, что-то вроде.

      Разница в том, что операции, которые в git требуют цепочки команд (resetcheckoutrebase и т.д.), в jj становятся атомарными и удобными. Плюс есть возможность откатить операцию, чего в git не хватает.

      Про конфликты — абсолютно согласен, их никуда не деть. В своем примере я больше делал акцент на том, что при перепрыгивании с ветки на ветку не возникает локальных конфликтов, которые очень тормозят работу и заставляют тебя пользоваться stash.

      И да, git push тоже может создать ветку, но в jj я могу выбрать «этап» разработки и получаю ссылку прямо в консоли.


      1. artptr86
        18.08.2025 11:26

        А в чем конкретно вы видите проблему?

        Да вот как раз в том, например, что вы можете на самом деле сделать ребейз общей ветки даже если не подозревали об этом. Для того я и привёл цепочку команд.

        Кстати термин «изменение» может вводить в заблуждение, потому что Git всё же хранит не изменения, а снапшоты файлов, в отличие от, например, Fossil.


  1. atd
    18.08.2025 11:26

    В jj вместо веток - закладки (bookmarks).

    Я открою вам страшнейшую тайну про git... Хотя нет, не сегодня )


    1. aamonster
      18.08.2025 11:26

      Да, эта тайна иногда подбешивает.


  1. AnthonyAxenov
    18.08.2025 11:26

    Свистоперделка для поиграться. Она даже не всё умеет, что умеет гит, но уже преподносится как что-то крутое и революционное, что уже можно использовать в работе.

    Гит стал стандартом спустя много-много лет и много-много боли (перешедшим с других vcs). За это время накопились тонны информации по его косточкам. Может быть, на нашем веку эта зумерская погремушка ещё успеет стать рабочим инструментом, но её ждёт трудный путь.


    1. akabrr
      18.08.2025 11:26

      зумерская погремушка

      сохраню себе :)


  1. mynameco
    18.08.2025 11:26

    когда нибудь, гит сделают с встроенной поддержкой бинарных файлов больших размеров. со встроенной поддержкой работы со срезами, как в свн и поддержкой папок. и жизнь наладится.


    1. PetyaUmniy
      18.08.2025 11:26

      Весьма маловероятно.
      Это крайне плохо ложится на концепцию локального репозитория. В котором все участники разработки будут хранить одновременно все версии всех блобов.
      А вот добавить поддержку сбоку, этакий lfs на максималках, наверное могли бы.


      1. domix32
        18.08.2025 11:26

        майкрософты активно занимаются именно вот этим вот расширением, ибо у них там монорепа. Они и с большими файлами возились и с shallow clone и прочие оптимизации, чтобы оно могло с монорепами такого уровня работать.


        1. PetyaUmniy
          18.08.2025 11:26

          Интересно. Как оно называется, не подскажите?
          Ну для честности монорепо не имеет прямого отношения к размеру репозитория. Если в нем хранится только код (без внешних модулей, пакетов и т.д.) то сложно добиться огромного его размера.
          С другой стороны если в репозитории хранить блобы, то его крайне просто сделать гиганским по размеру и без монорепов.


          1. kambala
            18.08.2025 11:26

            Scalar


  1. maxim_ge
    18.08.2025 11:26

    А потом замечаем: ошибка была в самом первом коммите. В feat: Add initial user authentication.
    Что делать?
    Не знаю, как у вас, у меня такое регулярно.

    Коммитить fix в ветку new-feature а потом pull request + squash.

    Вот тут так делали регулярно.

    О, а тут ещё начальник прибегает с криком: "hotfix срочно!"

    Ну в новую ветку переключаешься да и всё. Возможно, я чего-то не понял...


    1. Sitro23
      18.08.2025 11:26

      Ну в новую ветку переключаешься да и всё. Возможно, я чего-то не понял...

      Или git stash push, если вообще думать не хочется


  1. Tujh
    18.08.2025 11:26

    Могу немного поворчать, что судя по

    git checkout -b new-feature

    git освоен на уровне его состояния лет 10 назад. Git ведь тоже развивается в сторону удобства, появляются новые команды.

    Конкретно для этой из примера:

    git switch --create new-feature

    А там, глядишь, и проблем станет меньше.


    1. ahdenchik
      18.08.2025 11:26

      git switch

      THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.


      1. GamePad64
        18.08.2025 11:26

        В свежем релизе стабилизировали.


    1. Kaelns
      18.08.2025 11:26

      git switch - (да, просто тире) быстрый свап с предыдущей веткой. Суперполезно

      --create сокращённо -c

      switch вроде читал вообще только для веток сделана в отличие от checkout, надо будет весь функционал глянуть


  1. Dovgaluk
    18.08.2025 11:26

    Справедливости ради, git add можно не делать, если не добавлялись новые файлы, а сразу писать git commit -am "message"


  1. StjarnornasFred
    18.08.2025 11:26

    Это что-то типа MediaWiki для кода? Если да, то одобряю, мне нравится такой подход к истории правок и версиям.


    1. artptr86
      18.08.2025 11:26

      Думаю, что это не то же самое :)


  1. domix32
    18.08.2025 11:26

    Попробовал его однажды, попереключался между ветками, а потом посмотрел на историю через gitk и офигел, как половина экрана засрано тэгами этого jj. Демо было неплохим, но я видимо как-то не так работаю, ибо в такой проблемный флоу пока ни разу не попадал.


  1. pavelmvl
    18.08.2025 11:26

    Не понял, зачем нужно переписывать историю комитов каждый раз когда хочется исправить ошибку. Может вы используете git как-то не так? Из плюсов jj вижу только более красивый git log, но возможностей git log мне хватает.