jj / How jj next works

How jj next works

jj next moves the working copy forward along the commit graph. By default it starts a new empty working-copy commit on top of a child of @'s parent. With --edit it edits a child of @ directly instead.

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. 7. Errors and edge cases
  8. 8. Worked example

In short: jj decides whether to edit (--edit, --no-edit, or ui.movement.edit). In new-commit mode @ must be a head. It builds a target revset: offset generations below the start (@'s parents, excluding @ itself, or @ when editing). With --conflict, it takes the first conflicted commits from there instead. Exactly one target is used directly. Several targets prompt for a choice, and none is an error. Then one transaction either checks out a new commit on the target (next: A -> B) or edits it (next: A -> editing B).

jj next is a thin wrapper: cmd_next calls the shared move_to_commit with Direction::Next. jj prev is the same code going the other way. The two outcomes are the same as jj new and jj edit.

1. Command-line surface

FlagTypeDefaultMeaning
OFFSET (positional)unsigned integer1How many generations to move forward (source).
-e, --editbooloffEdit the target, like jj edit. Overrides ui.movement.edit = false (source).
-n, --no-editbooloffCreate a new commit on the target, like jj new. Overrides ui.movement.edit = true. Conflicts with --edit (source).
--conflictbooloffJump to the next conflicted descendant. Conflicts with OFFSET (source).

NextArgs (the help text includes the two ASCII diagrams)

2. Configuration

KeyDefaultEffect
ui.movement.editfalseEdit mode by default. The effective mode is --edit || (!--no-edit && ui.movement.edit) (default, source).
revset-aliases."immutable_heads()"(built-in)In edit mode the target must be mutable (source). New-commit mode can target immutable commits.

3. End-to-end flow

jj next: call flow cmd_next calls move_to_commit with Direction::Next. It loads the workspace, needs a working copy, and resolves edit mode. get_target_commit requires @ to be a head when not editing, starts from @ (edit) or @'s parents (new), and builds the target revset: descendants at offset from @, or children of the parents minus @ then descendants at offset minus one. --conflict takes the roots of conflicted descendants. One target is used, several prompt, none fails. In edit mode the target is checked rewritable and edited. Otherwise a new commit is checked out on it. tx.finish records "next: A -> B" or "next: A -> editing B". cmd_next → move_to_commit(Next): load workspace (snapshot); needs a working copy should_edit = --edit || (!--no-edit && ui.movement.edit) not editing and @ isn't a head → "The working copy must not have any children" start = @ (edit) or parents(@) (new commit) build_target_revset edit: descendants_at(@, offset) new: descendants_at(children(parents(@)) ~ @, offset − 1) --conflict: roots(descendants(that) & has_conflict) evaluate: one → target; several → "ambiguous next commit, choose one to target:" prompt none → "No other descendant found N commit(s) forward from the working copy parent(s)" edit mode check_rewritable(target); tx.edit(target) "next: A -> editing B" new-commit mode tx.check_out(target): new empty child, edit it "next: A -> B"

4. Core architecture touchpoints

StructureRead / writeHowExplained on
View.wc_commit_idsread, writeThe current @ is the starting point. It is replaced by the target (edit) or by a new child of it (check_out / edit).view
View.head_idsread, write"@ is a head" is the new-commit precondition. The target is added as a head.view
Revset enginereaddescendants_at, children, roots and the HasConflict filter.revsets
Commitsmaybe writeNew-commit mode writes one empty commit with the target's tree. The old @ is abandoned if it's discardable, unreferenced and a head (maybe_abandon_wc_commit).commits
Working copywriteUpdated to the new @ when the transaction finishes.working copy

5. Transaction and operation

  • Transaction: one, started after the target is chosen (edit, new).
  • Operation description: next: <old @ short id> -> <target short id>, or next: <old> -> editing <target> in edit mode (source). The second ID is the target commit, not the new working-copy commit.
  • Commits created: one empty working-copy commit in new-commit mode. None in edit mode.
  • Commits abandoned: the old @, if it was discardable and nothing else points at it.

6. Algorithm and invariants

The two modes count from different places (source). In edit mode the start is @, and the target is its descendants exactly offset generations down. In new-commit mode @ is a leaf sitting beside its siblings. The start is @'s parents, the first step is their other children (@ is excluded), and then offset − 1 more generations. So jj next after jj new X lands on top of a child of X.

With --conflict (source), the offset is left at its default of 1, since clap rejects passing both. The result is the roots of the conflicted commits at or below that first step: the nearest conflicts, one per branch.

Ambiguity is resolved by a prompt that lists the candidates numbered from 1 and takes an index or a unique change ID prefix. There is no quit choice: EOF cancels with an error, and without a terminal the prompt fails (choose_commit, parse_index_or_change_id_choice, test_next_choose_child_by_id).

Invariants: new-commit mode never leaves @ with children, which is why it requires @ to be a head. Edit mode never edits an immutable commit. Neither mode rewrites an existing commit.

7. Errors and edge cases

MessageWhenSource
The working copy must not have any children, hint Create a new commit on top of this one or use `--edit`New-commit mode while @ has descendants.movement_util.rs
No other descendant found N commit(s) forward from the working copy parent(s)New-commit mode, nothing that far down. The hint lists each parent.movement_util.rs
No descendant found N commit(s) forward from the working copyThe same, in edit mode.movement_util.rs
The working copy parent(s) have no other descendants with conflicts / The working copy has no descendants with conflicts--conflict found nothing.movement_util.rs
ambiguous next commit, choose one to target:, then Prompt canceled by EOF or a "cannot prompt" errorSeveral targets, and input ends or there's no terminal.movement_util.rs
Commit … is immutableEdit mode on an immutable target.movement_util.rs
This command requires a working copyThe workspace has no working-copy commit.movement_util.rs

8. Worked example

From upstream's test_next_simple. After jj new @--, @ (royxmykx) is an empty child of second, beside third. Then jj next runs:

jj next in new-commit mode Before: root, first, second, then two children of second: third and the empty working copy royxmykx. After jj next: royxmykx was abandoned because it was empty, and a new empty working-copy commit vruxwmqv sits on top of third. before after jj next root first second third @ royxmykx empty root first second third @ vruxwmqv new, empty

Upstream's snapshot:

$ jj next
Working copy  (@) now at: vruxwmqv 01f06d39 (empty) (no description set)
Parent commit (@-)      : kkmpptxz 7576de42 (empty) third

The start was @'s parent second. Its children other than @ are just third, and offset − 1 = 0 more steps leaves third. The old @ was empty and unreferenced, so it was abandoned. If second had two other children, jj would have prompted instead, as test_next_fails_on_bookmarking_children_no_stdin shows.