Undoing mistakes
Reset your progress
Reset your progress
To reset your progress to the start of this chapter, run the following command:
curl https://jj-for-everyone.github.io/reset.sh | bash -s undo
cd ~/jj-tutorial/repo
What we've learned so far can go a long way - until something goes wrong.
For example, assume you ran jj commit
and hastily entered a sloppy commit message.
Now you'd like to go back and improve it.
With our current skill set, it's impossible to edit the description of a commit after we ran jj commit
.
But what if we could simply undo a change we made to the repository?
Travel back in time?
Turns out we can, because Jujutsu stores not only a version history of our project in the form of commits, but it also keeps a log of all operations that were performed on the repository itself.
There are some low-level commands to inspect this "operation log" and restore the repository to a specific state in time.
For now though, I will only teach you about the high-level commands to undo and redo operations one-by-one.
The commands we're going to run behave slightly differently with older versions.
If you have an older one (check with jj --version
), please update to a more recent version before continuing.
If you installed Jujutsu with mise
as recommended in the installation chapter, you can update using the following commands:
mise install-into jujutsu@latest /tmp/jj-install
mv /tmp/jj-install/jj ~/.local/bin
rm -rf /tmp/jj-install
Let's remind ourselves what the state of our repo looks like
cd ~/jj-tutorial/repo
jj log
@ pzsymsll alice@local 2025-09-10 20:52:56 1be9c292 │ (empty) (no description set) ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main git_head() 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
Making a mistake
Let's change something, so we can create a commit with a sloppy description. Why not greet the world in a few more languages:
echo "print('Hallo, Welt!')" >> hello.py
echo "print('Bonjour, le monde!')" >> hello.py
Now let's create that commit with a bad description:
jj commit -m "code improvements"
@ yxmkmosm alice@local 2025-09-10 20:53:46 d28a8261 │ (empty) (no description set) ○ pzsymsll alice@local 2025-09-10 20:53:46 git_head() 050b543b │ code improvements ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
Undoing the mistake
Realizing our mistake, we'd like to fix it now. We can restore the state of our repository to a previous point by running:
jj undo
@ pzsymsll alice@local 2025-09-10 20:53:46 94a7ca39 │ (no description set) ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main git_head() 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
Great! We're back to the state before we wrote the bad commit message. Now we get a second chance to write a better one:
jj commit -m "Print German and French greetings as well"
@ npnpqkvz alice@local 2025-09-10 20:54:33 8f11c4f5 │ (empty) (no description set) ○ pzsymsll alice@local 2025-09-10 20:54:33 git_head() df82ee49 │ Print German and French greetings as well ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
Going further back
What happens if you make a mistake, then you do something else, and only later you realize your mistakes?
Can you undo multiple operations?
Sure you can!
It works exactly like the "undo" feature of typical graphical applications (which you may be used to using by pressing Ctrl+Z).
Every time you run jj undo
, you take one step further into the past.
Let's try it a couple of times.
The last thing we did was run jj commit
, so running jj undo
again will put the repository in the state before that:
jj undo
@ pzsymsll alice@local 2025-09-10 20:53:46 94a7ca39 │ (no description set) ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main git_head() 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
Next, what did we do before running jj commit
?
We modified the file hello.py
.
While we didn't use Jujutsu to do that, it was still recorded as a separate operation on the repository.
Every jj
command you run will first check the files in your repository and detect if any of them were changed.
If changes are detected, they will be recorded as a separate operation (called a snaphot).
That way, you can jj undo
those file changes separately from any action you took with a jj
command.
Let's observe what happens to the content of hello.py
when we run jj undo
:
# hello.py
print('Hello, world!')
print('Hallo, Welt!')
print('Bonjour, le monde!')
jj undo
# hello.py
print('Hello, world!')
What happens if you create a file and then delete it without running any jj
commands in between?
Jujutsu didn't get the chance to create a snapshot which stores the content of that file.
Therefore, it's impossible to get the file back using jj undo
.
Here's what the log looks like now:
@ pzsymsll alice@local 2025-09-10 20:52:56 1be9c292 │ (empty) (no description set) ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main git_head() 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
We're going to run jj undo
one last time.
What did we do before modifying hello.py
?
You might not remember, because it was in the previous chapter.
Let's just have a look:
jj undo
@ wkqpyqlm alice@local 2025-09-10 20:52:56 640e22bf │ (empty) (no description set) │ ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main 2ae165d3 │ │ Add submission instructions │ │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d │ ├─╯ WIP: Add for loop (need to fix syntax) │ ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f ╭─┤ (empty) Merge code and documentation for hello-world │ │ │ ~ │ ◆ uvxzzlmr bob@local 2025-09-10 20:52:55 git_head() 60e09461 │ Document hello.py in README.md ~
Ah, yes.
We were practicing navigating the history.
After that, we ran jj new main
to get back on the main branch.
jj undo
reverted that and brought us to the state of the old commit.
Now, that was good practice for jj undo
, but the three operations we just undid weren't actually mistakes.
Can we get them back?
In a typical graphical application, you might do that by clicking on a "redo" button or by pressing Ctrl+Shift+Z.
With Jujutsu, you run the command jj redo
.
Let's do it three times to get back to our most recent state:
jj redo
jj redo
jj redo
@ npnpqkvz alice@local 2025-09-10 20:54:33 8f11c4f5 │ (empty) (no description set) ○ pzsymsll alice@local 2025-09-10 20:54:33 git_head() df82ee49 │ Print German and French greetings as well ◆ lyquuqkl bob@local 2025-09-10 20:52:56 main 2ae165d3 │ Add submission instructions │ ○ vtnrtqsn alice@local 2025-09-10 20:52:56 push-vtnrtqsnrzqx d517a86d ├─╯ WIP: Add for loop (need to fix syntax) ◆ tlwyrrmy alice@local 2025-09-10 20:52:56 ffc6841f │ (empty) Merge code and documentation for hello-world ~
If you were to run jj redo
one more time, you'd get an error telling you there's nothing to redo.
Overwriting undone states
Remember the bad commit message we wrote?
It was "code improvements".
We'll, it's now impossible to get that commit back with only jj undo
and jj redo
.
If you create new states after having undone some others, those "undone" states will become "hidden".
This is exactly how the "undo" feature of typical graphical applications works, so you may not be surprised by this.
Unlike graphical applications, Jujutsu has more advanced commands to get even those states back. That's a topic for another day though.
Pushes to the remote can't be undone
Sending commits to the remote by pushing a bookmark is technically a normal operation on the repository.
Undoing such operations is almost always a bad idea though.
It doesn't tell the remote to restore its previous state, the remote is left intact.
The only thing jj undo
does in that situation is forget that commits were pushed to the remote.
This can lead to pretty nasty errors the next time you try to fetch from or push to the remote.
The most frequent errors that can happen include "conflicted bookmarks" and "divergent change IDs". Both of these problems can be solved, but now's not the right time for me to teach you how. So, take my advice and just don't do that.
If you accidentally undid a jj git push
operation, just jj redo
it and you should be fine.
In the future, jj undo
may outright refuse to undo operations that interacted with the remote.
Repository history is not backed up to the remote
Deleting a repository locally is not a big deal if you have a remote.
Just clone the repo again and you'll have all of your commit history back.
However, that's not the case for the history of operations you made on the repository.
That history is local only.
So, jj undo
works at most as far back as the state of when you cloned the repo.