Have you ever made a change in your version control system, only to realize a few steps later that you’ve gone down the wrong path? Perhaps you accidentally squashed commits, rebased incorrectly, or simply wish you could rewind to a previous state without losing your work. In traditional VCS like Git, recovering from such scenarios can range from trivial to terrifying, often involving arcane commands or the dreaded “force push.”

jj (Jujutsu) offers a profound solution to this common developer anxiety: the operation log. This isn’t just a simple history of your commits; it’s a comprehensive record of every action you take that modifies your repository’s state. Think of it as a super-powered undo stack for your entire VCS workflow. With the operation log, you can fearlessly experiment, knowing that you can always rewind to any previous point in your repository’s history with a single command. This chapter will dive deep into this safety net, showing you how to explore, undo, and even redo your way to a more confident development experience.

Before we begin, ensure you’ve set up jj and have a basic understanding of its core commands, as covered in previous chapters. We’ll be building on that foundation to manipulate repository history with unprecedented ease.

The Operation Log: Your Repository’s Memory

In jj, every command you run that changes the repository’s state – whether it’s creating a commit, amending one, rebasing, or even resolving a conflict – is recorded as an “operation.” These operations are stored in the operation log, an immutable, linear history of how your repository has evolved.

📌 Key Idea: While Git’s reflog tracks changes to references (like branch pointers), jj’s operation log tracks changes to the entire repository state, including all commits, working copy, and branches. This fundamental difference is what makes jj’s undo capabilities so powerful and comprehensive.

Why the Operation Log Matters

The operation log provides several critical benefits for engineers, transforming the way you interact with your project’s history:

  • Fearless Experimentation: Try out complex rebases, merges, or history rewrites without worry. If something goes wrong, you can simply undo it. This boosts productivity by removing the fear of breaking things and encourages more creative problem-solving.
  • Robust Recovery: Easily revert to any past state of your repository, effectively undoing multiple jj commands in one go. This is invaluable for recovering from mistakes, exploring alternative solutions, or even going back to a stable point for debugging.
  • Audit Trail: Understand exactly how your repository reached its current state, step by step. Each operation is logged with its command, timestamp, and author, making it invaluable for debugging or reviewing complex history manipulations, especially in team environments.
  • Collaboration Safety: If a team member accidentally pushes an undesirable change, you can often use the operation log to revert your local state to before that operation, then interact with the remote again, avoiding complex git revert or git reset --hard scenarios.

Step-by-Step: Exploring and Manipulating Your Operations with the Log

Let’s generate some operations and then learn how to navigate them.

First, make sure you’re in a jj repository. If you don’t have one, create a new one:

jj init my_project_with_ops
cd my_project_with_ops

Now, let’s make a series of changes to generate a meaningful operation log.

Generating Operations

  1. Create an initial commit with content:

    We’ll start by adding a greeting.txt file and committing it. The jj new -m "..." command creates a new commit and moves your working copy to it. This first command creates an operation.

    echo "Hello, Jujutsu!" > greeting.txt
    jj new -m "Initial commit with greeting"
    
  2. Create a main branch:

    Next, we’ll create a named branch reference. This action is also recorded as an operation.

    jj branch create main
    
  3. Make another change and commit:

    Let’s add more content to greeting.txt and create a new commit. This commit will be stacked on top of the previous one, generating another operation.

    echo "Welcome to the operation log tutorial." >> greeting.txt
    jj new -m "Added welcome message"
    
  4. Amend the last commit (both content and message):

    The jj amend command modifies the current commit in place, updating both its content (from the working directory) and its message. This is a common operation that rewrites history and creates a new operation in the log.

    echo "Adding a new line for clarity." >> greeting.txt
    jj amend --message "Updated greeting with a new line and clearer message"
    

Viewing the Operation Log

Now that we’ve performed a few actions, let’s inspect the operation log using jj op log.

jj op log

You’ll see output similar to this (IDs and timestamps will vary):

@  3a2f8b7e7a7e (2026-05-19 10:30:00) My Name <me@example.com>
   amend --message "Updated greeting with a new line and clearer message"
o  c1d3e2f1a0b9 (2026-05-19 10:29:00) My Name <me@example.com>
   new -m "Added welcome message"
