jj / How jj prev works

How jj prev works

jj prev moves the working copy backward along the commit graph. By default it starts a new empty working-copy commit on top of an ancestor of @'s parent. With --edit it edits an ancestor 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. The target is the ancestors exactly offset generations above the start: @'s parents, or @ when editing. With --conflict, it is the nearest conflicted ancestors instead. One target is used, several prompt for a choice, and none is an error. One transaction then checks out a new commit on the target (prev: A -> B) or edits it (prev: A -> editing B).

cmd_prev calls the shared move_to_commit with Direction::Prev. The code, preconditions and outcomes are those of jj next, except for how the target revset is built. This page covers what differs and repeats the rest briefly.

1. Command-line surface

FlagTypeDefaultMeaning
OFFSET (positional)unsigned integer1How many generations to move back (source). 0 is accepted in new-commit mode and targets @'s parents themselves, which starts a sibling of @ (upstream's test setup uses jj prev 0).
-e, --editbooloffEdit the ancestor, like jj edit (source).
-n, --no-editbooloffCreate a new commit on the ancestor. Conflicts with --edit (source).
--conflictbooloffJump to the previous conflicted ancestor. Conflicts with OFFSET (source).

PrevArgs

2. Configuration

KeyDefaultEffect
ui.movement.editfalseEdit mode by default, overridden by --edit/--no-edit (default, source).
revset-aliases."immutable_heads()"(built-in)In edit mode the target must be mutable. Going back often reaches immutable history, so prev --edit can fail where prev works.

3. End-to-end flow

jj prev: call flow cmd_prev calls move_to_commit with Direction::Prev. It resolves edit mode, requires @ to be a head when not editing, and starts from @ (edit) or @'s parents (new). The target revset is the ancestors at offset from the start, or with --conflict the heads of conflicted ancestors from there. One target is used, several prompt, none fails. Edit mode checks the target is rewritable and edits it. New-commit mode checks out a new commit on it. tx.finish records "prev: A -> B" or "prev: A -> editing B". cmd_prev → move_to_commit(Prev): should_edit from flags and 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 (Prev) ancestors_at(start, offset) — the same in both modes --conflict: heads(ancestors(that) & has_conflict) one → target; several (a merge) → "ambiguous prev commit, choose one to target:" prompt none → "No ancestor found N commit(s) back from the working copy parents(s)" edit mode check_rewritable(target); tx.edit(target) "prev: A -> editing B" new-commit mode tx.check_out(target) "prev: A -> B"

4. Core architecture touchpoints

StructureRead / writeHowExplained on
View.wc_commit_ids, head_idsread, writeAs for jj next: @ is replaced by the target or a new child of it (source).view
Revset enginereadancestors_at, heads and the HasConflict filter.revsets
Commitsmaybe writeOne new empty commit in new-commit mode. The old @ is abandoned if it's discardable and unreferenced (source).commits
Working copywriteUpdated to the new @.working copy

5. Transaction and operation

  • Transaction: one, after the target is chosen.
  • Operation description: prev: <old @> -> <target> or prev: <old @> -> editing <target>, with short commit IDs (source).
  • Commits: at most one created (new-commit mode) and at most one abandoned (the old @).

6. Algorithm and invariants

Unlike next, prev uses the same formula in both modes: ancestors_at(start, offset) (source). Only the start differs. With --edit, jj prev edits @'s parent. Without it, jj prev puts the new @ on @'s grandparent, so that @- ends up one commit further back. A merge has several parents, and following each one can give several targets, which leads to the prompt (test_prev_prompts_on_multiple_parents, test_prev_choose_parent_by_id).

With --conflict the result is the heads of the conflicted ancestors: the nearest conflicts going back. The source notes that roots() could be used to jump to the oldest one instead, pending feedback (source).

Invariants: the same as jj next. New-commit mode needs a childless @, edit mode never edits an immutable commit, and no existing commit is rewritten.

7. Errors and edge cases

MessageWhenSource
No ancestor found N commit(s) back from the working copy parents(s)New-commit mode, history isn't that deep. The hint lists each parent. "parents(s)" is spelled that way in the source.movement_util.rs
No ancestor found N commit(s) back from the working copyThe same, in edit mode.movement_util.rs
The working copy parent(s) have no ancestors with conflicts / The working copy has no ancestors with conflicts--conflict found nothing.movement_util.rs
The working copy must not have any childrenNew-commit mode while @ has descendants.movement_util.rs
ambiguous prev commit, choose one to target:Several targets, for example from a merge. Answer with an index or a unique change ID prefix; EOF cancels with an error.movement_util.rs
Commit … is immutableEdit mode on an immutable ancestor.movement_util.rs

8. Worked example

From upstream's test_prev_simple. A linear stack first → second → third, with the empty @ (zsuskuln) on top. Then jj prev runs:

jj prev in new-commit mode Before: root, first, second, third, then the empty working copy zsuskuln. After jj prev: zsuskuln was abandoned, and a new empty working-copy commit royxmykx is a child of second, beside third. before after jj prev root first second third @ zsuskuln empty root first second third @ royxmykx new, empty

Upstream's snapshot:

$ jj prev
Working copy  (@) now at: royxmykx 539f176b (empty) (no description set)
Parent commit (@-)      : rlvkpnrz 9439bf06 (empty) second

The start was @'s parent third, and one generation back is second. With --edit instead, the start is @ itself, so jj would edit third. test_prev_editing shows that from fourth: Working copy (@) now at: kkmpptxz 7576de42 (empty) third.