Branching and Merging
runic supports a branched revision DAG, similar to Alembic. Branches allow separate lines of development to evolve independently and be merged before deployment.
When branches appear
A branch is created whenever two revisions share the same down_revision:
┌── c1d2e3f4 (add vector index)
3f9a12c1 ──────┤
└── 7b3d9e2f (add fulltext index)This happens when:
- Two developers create new revisions at the same time from the same head.
- You explicitly create a revision off an older revision using
--head <older-rev-id>.
Detecting multiple heads
runic heads lists all head revisions:
$ runic heads
c1d2e3f4 add vector index (MULTIPLE HEADS — use merge to resolve)
7b3d9e2f add fulltext index (MULTIPLE HEADS — use merge to resolve)While multiple heads exist, runic upgrade (without an explicit target) will raise an error. Specify an explicit revision to apply one branch at a time, or create a merge revision to converge the two lines.
Creating a revision on a specific branch
Use --head <rev-id> with runic revision to create a new revision that extends a specific existing revision rather than the current head:
# Current head: 7b3d9e2f
# Create a revision branching off 3f9a12c1 instead:
$ runic revision -m "add vector index" --head 3f9a12c1
Created revision: runic/versions/c1d2e3f4_add_vector_index.pyThe new revision file will contain:
revision = "c1d2e3f4"
down_revision = "3f9a12c1"Branch labels
Assign a symbolic name to a branch with --branch-label:
$ runic revision -m "start feature-x" --branch-label feature-x
Created revision: runic/versions/a1b2c3d4_start_feature_x.pyBranch labels can be used as targets in upgrade and downgrade:
$ runic upgrade feature-x # upgrade to the head of branch feature-xrunic branches lists all branch points:
$ runic branches
3f9a12c1 add person email index ['7b3d9e2f', 'c1d2e3f4']Merging branches
Use runic merge to create a merge revision that declares two heads as its down_revision tuple:
$ runic merge 7b3d9e2f c1d2e3f4 -m "merge fulltext and vector indexes"
Created revision: runic/versions/fa2b3c4d_merge_fulltext_and_vector_indexes.pyThe generated file:
revision = "fa2b3c4d"
down_revision = ("7b3d9e2f", "c1d2e3f4")
branch_labels = []
depends_on = []
irreversible = False
snapshot = False
def upgrade(op) -> None:
pass # merge revisions usually have no operations
def downgrade(op) -> None:
passAfter the merge revision there is a single head again:
3f9a12c1 ──┬── 7b3d9e2f ──┐
└── c1d2e3f4 ──┴── fa2b3c4d (head)Applying across a merge
When upgrading from a state before the branch point, runic uses a topological sort (Kahn's BFS) to produce a valid application order. Both branch legs are applied in dependency order before the merge revision:
$ runic upgrade
# Applies: 7b3d9e2f, then c1d2e3f4, then fa2b3c4dThe exact order of the two branch legs (7b3d9e2f vs c1d2e3f4) is determined by BFS from the merge node, but both are guaranteed to run before fa2b3c4d.
Cross-branch dependencies
If a revision depends on another revision in a different branch (without a shared ancestor), use depends_on in the revision metadata or --depends-on on the CLI:
revision = "b5c6d7e8"
down_revision = "7b3d9e2f"
depends_on = ["c1d2e3f4"] # must also be applied before this runs$ runic revision -m "combined feature" --depends-on c1d2e3f4runic's topological sort respects depends_on edges in addition to down_revision edges.
Resolving accidental branches
When two developers push revisions from the same head simultaneously, the result is an accidental branch. The standard fix is:
- Both developers push their revision files.
- One developer runs
runic mergeagainst the two new heads. - The merge revision is committed alongside the two branch revisions.
- The pipeline runs
runic upgradeand all three revisions are applied.
See also
- integration — inspecting heads and branch points
- cli_reference —
merge,heads,branchescommand details