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
| Flag | Type | Default | Meaning |
|---|---|---|---|
OFFSET (positional) | unsigned integer | 1 | How many generations to move forward (source). |
-e, --edit | bool | off | Edit the target, like jj edit. Overrides ui.movement.edit = false (source). |
-n, --no-edit | bool | off | Create a new commit on the target, like jj new. Overrides ui.movement.edit = true. Conflicts with --edit (source). |
--conflict | bool | off | Jump to the next conflicted descendant. Conflicts with OFFSET (source). |
NextArgs (the help text includes the two ASCII diagrams)
2. Configuration
| Key | Default | Effect |
|---|---|---|
ui.movement.edit | false | Edit 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
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
View.wc_commit_ids | read, write | The 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_ids | read, write | "@ is a head" is the new-commit precondition. The target is added as a head. | view |
| Revset engine | read | descendants_at, children, roots and the HasConflict filter. | revsets |
| Commits | maybe write | New-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 copy | write | Updated 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>, ornext: <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
| Message | When | Source |
|---|---|---|
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 copy | The 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" error | Several targets, and input ends or there's no terminal. | movement_util.rs |
Commit … is immutable | Edit mode on an immutable target. | movement_util.rs |
This command requires a working copy | The 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:
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.