o  9b8a7c6d5e4f (2026-05-19 10:28:00) My Name <me@example.com>
   branch create main
o  6f5e4d3c2b1a (2026-05-19 10:27:00) My Name <me@example.com>
   new -m "Initial commit with greeting"
o  5a4b3c2d1e0f (2026-05-19 10:26:00) My Name <me@example.com>
   init

Let’s break down the output:

  • @ (Current Operation): The @ symbol indicates your current operation. This is the state your repository is currently in.
  • Operation ID: A unique hexadecimal ID for each operation (e.g., 3a2f8b7e7a7e).
  • Timestamp and Author: When and by whom the operation was performed.
  • Description: A concise summary of the jj command that generated the operation.
  • Parents: (Not explicitly shown in this default view, but operations can have parent operations, especially after undo or redo.)

Each line represents a distinct action you took. Notice how jj new, jj branch create, and jj amend are clearly listed with their arguments.

Inspecting a Specific Operation

You can dive deeper into any operation using jj op show <op-id>. Let’s look at the operation where we amended the commit. Find the op-id for the amend operation from your jj op log output (it’s typically the one marked with @ or the one immediately before it if you’ve done something else).

jj op show 3a2f8b7e7a7e # Replace with your actual amend op-id

The output will be verbose, showing:

  • Details about the operation itself (timestamp, user, command, parent operations).
  • Crucially, a diff showing how the set of commits changed as a result of this operation. This includes which commits were added, removed, or modified.

This detailed view is incredibly powerful for understanding the full impact of any jj command.

Unleashing Undo and Redo

Now for the fun part: undoing and redoing operations.

jj undo: Rewind to the Previous State

The jj undo command is your primary tool for reversing operations. It effectively moves your repository back to the state before the last operation, making the previous operation the new current one.

Let’s try it:

jj undo

After running this, jj will tell you what it undid. If you run jj op log again, you’ll see that the @ (current operation) has moved up one step, and the amend operation is no longer the current one. Your working copy will also reflect the state before the amend.

The file greeting.txt will now contain:

Hello, Jujutsu!
Welcome to the operation log tutorial.

The “Adding a new line for clarity.” text is gone from the commit and the file. Amazing, right?

jj redo: Re-apply Undone Operations

What if you undo something and then realize you did want that change after all? No problem! jj redo will re-apply the last undone operation.

jj redo

Now, your amend operation is back in effect, and greeting.txt will again include “Adding a new line for clarity.”

jj restore --op <op-id>: Pinpoint Recovery

jj undo and jj redo are great for stepping back and forth one operation at a time. But what if you want to jump back several operations, or restore to a specific point in the past without affecting more recent, unrelated operations? That’s where jj restore --op <op-id> comes in.

This command takes your repository back to the state immediately after the specified operation ID was performed. It’s like saying, “Make this operation the new current point in my history.” Importantly, jj restore --op achieves this by creating a new operation in the log. This new operation’s purpose is to declare that the repository state is now what it was after the specified op-id. This reinforces the immutable nature of the operation log itself; you’re not deleting history, you’re adding a new entry that points to an older state.

Let’s try a more complex scenario:

  1. Add a new file and commit:

    echo "This is a new feature." > feature.txt
    jj new -m "Added feature file"
    
  2. Add another file and commit:

    echo "Configuration settings." > config.ini
    jj new -m "Added config file"
    

    Now your jj op log will show these two new operations on top.

    jj op log
    

    You’ll see something like:

    @  new_op_id (time) My Name <me@example.com>
       new -m "Added config file"
    o  prev_new_op_id (time) My Name <me@example.com>
       new -m "Added feature file"
    o  3a2f8b7e7a7e (time) My Name <me@example.com>
       amend --message "Updated greeting with a new line and clearer message"
    ...
    

    Let’s say you want to go back to the state after the amend operation, but before the feature.txt and config.ini commits. Find the op-id of your amend operation (e.g., 3a2f8b7e7a7e).

    jj restore --op 3a2f8b7e7a7e # Use your amend op-id
    

    jj will report that it restored to that operation. Check your working directory: feature.txt and config.ini are gone! Your repository has been completely reset to the state right after you amended greeting.txt.

    This is incredibly powerful for discarding a series of experimental changes without manually reverting or resetting.

