jj / How jj parallelize works

How jj parallelize works

jj parallelize turns a chain of commits into siblings. Each target keeps its own changes and its parents from outside the set, but no longer depends on the other targets. Commits that used to sit on top of a target become merges of the targets they depended on.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. Transaction and operation
  6. 6. Algorithm and invariants
  7. New parents for targets
  8. New parents for non-target children
  9. Rebasing
  10. 7. Errors and edge cases
  11. 8. Worked example

In short: jj evaluates the target set and works through it parents-first. Each target's new parents are its old parents, with any target parent replaced by that parent's own new parents. Only the targets whose parents change must be mutable. A second map gives each target the set of targets that a non-target child must now merge. In one transform_descendants pass, targets get their new parents, non-target children of targets get the merged parent sets, and every commit whose parents changed is rebased, which keeps its own diff. The operation is parallelize N commits.

The command is cmd_parallelize. It uses the same rewrite machinery as jj rebase: a commit's tree is recomputed by merging its diff onto its new parents. How a rebased commit's tree is computed is on the trees page.

1. Command-line surface

FlagTypeDefaultMeaning
REVSETS (positional), -r (hidden alias)revsets, repeatablenoneThe commits to parallelize. Both forms are unioned (source).

The help text gives the canonical example, jj parallelize 1::2, and explains why jj parallelize '1 | 3' is a no-op: the commit 2 between them isn't in the set, so it still orders them (source).

ParallelizeArgs

2. Configuration

KeyDefaultEffect
revset-aliases."immutable_heads()"(built-in)Only targets whose parents change are checked. A target whose parents are all outside the set keeps its parents, so it may be immutable (source, test).

The command reads no other settings.

3. End-to-end flow

jj parallelize: call flow cmd_parallelize evaluates the targets, ordered children first, and walks them parents first to compute new_target_parents: a target parent is replaced by that parent's new parents. The targets that changed are checked rewritable. In a transaction it computes new_child_parents, the set of targets each target's non-target children must now have as parents. transform_descendants then sets new parents on targets, and on non-target children of targets, and rebases any commit whose parents changed. tx.finish records "parallelize N commits". load workspace (snapshot); targets = REVSETS ∪ -r, evaluated children-first new_target_parents (parents first): each old parent, or that parent's new parents if it's a target targets whose parents changed → needs_rewrite check_rewritable(needs_rewrite) start_transaction; new_child_parents (parents first) for target T: the union of new_child_parents of T's target parents, plus T itself = every target a child of T used to depend on through the chain transform_descendants(targets) target → set_new_rewritten_parents(new_target_parents[T]) non-target with a target parent → replace each target parent P with new_child_parents[P] parents changed → rebase() (merge its own diff onto the new parents) and write tx.finish(ui, "parallelize N commits")

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Commit parentsread, writeThe whole point. Targets lose their target parents, and their non-target children gain several parents.commits
Treeswriterewriter.rebase() recomputes each rewritten commit's tree by applying its old diff to the new parents' merged tree. Removing a dependency can therefore create conflicts.trees
Rewrite recordswriteEvery rewritten commit records its predecessor. Bookmarks and @ follow the rewritten commits.transactions
OperationwriteOne operation.operations

5. Transaction and operation

  • Transaction: one, after the rewritability check (source).
  • Operation description: parallelize <N> commits, where N counts every target, including those left unchanged (source).
  • Commits rewritten: targets with a target parent, non-target children of targets, and all their descendants (rebased with new IDs).
  • Commits unchanged: targets whose parents are all outside the set. In upstream's tests, target 1 keeps its commit ID (test).

6. Algorithm and invariants

New parents for targets

Targets are processed parents-first (source). For each parent of target T: if the parent is a target, T takes that parent's already computed new parents. Otherwise T keeps the parent. In a chain 0 ← 1 ← 2 ← 3 with targets 1::3, that gives 1 → [0], 2 → [0] and 3 → [0], so all three become children of 0.

New parents for non-target children

A non-target commit that sat on a target used to see every target below it in its ancestry. To keep those ancestors, new_child_parents maps each target to itself plus everything its target parents mapped to. For the chain above, that maps 3 to {1, 2, 3}. A child of 3 therefore becomes a merge of 1, 2 and 3 (source). This is why the help text says descendants outside the set "remain descendants".

Rebasing

Only commits whose parents changed are written (source). They use rebase(), not reparent(), so each commit keeps the changes it introduced, not its old snapshot. A target that depended on an earlier target's edits can therefore end up conflicted.

Invariants: ancestry with commits outside the set is preserved in both directions. Ancestry among targets is removed. Every changed target is mutable.

7. Errors and edge cases

MessageWhenSource
Commit <id> is immutable, with This operation would rewrite N immutable commits.A target whose parents would change is immutable.parallelize.rs, test
Nothing changed.No commit's parents changed, for example with one target, disconnected targets, or '1 | 3' as in the help.cli_util.rs, help text
(conflicts after the rebase)A target that depended on another target's changes. jj doesn't refuse this. The result just shows conflicts.parallelize.rs

8. Worked example

From upstream's test_parallelize_with_descendants_simple. A linear stack 1 → 2 → 3 → 4 → 5 → 6 (@). Then jj parallelize 'subject(1)::subject(4)' runs:

jj parallelize 1::4 Before: a linear chain root, 1, 2, 3, 4, 5, 6. After parallelizing 1 through 4: commits 1, 2, 3 and 4 are all children of root, 5 is a merge of 1, 2, 3 and 4, and 6 stays on 5. Commit 1 keeps its ID 884fe9b9 because its parent didn't change. before after parallelize 1::4 root 1 2 3 4 5 @ 6 root 1 884fe9b9 2 3 4 5 merge of 1 2 3 4 @ 6 rebased

Upstream's log after the command (template: commit ID, description, parents' descriptions):

@  75ac07d7dedc 6 parents: 5
○        39791a4c42c5 5 parents: 1 2 3 4
├─┬─┬─╮
│ │ │ ○  823bf930aefb 4 parents:
│ │ ○ │  3b6586259aa9 3 parents:
│ │ ├─╯
│ ○ │  dfd927ce07c0 2 parents:
│ ├─╯
○ │  884fe9b9c656 1 parents:
├─╯
◆  000000000000 parents:

1 keeps its ID because its parent (the root) is outside the set. 2, 3 and 4 now sit on the root. 5 was a non-target child of 4, and new_child_parents[4] = {1, 2, 3, 4}, so it becomes a four-way merge. When a non-target child hangs off an interior target, it merges only the targets up to that point, as in test_parallelize_where_interior_has_non_target_children (2c parents: 1 2).