In short: jj resolves the revisions (default revsets.arrange, which is
reachable(@, mutable())), checks they are mutable, and refuses a set with gaps. It loads
the set plus its external parents and children as context, and builds an in-memory State
where each commit has a planned parent list and a keep/abandon action. Key presses only edit that state.
On c it builds a RewritePlan and executes it parents-first: abandon, or rebase
onto the new parents if they changed. The operation is arrange revisions. q
or Ctrl-C discards everything.
The command is cmd_arrange. Nothing touches the
repository until you confirm. The UI works on a copy of the graph, and the only write path is
RewritePlan::execute. For non-interactive
reordering, see jj rebase.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
REVSETS (positional), -r (hidden alias) | revsets, repeatable | revsets.arrange | The commits to arrange. Both forms are unioned (source). |
Keys inside the UI (help line, handling):
| Key | Effect |
|---|---|
↓ / j, ↑ / k | Move the selection down or up the list of target commits. |
⇧↓ / J | Swap the selected commit with its parent. Only when it has exactly one parent and that parent is in the set (source). |
⇧↑ / K | Swap the selected commit with its child. Only when it has exactly one child and that child is in the set (source). |
a / p | Mark the selected commit to abandon, or back to keep. |
c | Confirm and apply. |
q, Ctrl-C | Quit without changes. The command then fails with Canceled by user. |
2. Configuration
| Key | Default | Effect |
|---|---|---|
revsets.arrange | reachable(@, mutable()) | The commits to arrange when none are given (default, source). |
templates.arrange | builtin_log_compact | How each commit row is rendered in the UI (default, source). |
colour label arrange context commit | { dim = true } | External parents and children are drawn dimmed (default). |
revset-aliases."immutable_heads()" | (built-in) | Every target must be mutable (source). |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Revset engine | read | connected() for the gap check, children() for the external children. | revsets |
| Commits (parents, trees) | read, write | Kept commits whose parents changed are rebased, so each keeps its own diff and may conflict. Abandoned commits are recorded with their planned parents, and their descendants follow. | commits, trees |
| Rewrite records | write | mut_repo.new_parents maps planned parents to their already-rewritten versions, which is why the plan runs parents first (source). | transactions |
Bookmarks, @ | write | Follow rewritten and abandoned commits when the transaction finishes. | view |
| Terminal | — | crossterm's alternate screen and raw mode, with ratatui for layout and renderdag for the graph column (source). | — |
5. Transaction and operation
- Transaction: one, started only after you confirm (source). Quitting starts none.
- Operation description:
arrange revisions. - Commits rewritten: kept commits whose planned parents differ from their old ones (including external children), plus the commits you marked to abandon.
- Unchanged: a commit keeps its ID only if its planned parents are unchanged and none of them was rewritten. Descendants beyond the external children are rebased when the transaction finishes.
6. Algorithm and invariants
The state model
State holds every loaded commit with its planned
parents and action. It also holds head_order (heads of the set, in input order, for a
stable display), current_order (the display order of the targets), the selection, and the
external children and parents. update_commit_order recomputes the display order with a
reverse topological walk over planned parents, ignoring external parents
(source). Each key press works on a copy of the state,
which is kept only if it changed (source). After the first key the loop keeps reading while event::poll reports
more input, so fast typing doesn't trigger a redraw per key (source).
Swapping two commits
swap_commits(a, b) exchanges the two commits'
positions in the graph. It swaps them in the display order and in head_order, rewrites every
commit's parent list so that references to a point to b and vice versa, and
then swaps a's and b's own parent lists. The result is that the two commits
trade places while everything around them stays attached to the same position. Upstream's
unit test swaps a merge D with its parent
C: C becomes the merge of B and D.
Executing the plan
execute orders the rewrites with a forward
topological walk over planned parents, so a commit is handled after its new parents. For each
one, it maps the planned parents through mut_repo.new_parents to their rewritten versions,
builds a CommitRewriter, and either abandons the commit or, if the parents changed,
rebases it. A gap in the set would leave a commit outside it between two targets, unable to move with
them, which is why gaps are rejected up front.
Invariants: nothing is written unless you confirm. External parents are never rewritten. Every target is mutable.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Cannot arrange revset with gaps in., hint Revision <id> would need to be in the set. | Some commit lies between two targets but isn't one. | arrange.rs, test |
No revisions to arrange. | The set is empty. This is a status message, not an error. | arrange.rs, test |
Canceled by user | You pressed q or Ctrl-C. | arrange.rs |
Failed to draw TUI: … / Failed to read TUI events: … | Terminal I/O errors, reported as internal errors. | arrange.rs |
Commit … is immutable | A target is immutable. | arrange.rs |
| (swap key ignored) | The selected commit has zero or several parents (or children), or its neighbour is external. | arrange.rs |
8. Worked example
The interactive part has no CLI snapshot upstream, so this example uses the plan executor's unit test
test_execute_plan_abandon. In a stack
A → B → C → D, C is moved onto A and marked to abandon:
The test asserts that only D is rewritten, that its new parent is A, and that
the heads are now B and the new D (source).
C is processed before D because D's planned parent is
C. Abandoning C records A as its replacement, so
new_parents([C]) returns [A] for D.
The CLI test covers the checks that run before the UI opens (source):
$ jj arrange none()
No revisions to arrange.
$ jj arrange a c
Error: Cannot arrange revset with gaps in.
Hint: Revision 123b4d91f6e5 would need to be in the set.