Megamerges в Jujutsu: как работать с 5 ветками сразу через один octopus merge
Один octopus merge со всеми вашими ветками сразу, absorb вместо squash и автоматический restack на trunk. Собрали перевод гайда, который объясняет, почему megamerge-воркфлоу в Jujutsu экономит часы на переключении между задачами.
Если вы каждый день переключаетесь между 3–5 ветками в Git — фичи, багфиксы, PR на ревью, чужие ветки, от которых зависит ваш код — и каждое переключение ломает темп разработки, у Jujutsu есть рабочий процесс, который убирает эту боль. Он называется megamerge. Айзек Корбри описывает его так: один большой octopus merge (merge-коммит с тремя и более родителями), в который вы включаете все рабочие ветки сразу. Вы всегда работаете поверх суммы всех ваших изменений.
Jujutsu (сокращённо jj) — система контроля версий, совместимая с Git по протоколу, но с другой моделью коммитов: конфликты — сущность первого класса, commit и rebase — базовые операции, immutable и mutable коммиты отделены. В посте — перевод статьи Корбри с HN (249 points): зачем нужны megamerges, как их собирать и как поддерживать в актуальном состоянии через алиасы jj.
Пост написан для пользователей Jujutsu среднего уровня и для гитовых пользователей, которым любопытен jj. Специальные команды приведены в оригинальном виде (их не локализуют), пояснения — в комментариях к коду. Ключевые термины jj: revset — язык запросов к графу коммитов (аналог Git-revisions, но расширенный); bookmark — аналог ветки в Git, но не двигается автоматически за рабочей копией; WIP — work in progress, незавершённые изменения.
Ключевые выводы
Megamerge за минуту
Что это и зачем
- Megamerge — octopus merge (коммит с тремя и более родителями), куда вы включаете все рабочие ветки: фичи, багфиксы, PR, чужие ветки-зависимости, локальные инструменты. Вы работаете поверх него.
- Выгода: всегда видите сумму всех изменений разом; почти не ловите сюрприз-конфликты на пуше; быстро переключаетесь между задачами (просто редактируете код); можно легко делать drive-by-фиксы.
- Собрать просто:
jj new x y z+jj commit -m megamerge. Megamerge в remote не пушится — пушатся только ветки, из которых он собран. - Сабмит WIP-изменений:
jj absorb(автоматически разбрасывает строки по «родным» коммитам) иjj squash --to X --interactive(вручную выбирать куски). - Поддержка актуальности — алиас
restack: ребейзит только ваши mutable-коммиты на trunk, оставляя в покое чужие ветки.
Merge-коммиты в jj: обычный коммит с несколькими родителями
Если вы обычный гит-пользователь (или пользователь Jujutsu, который ещё не дошёл до продвинутых процессов), возможно, вы удивитесь: в merge-коммите нет ничего особенного. Это не специальный случай с отдельными правилами — это обычный коммит, у которого несколько родителей. Он даже не обязан быть пустым.
Легенда вывода jj log: @ — текущая рабочая копия; ○ — mutable-коммит; ◆ — immutable-коммит (обычно trunk); ├─╮ — граф связей.
Удивит и второе: merge-коммиты не ограничены двумя родителями. Мерджи с тремя и более родителями в сообществе неофициально называют octopus merge. И если вы думаете: «в каком мире мне может понадобиться слить больше двух веток?» — на практике эта идея очень мощная. Именно octopus merges дают весь megamerge-воркфлоу.
Так что же такое megamerge
Суть в том, что в megamerge-воркфлоу вы редко работаете прямо поверх тика одной ветки. Вместо этого вы создаёте octopus merge (его и называем megamerge) как дочерний коммит от каждой рабочей ветки, которая вам важна. Это могут быть багфиксы, фича-ветки, ветки, ждущие PR, чужие ветки, от которых зависит ваш код, локальные окружения и даже приватные коммиты, которые не принадлежат ни к одной ветке. Всё, что вам важно, входит в megamerge. Главное помнить: megamerge не пушится в remote — пушатся только ветки, из которых он собран.
Если звучит как много — нормально. Вы же знаете, сколько сил уходит на смену контекста при возврате к старому PR. Но такой подход даёт несколько реально ценных вещей:
- Вы всегда работаете поверх суммы всех своих изменений. Если ваша рабочая копия собирается и запускается — вся ваша работа гарантированно согласуется друг с другом.
- Почти не встречаются сюрприз-конфликты. В Jujutsu конфликты и так first-class (их не нужно «решать прямо сейчас»), а в megamerge-воркфлоу вы постоянно сливаете свои изменения — поэтому на стороне удалённого хостинга (GitHub/GitLab) конфликтов не возникает. Иногда проблемы случаются с изменениями контрибьюторов, но на практике это редкость.
- Намного меньше трения при переключении задач. Поскольку вы всегда работаете поверх megamerge, не нужно ходить в VCS — можно просто пойти править нужный код. Легко делать маленькие PR для попутных рефакторов и фиксов.
- Проще держать ветки актуальными. Небольшой алиас — и весь megamerge обновляется относительно trunk одной командой rebase. Разберём ниже.
Как собрать megamerge
Старт — простой: новый коммит с каждой нужной веткой в качестве родителя. Автор любит давать этому коммиту имя и оставлять пустым:
Получаете пустой коммит поверх всего. Вот тут и идёт работа. Всё, что выше megamerge, считается WIP. Можно сплитить, создавать несколько веток из megamerge — всё, что хотите. Всё, что вы напишете, будет опираться на сумму содержимого megamerge — как и задумывалось.
В какой-то момент вы будете довольны результатом — и встанет вопрос:
Как затем сабмитить изменения
Как дотащить изменения до megamerge — зависит от того, куда они должны попасть. Основной инструмент — команда absorb: она сама определяет, в какой mutable-коммит ниже по дереву должен пойти каждый хунк или каждая строка, и автоматически разбрасывает их по нужным коммитам. «Каждый раз выглядит как фокус — логика absorb прозрачна: команда смотрит, в каком коммите ниже по дереву впервые появилась каждая изменяемая строка, и отправляет её туда». Это одна из ключевых фич Jujutsu, благодаря которой megamerge-воркфлоу работает плавно.
Но Jujutsu — красивый кусок софта, и у него есть автоматика. Команда absorb сделает бо́льшую часть работы за вас: определит, в какой mutable-коммит ниже по дереву должен пойти каждый ваш хунк или каждая строка, и автоматически их туда сквошит. «Каждый раз ощущается как магия — и не та чёрная магия, в которой ничего не разобрать, а хорошая, понятная». Это одна из ключевых фич Jujutsu, благодаря которой megamerge-воркфлоу вообще работает плавно.
Absorb не всегда ловит всё, но обычно укладывает около 90% изменений. Остальное — либо ручной squash --to (отправляет изменения в конкретный коммит), либо squash --to X --interactive (для выборочного переноса кусков). Если WIP-коммит содержит изменения для нескольких целей — сначала сплит командой split, либо тот же --interactive.
Если изменения должны попасть в новый коммит — не сильно сложнее. Если коммит относится к одной из ваших веток, просто ребейзим и двигаем bookmark:
Разложим этот rebase, чтобы было понятнее:
А если вы начали работу над совершенно новой фичей или наткнулись на баг — ещё проще. С парой алиасов можно легко добавить новый материал в megamerge:
Короткое пояснение, что делает closest_merge(to):
С этим revset-алиасом stack берёт произвольный revset и вставляет его между trunk() (основной веткой разработки) и megamerge:
Это полезнее, если несколько стеков изменений хочется включить параллельно. А если стек один, есть ещё один алиас, который подхватывает весь стек после megamerge:
Этот алиас не требует аргументов. Достаточно накоммитить и ввести stage:
Последний кусок пазла — неприятная реальность: есть ещё другие люди.
Как держать всё это в актуальном состоянии
Хороший вопрос — автор потратил пару месяцев, чтобы ответить на него в общем виде. У Jujutsu есть простой способ ребейзить всю рабочую ветку на основную ветку:
Но это работает, только если всё ваше рабочее дерево (worktree) состоит только из ваших изменений. Когда в графе есть коммиты, которые вам не принадлежат (untracked bookmark или чужие ветки), Jujutsu остановится, чтобы защитить их от перезаписи.
Решение — ребейзить только те коммиты, которыми вы реально управляете. Спасибо Стивену Дженнингсу за отличный revset:
Вместо того чтобы пытаться ребейзить всё дерево (как делает jj rebase --onto trunk()), этот алиас трогает только коммиты, которые можно двигать. Чужие ветки и работа поверх них остаются на месте. Флаг --simplify-parents заодно убирает лишние рёбра, которые могут остаться после ребейза. У автора эта команда не ломалась ни разу — даже на мегамерджах с девятью родителями разных контрибьюторов.
TL;DR: рабочая конфигурация
Megamerges в Jujutsu — рабочий способ уложить несколько параллельных задач в одно рабочее дерево. Для полноценной эргономики добавьте в конфиг через jj config edit --user:
Краткая шпаргалка по командам:
Ещё раз: megamerge не задуман для пуша в remote — это удобный способ увидеть всю картину целиком. Ветки всё равно публикуются отдельно, как обычно.
Megamerges подходят не всем — автор рассказывает, что получал испуганные взгляды, показывая своё рабочее дерево. Но, попробовав их один раз, вы с большой вероятностью обнаружите, что переключаетесь между задачами почти без усилий.
Дополнительно: замечания автора
- Флаг
--simplify-parentsв restack важен — он вычищает избыточные рёбра (если есть A→B→C и A→C, simplify-parents удалит A→C). stageиспользуетclosest_merge(@)+::, а неclosest_merge(@)..— операторx..эквивалентен~::xи включает всё, что не является предком x. Это может затянуть лишнее.- В Git merge-коммиты, куда вносят изменения поверх разрешения конфликтов, называют evil merge. В Jujutsu такие мерджи уже не «злые» — модель Jujutsu устроена более консистентно.
- Алиасы Jujutsu. Есть несколько типов: revset-алиасы (кастомные функции, которые возвращают коммиты через revset language); command-алиасы (расширяют стандартные команды); template-алиасы (меняют формат вывода jj в терминале через templating language); fileset-алиасы (работают с файлами через fileset language).
- Концепция mutable/immutable. Mutable-коммиты — те, что можно менять на регулярной основе. Это в основном lint (есть флаг
--ignore-immutable), но он помогает не влипнуть. Алиасыmutable()иimmutable()выбирают соответствующие коммиты.
FAQ
FAQ
Это заменяет Git?
Нет. Jujutsu — Git-совместимая VCS: использует те же remote-протоколы и репозитории, но с собственной моделью коммитов (conflicts как first-class, более простой rebase, workspaces). Рабочая копия может лежать поверх существующего Git-репо. Migration — опциональная.
Зачем octopus merge, если достаточно rebase на trunk?
Octopus merge объединяет несколько рабочих веток в один вершинный коммит, сохраняя родительские связи. Rebase «выпрямляет» историю и ломает связь с исходными ветками. Megamerge позволяет работать поверх суммы всех веток и при этом пушить их отдельно без конфликтов. Подробный разбор — в Git rebase vs merge.
Можно ли использовать это с GitHub/GitLab?
Да. Megamerge остаётся локальным — в remote уходят только отдельные ветки. GitHub/GitLab видят их как обычные PR/MR. Никакой дополнительной настройки форджа не требуется.
Что может сломать megamerge?
Любая операция с флагом --ignore-immutable на immutable-коммитах — это сознательный «рискованный» путь. Без него — absorb безопасен (работает только с mutable-коммитами), squash --to тоже. stage и stack — алиасы поверх rebase: если megamerge включает immutable-коммиты чужих веток, rebase остановится, чтобы их защитить. На практике это и есть «защита от дурака».
Jujutsu доступен в РФ?
Да. Это open-source инструмент (лицензия Apache 2.0), установка через cargo, homebrew, пакетные менеджеры Linux. Не требует облачных сервисов. Документация и репозиторий — на GitHub.
Выводы
Megamerge-воркфлоу убирает одну конкретную боль: необходимость помнить, какие ветки с чем конфликтуют, и постоянно переключать контекст. Базовая схема: octopus merge как «рабочая площадка», absorb и squash для отправки изменений в нужные коммиты, restack для поддержания актуальности. Ничего магического — обычные коммиты и revsets, собранные в удобный процесс. Кто работает в воркфлоу Trunk-Based Development, увидит знакомые паттерны — megamerge хорошо с ними сочетается.
Оригинал статьи: isaaccorbrey.com/notes/jujutsu-megamerges-for-fun-and-profit. Обсуждение на Hacker News: news.ycombinator.com/item?id=47841129. Официальная документация Jujutsu: jj-vcs.github.io/jj.