jj / How jj operation restore works

How jj operation restore works

jj op restore X writes a new operation whose view is operation X's view, so the repo looks the way it did at X. Nothing in the operation log is removed: the later operations stay there, and the restore itself can be undone.

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 loads the workspace as usual and resolves X. It builds a new View from X's view and the current one: heads, local bookmarks and tags, and working-copy commits come from X; remote bookmarks and tags come from X too unless --what leaves them out; the Git ref mirrors stay current. It installs that view in one transaction, prints Restored to operation: …, and finishes as restore to operation <id>, which checks out the restored @.

The command is cmd_op_restore. It shares its view logic with jj undo and jj op revert. The operation log itself is described on the operations page.

1. Command-line surface

ArgumentTypeDefaultMeaning
OPERATION (positional)operation expressionrequiredThe operation to restore to, such as an ID prefix or @-. Its help suggests jj --at-op=<id> log to preview the state first (source).
--whatrepo | remote-tracking, repeatablebothWhich parts to restore. Experimental (source, values).

OperationRestoreArgs

2. Configuration

No restore-specific keys. templates.op_summary formats the Restored to operation: line (source). The global --at-op changes which operation the command starts from, not the target.

3. End-to-end flow

jj op restore: call flow cmd_op_restore loads the workspace through the shared lifecycle, which snapshots it, and resolves the target operation. It starts a transaction and builds a new view with view_with_desired_portions_restored from the target's view and the current view, according to --what. set_view installs it. It prints Restored to operation, and tx.finish records "restore to operation" and checks out the restored working-copy commit. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) resolve_single_op(OPERATION) → X; start_transaction view_with_desired_portions_restored repo: heads, local bookmarks/tags, wc_commit_ids from X remote-tracking: remote_views from X git_refs, git_heads: always current set_view(new view) installed un-normalized "Restored to operation: <summary>" tx.finish(ui, "restore to operation <id>") "Nothing changed." if the view is identical; otherwise write the operation and check out the restored @

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Target operation's ViewreadLoaded with target_op.view() (source).view
Current ViewreplacedBy the combined view, through MutableRepo::set_view (source).view
Commitsnone writtenCommits that were hidden become visible again simply because the restored view's heads point at them.commits
Operation logappendOne new operation whose parent is the current one.operations
Working copymaybe writtenUpdated to the restored @. If the workspace isn't in the restored view, it's left alone with a warning.working-copy

5. Transaction and operation

  • Transaction: one (source).
  • Operation description: restore to operation <full hex id of X> (source).
  • No change: restoring to the current operation prints the summary and then Nothing changed.; nothing is recorded (test).
  • Undoing it: jj undo, or jj op restore to the operation before the restore.
  • Before it: the load's snapshot may record a snapshot working copy operation, so unrecorded edits are kept in the log even though the restore moves away from them.

6. Algorithm and invariants

view_with_desired_portions_restored picks each field of the new View:

FieldTaken from
head_ids, local_bookmarks, local_tags, wc_commit_idsX if --what includes repo, else current
remote_views (remote bookmarks and tags)X if --what includes remote-tracking, else current
git_refs, git_headsalways current

The remote-tracking help notes not to restore those "if you'd like to push after the undo" (source): restoring them makes jj forget what a push or fetch told it about the remote. The view is installed as-is with set_view, which marks the heads as not yet normalized (source).

Invariants: append-only: no operation or commit is deleted. Only the view changes. The Git ref mirrors are never rolled back, so the next import or export still compares against what Git actually has.

7. Errors and edge cases

MessageWhenSource
Warning: The current workspace '<name>' no longer exists after this operation. The working copy was left untouched., hint to restore to an operation that contains itX predates the workspace, e.g. the root operation.cli_util.rs, test
Nothing changed.X's view equals the current one.cli_util.rs
Operation-resolution errors (no such operation, ambiguous prefix)X can't be resolved to exactly one operation.cli_util.rs
clap: missing <OPERATION>No argument; unlike op revert, there's no default.restore.rs

8. Worked example

From upstream's test_git_push_revert, which uses jj op restore to take back a push. main was pushed at AA, then described BB and pushed again; the test restores to the operation before that second push:

jj op restore to the operation before a push Before the push: local main is BB and main@origin is AA. After jj git push: both are BB, and the Git remote has BB. After jj op restore to the pre-push operation: local main is still BB and main@origin is AA again, because remote-tracking bookmarks were restored, while the actual remote still has BB. A new operation is appended; the push operation stays in the log. pre-push operation main BB main@origin AA actual remote AA after jj git push main BB main@origin BB actual remote BB after jj op restore main BB main@origin AA (restored) actual remote BB op log: … → push → restore to operation <pre-push id> (nothing is removed) jj's remote-tracking state and the real remote now disagree, until the next fetch

Upstream's bookmark listing after the restore (source):

main: qpvuntsm d9a9f6a0 (empty) BB
  @origin (ahead by 1 commits, behind by 1 commits): qpvuntsm/1 3a44d6c5 (hidden) (empty) AA

A later jj git fetch, after describing main as CC, ends with main conflicted. The test's own TODO explains why: the restore made jj forget the remote was already at BB, so the fetch looked like a remote update. --what repo avoids that by keeping the current remote-tracking bookmarks (test_git_push_revert_repo_only).