gitum

gitum manual

Contents

gitum helps you develop in parallel with an external upstream and stay in sync with it. It is meant for distribution packages that carry patches, forks with hundreds of local changes (wine-rebased), and series of changes waiting to be accepted upstream. At any time it shows where each of your changes stands relative to upstream.

Ideas

Pictures

In the pictures blue dots are upstream commits, green dots new upstream commits, yellow dots our changes, purple squares the recorded states of the history branch.

Why gitum: merge buries our changes, rebase loses their history, gitum keeps both.

merge, rebase and gitum

A new upstream release: gitum sync puts our changes on top again and records a new state.

gitum sync

Our own work with plain git (new commits, an edited old one): gitum save records it.

gitum save

Upstream accepted one of our changes: the next sync drops it from the layer by itself.

accepted upstream

How it works

A stack of layers moves to a new upstream in one pass

flowchart LR
  subgraph before["before"]
    direction BT
    U1["winehq<br/>wine-11.19"] --> S1["rebased<br/>wine-staging"] --> B1["rebased-basealt<br/>15 patches"] --> E1["rebased-etersoft<br/>156 patches"]
  end
  subgraph after["after gitum sync --next-release"]
    direction BT
    U2["winehq<br/>wine-11.20"] --> S2["rebased<br/>rebuilt from wine-staging v11.20"] --> B2["rebased-basealt<br/>moved"] --> E2["rebased-etersoft<br/>moved"]
  end
  before ==>|private work tree,<br/>all branches move at once| after

A layer is moved only after the layer below it has moved successfully. Until the whole stack is done, no branch of yours changes.

What happens to each change during a sync

flowchart TD
  C(["a change of the layer"]) --> R{"patch + revert pair?"}
  R -- yes --> AN["annihilated"]
  R -- no --> F{"its fate says drop?<br/>merged, fixed differently, disabled"}
  F -- yes --> DM["dropped-marked"]
  F -- no --> O{"its Origin commit<br/>is in the new base?"}
  O -- yes --> DB["dropped-backport"]
  O -- no --> P{"the same patch<br/>is already upstream?"}
  P -- yes --> DU["dropped-upstream"]
  P -- no --> CP["cherry-pick onto the new base"]
  CP -- clean --> PK["picked"]
  CP -- nothing left --> EM["emptied:<br/>accepted with edits?"]
  CP -- conflict --> RR["rerere"]
  RR -- not solved --> RU["your rules"]
  RU -- not solved --> AG["agent"]
  AG -- not solved --> H["stop with exit code 2:<br/>a person resolves,<br/>gitum continue or skip"]
  RR -- solved --> RS["resolved,<br/>the resolver is recorded"]
  RU -- solved --> RS
  AG -- solved --> RS
  H --> RS

