gitum

Руководство gitum

Содержание

gitum помогает вести разработку параллельно внешнему апстриму и синхронизироваться с ним. Он сделан для пакетов дистрибутивов, обросших патчами, для форков с сотнями своих изменений (wine-rebased) и для серий изменений, которые ждут приёма в апстрим. В любой момент видно, где каждое ваше изменение находится относительно апстрима.

Идеи

Картинки

На картинках синие точки — коммиты апстрима, зелёные — новые коммиты апстрима, жёлтые — наши изменения, фиолетовые квадраты — записанные состояния ветки истории.

Зачем gitum: при merge наши изменения тонут, при rebase теряется их история, gitum сохраняет и то и другое.

merge, rebase и gitum

Новый релиз апстрима: gitum sync снова кладёт наши изменения сверху и записывает новое состояние.

gitum sync

Наша работа обычным git (новые коммиты, правка старого): gitum save её записывает.

gitum save

Апстрим принял одно из наших изменений: следующий sync сам убирает его из слоя.

принято апстримом

Как это устроено

Стек слоёв переносится на новый апстрим за один проход

flowchart LR
  subgraph before["до"]
    direction BT
    U1["winehq<br/>wine-11.19"] --> S1["rebased<br/>wine-staging"] --> B1["rebased-basealt<br/>15 патчей"] --> E1["rebased-etersoft<br/>156 патчей"]
  end
  subgraph after["после gitum sync --next-release"]
    direction BT
    U2["winehq<br/>wine-11.20"] --> S2["rebased<br/>собран из wine-staging v11.20"] --> B2["rebased-basealt<br/>перенесён"] --> E2["rebased-etersoft<br/>перенесён"]
  end
  before ==>|приватное рабочее дерево,<br/>все ветки двигаются разом| after

Слой переносится только после успешного переноса слоя под ним. Пока весь стек не готов, ни одна ваша ветка не меняется.

Что происходит с каждым изменением при sync

flowchart TD
  C(["изменение слоя"]) --> R{"пара патч + revert?"}
  R -- да --> AN["annihilated:<br/>схлопнуто"]
  R -- нет --> F{"судьба велит выбросить?<br/>принято, исправлено иначе, отключено"}
  F -- да --> DM["dropped-marked"]
  F -- нет --> O{"коммит из Origin<br/>уже в новой базе?"}
  O -- да --> DB["dropped-backport"]
  O -- нет --> P{"такой же патч<br/>уже в апстриме?"}
  P -- да --> DU["dropped-upstream"]
  P -- нет --> CP["cherry-pick на новую базу"]
  CP -- чисто --> PK["picked"]
  CP -- ничего не осталось --> EM["emptied:<br/>принято с правками?"]
  CP -- конфликт --> RR["rerere"]
  RR -- не решил --> RU["ваши правила"]
  RU -- не решили --> AG["агент"]
  AG -- не решил --> H["стоп с кодом 2:<br/>человек разрешает,<br/>gitum continue или skip"]
  RR -- решил --> RS["resolved,<br/>записано, кто разрешил"]
  RU -- решили --> RS
  AG -- решил --> RS
  H --> RS

