Throwing away changes & jj undo
We've talked a lot about how to make changes, but haven't talked about getting rid of them!
Getting rid of changes is very easy in jj
: we can do it with jj abandon
.
If you remember, we're in the middle of some stuff:
$ jj st
Working copy changes:
M src/main.rs
Working copy (@) : opqvmvrn c920ae70 (no description set)
Parent commit (@-): tnmounps 326253c2 goodbye-world | Goodbye, world!
If you are coming to this section fresh, just type jj new
a few times to
give yourself some good changes. Done? Great. Let's throw them away.
jj restore
to reset contents
Let's say we don't like that "hello and goodbye world" stuff. We're not going to pursue that further. Getting rid of it is as easy as:
$ jj restore --from p src/main.rs
Created opqvmvrn b37efa21 (no description set)
Working copy (@) now at: opqvmvrn b37efa21 (no description set)
Parent commit (@-) : tnmounps 326253c2 goodbye-world | Goodbye, world!
Added 0 files, modified 1 files, removed 0 files
By default, jj restore
takes changes from your parent change, and puts them
into @
. But there's --from
and even --into
flags you can pass as well.
Let's grab the diff from our first commit, and apply it to @
:
$ jj restore --from p src/main.rs
Created opqvmvrn b37efa21 (no description set)
Working copy (@) now at: opqvmvrn b37efa21 (no description set)
Parent commit (@-) : tnmounps 326253c2 goodbye-world | Goodbye, world!
Added 0 files, modified 1 files, removed 0 files
As you can see we aren't empty any more. Well, what does our code look like?
Let's use jj diff
to see:
$ jj diff
Modified regular file src/main.rs:
1 1: fn main() {
2 2: println!("GoodbyeHello, world!");
3 3: }
This format is different than git
's: we have red and green to indicate
what's changed, for example.
If you want to get a git
style diff instead, that is easy as well:
$ jj diff --git
diff --git a/src/main.rs b/src/main.rs
index 865c8c8225..e7a11a969c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
fn main() {
- println!("Goodbye, world!");
+ println!("Hello, world!");
}
We only had one file that was changed, so we didn't need to pass the path to
jj restore
, but jj restore
is mostly used with individual paths. If we
passed no arguments to jj restore
, it would restore
every file, that is,
move the entire diff from your parent to @
, effectively emptying out the
change.
But what if we want to delete a change entirely?
jj abandon
At any time, you can get rid of a change with jj abandon
. It's tons of fun!
Let's try it:
$ jj abandon
Abandoned 1 commits:
opqvmvrn b37efa21 (no description set)
Working copy (@) now at: nvnlxpxw 0d1ce6d4 (empty) (no description set)
Parent commit (@-) : tnmounps 326253c2 goodbye-world | Goodbye, world!
This throws away our current change. We abandoned opqvmvrn
, and since that
was the same as @
, jj
makes a new change for us, in this case, called
nvnlxpxw
.
But what if we abandon something that's not @
? Like, let's say, t
, the
change that we're currently on top of. What's the worst that could happen?
$ jj config set --repo debug.randomness-seed 12365
$ jj abandon -r t
Abandoned 1 commits:
tnmounps 326253c2 goodbye-world | Goodbye, world!
Deleted bookmarks: goodbye-world
Rebased 1 descendant commits onto parents of abandoned commits
Working copy (@) now at: nvnlxpxw dfde4351 (empty) (no description set)
So what happened here?
$ jj config set --repo debug.randomness-seed 12366
$ jj log
@ nvnlxpxw steve@steveklabnik.com 2025-02-06 09:43:33 dfde4351
│ (empty) (no description set)
◆ ptrqnyzv steve@steveklabnik.com 2024-09-24 12:43:36 trunk git_head() 0c72abbb
As you can see, because we got rid of the commit we were standing on, instead of throwing
us away too, jj
just re-parented us onto the abandoned commit's parent. We're still on
change nvnlxpxw
, but now our parent is ptrqnyzv
, not tnmounps
.
But what if that was a mistake? What if we didn't actually want to throw away tnmounps
,
and we regret our actions here?
I have good news.
jj undo
There's a really useful subcommand that goes by jj undo
:
$ jj config set --repo debug.randomness-seed 12367
$ jj undo
Undid operation: 57a0967c9ede (2025-02-06 09:43:34) abandon commit 326253c27c9867c92d11422133a39b67fdcb602f
Working copy (@) now at: nvnlxpxw 0d1ce6d4 (empty) (no description set)
That's it! We're good again:
$ jj config set --repo debug.randomness-seed 12368
$ jj log
@ nvnlxpxw steve@steveklabnik.com 2025-02-06 09:43:32 0d1ce6d4
│ (empty) (no description set)
○ tnmounps steve@steveklabnik.com 2025-02-06 09:43:34 goodbye-world git_head() 326253c2
│ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-24 12:43:36 trunk 0c72abbb
Everything is back to where we put it. We can always jj undo
to undo
any of our last operations, and jj
will make things right again.
Don't underestimate how good this feels: you can really try out things
and not worry about messing up the state of the world, it's very freeing.
It even brought our bookmark back!
There is one funny thing about jj undo
I feel compelled to mention,
though. What do you think would happen if we jj undo
'd again right now?
Make your guess, and then give it a try:
$ jj config set --repo debug.randomness-seed 12369
$ jj undo
Undid operation: f4d229f0501c (2025-02-06 09:43:34) undo operation 57a0967c9ede1d9ea8a06f733fddf8cf6bcd632857444936fc8f1b2d17bfdb8e1e525cc3baa3264afb92adb480499cc23fde7348ca51fc8807081462071fe211
Working copy (@) now at: nvnlxpxw dfde4351 (empty) (no description set)
Parent commit (@-) : ptrqnyzv 0c72abbb trunk | Hello, world!
Added 0 files, modified 1 files, removed 0 files
That's right: the last thing you did was an undo
, so an undo
just
undo
es that undo
. Hilarious, but kind of frustrating. There's a
desire to let you go back an arbitrary number of undo
s, but it's
a bit trickier than it sounds.
Regardless, we can fix this: there's no problem with jj undo
that you
can't solve by throwing more jj undo
s at it:
$ jj config set --repo debug.randomness-seed 12370
$ jj undo
Undid operation: 588ca5b61206 (2025-02-06 09:43:34) undo operation f4d229f0501caf00432edafeacfbc0f733188ff7ba910e490dd091dc5fed7a3d9bbb2bb6227c41cb7bc518452753cdfccb97546752058495e223f51b6c7c352e
Working copy (@) now at: nvnlxpxw 0d1ce6d4 (empty) (no description set)
Parent commit (@-) : tnmounps 326253c2 goodbye-world | Goodbye, world!
Added 0 files, modified 1 files, removed 0 files
Whew. That's enough of that.
Automatically abandoning changes
Having an empty change with no description is fine to have if it's
@
, or if it has children. Here's a fun party trick:
$ jj config set --repo debug.randomness-seed 12371
$ jj new --before n --no-edit
Created new commit wmznvnuw 24fb2f9f (empty) (no description set)
Rebased 1 descendant commits
That's right: jj new
can take --before
or --after
flags to
squish a change in between others. (Yes, we're trying to make squish happen.)
And --no-edit
means that we don't want to move our working copy
to the new change: @
stays right where it is:
$ jj config set --repo debug.randomness-seed 12371
$ jj log
@ nvnlxpxw steve@steveklabnik.com 2025-02-06 09:43:34 6ac2036e
│ (empty) (no description set)
○ wmznvnuw steve@steveklabnik.com 2025-02-06 09:43:31 git_head() 24fb2f9f
│ (empty) (no description set)
○ tnmounps steve@steveklabnik.com 2025-02-06 09:43:34 goodbye-world 326253c2
│ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-24 12:43:36 trunk 0c72abbb
So that change is fine. But what if we move away from these changes?
Let's make a new change on top of trunk
:
$ jj config set --repo debug.randomness-seed 12372
$ jj new trunk
Working copy (@) now at: oquwwlss 09f65a23 (empty) (no description set)
We had two empty commits on top of goodbye-world
before, but
what about now?
$ jj config set --repo debug.randomness-seed 12373
$ jj log
@ oquwwlss steve@steveklabnik.com 2025-02-06 09:43:32 09f65a23
│ (empty) (no description set)
│ ○ wmznvnuw steve@steveklabnik.com 2025-02-06 09:43:31 24fb2f9f
│ │ (empty) (no description set)
│ ○ tnmounps steve@steveklabnik.com 2025-02-06 09:43:34 goodbye-world 326253c2
├─╯ Goodbye, world!
◆ ptrqnyzv steve@steveklabnik.com 2024-09-24 12:43:36 trunk git_head() 0c72abbb
Our empty change nvnlxpxw
was discarded, automatically. You don't have to
worry about jj new
littering up your repository, empty changes will end up
abandoned.