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
jjcommands 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 revertorgit reset --hardscenarios.
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
Create an initial commit with content:
We’ll start by adding a
greeting.txtfile and committing it. Thejj 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"Create a
mainbranch:Next, we’ll create a named branch reference. This action is also recorded as an operation.
jj branch create mainMake another change and commit:
Let’s add more content to
greeting.txtand 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"Amend the last commit (both content and message):
The
jj amendcommand 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
jjcommand that generated the operation. - Parents: (Not explicitly shown in this default view, but operations can have parent operations, especially after
undoorredo.)
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:
Add a new file and commit:
echo "This is a new feature." > feature.txt jj new -m "Added feature file"Add another file and commit:
echo "Configuration settings." > config.ini jj new -m "Added config file"Now your
jj op logwill show these two new operations on top.jj op logYou’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
amendoperation, but before thefeature.txtandconfig.inicommits. Find theop-idof youramendoperation (e.g.,3a2f8b7e7a7e).jj restore --op 3a2f8b7e7a7e # Use your amend op-idjjwill report that it restored to that operation. Check your working directory:feature.txtandconfig.iniare gone! Your repository has been completely reset to the state right after you amendedgreeting.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 targetop-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:
- Create a file
data.jsonwith some JSON content and commit it.echo '{"version": 1}' > data.json jj new -m "Initial data.json" - Add a new line to
greeting.txtand commit it.echo "This is an important update." >> greeting.txt jj new -m "Added important update to greeting" - Rename
data.jsontoconfig.jsonand 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
Confusing
jj undowith Git’sgit revertorgit reset:jj undoliterally 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 revertcreates a new commit that undoes the changes of a previous commit. It doesn’t rewrite history.git resetmoves your branch pointer and potentially discards commits, but it’s often more destructive and less easily reversible thanjj undo.- Pro Tip: Always think of
jj undoas a true “undo” button for yourjjcommands, affecting your entire repository state.
Not inspecting the operation log before
undoorrestore:- Blindly running
jj undocan sometimes be confusing if you don’t remember the exact sequence of your last few commands. - Best Practice: Always run
jj op logfirst to get your bearings and identify theop-idyou want to target, especially when usingjj restore --op. This prevents unexpected state changes.
- Blindly running
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 lastnoperations. For example,jj op log -n 5shows the 5 most recent operations. You can also pipejj op logtogrepif you remember keywords from the operation descriptions (e.g.,jj op log | grep "amend").
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 runjj undoagain, you are undoing that undo operation. This meansjjwill move your@pointer back one more step in the log, effectively reapplying the change that the firstundohad reversed. It feels like aredobecause you’re moving past theundooperation in the linear log history.
- When you run
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
- Jujutsu Official GitHub Repository: https://github.com/jj-vcs/jj
- Jujutsu Tutorial - The Operation Log: https://github.com/martinvonz/jj/blob/main/docs/tutorial.md#the-operation-log
- Jujutsu Commands Documentation: https://github.com/jj-vcs/jj/blob/main/docs/commands.md
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.