Каждый результат попадает в отчёт прогона и в историю изменения. Результаты резолверов, которым вы ещё не доверяете (например, агента), сначала уходят на ревью в ветки candidate/*.

Что переписывается, а что только растёт

flowchart LR
  subgraph rewritten["переписываются при каждом sync"]
    L["ветки слоёв<br/>rebased, rebased-basealt,<br/>rebased-etersoft"]
  end
  subgraph growing["только растут: можно делиться"]
    J["gitum/journal<br/>коммит на состояние:<br/>gitum.toml, слои, изменения,<br/>серии, отчёты, rr-cache"]
    H["gitum/history/СЛОЙ<br/>коммит на состояние<br/>с деревом слоя:<br/>git log -p и bisect"]
  end
  L -- состояние записано --> J
  L -- дерево записано --> H
  J -. старые вершины остаются достижимы .-> L

Ветки слоёв публикуются с lease: коммиты, которые коллега тем временем запушил, встают сверху. Журнал и ветки истории публикуются без force. Журналы двух сессий сливаются при gitum pull, если те меняли разные серии или изменения.

Судьба изменения

stateDiagram-v2
  direction TB
  state "local" as local
  state "draft" as draft
  state "review" as review
  state "queued" as queued
  state "sent" as sent
  state "merged" as merged
  state "merged-modified" as merged_modified
  state "rejected" as rejected
  state "fixed-differently" as fixed
  state "distro-only" as distro
  state "backport" as backport
  [*] --> local: добавлено в слой
  [*] --> backport: gitum backport
  local --> draft
  local --> distro: не для апстрима
  local --> fixed: апстрим сделал иначе
  draft --> review: внутренний MR
  review --> queued: gitum queue add
  queued --> sent: MR в апстрим
  sent --> merged: принято как есть
  sent --> merged_modified: принято с правками
  sent --> rejected
  rejected --> draft: доработка
  merged --> [*]: выброшено при sync
  merged_modified --> [*]: выброшено при sync
  fixed --> [*]: выброшено при sync
  backport --> [*]: выброшено, когда база его содержит

gitum check-upstream находит принятые изменения по patch-id или по сходству, а gitum forge refresh читает состояния merge request'ов.

Путь серии в апстрим

flowchart TB
  BR["ветка<br/>series/ntdll-fix"] -->|gitum series add| SE[("серия в журнале:<br/>definition, патчи,<br/>баги, ссылки на MR")]
  SE -->|gitum port| VA["вариант<br/>series/ntdll-fix@etersoft<br/>для наших сборок"]
  SE -->|gitum queue| QU["очередь:<br/>по одной,<br/>сначала зависимости"]
  QU -->|gitum prepare --push| PR["ветка в форке на свежем master:<br/>новые даты, без Co-Authored-By"]
  PR -->|MR вручную| UP["апстрим"]
  UP -->|forge refresh,<br/>check-upstream| DONE["принято:<br/>выброшено из слоёв<br/>при следующем sync"]
  VA -. gitum variants:<br/>расходятся ли? .- SE

Требования

Установка

pip install ./gitum

Для ALT Linux пакет собирается из gitum.spec с помощью gear.

Быстрый старт

cd wine-rebased
gitum init --upstream winehq=winehq/master --release-tags 'wine-*' \
           --layer rebased=rebased:winehq \
           --layer basealt=rebased-basealt:rebased \
           --layer etersoft=rebased-etersoft:basealt
gitum status
gitum sync --next-release        # следующий тег wine-X.Y, все слои
# при конфликте: исправить файлы в указанном рабочем дереве, git add, затем
gitum continue                   # или gitum skip / gitum abort
gitum publish                    # опубликовать слои, историю и журнал

init берёт базу каждого слоя из тега <ветка>-START, вершины родительского слоя или общей базы с апстримом.

Сценарии

Робот обновляет форк на каждый релиз

Робот по cron выполняет:

gitum sync --next-release --candidate auto --publish

Серия на пути в апстрим

git checkout -b series/ntdll-fix winehq/master   # работа над серией
gitum series add ntdll-fix --base upstream:winehq
gitum port ntdll-fix --to layer:etersoft         # вариант для наших сборок
gitum queue add ntdll-fix
gitum queue next                                 # что отправлять следующим
gitum prepare ntdll-fix --push fork              # ветка в форке на свежем master
gitum series set ntdll-fix state=sent pr=wine/wine!12345
gitum forge refresh                              # одобрено, принято
gitum check-upstream series:ntdll-fix --apply    # подтвердить принятие по содержимому

Когда серия принята, её изменения уходят из слоёв при следующем sync, а очередь переходит к следующей серии.

Текст merge request хранится вместе с серией, вместе с картинками. Его пишут один раз (или берут из внутреннего merge request) и выводят для каждого forge:

gitum mr set ntdll-fix mr.md                     # mr.md и локальные картинки из него
gitum mr import ntdll-fix internal:etersoft/wine!7   # или: заголовок, текст и картинки MR
gitum mr render ntdll-fix --forge upstream -o out.md # без внутренних частей

Первая строка # Заголовок — это заголовок. Строки между <!-- internal --> и <!-- /internal --> (или от <!-- internal --> до конца) только для внутреннего ревью и не выводятся без --internal. С [series] mr-internal-separator = "---" команды mr set и mr import превращают первую строку --- текста без маркеров в <!-- internal --> — для внутренних merge request, где заметки для ревью идут после такой строки. Текст хранится в series/ИМЯ/mr.md, картинки — в series/ИМЯ/assets/; series save и экспорт их не трогают. Для GitLab с токеном render загружает картинки в проект (--project, или проект из ссылки pr/internal_mr серии, или [forge.ИМЯ] project, или проект из remote апстрима серии, если он на хосте этого forge) и запоминает их URL по содержимому, поэтому новая версия текста загружает только новые или изменённые картинки. Для других forge или с --no-upload в тексте остаются заглушки {{gitum-asset:ФАЙЛ}}, а картинки записываются в <common dir>/gitum/mr/ИМЯ/ (или в КАТАЛОГ/ИМЯ/ с --assets-dir КАТАЛОГ или [series] mr-assets-dir; список pending в --json); после загрузки другим способом gitum mr record ИМЯ ФАЙЛ=URL... --forge F заставит следующие выводы использовать эти URL. Сам gitum merge request не открывает и не правит: это делает человек или агент с выведенным текстом.

Пакет дистрибутива

Изменения пакета ведутся коммитами в слое поверх настоящей истории апстрима. gitum export layer:pkg --format spec --dir ГИР-РЕПО --spec пакет.spec пишет патчи и строки Patch в spec-файле; gitum import пакет.spec --into pkg забирает существующие патчи пакета в слой.

Конфигурация

Конфигурация — файл gitum.toml в журнале. Посмотреть: gitum config show; заменить: gitum config load ФАЙЛ.

version = 2

[upstream.winehq]
remote = "winehq"
ref = "winehq/master"
release-tags = "wine-*"

[layer.rebased]                 # собирается из wine-staging, а не переносится
base = "upstream:winehq"
branch = "rebased"
generator = "staging"
staging-repo = "../wine-staging"
staging-ref = "v{version}"
post-command = "autoreconf -f && tools/make_requests"

[layer.basealt]
base = "layer:rebased"
branch = "rebased-basealt"
compat-start-tag = "rebased-basealt-START"

[layer.etersoft]
base = "layer:basealt"
branch = "rebased-etersoft"
annihilate-reverts = true

[resolve]
chain = ["rerere", "rules", "agent"]
rules = "tools/conflict-resolve.py"
agent = "tools/conflict-agent.sh"
review-unless = ["rerere"]       # прочие — через candidate/*; человеку (manual) доверие всегда

[publish]
remote = "origin"
check = "./build.sh"             # должна пройти до того, как двинутся ветки

[series]
subject-lint = '^[a-z0-9_./-]+: [A-Z].*\.$'
bug-tracker = "etersoft"         # голый номер бага относится к нему
fixes-tracker = "winehq"         # его баги — строки Fixes: в definition

[tracker.etersoft]
url = "https://bugs.etersoft.ru/show_bug.cgi?id={n}"

[tracker.winehq]
url = "https://bugs.winehq.org/show_bug.cgi?id={n}"
label = "winehq {n}"             # по умолчанию "#{n}" для bug-tracker, "{tracker} {n}" для остальных

[hooks]                          # получают JSON-события на stdin
change = "tools/notify-bugs.py"
run = "tools/notify-chat.sh"
publish = "tools/set-version-branches.sh"
series = "tools/comment-in-bugs.py"  # серия сменила состояние

Параметры апстрима: - rewritten = true: апстрим пересоздаётся, а не продолжается, как ветка, собранная patchinstall. - prefix = "third-party/lib": апстрим вложен в ваше дерево (vendored).

Команды

Все команды принимают --json и --repo ПУТЬ (в любом месте командной строки). Коды возврата:

Код Значение
0 успех
1 ошибка
2 нужен человек: конфликт, ревью, разошедшаяся история
10 что-то обновлено (с --detailed-exitcode)
75 репозиторий занят

Интерактивных вопросов нет.

Команда Что делает
init Создать журнал из существующих веток. Слои необязательны: для работы только с сериями достаточно апстримов.
status [--changes] [--series [--variants]] Апстримы, слои, незавершённые операции, блокировки, таблица «изменение → судьба → действие», серии деревом по зависимостям (состояние, отставание базы, захваты, разошедшиеся варианты).
sync [--to REF \| --next-release] [--step] [--candidate auto\|always\|never] [--publish] [--find-culprit] [--allow-rewrite] Перенести стек на новый апстрим.
continue, skip, abort Продолжить, выбросить конфликтующее изменение или отменить остановленный sync.
accept, drop-candidate Опубликовать или выбросить результат, ждущий ревью в candidate/*.
save Записать работу, сделанную в ветках слоёв обычным git.
publish, pull Обмен состоянием. Коммиты, запушенные другими за это время, встают сверху; журналы параллельных сессий сливаются.
change list\|show\|set\|confirm Изменения и их судьбы: gitum change set ID fate=merged upstream_commit=SHA; gitum change confirm ID [SHA] подтверждает suspect, найденный check-upstream.
check-upstream [layer:X\|series:Y] [--since REF] [--apply] Найти наши изменения в апстриме: patch-id, затем сходство заголовка и диффа. Совпадение считается «принято с правками», только если содержимое подтверждено (наше изменение поверх апстрима ничего не меняет); иначе — suspect, решает человек.
series add\|save\|list\|show\|set\|remove\|export\|import-staging\|import-json\|checkout\|claim\|release Серии изменений. Состояния: draft → review (внутреннее ревью) → queued → sent (версия vN) → approved → merged, rejected или superseded. Серия может лежать на любой глубине каталога экспорта (export_dir); экспорт трогает только definition и те *.patch, которые писала сама серия, поэтому несколько серий могут делить один каталог: номера патчей продолжаются после зависимостей, а общий definition объединяет их Fixes. series save ИМЯ всегда пересчитывает названную серию. Если коммиты серии на upstream:X закоммичены тем, кто коммитит в этот апстрим (и не вами), ветка, скорее всего, стоит на более свежем апстриме, чем локальный ref: series add, save и import-json предупреждают и выходят с кодом 2, а status показывает предупреждение, пока апстрим не скачан и серия не сохранена заново. import-json не откатывает серию к более раннему состоянию, если она менялась позже записи (с --force откатывает). Первый экспорт патчей, сделанных вручную, перепишет их в нормализованном виде gitum (From 0000…, без подписи). claim отмечает, что серией занимается сессия (GITUM_SESSION); другие сессии получат по ней код 75. pr и internal_mr принимают URL или [forge:]группа/проект!N, который разворачивается через [forge.ИМЯ] (по умолчанию [series] pr-forge, internal-mr-forge); для forge с другими путями задаётся mr-url = "{url}/{project}/merge_requests/{n}".
mr set\|import\|render\|record СЕРИЯ Текст merge request серии с картинками; внутренние части; картинки загружаются один раз на проект forge (см. «Серия на пути в апстрим»).
bugs [[ТРЕКЕР:]N] [--format md] [-o ФАЙЛ] Индекс багов: баг (ссылкой) → его серии → состояние и версия → merge request → релиз, когда принято; по трекеру и номеру. Серия перечисляет свои баги как bugs=etersoft:19979,winehq:60741 (series set, import-json); голый номер относится к [series] bug-tracker. С [series] fixes-tracker баги этого трекера пишутся строками Fixes: [N] в definition (с текстом из заголовка серии, заголовка её merge request или темы первого коммита, если строка Fixes их ещё не называет), а строки Fixes: [N], прочитанные import-staging или заданные как fixes=, становятся багами. series list --bug Б и status --series --bug Б показывают серии одного бага; markdown очереди выводит баги ссылками, а sections = [..., "bugs"] добавляет в него индекс. Даты в markdown выводятся в местном часовом поясе или в [queue.markdown] timezone ("Europe/Moscow" или "+03:00").
port, variants Серия на другой базе и чем её варианты отличаются.
queue [next\|add\|pause\|resume] [--target T], queue --format md Очереди отправки в апстрим, по одной на цель (поле серии target). По одной серии, пока её не одобрят или не примут; сначала зависимости. pause --reason останавливает очередь. Генерирует UPSTREAM-QUEUE.md: [queue.markdown] задаёт разделы (sent, queued, review, draft, done), поля-колонки (fields, done-fields: любые поля серии, например reviewer, release, merged_at) и все заголовки (titles).
prepare СЕРИЯ [--push ФОРК] [--force] Серия в виде для апстрима: свежая база, новые даты с шагом в секунду, без trailer'ов Co-Authored-By/Gitum-Id, проверка заголовков, перераскладка путей для vendored-деревьев, range-diff с сохранённой серией. Зависимости должны быть уже приняты (--force включает их коммиты). Merge request вы открываете сами; forge refresh предупредит, если его голова отличается от подготовленной ветки.
backport SHA\|URL --into СЛОЙ Взять коммит апстрима с trailer'ом Origin:; он выбрасывается сам, когда база его содержит.
branches [--remote ФОРК] [--prune] Ветки серий и апстрима; устаревшие после merge.
forge refresh\|import-json\|export-json Состояния merge request'ов из GitHub, GitLab или Forgejo либо в виде JSON от ваших скриптов.
export, import Наборы патчей для spec-файлов RPM (строки Patch между # BEGIN gitum patches / # END gitum patches), quilt, wine-staging.
log [ИЗМЕНЕНИЕ\|series:X\|layer:X], diff СОСТОЯНИЕ1 [СОСТОЯНИЕ2], report [ПРОГОН] [--html ФАЙЛ] История и отчёты.
mirror ИМЯ Общее зеркало апстрима в ~/.cache/gitum вместо разовых клонов.

Команды, которые запускает gitum

gitum запускает ваши команды для разрешения конфликтов, проверок, генерируемых слоёв и хуков. Все они неинтерактивны и получают одинаковое окружение GITUM_*:

Переменная Значение
GITUM_REPO основное рабочее дерево репозитория
GITUM_COMMON_DIR общий каталог git
GITUM_WORKTREE приватное рабочее дерево операции (только sync)
GITUM_RUN, GITUM_RUN_DIR идентификатор прогона и каталог его логов
GITUM_LAYER обрабатываемый слой
GITUM_UPSTREAM, GITUM_TARGET, GITUM_TARGET_TAG, GITUM_VERSION апстрим, целевой коммит, его релизный тег и версия из тега (wine-11.20 → 11.20)
GITUM_LOCK_FILE файл блокировки репозитория
GITUM_LOCK_HELD путь к файлу блокировки, пока gitum её держит
Команда Рабочий каталог Дополнительные переменные Результат
resolve.rules, resolve.agent приватное дерево с конфликтом GITUM_COMMIT, GITUM_SUBJECT, GITUM_FILES (по одному в строке), GITUM_CONFLICT_DIR, GITUM_RESOLVER код 0, все конфликты разрешены и добавлены через git add
publish.check приватное дерево на вершине каждого листового слоя GITUM_TIP код 0, иначе ветки не меняются
generator = "command" приватное дерево на новой базе GITUM_BASE новые коммиты на HEAD
post-command генерируемого слоя приватное дерево коммиты; остаток коммитит gitum
hooks.change, hooks.run, hooks.publish, hooks.series основное рабочее дерево GITUM_EVENT; для hooks.series ещё GITUM_SERIES, GITUM_STATE, GITUM_PREVIOUS_STATE JSON-событие на stdin; ошибка хука не роняет прогон

Хуки: - hooks.change получает по событию на каждое изменение, которое не применилось просто так. - hooks.run получает весь отчёт sync (event — sync или candidate). - hooks.series запускается, когда серия меняет состояние (series set, queue add, forge refresh, forge import-json): {"event": "series-state", "series", "state", "previous", "version", "pr", "internal_mr", "upstream_commit", "release", "source", "bugs": [{"bug": "winehq:60741", "tracker", "id", "url", "label"}]}. Сам gitum в трекеры не пишет. - hooks.publish запускается после успешного gitum publish. Его событие выглядит так:

{"event": "publish", "remote": "origin", "journal": "<sha>",
 "upstreams": {"winehq": {"ref": "winehq/master", "sha": "<sha>", "tag": "wine-11.20", "version": "11.20"}},
 "layers": {"etersoft": {"branch": "rebased-etersoft", "old": "<sha до пуша>", "new": "<sha>",
                         "base": "<sha>", "start_tag": "rebased-etersoft-START"}},
 "integrated": {"etersoft": ["заголовки коммитов, поставленных сверху"]}}

Блокировка — стабильный интерфейс для ваших скриптов. - Это эксклюзивный flock на $GIT_COMMON_DIR/gitum/lock. - Пока она взята, в файле лежит JSON владельца: {"pid", "host", "user", "session", "since"}. Сессия берётся из GITUM_SESSION. - Команды, запущенные gitum, получают GITUM_LOCK_HELD, и запущенный из них gitum не берёт блокировку повторно.

Ветки, checked out в рабочих каталогах. Ветка слоя, checked out в чистом рабочем дереве, переезжает вместе с ним. Если в нём есть локальные изменения, gitum останавливается с кодом 2 и ничего не меняет.

Надёжность

gitum можно прервать в любой момент — kill, нехватка памяти, отключение питания, — и ничего не потеряется и не останется сделанным наполовину:

gitum status показывает прерванную операцию и что с ней делать. Тесты убивают gitum во многих точках (GITUM_TEST_CRASH) и проверяют, что gitum continue приходит ровно к результату непрерванного прогона.

Разработка

cd gitum
python3 -m pytest
python3 -m gitum --help

Почему gitum устроен так и какие решения приняты — в docs/design.ru.md.

Ссылки

Лицензия

GPL-2.0-or-later, см. gitum/COPYING.