jj / How jj redo works

How jj redo works

jj redo takes back the most recent jj undo. Like undo, it never deletes anything: it records a new operation whose view is the one the undo replaced. Repeated undo and redo behave like the Undo and Redo commands in a text editor.

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 takes the current operation. If that is itself a redo, it takes the operation that redo restored. That operation must be an undo, or jj reports Nothing to redo. As with undo, an operation from another workspace is refused. jj then restores the undo's parent, the state just before the undo. If that parent is itself a redo, it restores what that redo restored instead. The new operation is recorded as redo: restore to operation <id>.

The command is cmd_redo. It is the mirror image of jj undo: same view restoration, same cross-workspace guard, and the same trick of encoding the stack in operation descriptions. The operations page compares both with op restore and op revert. The two stacks are tracked separately, and redo reads undo's prefix through UNDO_OP_DESC_PREFIX.

1. Command-line surface

FlagTypeDefaultMeaning
--allow-cross-workspaceboolfalseRedo an undo operation even if it was recorded in a different workspace (source).

RedoArgs and its help text

2. Configuration

jj redo reads no settings of its own. templates.op_summary formats the Restored to operation: line (source), and the working-copy settings govern the snapshot and checkout. Like undo, it always restores both the repo portion and the remote-tracking portion of the view (DEFAULT_REVERT_WHAT).

3. End-to-end flow

jj redo: call flow cmd_redo loads the workspace through the shared lifecycle, which snapshots the working copy. It takes the current operation as the target; if its description starts with "redo: restore to operation", it loads the operation named there instead. The target must be an undo operation, otherwise "Nothing to redo". A target from another workspace is refused unless --allow-cross-workspace. The target's single parent is the state before the undo; if that parent is itself a redo, it is replaced by the operation it restored. A transaction sets the view from that operation, prints "Restored to operation", and tx.finish records "redo: restore to operation" plus its ID. Shared lifecycle: load workspace and repo, snapshot (may record an operation) (CLI page) target = current operation a redo? → target = the op it restored (parsed from "redo: restore to operation <hex>") target not an undo? "Nothing to redo" (description lacks "undo: restore to …") undo from another workspace? "Refusing to redo operation …" unless --allow-cross-workspace parent = the undo's single parent (the state just before the undo) a redo? → parent = the op it restored start_transaction; set_view(view_with_desired_portions_restored(parent's view, current view, [Repo, RemoteTracking])) same field-by-field restore as jj undo: heads, bookmarks, tags, working copies and remote refs from the parent; git_refs and git_heads kept from the current view print "Restored to operation: …"; tx.finish(ui, "redo: restore to operation <id>"): write the operation, check out the restored @ (CLI page)

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Current operation and its metadatareaddescription, to tell whether it's a redo and which operation it restored, and whether the target is an undo. Also workspace_name and parents.operations
Op storereadload_operation for operations named in descriptions, and the restored operation's view.operations
View (all but the Git ref mirrors)writeReplaced by the restored operation's heads, local bookmarks and tags, working-copy pointers, and remote views.undo
CommitsnoneNothing is created or rewritten. The commits the undo hid become visible again.commits
OperationwriteA new operation on top of the current one.operations
Working copy (TreeState)read, then maybe writeSnapshotted at the start, then the restored @ is checked out.working-copy

5. Transaction and operation

  • Transaction: one, started after the checks (source).
  • Operation description: redo: restore to operation <full hex id> (REDO_OP_DESC_PREFIX, source). Like undo's, this text is how the next redo finds its place.
  • Commits created or rewritten: none.
  • Working copy: as with undo, the workspace is loaded with a snapshot. Unrecorded edits made after an undo are committed as a snapshot operation first, and that becomes the current operation. It isn't an undo, so jj redo then has nothing to redo. Making any new operation after an undo ends the redo chain.
  • jj undo after a redo treats the redo as an ordinary operation and restores its parent. A following jj redo restores past that redo, which is the "adjacent redo stacks" case in the example below.

6. Algorithm and invariants

The redo stack mirrors the undo stack, with two substitutions (source comment):

  1. What to redo. If the current operation is a redo, the next candidate is the operation it restored (source). Only undo operations can be redone. Anything else, a regular operation or a chain that leads to one, ends with Nothing to redo (source).
  2. What to restore. The undo's parent, which must be unique. A second parent would be an internal error, since undo operations always have exactly one. If that parent is itself a redo, jj restores what it restored, so repeated jj undo; jj redo never builds a linked list of redos (source).

Note that redo checks for the undo prefix, not for an operation recorded by jj undo as such: an operation's identity as an undo is purely its description. jj op revert writes revert operation <id> (source), so jj redo right after it reports Nothing to redo.

Invariants: redo only ever reverses an undo. It never moves past the newest non-undo state. It appends one operation and deletes nothing. Its result is the exact view the undo replaced, apart from the Git ref mirrors, which keep their current values.

7. Errors and edge cases

MessageWhenSource
Nothing to redoThe current operation, after following redos, isn't an undo. For example after a regular command, or after redoing everything (upstream test).redo.rs
Refusing to redo operation <id> because it was performed in workspace <name>, hint about --allow-cross-workspace or jj op revertThe undo being redone was recorded in another workspace (upstream test).redo.rs
internal error Undo operation should have a single parentThe undo has zero parents or several.redo.rs
internal error Failed to parse ID of target operation in redo-stack (and … target operation's parent …)A description has the redo prefix but no valid hex ID.redo.rs, redo.rs

8. Worked example

As on the undo page, this is drawn as an operation log, because redo changes views, not commits. It is upstream's test_jump_over_old_redo_stack. Operations A–D each write their letter to a file named state, followed by three undos, two redos, an undo, and redos until nothing is left. The test checks the file after every step.

The operation log after test_jump_over_old_redo_stack Oldest to newest: add workspace, A, B, C, D, then undo restoring C, undo restoring B, undo restoring A. Redo 1 restores the parent of the last undo, the undo that restored B. Redo 2 follows redo 1 to that undo and restores its parent, the undo that restored C. Then an undo treats redo 2 as ordinary and restores redo 1. Redo 3 targets that undo, whose parent is redo 2, so it restores what redo 2 restored. Redo 4 follows redo 3 to the first undo and restores its parent D. A further redo finds D is not an undo and reports Nothing to redo. The file reads C, B, A, B, C, B, C, D. add workspace 'default' A snapshot state = A B snapshot state = B C snapshot state = C D snapshot state = D undo U1: restore to C state C undo U2: restore to B state B undo U3: restore to A state A redo R1: restore to U2 parent of U3 → state B redo R2: restore to U1 via R1 → U2; parent U1 → C undo U4: restore to R1 R2 is ordinary → state B redo R3: restore to U1 U4's parent R2 is a redo → C redo R4: restore to D via R3 → U1; parent D → D then: jj redo → via R4 → D, not an undo → "Nothing to redo" solid arcs: undo · dashed arcs: redo

The file reads C, B, A after the undos, then B, C after two redos, B after the undo, and C, then D after the final redos. Redo 3 shows the parent substitution: U4's parent is the redo R2, so jj restores what R2 restored (U1) instead of R2 itself. Redo 4 shows target substitution: the current operation R3 is a redo, so its target is the undo R3 restored, U1, and U1's parent D is restored. The two adjacent redo stacks are jumped over.