Every result goes to the run report and to the history of the change. Results from resolvers you do not trust yet (an agent, for example) first go to candidate/* branches for review.

What is rewritten and what only grows

flowchart LR
  subgraph rewritten["rewritten by every sync"]
    L["layer branches<br/>rebased, rebased-basealt,<br/>rebased-etersoft"]
  end
  subgraph growing["only grow: safe to share"]
    J["gitum/journal<br/>a commit per state:<br/>gitum.toml, layers, changes,<br/>series, run reports, rr-cache"]
    H["gitum/history/LAYER<br/>a commit per state<br/>with the tree of the layer:<br/>git log -p and bisect"]
  end
  L -- state recorded --> J
  L -- tree recorded --> H
  J -. old tips stay reachable .-> L

The layer branches are pushed with a lease; commits a colleague pushed meanwhile are put on top. The journal and the history branches are pushed without force. The journals of two sessions merge on gitum pull when they changed different series or changes.

The fate of a change

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: added to a layer
  [*] --> backport: gitum backport
  local --> draft
  local --> distro: not for upstream
  local --> fixed: upstream did it another way
  draft --> review: internal MR
  review --> queued: gitum queue add
  queued --> sent: upstream MR
  sent --> merged: accepted as is
  sent --> merged_modified: accepted with edits
  sent --> rejected
  rejected --> draft: rework
  merged --> [*]: dropped by sync
  merged_modified --> [*]: dropped by sync
  fixed --> [*]: dropped by sync
  backport --> [*]: dropped when the base has it

gitum check-upstream finds accepted changes by patch-id or by similarity, and gitum forge refresh reads merge request states from the forge.

A series on its way upstream

flowchart TB
  BR["branch<br/>series/ntdll-fix"] -->|gitum series add| SE[("series in the journal:<br/>definition, patches,<br/>bugs, MR links")]
  SE -->|gitum port| VA["variant<br/>series/ntdll-fix@etersoft<br/>for our builds"]
  SE -->|gitum queue| QU["queue:<br/>one at a time,<br/>dependencies first"]
  QU -->|gitum prepare --push| PR["fork branch on a fresh master:<br/>new dates, no Co-Authored-By"]
  PR -->|MR by hand| UP["upstream"]
  UP -->|forge refresh,<br/>check-upstream| DONE["merged:<br/>dropped from the layers<br/>at the next sync"]
  VA -. gitum variants:<br/>do they diverge? .- SE

Requirements

Installation

pip install ./gitum

For ALT Linux, build the RPM package from gitum.spec with gear.

Quick start

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        # the next wine-X.Y tag, all layers
# on a conflict: fix the files in the printed work tree, git add, then
gitum continue                   # or gitum skip / gitum abort
gitum publish                    # push layers, history and journal

init takes the base of each layer from its <branch>-START tag, the tip of the parent layer, or the merge base with the upstream.

Scenarios

A robot updates a fork for every release

A robot (cron) runs:

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

A series on its way upstream

git checkout -b series/ntdll-fix winehq/master   # work on the series
gitum series add ntdll-fix --base upstream:winehq
gitum port ntdll-fix --to layer:etersoft         # a variant for our builds
gitum queue add ntdll-fix
gitum queue next                                 # what to send next
gitum prepare ntdll-fix --push fork              # a branch in the fork on a fresh master
gitum series set ntdll-fix state=sent pr=wine/wine!12345
gitum forge refresh                              # approved, merged
gitum check-upstream series:ntdll-fix --apply    # confirm the acceptance by content

Once the series is accepted, its changes leave the layers at the next sync and the queue moves on.

The merge request text travels with the series, images included. Write it once (or take it from the internal merge request) and render it for each forge:

gitum mr set ntdll-fix mr.md                     # mr.md and the local images it shows
gitum mr import ntdll-fix internal:etersoft/wine!7   # or: title, text and images of an MR
gitum mr render ntdll-fix --forge upstream -o out.md # without the internal parts

The first line # Title is the title. Lines between <!-- internal --> and <!-- /internal --> (or from <!-- internal --> to the end) are for internal review only and are left out unless --internal is given. With [series] mr-internal-separator = "---", mr set and mr import turn the first line --- of a text without markers into <!-- internal -->, for internal merge requests that put the review notes after such a line. The text is stored as series/NAME/mr.md, the images as series/NAME/assets/; series save and export leave them alone. For a GitLab forge with a token, render uploads the images to the project (--project, or the project of the series' pr/internal_mr link, or [forge.NAME] project, or the project of the series' upstream remote when it is on that forge's host) and remembers their URLs by content, so a new version of the text uploads only new or changed images. For other forges, or with --no-upload, the text gets placeholders {{gitum-asset:FILE}} and the images are written to <common dir>/gitum/mr/NAME/ (or DIR/NAME/ with --assets-dir DIR or [series] mr-assets-dir; listed under pending in --json); after uploading them some other way, gitum mr record NAME FILE=URL... --forge F makes later renders use those URLs. gitum does not open or edit merge requests itself: a person or an agent does that with the rendered text.

A distribution package

The changes of a package are commits in a layer on top of the real upstream history. gitum export layer:pkg --format spec --dir GEAR-REPO --spec pkg.spec writes the patches and the Patch lines of the spec file; gitum import pkg.spec --into pkg takes the existing patches of a package into the layer.

Configuration

The configuration is gitum.toml in the journal. Show it with gitum config show; replace it with gitum config load FILE.

version = 2

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

[layer.rebased]                 # rebuilt from wine-staging, not cherry-picked
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"]       # others go through candidate/*; a person (manual) is always trusted

[publish]
remote = "origin"
check = "./build.sh"             # must pass before the branches move

[series]
subject-lint = '^[a-z0-9_./-]+: [A-Z].*\.$'
bug-tracker = "etersoft"         # a bare bug number belongs to it
fixes-tracker = "winehq"         # its bugs are the Fixes: lines of a 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}"             # default: "#{n}" for bug-tracker, "{tracker} {n}" for others

[hooks]                          # get JSON events on stdin
change = "tools/notify-bugs.py"
run = "tools/notify-chat.sh"
publish = "tools/set-version-branches.sh"
series = "tools/comment-in-bugs.py"  # a series changed its state

Upstream options: - rewritten = true: the upstream is regenerated rather than continued, like a branch built by patchinstall. - prefix = "third-party/lib": the upstream is vendored inside your tree.

Commands

All commands take --json and --repo PATH (anywhere on the command line). Exit codes:

Code Meaning
0 success
1 error
2 needs a person: conflict, review, diverged history
10 something was updated (with --detailed-exitcode)
75 the repository is busy

Nothing asks questions interactively.

Command What it does
init Create the journal from existing branches. Layers are optional: for series only, give just the upstreams.
status [--changes] [--series [--variants]] Upstreams, layers, operations in progress, locks, the "change → fate → action" table, series as a tree by dependencies (state, base lag, claims, diverged variants).
sync [--to REF \| --next-release] [--step] [--candidate auto\|always\|never] [--publish] [--find-culprit] [--allow-rewrite] Move the stack onto a new upstream.
continue, skip, abort Resume, drop the conflicting change, or cancel a stopped sync.
accept, drop-candidate Publish or drop a result waiting for review in candidate/*.
save Record work done on layer branches with plain git.
publish, pull Share the state. Commits others pushed meanwhile are put on top; journals of parallel sessions merge.
change list\|show\|set\|confirm Changes and their fates: gitum change set ID fate=merged upstream_commit=SHA; gitum change confirm ID [SHA] accepts a suspect found by check-upstream.
check-upstream [layer:X\|series:Y] [--since REF] [--apply] Find our changes upstream: patch-id, then subject and diff similarity. A match counts as accepted with edits only when the content is verified (our change applied onto upstream changes nothing); anything else is suspect and waits for a person.
series add\|save\|list\|show\|set\|remove\|export\|import-staging\|import-json\|checkout\|claim\|release Series of changes. States: draft → review (internal) → queued → sent (version vN) → approved → merged, rejected or superseded. A series may sit at any depth of an export directory (export_dir); export touches only definition and the *.patch files the series wrote itself, so several series can share a directory: patch numbers continue after their dependencies, and the shared definition combines their Fixes. series save NAME always recounts a named series. When commits of a series on upstream:X were committed by someone who commits to that upstream (and not by you), the branch most likely stands on a newer upstream than the local ref: series add, save and import-json warn and exit with 2, and status shows the warning until the upstream is fetched and the series saved again. import-json never moves a series back to an earlier state when the series changed after the record (--force does). The first export of hand-made patches rewrites them in gitum's normalized form (From 0000…, no signature). claim marks a series as worked on by a session (GITUM_SESSION); other sessions get exit code 75 for it. pr and internal_mr take a URL or [forge:]group/project!N, resolved through [forge.NAME] (defaults: [series] pr-forge, internal-mr-forge); a forge with other paths sets mr-url = "{url}/{project}/merge_requests/{n}".
mr set\|import\|render\|record SERIES The merge request text of a series with its images; internal parts; images uploaded once per forge project (see "A series on its way upstream").
bugs [[TRACKER:]N] [--format md] [-o FILE] The bug index: bug (a link) → its series → state and version → merge requests → release once merged, sorted by tracker and number. A series lists its bugs as bugs=etersoft:19979,winehq:60741 (series set, import-json); a bare number belongs to [series] bug-tracker. With [series] fixes-tracker, the bugs of that tracker are written as Fixes: [N] lines of the definition (with the series title, its merge request title or its first subject as the text, unless a Fixes line already names them), and Fixes: [N] lines read by import-staging or set as fixes= become bugs. series list --bug B and status --series --bug B show the series of one bug; the queue markdown shows bugs as links, and sections = [..., "bugs"] adds the index to it. Dates in markdown are shown in the local time zone, or in [queue.markdown] timezone ("Europe/Moscow" or "+03:00").
port, variants A series on another base, and how its variants differ.
queue [next\|add\|pause\|resume] [--target T], queue --format md Upstream submission queues, one per target (series field target). One series at a time until it is approved or merged, dependencies first. pause --reason stops a queue. Generates UPSTREAM-QUEUE.md: [queue.markdown] sets its sections (sent, queued, review, draft, done), the fields shown as columns (fields, done-fields: any series field such as reviewer, release, merged_at) and all titles (titles).
prepare SERIES [--push FORK] [--force] Upstream form of a series: fresh base, new dates one second apart, no Co-Authored-By/Gitum-Id trailers, subject check, path remapping for vendored trees, a range-diff against the saved series. Dependencies must be merged first (--force includes them). You open the merge request yourself; forge refresh warns if its head differs from the prepared branch.
backport SHA\|URL --into LAYER Take an upstream commit with an Origin: trailer; it is dropped automatically once the base contains it.
branches [--remote FORK] [--prune] Series and upstream branches; stale ones after merge.
forge refresh\|import-json\|export-json Merge request states from GitHub, GitLab or Forgejo, or as JSON from your own scripts.
export, import Patch sets for RPM spec files (Patch lines between # BEGIN gitum patches / # END gitum patches), quilt, wine-staging.
log [CHANGE\|series:X\|layer:X], diff STATE1 [STATE2], report [RUN] [--html FILE] History and reports.
mirror NAME A shared mirror of an upstream in ~/.cache/gitum instead of throwaway clones.

Commands that gitum runs

gitum runs your commands for conflict resolution, checks, generated layers and hooks. None of them is interactive. They all get the same GITUM_* environment:

Variable Value
GITUM_REPO the main work tree of the repository
GITUM_COMMON_DIR the git common directory
GITUM_WORKTREE the private work tree of the operation (sync only)
GITUM_RUN, GITUM_RUN_DIR the run id and its log directory
GITUM_LAYER the layer being processed
GITUM_UPSTREAM, GITUM_TARGET, GITUM_TARGET_TAG, GITUM_VERSION the upstream, the target commit, its release tag and the version from the tag (wine-11.20 → 11.20)
GITUM_LOCK_FILE the repository lock file
GITUM_LOCK_HELD set to the lock file while gitum holds the lock
Command Working directory Additional variables Result
resolve.rules, resolve.agent the private work tree with the conflict GITUM_COMMIT, GITUM_SUBJECT, GITUM_FILES (one per line), GITUM_CONFLICT_DIR, GITUM_RESOLVER exit 0 with all conflicts resolved and git added
publish.check the private work tree at the top of each leaf layer GITUM_TIP exit 0, or the branches stay unchanged
generator = "command" the private work tree at the new base GITUM_BASE new commits on HEAD
post-command of a generated layer the private work tree commits; leftovers are committed by gitum
hooks.change, hooks.run, hooks.publish, hooks.series the main work tree GITUM_EVENT; for hooks.series also GITUM_SERIES, GITUM_STATE, GITUM_PREVIOUS_STATE JSON event on stdin; failures never fail the run

Hooks: - hooks.change gets one event per change that was not simply applied. - hooks.run gets the whole sync report (event is sync or candidate). - hooks.series runs when a series changes its state (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 itself never writes to bug trackers. - hooks.publish runs after a successful gitum publish. Its event looks like this:

{"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 before the push>", "new": "<sha>",
                         "base": "<sha>", "start_tag": "rebased-etersoft-START"}},
 "integrated": {"etersoft": ["subjects of commits put on top"]}}

The lock is a stable interface for your own scripts. - It is an exclusive flock on $GIT_COMMON_DIR/gitum/lock. - While it is held, the file contains the owner as JSON: {"pid", "host", "user", "session", "since"}. The session comes from GITUM_SESSION. - Commands run by gitum get GITUM_LOCK_HELD, and a gitum started from them does not lock again.

Checked-out branches. A layer branch checked out in a clean work tree moves together with its work tree. If that work tree has local changes, gitum stops with exit code 2 and changes nothing.

Reliability

gitum may be interrupted at any moment — killed, out of memory, power loss — and nothing is lost or left half-done:

gitum status shows an interrupted operation and what to do about it. The test suite kills gitum at many points (GITUM_TEST_CRASH) and checks that gitum continue reaches exactly the result of an uninterrupted run.

Development

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

Why gitum is built this way, and the decisions behind it, are in docs/design.md.

License

GPL-2.0-or-later. See gitum/COPYING.