In short: jj resolves X (default @) and requires it to have exactly one parent. In a
transaction it loads the repo at X and at X's parent and merges them into the current repo, with X as
the base and the parent as the other side. Rewrites and abandons implied by that merge are recorded, so
later descendants get rebased. --what can keep the current repo state or remote-tracking
bookmarks instead of the merged ones. The operation is revert operation <id>.
The command is cmd_op_revert. Compare
jj op restore, which jumps back
to X's state and drops everything after it, and jj undo,
which restores the current operation's parent. Merging views is the same machinery jj uses for
concurrent operations.
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
OPERATION (positional) | operation expression | @ | The operation to revert (source). |
--what | repo | remote-tracking, repeatable | both | Which parts of the merged state to keep. Experimental (source, values). |
2. Configuration
No revert-specific keys. templates.op_summary formats the Reverted operation:
line (source). Descendants are rebased by tx.finish
as for any other command.
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Repos at X and parent(X) | read | Loaded with repo_loader.load_at, views and indexes (source). | operations |
| Index | merged | Both indexes are merged in first, so the view merge can see every commit involved (source). | index |
View | merged, then filtered | Heads, local bookmarks, tags, Git refs, remote refs, Git HEADs and working-copy commits are merged field by field (source). | view |
| Commits | descendants rewritten | Rewrites X made are recorded in reverse, so descendants created after X are rebased by tx.finish. | transactions |
| Operation log | append | One operation; X stays in the log. | operations |
5. Transaction and operation
- Transaction: one, started after the parent check (source).
- Operation description:
revert operation <full hex id of X>(source). Unlike undo's description,jj undodoesn't treat it specially, so undoing afterop revertundoes the revert. - Commits rewritten: descendants of commits whose rewrite is reversed, rebased by
tx.finish(source). - Nothing to revert: if the merge leaves the view unchanged,
tx.finishprintsNothing changed.and records nothing.
6. Algorithm and invariants
The inverse as a merge
MutableRepo::merge(base, other) applies
"other minus base" to the repo being built. With base = X and other = parent(X), that is the inverse of
X's change, applied on top of the current state (source). Field by
field (merge_view):
- Heads: with the default index,
record_rewritescompares the commits X has and its parent doesn't. A commit X introduced whose change ID also appears in parent(X) is recorded as rewritten back to the parent's version; one whose change disappears is recorded as abandoned. Heads present in parent(X) but not X are added back. - Bookmarks, tags, Git refs, remote refs, Git HEADs: each name that differs between X and parent(X) is three-way merged into the current value, which can produce a conflicted bookmark if it moved again since.
- Working-copy commits: merged per workspace the same way.
Then --what
view_with_desired_portions_restored is called with the
merged view as the one to "restore" and the transaction's base view as current
(source). Leaving out repo keeps the current heads,
local bookmarks, tags and working-copy commits; leaving out remote-tracking keeps the current
remote bookmarks. Git ref mirrors always come from the current view.
Invariants: append-only. Later operations' effects survive unless they conflict with the inverse. Root and merge operations can't be reverted, because there's no single "before".
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Cannot revert root operation | X is the root operation. | revert.rs, test |
Cannot revert a merge operation | X has several parents, e.g. it reconciled concurrent operations. | revert.rs, test |
Nothing changed. | The merge leaves the view as it is. | cli_util.rs |
| Operation-resolution errors | X doesn't resolve to exactly one operation. | cli_util.rs |
8. Worked example
From upstream's test_revert_rewrite_with_child. The commit is
described initial, then modified, and a child is created on top.
Then jj op revert @- reverts the second describe, which is no longer the latest
operation:
Upstream's log after the revert (source):
@ child
○ initial
◆
The source comment states the intent: "Since we undid the description-change, the child commit should now be on top of the initial commit."