In short: jj takes the search space (-r, or revsets.converge =
mutable() & divergent()) and groups it by change ID. If several changes diverge it
makes you pick one, or errors in --no-interactive mode. For the chosen change it builds
the truncated evolution graph: the change's commits linked by predecessor edges, starting from
where the divergence forked. For each attribute it finds the dominator value in that history
and merges each divergent commit's value against it, accepting a trivial result. Parents are settled
first, then the trees are rebased onto those parents and merged the same way. The solution commit gets
the change ID, the divergent commits as predecessors, and replaces them. Descendants are rebased. The
operation is converge <change> with N predecessors.
The command is cmd_converge, driving
Converge::run. The algorithm lives in jj-lib's
converge_change, whose module doc points at upstream's
design note docs/design/jj-converge-command.md (source).
How predecessors are recorded is on the commits page,
and how trees are merged on the trees page.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
-r, --revision, --revisions REVSETS | revsets, repeatable | revsets.converge | The search space for divergent commits (source). Only commits inside it are replaced. Others with the same change ID stay divergent (help). |
--no-interactive | bool | off | Never prompt. If anything can't be decided automatically, print what's missing and fail without changes (source). |
-i, --interactive | bool, hidden | off | A no-op partner for --no-interactive (source). |
ConvergeArgs (with the full help text)
2. Configuration
| Key | Default | Effect |
|---|---|---|
revsets.converge | mutable() & divergent() | The default search space (default, source). |
ui.editor | (built-in) | Used if you choose to merge conflicting descriptions in an editor. The temp file is named .jj-converge-description (source). |
revset-aliases."immutable_heads()" | (built-in) | The whole search space must be mutable (source). |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Commit predecessors (evolution) | read, write | Walked with walk_predecessors to build the history (source). The solution records every divergent commit as a predecessor, so jj evolog shows them merging. | commits |
| Operation log | read | walk_predecessors may walk back through operations to find old predecessors (see the note in source). | operations |
| Trees | read, write | Each history commit's tree is rebased onto the solution parents, then merged with MergedTree::merge. The result may be conflicted (source). | trees |
| Rewrite records | write | set_rewritten_commit(divergent → solution) for each divergent commit, then rebase_descendants. | transactions |
| Index (ancestry) | read | remove_descendants uses roots() to drop divergent commits that descend from other divergent commits before choosing parents (source). | index |
5. Transaction and operation
- Transaction: one, started before divergent changes are even looked up (source). The early exits (no divergence, abort) don't finish it, so they record nothing.
- Operation description:
converge <short change ID> with <N> predecessors, where N counts the change's divergent commits in the search space (source). - Commits created: exactly one solution commit. Descendants of the divergent commits are rebased onto it.
- One change per run: if several changes diverge, a hint says how many remain (source).
jj undorestores the divergence. The help text suggestsjj op show -pto review the result first.
6. Algorithm and invariants
The truncated evolution graph
TruncatedEvolutionGraph::new walks predecessors
from the divergent commits. It ignores commits with other change IDs, and stops once every pending
commit is accounted for and an initial commit (one with no same-change predecessors) has been
reached. Edges point from predecessor to successor. If several initial commits are found, a virtual
start node (the root commit ID) is added above them, so the graph is a flow graph with one start.
Merging an attribute against its dominator value
For author, description and parents, create_value_merge
computes each history commit's value and finds the closest common dominator value of the
divergent commits' values, in the graph of value transitions
(find_dominator_value_with_value_cache). That
value D is what the history agreed on before it forked. The merge is
D + (v₁ − D) + (v₂ − D) + …. If it resolves trivially (with SameChange::Accept,
so identical edits count once), the attribute is Solved. Otherwise it is
Unsolved, carrying a base commit that produces D. When several commits produce D, the base
is the one with the lowest change offset, then input order, then newest committer time, then commit ID
(get_value_producer).
Parents are special. Divergent commits that descend from another divergent commit are left out,
since using their parents would create a cycle once everything is rebased onto the solution
(converge_parents).
Trees
The tree is only computed once parents are solved (source).
converge_trees rebases every history commit's
tree onto the solution parents: P + (X.tree − X.parents), or X.tree if X
already has those parents. It finds the dominator of those rebased tree IDs (labels ignored, to find a
closer common value), takes a commit F producing it, and returns
F′ + (A′ − F′) + (B′ − F′) + …. The source comment at
L459 gives the derivation.
Prompts
generic_solver accepts Solved values.
For Unsolved ones it prompts, or returns nothing with --no-interactive. The
change prompt lists the divergent change IDs and takes an index or a unique change ID prefix; EOF
cancels it with an error (choose_change,
parse_index_or_change_id_choice). The
author and parent prompts list the distinct values and ask for an index (q aborts). The
description prompt offers to open an editor on a conflicted description built from the divergent
descriptions against the base commit's, or to keep the conflict markers as is
(source, materialize_conflicted_description).
Invariants: exactly one change is converged per run. Only divergent commits in the search space are replaced. The solution keeps the change ID. Nothing is written unless author, description, parents and tree are all decided.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No revisions were found to be divergent with each other in revsets.converge: REVSET / … among the specified revisions. | Nothing diverges in the search space (default or -r). Not an error. A hint follows: if some commit in the search space is divergent with commits outside it, it suggests jj converge -r 'change_id(ID)' for one such change; otherwise it explains that divergent revisions must share a change ID. | converge.rs, test |
Cannot automatically choose which change to converge | Several divergent changes with --no-interactive. The hint suggests a narrower revset. | converge.rs |
Could not determine which author/description/parents to use. then Could not converge change | An attribute stayed unsolved, for example in non-interactive mode. | converge.rs |
Failed to converge tree | The second converge_change call still produced no tree. | converge.rs |
Aborting... nothing changed. | You chose q at an author or parent prompt. | converge.rs |
Prompt canceled by EOF | Input ended at the change prompt, which has no q choice. | converge.rs |
Commit … is immutable | The search space includes immutable commits. | converge.rs |
| (conflicts in the solution) | The tree merge didn't resolve. The help text warns about this. | help |
8. Worked example
From upstream's test_converge_simple. Change zsuskuln
("b2") was rebased from a onto c, but its old version is still visible through
bookmark b1, with child d on top. So the change has two visible commits:
59a77004 on a and 2c2bd25d on c. The evolog is
b2852eb2 → 59a77004 → 2c2bd25d.
Upstream's snapshot, which needed no prompts:
$ jj converge
Found 1 divergent change(s) in the specified revset:
- Change: zsuskulnrvyr with 2 commits:
zsuskuln/0 2c2bd25d b2 | (divergent) b2
zsuskuln/1 59a77004 b1 | (divergent) b2
Attempting to converge change zsuskulnrvyr...
Successfully converged change: created commit 6b1ce5bc4cbe.
Rebased 1 descendants
Working copy (@) now at: znkkpsqq 696cf5e0 d | d
Parent commit (@-) : zsuskuln 6b1ce5bc b1 b2 | b2
Added 1 files, modified 0 files, removed 0 files
Here 59a77004 is itself a predecessor of 2c2bd25d. For the parents, the
history agreed on [a], and only one side moved to [c], so the merge
[a] + ([a] − [a]) + ([c] − [a]) resolves to [c]. The descriptions are all
"b2", and the tree follows the same pattern. Afterwards the evolog shows 6b1ce5bc with
both old commits as predecessors (test).