Устройство gitum
Содержание
Здесь записано, почему gitum устроен именно так: из каких задач он вырос, какие решения приняты и какие варианты отвергнуты. Как им пользоваться и что делает каждая команда — в руководстве; этот документ его не повторяет, а ссылается на него.
Задача
Вести свои изменения параллельно чужому апстриму и синхронизироваться с ним удобно и наглядно: в любой момент видно, где каждое наше изменение находится относительно апстрима.
Требования собраны из трёх реальных сценариев:
- Форк с роботом (wine-rebased). Стек слоёв winehq → rebased (wine-staging) → rebased-basealt → rebased-etersoft, сотни своих патчей. Обновление по cron на каждый релиз, цепочка разрешения конфликтов rerere → правила → агент, ревью кандидата, отчёты в баг-трекер, экспорт в RPM.
- Серии для апстрима (wine-kloder). Около сотни серий, у каждой своя ветка от master апстрима. Внутренний MR, потом MR в апстрим строго по одному. Одна серия живёт в двух вариантах (на master и в своём слое). Апстрим принимает патчи с правками. Работают несколько сессий в разных worktree.
- Пакет дистрибутива (ALT). Судьба каждого изменения: PR, принят в релиз
X, исправлен иначе, только для дистрибутива. Прыжок на релизный тег,
бэкпорт с
Origin:, несколько апстримов в одном репозитории (vendored).
Решение было развивать gitum, даже если от прежнего кода останется одно название. Так и вышло: gitum 2 написан заново.
Чему научил wine-staging
wine-staging — работающий пример того же подхода: около 120 серий параллельно апстриму и почти 2000 коммитов «Rebase against SHA».
- Серия — каталог
patches/<серия>/:definition(Fixes:,Depends:,Disabled:с причиной) плюс0001-*.patch. - База — один sha апстрима на весь набор.
- Принятые апстримом серии удаляют руками в коммите ребейза, сломанные
получают
Disabled: true. - История репозитория — это история состояний набора.
Им не хватает связи серии с MR и багами (кроме Fixes), автоматического
определения принятия (тем более с правками), вариантов серии на разных
базах, журнала разрешения конфликтов, машиночитаемого отчёта и работы в
git-ветках: патчи правят как файлы, а собирают серию в дереве wine, чтобы
проверить.
Отсюда два решения. Берём формат каталога серии и definition (gitum
читает и пишет эту раскладку), граф Depends, базу как явный sha, коммит
журнала на каждое состояние. Добавляем серию как обычную git-ветку
(каталог в журнале генерирует gitum), метаданные, варианты на базах,
отслеживание принятия и журнал конфликтов.
Модель: зачем три сущности
- Слой — для робота: ветка поверх апстрима или другого слоя, сотни коммитов, которые не обязаны быть разложены по сериям. Так существующий форк можно взять под gitum сразу, без предварительной сортировки.
- Серия — для работы с апстримом: своя база, зависимости, MR, состояние, очередь. Это единица, которую отправляют и принимают.
- Изменение — для дистрибутива: у каждого коммита своя судьба, которая переживает любые переносы.
Слои складываются стопкой, потому что так устроены реальные форки: каждый следующий слой добавляет своё к предыдущему, и переносить их надо вместе.
Журнал: почему так хранится состояние
Что лежит в журнале, описано в руководстве: что переписывается, а что только растёт. Здесь — почему.
- Журнал только дописывается. Его публикация — fast-forward, force-push не нужен, поэтому его можно смело делить с коллегами и роботами. Ветки слоёв переписываются, как и раньше, но источник истины — журнал.
- Видимая ветка
gitum/journal, а не скрытая ссылка: её видно вgit log, её публикуют и забирают обычными средствами. - Вершины слоёв — дополнительные родители коммита журнала. После rebase
старые вершины перестают быть достижимыми из веток и пропали бы при
git gc. Так любое прошлое состояние восстанавливается точно, по исходным коммитам, а основная линия читается через--first-parent. - Ветка истории на слой
gitum/history/<слой>— коммиты из готовых деревьев вершин (commit-tree), поэтомуgit log -pиgit bisectработают между нашими состояниями. В gitum 1 для этого накладывались патчи; теперь дерево берётся готовое, и это почти ничего не стоит. - Каждая серия и каждое изменение — отдельный файл. Две сессии, которые
правят разные серии, создают коммиты журнала, сливающиеся без конфликтов
(
pullсливает их черезgit merge-tree). Это и есть параллельная работа без блокировок на весь репозиторий. Одновременная правка одной серии — код 2, решает человек. - Конфигурация лежит в журнале, а не в рабочем дереве: она общая для всех клонов и меняется вместе с состоянием, которое описывает.
Принятые решения
Решения приняты 2026-11-11, когда начиналась работа над gitum 2, и уточнялись по ходу.
- Тонкая обёртка над
git(subprocess), без GitPython. Всё, что нужно gitum, — rebase, cherry-pick с rerere, merge-tree, patch-id, update-ref --stdin — есть только в самом git. Библиотеки либо оборачивают тот же процесс git, либо не умеют этих операций, либо тянут скомпилированную зависимость. Обёртка прозрачна: в журнале прогона видна каждая команда git. - Python ≥ 3.11 и никаких зависимостей, кроме git:
tomllibесть в стандартной библиотеке. Пакет ставится и проверяется в любом дистрибутиве. - gitum 1 не поддерживается: ни его команды, ни перевод старых репозиториев. Совместимость с его моделью (один слой, перенос апстрима по одному коммиту) мешала бы новой.
- Коды возврата для автоматизации: 0 успех (10 — обновлено, с
--detailed-exitcode), 1 ошибка, 2 нужен человек, 75 занято. Ни одна команда ничего не спрашивает: где нужен ввод, она завершается с кодом 2 и говорит, что именно нужно. У каждой команды есть--json. - Идентичность изменения хранится в журнале (
changes/<id>.json), а не в trailer'е коммита. Изначально планировался обязательныйGitum-Idв каждом коммите, но тогда пришлось бы переписать сотни существующих коммитов. Теперь изменение узнаётся по trailer'у, если он есть, по соответствию sha при sync, по patch-id и по заголовку. - Надёжность — главное требование. Любую операцию можно прервать в
любой точке, после чего её можно продолжить или отменить, а следующая
команда сама доделывает или откатывает прерванную фиксацию. Как это
устроено — в разделе Надёжность руководства.
Каждый новый путь проверяется тестами, которые убивают gitum на
выбранном вызове git (
GITUM_TEST_CRASH). - Работа только в приватных рабочих деревьях. Чужое рабочее дерево gitum не сбрасывает и не чистит. Ветка, выбранная в чистом дереве, переносится вместе с ним; если в дереве есть изменения — код 2.
- Ссылки двигаются одной транзакцией в конце операции
(
update-ref --stdinс проверкой старых значений): либо все ветки, журнал и история обновились, либо ничего. - Генерируемые слои пересобираются, а не переносятся
(
generator = "staging"или"command"). Слой из серий wine-staging правильнее собрать заново на новой базе, чем переносить его коммиты. Генератор staging воспроизвёл слойrebased-11.20робота целиком, кромеconfigure, который зависит от версии autoconf. - Python не узкое место. Почти всё время уходит на процессы git (5–12 мс
каждый на репозитории размером с wine), сам Python занимает меньше 5%.
Поэтому правило одно: не запускать git в цикле по объектам, а читать
пачкой (
cat-file --batch, одинlogна много коммитов). Это ускорило, например,statusс 1,34 до 0,28 с. - Тесты полного цикла запускаются при сборке пакета (
%check): только git, без сети, во временных каталогах.
Решения по работе с апстримом
- gitum не создаёт и не правит MR сам. Он готовит ветку и текст и отправляет ветку в форк, а MR открывает человек или агент. У каждого forge свои правила и права, а ошибка в чужом проекте стоит дорого.
- Принятие с правками подтверждается содержимым. Похожий по заголовку и
диффу коммит апстрима — только
suspect;merged-modifiedставится, лишь когда трёхстороннее наложение на апстрим ничего не меняет. Ложное «принято» молча выбросило бы наше изменение. - Устаревший ref апстрима видно. Ветка серии может стоять на апстриме новее локального ref, и тогда в серию попали бы чужие коммиты. Без fetch этого не узнать наверняка, поэтому gitum ищет в серии коммиты тех, кто коммитит в апстрим (кроме вас), и предупреждает с кодом 2.
- Баги — список с трекером (
etersoft:19979,winehq:60741), а не часть имени серии. Имя серии служит одновременно веткой, каталогом патчей и веткой MR в публичном форке. Связь не один к одному (один баг — три серии, у серии баги в двух трекерах), а закрытие бага как дубликата не должно ломать ветки и ссылки. - Текст MR с картинками хранится с серией. Внутренний MR и MR в апстрим — на разных forge, картинки надо загружать заново, а внутренние заметки не должны уйти наружу. Поэтому текст и картинки лежат в журнале, внутренняя часть размечена, а URL загруженных картинок запоминаются по содержимому.
- Хуки вместо интеграций. gitum не пишет в трекеры и чаты: он передаёт события JSON на stdin хукам, и ошибка хука никогда не роняет операцию.
Чего не будет
- gitum не создаёт MR/PR сам: только подготовка, текст и push в форк.
- Нет своей системы сборки и уведомлений: только проверки и хуки.
- gitum не заменяет git: ветки остаются обычными ветками, gitum лишь синхронизирует их с журналом.
- Нет специальной логики под один проект: всё, что понадобилось wine, сделано общими механизмами (генераторы, правила, хуки, трекеры).
История
- gitum 1 (2012). Один слой патчей над одним апстримом, ветки
upstream/rebased/mainline/patches, перенос апстрима по одному коммиту, GitPython. Идеи, которые перешли в gitum 2: журнал как ветка, где история ветки — история состояний, и линейная история сgit log -pи bisect между состояниями. - gitum 2.0.0 (2026). Написан заново по плану из пяти этапов: фундамент (обёртка git, транзакции, блокировки, журнал), sync, изменения и серии, работа с апстримом, упаковка. Все этапы реализованы.
Проверка на реальных данных (клон wine-rebased): слои basealt (15 патчей) и
etersoft (156 патчей), перенесённые с 11.19 на 11.21, дали патчи,
идентичные результату робота (154 из 156; два отсутствующих робот позже
переделал). check-upstream нашёл принятый в wine-11.5 патч taskschd.