lander's posts

Saving My Commit With `jj evolog`


jj (jujutsu) is a newish git-compatible version control system that has some fresh ideas and a pretty great CLI UX (compared to git). I had a moment recently where I hadn't yet committed my changes and while attempting to format the code I inadvertently made my diff way larger than it should have been.

Here's what happened:

  1. I made some changes that I was quite happy with and did a quick jj diff / jj status to make sure there was nothing I was accidentally including in the change.
 : jj status
Working copy changes:
M Cargo.toml
M src/de.rs
M src/lib.rs
M src/ser.rs
M src/value.rs
M src/value_impls.rs
Working copy  (@) : zxsrvopz 43a4bc7d master | de: very rough PoC of refcounted data
Parent commit (@-): xlzooroo 9b8d55a1 core: update quickcheck + rand and tests (tests failing)
  1. I noticed that the changes were not well-formatted, so I ran cargo fmt

  2. I ran jj status and had an oh shit moment when I realized way more files were changed from the cargo fmt than I thought would be.

 : jj status
Working copy changes:
M Cargo.toml
M src/consts.rs
M src/de.rs
M src/error.rs
M src/lib.rs
M src/ser.rs
M src/value.rs
M src/value_impls.rs
M test/arby.rs
M test/mod.rs
Working copy  (@) : zxsrvopz 69322ff0 master | de: very rough PoC of refcounted data
Parent commit (@-): xlzooroo 9b8d55a1 core: update quickcheck + rand and tests (tests failing)

So now I have a problem: how do I undo the cargo fmt step?

Enter jj evolog

One of jj's commands is evolog, which "shows how a change has evolved over time". jj takes a snapshot of the working copy every time a command is run, so when I ran jj diff to check my work, it recorded a snapshot of my working copy from the last time I ran a jj command.

Running jj evolog you'll see something like:

 : jj evolog
  mmxmynwt hidden example@example.com 2025-10-03 18:08:05 7aa68914
  (no description set)
  -- operation f606802e1b09 (2025-10-03 18:08:05) snapshot working copy
  mmxmynwt hidden example@example.com 2025-10-03 18:07:58 4970219f
   (empty) (no description set)
   -- operation bb8323da564f (2025-10-03 18:07:58) new empty commit

Pretending that there's more than the empty commit and snapshot working copy operations, this on its own is not super helpful in identifying what happened in each operation.

Running jj evolog -p shows more details:

❯ : jj evolog -p
@  zxsrvopz example@example.com 2025-10-03 18:07:03 master 69322ff0
│  de: very rough PoC of refcounted data
│  -- operation 0df245508eb8 (2025-10-03 18:07:03) snapshot working copy
│  src/consts.rs --- Rust
│  No syntactic changes.
│  src/error.rs --- 1/3 --- Rust
│   6                                                                     6
│   7 //! Error objects and codes                                         7 //! Error objects and codes
│   8                                                                     8
│   9 use std::fmt;                                                       9 use serde::{de, ser};
│  10 use std::io;                                                       10 use std::error;
│  11 use std::error;                                                    11 use std::fmt;
...skipping...
○  zxsrvopz hidden example@example.com 2025-10-02 19:31:08 43a4bc7d
   de: very rough PoC of refcounted data
   Cargo.toml --- TOML
   10 edition = "2024"
   11
   12 [dependencies]
   13 serde = { version = "1.0.104", features = ["rc"] }
   14 byteorder = "1.3.2"
   15 num-bigint = "0.4.0"
   16 num-traits = "0.2.10"

   src/lib.rs --- Rust
   68 68 //!
   69 69 //! The minimum supported version of the toolchain is 1.41.1.
   70 ..
   71 .. #![cfg_attr(feature = "unstable", feature(test))]
   72 70
   73 71 pub use self::ser::{
   74 72     Serializer,


/* The rest */

There are two important lines here:

@  zxsrvopz example@example.com 2025-10-03 18:07:03 master 69322ff0

And

○  zxsrvopz hidden example@example.com 2025-10-02 19:31:08 43a4bc7d

zxsrvopz is the change ID (which is the same for both lines) while 43a4bc7d and 69322ff0 are the unique commit IDs. In the above output I just searched in the output for zx to jump from the first commit to the second and saw that the second is what I wanted to restore to.

So now I can jj edit 43a4bc7d to jump back to that specific operation and I see the following output:

❯ : jj edit 43a4bc7d
Working copy  (@) now at: zxsrvopz?? 43a4bc7d de: very rough PoC of refcounted data
Parent commit (@-)      : xlzooroo 9b8d55a1 core: update quickcheck + rand and tests (tests failing)
Added 0 files, modified 8 files, removed 0 files

See the zxsrvopz?? change ID? If the above was appropriately highlighted you'd see it in red, but jj now is telling me that I have a conflicted change by placing that text in red and showing the ?? after the change ID. If I run jj log I see my history has branched and I have two changes with the same ID:

❯ : jj log
@  zxsrvopz?? example@example.com 2025-10-02 19:31:08 43a4bc7d
│  de: very rough PoC of refcounted data
│ ○  zxsrvopz?? example@example.com 2025-10-03 18:07:03 master 69322ff0
├─╯  de: very rough PoC of refcounted data
○  xlzooroo example@example.com 2025-10-02 17:00:07 git_head() 9b8d55a1
│  core: update quickcheck + rand and tests (tests failing)

The change starting with @ is the revision I'm currently on while the other with a is the forked revision. I can now resolve this conflict by simply abandoning the other change:

> : jj abandon 69322ff0
Abandoned 1 commits:
  zxsrvopz?? 69322ff0 master | de: very rough PoC of refcounted data
Deleted bookmarks: master

❯ : jj log
@  zxsrvopz example@example.com 2025-10-02 19:31:08 43a4bc7d
│  de: very rough PoC of refcounted data
○  xlzooroo example@example.com 2025-10-02 17:00:07 git_head() 9b8d55a1
│  core: update quickcheck + rand and tests (tests failing)

And we're fixed!

I've reproduced what happened here in a short shell session which will have better highlighting and more info (note: clicking will take you to asciinema.org):

asciicast