🧠 Important: jj restore --op vs. jj undo

Understanding the subtle but crucial difference between these two commands is key to effectively navigating your jj history:

  • jj undo: Reverses the last operation. It’s a single step back, moving the @ pointer to the immediate parent operation. It’s typically used for quickly correcting the very last thing you did.
  • jj restore --op <op-id>: Reverts the repository to the state after a specific operation, effectively discarding all operations that happened after the target op-id. It does this by creating a new operation that points to that specific past state. This is like jumping to an arbitrary point in your operation history, making it ideal for larger rewinds or targeted recovery.

Mini-Challenge: Precise History Rewind

You’ve been working on a new feature and made a few changes. Let’s simulate that:

  1. Create a file data.json with some JSON content and commit it.
    echo '{"version": 1}' > data.json
    jj new -m "Initial data.json"
    
  2. Add a new line to greeting.txt and commit it.
    echo "This is an important update." >> greeting.txt
    jj new -m "Added important update to greeting"
    
  3. Rename data.json to config.json and commit it.
    jj mv data.json config.json
    jj new -m "Renamed data.json to config.json"
    

Now, your challenge is to undo only the renaming of data.json to config.json, reverting that specific operation, while keeping the other changes (the new line in greeting.txt and the initial content of data.json under its original name).

Hint: Use jj op log to identify the operation ID for the rename. Then, identify the operation ID just before the rename operation occurred. This is the state you want to restore to using jj restore --op.

What to observe/learn: The precision with which jj allows you to manipulate your operational history. You’re not just undoing the last thing; you’re surgically reverting to a specific state.

Common Pitfalls & Troubleshooting

  1. Confusing jj undo with Git’s git revert or git reset:

    • jj undo literally rewinds your entire repository state (working copy, commits, branches) to a previous point in the operation log. It’s an undo of your actions, providing a true “oops” button.
    • git revert creates a new commit that undoes the changes of a previous commit. It doesn’t rewrite history.
    • git reset moves your branch pointer and potentially discards commits, but it’s often more destructive and less easily reversible than jj undo.
    • Pro Tip: Always think of jj undo as a true “undo” button for your jj commands, affecting your entire repository state.
  2. Not inspecting the operation log before undo or restore:

    • Blindly running jj undo can sometimes be confusing if you don’t remember the exact sequence of your last few commands.
    • Best Practice: Always run jj op log first to get your bearings and identify the op-id you want to target, especially when using jj restore --op. This prevents unexpected state changes.
  3. Losing track of complex operation history:

    • The operation log can grow long, making it hard to find a specific point.
    • Solution: Use jj op log -n <count> to show only the last n operations. For example, jj op log -n 5 shows the 5 most recent operations. You can also pipe jj op log to grep if you remember keywords from the operation descriptions (e.g., jj op log | grep "amend").
  4. What happens if you undo an undo?

    • When you run jj undo, Jujutsu creates a new operation in the log. This new operation’s purpose is to revert your repository to the state of the previous operation. If you then run jj undo again, you are undoing that undo operation. This means jj will move your @ pointer back one more step in the log, effectively reapplying the change that the first undo had reversed. It feels like a redo because you’re moving past the undo operation in the linear log history.

Summary

The operation log is one of jj’s most powerful and distinguishing features. By keeping a comprehensive, immutable record of every state-changing command, jj provides:

  • A safety net for all your development activities, encouraging fearless experimentation and reducing anxiety.
  • Precise control over your repository’s history, allowing you to undo and redo operations with ease.
  • The ability to jump to any past state using jj restore --op, making complex history corrections simple and efficient.

Mastering the operation log transforms your VCS experience from a cautious dance around immutable history to a confident exploration of possibilities. You can now manipulate your project’s past without fear, knowing that jj has your back.

In the next chapter, we’ll build on this newfound control by exploring revsets, jj’s powerful syntax for selecting and filtering commits. This will allow you to precisely target the commits you want to manipulate, further enhancing your ability to craft perfect history.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.