jj / How jj arrange works

How jj arrange works

jj arrange opens a full-screen terminal UI over a set of commits. In it you can reorder commits by swapping them with a parent or child, and mark commits to abandon. Confirming turns the edited graph into a rewrite plan, applied in one transaction.

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. The state model
  8. Swapping two commits
  9. Executing the plan
  10. 7. Errors and edge cases
  11. 8. Worked example

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

FlagTypeDefaultMeaning
REVSETS (positional), -r (hidden alias)revsets, repeatablerevsets.arrangeThe commits to arrange. Both forms are unioned (source).

Keys inside the UI (help line, handling):

KeyEffect
↓ / j, ↑ / kMove the selection down or up the list of target commits.
⇧↓ / JSwap the selected commit with its parent. Only when it has exactly one parent and that parent is in the set (source).
⇧↑ / KSwap the selected commit with its child. Only when it has exactly one child and that child is in the set (source).
a / pMark the selected commit to abandon, or back to keep.
cConfirm and apply.
q, Ctrl-CQuit without changes. The command then fails with Canceled by user.

2. Configuration

KeyDefaultEffect
revsets.arrangereachable(@, mutable())The commits to arrange when none are given (default, source).
templates.arrangebuiltin_log_compactHow 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

jj arrange: call flow cmd_arrange resolves the target revset, checks it is rewritable, and fails if connected(targets) minus targets is non-empty. It loads the external children and returns "No revisions to arrange." for an empty set. It enters the alternate screen and raw mode, builds State with the targets, external children and external parents, and runs the TUI loop: draw, then apply every key press waiting to be read, each to a copy of the state, before drawing again. q or Ctrl-C returns nothing and the command fails with "Canceled by user". c returns the state, which becomes a RewritePlan executed in parents-first order inside one transaction, recorded as "arrange revisions". resolve targets (REVSETS ∪ -r, or revsets.arrange); check_rewritable_expr connected(targets) ~ targets non-empty → "Cannot arrange revset with gaps in." external children = children(targets) ~ targets no targets → "No revisions to arrange." and return State::new commits: targets + external children + external parents, each with action Keep and a copy of its parent list head_order, current_order (topo) run_tui loop (alternate screen, raw mode) draw rows with renderdag + templates.arrange key → handle_key_event(copy of state) changed → keep it, recompute order q / Ctrl-C → None; c → Some(state) to_rewrite_plan one Rewrite per target and external child: old commit, planned parents, Keep/Abandon (external parents are never rewritten) RewritePlan::execute (parents first) parents = mut_repo.new_parents(planned) Abandon → rewriter.abandon() Keep and parents changed → rebase().write() leave the alternate screen; confirmed → start_transaction, execute, tx.finish("arrange revisions"); else "Canceled by user"

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Revset enginereadconnected() for the gap check, children() for the external children.revsets
Commits (parents, trees)read, writeKept 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 recordswritemut_repo.new_parents maps planned parents to their already-rewritten versions, which is why the plan runs parents first (source).transactions
Bookmarks, @writeFollow 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

MessageWhenSource
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 userYou 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 immutableA 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:

jj arrange: move C onto A and abandon it Before: root, A, B, C, D in a line. The plan sets C's parents to A and its action to Abandon. After executing: C is abandoned, so D, whose planned parent is still C, is rebased onto C's new parent A. The visible heads are B and the new D. before after confirming root A B C planned: parent A, abandon D root A B head D′ rebased onto A C abandoned

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.