In short: jj takes the current operation. If it is itself an undo, jj takes the operation that
undo restored, read back from its description. It refuses if the operation came from another
workspace, is the root, or is a merge, and warns about undoing a push or a workspace removal. It then
picks the operation to restore: the target's parent, or, if that parent is an undo, what that undo
restored. A transaction copies that operation's view (commits, bookmarks, tags, working-copy
pointers and remote-tracking refs) over the current one. It is recorded as
undo: restore to operation <id>. That description is how the next
jj undo finds its place.
The command is cmd_undo. The
operations page compares undo with
redo, op restore and op revert, and explains what a view is. This
page follows undo line by line. Restoring an operation never touches commits, which are
immutable and stay in the store. Only the view, meaning which commits are visible and where refs
point, changes.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
--allow-cross-workspace | bool | false | Undo the operation even if it was recorded in a different workspace (source). |
There is no argument naming an operation. To restore or revert a particular one, the help text points
to jj op restore and jj op revert. The global --at-operation
flag changes which operation the repository is loaded at, and therefore which one is "current".
2. Configuration
jj undo reads no settings of its own. templates.op_summary formats the
Undid operation: and Restored to operation: lines
(source). The working-copy settings on the
working-copy page govern the snapshot at the
start and the checkout at the end. jj undo always restores both the repo portion and the
remote-tracking portion of the view; the choice that op restore --what offers isn't
exposed (DEFAULT_REVERT_WHAT).
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Current operation and its metadata | read | description (is it an undo or a push? which operation did it restore?), workspace_name (cross-workspace check), and parents. | operations |
| Op store | read | load_operation for the operation named in an undo's description, and that operation's parents and view. | operations |
View.head_ids, local_bookmarks, local_tags, wc_commit_ids | write | Replaced wholesale by the restored operation's values. That is the Repo portion. | view |
View.remote_views | write | Replaced by the restored operation's remote bookmarks and tags. That is the RemoteTracking portion. | view |
View.git_refs, git_heads | kept | Always taken from the current view (source). | view |
| Commits | none | No commit is created or rewritten. Commits that become invisible stay in the store, and commits that become visible again were never deleted. | commits |
| Operation | write | A new operation whose parent is the current one, holding the restored view. | operations |
Working copy (TreeState) | read, then maybe write | Snapshotted at the start. If this workspace's @ differs in the restored view, its tree is checked out. If the workspace doesn't exist in the restored view, the files are left alone, with a warning. | working-copy |
5. Transaction and operation
- Transaction: one, started after every check passes (source).
-
Operation description:
undo: restore to operation <id>, where<id>is the full hex ID of the restored operation (UNDO_OP_DESC_PREFIX, source). This exact text is functional: the nextjj undostrips the prefix and parses the ID.jj op revertuses a different description, so an undo afterop revertundoes the revert rather than continuing a stack (upstream test). - Commits created or rewritten: none. Only the view changes.
-
Working copy: the workspace is loaded with the usual snapshot. If you have
unrecorded edits, that snapshot is committed as its own operation and becomes the current one
(source), so it is what
jj undoundoes. After the undo operation is written, the restored@is checked out. jj undoagain continues further back.jj redoundoes the undo, by restoring the operation the undo replaced.
6. Algorithm and invariants
The undo stack, encoded in descriptions
There is no separate undo stack. It is reconstructed from operation descriptions, using two substitutions (source comment):
-
Choosing what to undo. If the current operation is an undo, the thing to undo next
is the operation it restored, not the undo itself
(source). So repeated
jj undokeeps stepping back instead of toggling between two states. -
Choosing what to restore. Normally that's the target's parent. If the parent is
itself an undo, jj restores what that undo restored instead
(source). The source comment explains that otherwise,
repeated
jj new; jj undowould build an ever-longer linked list of undos that a later undo would have to walk.
Together these make the stack "jump over" older undo stacks it runs into, as the worked example below shows.
What restoring means
view_with_desired_portions_restored builds a new
View field by field. With Repo selected, head_ids,
local_bookmarks, local_tags and wc_commit_ids come from the
restored view. With RemoteTracking selected, so does remote_views. The Git
ref mirrors git_refs and git_heads always come from the current view
(RevertWhatToRestore). MutableRepo::set_view
installs it un-normalized (source), and the
transaction commits it as usual.
Guards
- Other workspaces. An operation records the workspace it ran in. Undoing another workspace's operation would usually surprise you, so it needs
--allow-cross-workspace. Operations with no recorded workspace pass the check (source). - Pushes. Undoing a
push …operation (is_push_operation) restores remote-tracking bookmarks to their pre-push positions, while the remote keeps the pushed ones. That's why it warns that the bookmarks will often end up conflicted. - Workspace removal. The workspace's entry in the view comes back, but its directory on disk doesn't.
- Shape. The root operation has nothing to restore. A merge operation, created when concurrent operations are reconciled, has no single "before", so jj points you to
op restore.
Invariants: undo is append-only, because it always adds an operation and never
deletes one. Undo can itself be undone (jj redo, or jj op restore to the
undo's parent). No commit is ever destroyed. Repeated undos step back monotonically through the
non-undo history, without building chains.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Refusing to undo operation <id> because it was performed in workspace <name>, hint about --allow-cross-workspace or jj op revert | The target operation's workspace_name is set and differs from the current workspace. | undo.rs |
Cannot undo root operation | The target has no parent, for example after undoing back to repository creation. | undo.rs |
Cannot undo a merge operation, hint Consider using `jj op restore` instead | The target has several parents. | undo.rs |
Warning Undoing a push operation often leads to conflicted bookmarks., hint To avoid this, run `jj redo` now. | The target's description starts with push . | undo.rs |
Warning Undoing a workspace removal restores the workspace, but does not restore the workspace directory. and a hint | The target's description starts with remove workspace. | undo.rs |
Warning The current workspace '…' no longer exists after this operation. The working copy was left untouched. | The restored view has no entry for this workspace, for example after undoing add workspace 'default'. | cli_util.rs |
internal error Failed to parse ID of target operation in undo-stack (and … target operation's parent …) | An operation description has the undo prefix but no valid hex ID after it. | undo.rs, undo.rs |
Upstream's tests cover the root case, including the missing-workspace warning on the first undo
(source), the merge case, created with --at-op
(source), and the push warning
(source).
8. Worked example
undo moves through operations, not commits, so this example draws the operation
log. It is upstream's test_jump_over_old_undo_stack. Each of
operations A–D writes its own letter to a file named state and snapshots. The test then
runs undo, undo, writes and snapshots E and F, and runs undo three more times. After each command it
checks the file.
The test asserts the file reads C, then B, then E,
B and A. The fourth undo is the interesting one. Its target is
E, the operation undo 3 restored, and E's parent is undo 2. Rather than
restore undo 2, it restores what undo 2 restored, B. The old stack (undo 1, undo 2, C, D)
is jumped over, so the fifth undo goes straight from B to A.