Recovering Clean History in Jujutsu (jj)
Table of Contents
-
- Fixing Accidental Amendments with Splits and Evolution Logs
- Introduction
- Recreating the Problem
- Accidental Amendment
- Using jj evolog to Identify the Problem
- Analysis
- Solution: Splitting the Commit to Recover Clean History
- Step 1: Use jj split
- Step 2: Verify the Original Commit
- Step 3: Push the Updated History
- Key Learnings
- Conclusion
Fixing Accidental Amendments with Splits and Evolution Logs
Yes, something like this happened to me. I changed an already-pushed commit by accident. I believe it happened by using lazyjj to push, which does not create a new, empty commit automatically. In contrast, the command-line jj git push behaves more nicely:
jj git push
Changes to push to origin:
Move forward bookmark master from f5f200a0c577 to 980383609075
Warning: The working-copy commit in workspace 'default' became immutable, so a new commit has been created on top of it.
Working copy now at: pqoollns 32a643f2 (empty) (no description set)
Parent commit : sywonovy 98038360 master | my super change
Note the “Warning: The working-copy commit in workspace ‘default’ became immutable, so a new commit has been created on top of it.”
So, be careful with lazyjj!
Introduction
Imagine this scenario: You’re working on a feature branch called feature-A. In Jujutsu (jj), this branch is represented by the bookmark feature-A (local) and feature-A@origin (remote). To ensure you’re up-to-date, you run jj git fetch, which pulls in the latest remote changes and advances the bookmarks (feature-A and feature-A@origin) to the most recent commits.
After fetching, you continue your work, but then it happens: you accidentally add a new file, notes.txt, and modify the working copy. You forget to create a new commit first and only realize later that these changes have been implicitly added to the feature-A bookmark. Now, the bookmark no longer reflects the clean state of the fetched changes, and the branch history has evolved unexpectedly.
How do you recover? This post will guide you through a practical example of using Jujutsu’s tools like jj split and jj evolog to recover clean history, often without requiring a force-push.
Recreating the Problem
Let’s say you just fetched the latest changes from the remote.
jj git fetch
This fetch updates your local feature-A bookmark to include the newly pulled commit. To avoid accidentally modifying the fetched commit, you decide to create a new, empty commit immediately:
jj new feature-A
Working copy now at: powtnvyo 1d7e19fb (empty) (no description set)
Parent commit : wkmuoqnv e33c6025 feature-A | server: fix output length
Added 46 files, modified 8 files, removed 0 files
However, for some reason, you intentionally switch back to the parent commit (the fetched commit) to make edits:
jj edit @- # note how @- marks the change before the most recent change
Working copy now at: wkmuoqnv e33c6025 feature-A | server: fix output length
Parent commit : mppkovuv 626c67f2 server: close connection
Now you’re back at the fetched commit (wkmuoqnv) to inspect and edit.
Accidental Amendment
While inspecting the fetched changes, you accidentally add a new file:
echo "a note" > notes.txt
Running jj st shows:
jj st
Working copy changes:
M server.zig
A notes.txt
Working copy : wkmuoqnv cbd58b07 feature-A* | server: fix output length
Parent commit: mppkovuv 626c67f2 server: close connection
Notice:
- The working copy now has a new Git hash (
cbd58b07), replacing the original (e33c6025). - The asterisk (
*) next tofeature-Aindicates the bookmark has been modified.
Using jj evolog to Identify the Problem
The jj evolog command is a crucial tool for understanding how your branch’s history has evolved. It shows all previous Git commit hashes associated with a change. We can use it to inspect what happened to the change wkmuoqnv that now has an asterisk next to its bookmark.
Run:
jj evolog -r feature-A
This might produce:
○ ymwzwypn bot@zml.ai 2024-12-06 12:58:57 feature-A* cbd58b07
│ server: fix output length
○ wkmuoqnv hidden bot@zml.ai 2024-12-06 12:58:51 git_head() 33ea9c76
│ server: fix output length
○ mppkovuv bot@zml.ai 2024-12-06 12:40:22 feature-A@origin 626c67f2
│ server: close connection
Analysis
- Current Commit (
cbd58b07): Contains the changes, including the accidental addition of notes.txt. - Previous Commit (
33ea9c76): The original commit before the accidental changes, verified withgit_head(). - Origin Commit (
626c67f2): The state offeature-A@originafter the fetch.
This helps identify exactly which changes have been made locally (notes.txt) and confirms the Git hash of the original commit that should remain unchanged.
Solution: Splitting the Commit to Recover Clean History
Step 1: Use jj split
Since the accidental file addition (notes.txt) should not be part of the original commit, you can split the @ change into two commits:
jj split # -r @
The interactive editor launches, showing the files modified in this commit:
First, select
server.zigand confirm with the original commit message:server: fix output length. Keeping the original message ensures the Git hash of this commit remains unchanged (33ea9c76).Next, select
notes.txtfor the second commit. Confirm and provide a meaningful commit message:Add notes.txt.
After splitting, the log looks like this:
jj log
@ ymwzwypn bot@zml.ai 2024-12-06 12:58:57 feature-A* c533dfd4
│ Add notes.txt
○ wkmuoqnv bot@zml.ai 2024-12-06 12:58:51 git_head() 33ea9c76
│ server: fix output length
Step 2: Verify the Original Commit
To confirm the original commit remains intact, check its Git hash:
jj show wkmuoqnv
Output:
Commit ID: 33ea9c76
Change ID: wkmuoqnvx...
Bookmarks: feature-A
Author: User <user@example.com>
Committer: User <user@example.com>
server: fix output length
Since the Git hash matches the original state (33ea9c76), no force-push is required.
Step 3: Push the Updated History
Finally, push the corrected history:
jj bookmark set feature-A # -r @
jj git push
Because the original commit hash (33ea9c76) is preserved, this push does not require a force-push. The remote remains compatible with the local changes.
Key Learnings
- Interactive Splitting:
- Use
jj splitto separate accidental changes into distinct commits. - Ensure the original commit message remains identical to avoid altering the Git hash.
- Use
- Evolution Log (jj evolog):
- Provides a detailed history of all commits associated with a branch.
- Helps identify which changes were part of the original commit versus newly introduced changes.
- Bookmark Indicators:
- The asterisk (*) next to the bookmark in the log or status output helps identify if the working copy has diverged.
- Force-Push Avoidance:
- Keeping the original commit message and content unchanged ensures compatibility with the remote history.
Conclusion
Recovering clean history in Jujutsu is straightforward with tools like jj split, jj evolog, and bookmark indicators. By carefully managing commit messages and using interactive splitting, you can recover from accidental amendments without needing a force-push.
yes, part of this blog post is authored by ChatGPT, in case you were wondering 😄