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:

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

$ jj config set --repo debug.randomness-seed 12362

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 @:

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

$ jj config set --repo debug.randomness-seed 12362

As you can see we aren't empty any more. Well, what does our code look like? Let's use jj diff to see:

   1    1: fn main() {
   2    2:     println!("GoodbyeHello, world!");
   3    3: }

$ jj config set --repo debug.randomness-seed 12363

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:

index 865c8c8225..e7a11a969c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,3 @@
 fn main() {
-    println!("Goodbye, world!");
+    println!("Hello, world!");
 }

$ jj config set --repo debug.randomness-seed 12364

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:

  opqvmvrn b37efa21 (no description set)
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

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?

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)
Parent commit (@-)      : ptrqnyzv 0c72abbb trunk | Hello, world!
Added 0 files, modified 1 files, removed 0 files

So what happened here?

$ 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
│  Hello, world!
~

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 undo
Undid operation: 57a0967c9ede (2025-02-06 09:43:34) abandon commit 326253c27c9867c92d11422133a39b67fdcb602f
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

That's it! We're good again:

$ 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
│  Hello, world!
~

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 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
Hint: This action reverted an 'undo' operation. The repository is now in the same state as it was before the original 'undo'.
Hint: If your goal is to undo multiple operations, consider using `jj op log` to see past states, and `jj op restore` to restore one of these states.

That's right: the last thing you did was an undo, so an undo just undoes that undo. Hilarious, but kind of frustrating. There's a desire to let you go back an arbitrary number of undos, 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 undos at it:

$ 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
Hint: This action reverted an 'undo' operation. The repository is now in the same state as it was before the original 'undo'.
Hint: If your goal is to undo multiple operations, consider using `jj op log` to see past states, and `jj op restore` to restore one of these states.

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 new --before n --no-edit
Created new commit wmznvnuw 24fb2f9f (empty) (no description set)
Rebased 1 descendant commits
Working copy  (@) now at: nvnlxpxw 6ac2036e (empty) (no description set)
Parent commit (@-)      : wmznvnuw 24fb2f9f (empty) (no description set)

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 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
│  Hello, world!
~

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 new trunk
Working copy  (@) now at: oquwwlss 09f65a23 (empty) (no description set)
Parent commit (@-)      : ptrqnyzv 0c72abbb trunk | Hello, world!
Added 0 files, modified 1 files, removed 0 files

We had two empty commits on top of goodbye-world before, but what about now?

$ 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
│  Hello, world!
~

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.