Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Undoing mistakes with jj undo

Let’s make a mess on purpose. Here’s a small repository with three changes:

$ jj log
@  ozylkotm steve@steveklabnik.com 2024-03-20 09:14:31 3c267bd1
│  add goodbye
○  rxwzpxqt steve@steveklabnik.com 2024-03-20 09:14:20 0570dd0d
│  add a comment
○  zzzzmznr steve@steveklabnik.com 2024-03-20 09:14:20 3e44726c
│  hello world
◆  zzzzzzzz root() 00000000

Now let’s do something we regret. We meant to abandon some other change, but we typed the wrong one and threw away “add a comment”:

$ jj abandon rxwzpxqt
Abandoned 1 commits:
  rxwzpxqt 0570dd0d add a comment
Rebased 1 descendant commits onto parents of abandoned commits.
Working copy  (@) now at: ozylkotm 404f6b4e add goodbye
Parent commit (@-)      : zzzzmznr 3e44726c hello world
Added 0 files, modified 1 files, removed 0 files

It’s gone, and jj helpfully rebased “add goodbye” onto “hello world” for us, so our work is now sitting on top of a commit that doesn’t have the comment in it:

$ jj log
@  ozylkotm steve@steveklabnik.com 2024-03-20 09:14:31 404f6b4e
│  add goodbye
○  zzzzmznr steve@steveklabnik.com 2024-03-20 09:14:20 3e44726c
│  hello world
◆  zzzzzzzz root() 00000000

In git, recovering this change would take a few steps. In jj, we can run one command:

$ jj undo
Undid operation: 897f241ce8c8 (2024-03-20 09:14:31) abandon commit 0570dd0df085
Restored to operation: 4079800aed31 (2024-03-20 09:14:20) snapshot working copy
Working copy  (@) now at: ozylkotm 3c267bd1 add goodbye
Parent commit (@-)      : rxwzpxqt 0570dd0d add a comment
Added 0 files, modified 1 files, removed 0 files

And we’re back:

$ jj log
@  ozylkotm steve@steveklabnik.com 2024-03-20 09:14:20 3c267bd1
│  add goodbye
○  rxwzpxqt steve@steveklabnik.com 2024-03-20 09:14:20 0570dd0d
│  add a comment
○  zzzzmznr steve@steveklabnik.com 2024-03-20 09:14:20 3e44726c
│  hello world
◆  zzzzzzzz root() 00000000

Notice that rxwzpxqt came back with the same change ID and the same commit ID, 0570dd0d. This isn’t a fresh commit that happens to have the same contents. It’s the same commit; it was never really destroyed, just made invisible.

What jj undo actually undoes

Here’s the important bit: jj undo undoes the last operation, not the last commit. This is what makes it different from the closest commands in git.

An operation is a recorded transition between repository states. Commands such as jj abandon, jj describe, jj rebase, and jj new create operations. Read-only commands don’t. And — this surprises people — the automatic snapshot jj takes of your working copy can be a separate operation from the command that triggered it. Each operation records the complete state of the repository afterwards.

So jj undo doesn’t reason about what your command meant, or try to construct an inverse of it. It puts the repository back the way it was. That’s why it works uniformly on every command, including ones that touched a dozen commits at once, and why the output tells you which operation it undid and which one it went back to.

Running it more than once

There’s one detail that can be surprising. If we run jj undo a second time, it does not undo the first undo:

$ jj undo
Undid operation: 4079800aed31 (2024-03-20 09:14:20) snapshot working copy
Restored to operation: 84248e9a9cb9 (2024-03-20 09:14:20) new empty commit

Look at what it undid: the working copy snapshot from before the abandon. Repeated jj undo walks further and further back into the past, one operation at a time. It’s a rewind button, not a toggle.

If we do want to go the other way, there’s jj redo:

$ jj redo
Restored to operation: c8fa9084a76c (2024-03-20 09:14:20) undo: restore to operation 4079800aed31
Working copy  (@) now at: ozylkotm 3c267bd1 add goodbye
Parent commit (@-)      : rxwzpxqt 0570dd0d add a comment

jj redo moves in the direction of the future, so between the two of them you can scrub back and forth until you find the state you wanted.

Undoing something other than the last thing

Rewinding one step at a time is fine when you’ve just made one mistake. When you’ve made four, and you only want to reach past three of them, you want to name the state you’re going back to directly. jj undo won’t help there — it takes no arguments, and only ever walks backwards from where you are.

For that you need to see the operations and refer to them by name, and that’s the operation log. Let’s look at it.