Jujutsu undoes a botched rebase back to the exact original commit IDs
Every jj command commits the working copy and writes itself into one continuous operation log, and jj undo works by stepping back through that log to the exact prior state.
Every edit in Jujutsu is already a commit
In Jujutsu, an edit to a tracked file is already inside a commit before you ask it to be. There is no git add, no staged area holding changes apart from history, and no moment when a change exists only on disk and not yet in the record. Almost every command starts by folding whatever changed in the working directory into the commit currently checked out, then does whatever it was asked to do, then checks the result back out again.1 jj labels that checked-out commit @ in its own log output, and that same loop — fold in the changes, act, check out — runs before nearly everything the tool does.1
Every command that changes the repository also writes what it did into a second record, the operation log. One operation per command, each one holding where every bookmark and every workspace's working copy pointed at that moment, linked back to the operation before it.2 A live jj 0.44.0 install now proves it, not the documentation's own examples: a rebase typed into the wrong destination, and undone.
The @ commit and what the operation log keeps
jj's log prints two IDs per commit, and the difference between them is the reason a later undo can be exact. The first, the change ID, stays fixed for as long as that piece of work exists, however many times it gets rewritten. The second, the commit ID, is a hash over the commit's actual content and position in history, and it changes every time either one does.34 A rebase, an amend, a squash: each rewrites the commit, so each produces a new commit ID for a change whose ID never moved.3
There is no staging area separating a written change from a committed one. What Git does with git add -p and git commit, jj does with jj split. What Git does with git add -p and git commit --amend, jj does with jj squash, aimed at whichever earlier commit the edit actually belongs to. Rewrite an ancestor this way and every descendant commit is rebased onto the new version automatically, each getting a new commit ID while keeping the change ID and content it already had.5
Every one of those rewrites is recorded as an operation: the repository's full state at that moment, bookmark and workspace positions included, linked to the operation it came from.2 jj undo steps back through that chain one operation at a time. jj op restore skips straight to any operation still in the log, however many steps back it sits.2
A botched rebase, undone exactly
The setup: three commits on a fresh, colocated jj 0.44.0 repository, one adding a config module, one adding an auth module that imports from it, one adding the entrypoint. Intending to rebase the auth-and-main stack onto a new base, root() gets typed instead of the real destination, detaching that whole stack from the config commit it depends on. jj rebase -s uomvmzxo -d root() runs cleanly, no error, no prompt: jj does exactly what it was told. The log shows the damage immediately, two disconnected lines where there was one, and config.py is gone from the working directory.
$ jj log
@ pynpyzxp 01a1182e
│ add main entrypoint
○ uomvmzxo c323dd04
│ add auth module
○ zkuqvpzv 4a46c9d1
│ add config module
◆ zzzzzzzz root() 00000000
$ jj rebase -s uomvmzxo -d root()
Rebased 2 commits to destination.
Working copy (@) now at: pynpyzxp cdc6bf8c add main entrypoint
Parent commit (@-) : uomvmzxo a7677b0a add auth module
Added 0 files, modified 0 files, removed 1 files
$ jj log
@ pynpyzxp cdc6bf8c
│ add main entrypoint
○ uomvmzxo a7677b0a
│ add auth module
│ ○ zkuqvpzv 4a46c9d1
├─╯ add config module
◆ zzzzzzzz root() 00000000
$ ls proj
auth.py main.py
$ jj undo
Undid operation: 88b3604a313c (2026-08-14 06:59:40) rebase commit c323dd040fbbae5c93736a3cc95ec5bbcad3ed5d and descendants
Restored to operation: e7af953a3da7 (2026-08-14 06:59:05) squash commits into caebfb3e0a4fdd641a3c87371696c4b81fac2894
Working copy (@) now at: pynpyzxp 01a1182e add main entrypoint
Parent commit (@-) : uomvmzxo c323dd04 add auth module
$ jj log
@ pynpyzxp 01a1182e
│ add main entrypoint
○ uomvmzxo c323dd04
│ add auth module
○ zkuqvpzv 4a46c9d1
│ add config module
◆ zzzzzzzz root() 00000000
jj undo runs.2
The commit IDs jj undo restores, 01a1182e, c323dd04, 4a46c9d1, are the exact ones that existed before the mistake, not new commits with matching content. One command reversed a two-commit rebase across the whole subtree, because undoing it did not mean reconstructing history. It meant pointing back at an operation that was already sitting in the log.2
A single jj undo like the one above reverses one operation, which jj has always been able to do. Reaching back through more than one is the part that is newer than it looks. Before jj 0.33.0, released 2025-09-03, jj undo only ever reversed the single most recent operation, and calling it again undid that undo instead of stepping further back.6 That was real enough to draw complaints from people trying to walk back more than one mistake.7 jj 0.33.0 made jj undo sequential, so repeated calls now step back through the operation log one entry at a time.6 That multi-step reach is a maintained, dated feature, not a constant the tool has always had.
Where the old rebase instincts still work
Some of what a Git-fluent rebase instinct already knows still applies here. A rebase is still "move this set of commits onto a new parent." The graph is still a DAG of commits with parents and children. And rewriting an ancestor still cascades onto every descendant automatically, the same behavior that let one wrong -d root() argument detach two commits from their parent in a single step rather than a chain of separate rebases.5
Two pieces of Git muscle memory do not transfer. The first is reaching for a branch to mean "where I am right now." jj has bookmarks instead, and a bookmark does not move on its own when a new commit lands on top of it. Moving it and pushing it are two separate commands, jj bookmark move (or set) and then jj git push, where Git does both in one.8 The push itself checks more than Git's default does: it fails, rather than silently overwriting, if the remote bookmark has moved somewhere the local push did not expect. That is the equivalent of git push --force-with-lease running by default instead of by flag.8
The second break is what a conflict does to the command that created it. In Git, a rebase that lands two conflicting changes on the same line stops: the operation halts, the working tree fills with markers, and nothing else happens until git rebase --continue says the conflict is resolved. In jj, the command that creates a conflict finishes anyway. The conflict becomes a property of the resulting commit, labeled (conflict) in the log, and markers only appear when that commit is actually checked out or diffed.9 Resolve the file and let the next command see it, and the label clears on its own. There is no --continue to remember, because there was never a halted operation waiting for one.9
Four costs the project doesn't hide
None of the above is free, and the project's own documentation says so plainly rather than leaving a practitioner to find out.
The Git backend is not fully lock-free. jj's storage is deliberately safe for concurrent and even network-synced use, rsync, NFS, Dropbox, because its view and operation objects are content-addressed and never mutated in place. The one documented exception is the Git backend's change-ID index: "with the Git backend, repository corruption is possible because the backend is not entirely lock-free," the project's own concurrency documentation states.10 It names a specific, still-open bug where a repository synced by something other than native git or jj commands can end up with commits wrongly reported missing or hidden.11 The recovery is one documented command, jj debug reindex. The bug itself remains open.1011
First-class conflicts have a documented edge case where the convenience costs real information. When two sides of a merge make the identical change, jj's "same-change rule" auto-resolves the conflict rather than surfacing it. The project's own technical documentation names the trade-off plainly, calling the automatic resolution "lossy in terms of conflict algebra."12 Rebase a commit onto one with the same change, then rebase it back, and the documentation says outright that the result "will lose changes."12 A tracked, reproducible bug shows the loss on a real criss-cross merge, not only a hypothetical one.13
Git hooks do not run under jj. The feature-support table is direct about it: "Hooks: No."14 One independent practitioner review hit this directly: a FreeBSD codebase whose workflow depended on a prepare-commit-msg hook forced a fallback to plain Git, because jj had nothing there to invoke.15 The same review found no equivalent to git commit --fixup. Folding a working-copy edit into an earlier commit in a patch stack means writing a fixup!-prefixed description by hand, retyping the target commit's message rather than pointing at its hash.15 jj's own partial answer, jj absorb, moves working-copy hunks into whichever ancestor last touched that line automatically. The project documents its limit in the same breath: it "doesn't solve all cases," and will not move a change if multiple commits in the stack touched that same line.16
The two-command bookmark push adds up in practice, not just on paper. The same practitioner review calls it "a small friction, but it adds up."15 Colocation, running a real .git and .jj side by side so ordinary Git commands keep working, carries its own list of documented downsides: slower commands in repositories with many refs, and "bugs when interleaving mutating jj and git commands."14 A second, independent account hit a version of that same seam directly. A colocated repository combined with a jj workspace behaved as a pure jj repo to a tool, Claude Code, that expected a real Git working tree. That sent the author back to Git worktrees for that specific integration instead.17
The project's own paperwork disagrees about who backs it
Jujutsu ships often enough to trust the release notes as a live record, not a historical one. Fifteen tagged releases landed between v0.30.0 on 2025-06-04 and v0.44.0 on 2026-08-05, about one every four weeks, with no gap longer than five.6 Governance is just as formal. There are nine named maintainers, a documented process for adding or removing one, and a rule written specifically to prevent one employer from controlling the project: no single company may pay more than a third of the maintainers.18 Cross-referencing the project's own maintainer roster against its own paid-contributors disclosure puts three of those nine, Martin von Zweigbergk, Ilya Grigoriev, and Austin Seipp, on the payroll of one company, East River Source Control. That is exactly the cap the governance document allows, not comfortably under it.19
Who employs those three people, and the project's founder among them, is where the record stops agreeing with itself. The README's own "Mandatory Google Disclaimer" section states, in Martin von Zweigbergk's voice, that Jujutsu "has evolved into my full-time project at Google, with several other Googlers (now) assisting development in various capacities."20 The paid-contributors disclosure, maintained by the project for the express purpose of naming who pays whom, lists that same person as compensated by East River Source Control, a separate company, alongside two other current maintainers, not by Google.19 East River Source Control is a real, operating company. Its own site describes it as building "high-quality tooling to help you manage your most important asset: your code," and links out to Jujutsu's documentation. It does not itself say how the company is funded or state a formal relationship to the project.21 Neither document carries a visible date, and nothing reconciles a founder whose own README calls this his full-time job at Google with a disclosure that pays him through a different company instead. Both statements are current in the same source tree, and neither should be taken as settled on its own.
The roadmap carries a second version of the same tension. It excuses the absence of target dates anywhere on the roadmap by saying "most people contributing to Jujutsu do so in their spare time."22 A governance rule caps one paying employer's influence. A disclosure list names more than forty people paid by three different companies. The two only make sense together if a real share of the work is professional, not spare-time, contribution. Both descriptions come from the project itself, and both are current. Neither is softened here into "Google's tool" or "a funded startup's tool": the record does not support either claim cleanly, so both stand, unreconciled, as what the project's own documents say.
- A release about every four weeks for fourteen straight months, with no gap over five.6
- An operation log that turned a botched two-commit rebase back into byte-identical commit IDs with one command.2
- Every cost named above, index corruption, lossy conflicts, missing hooks, is documented by the project against itself, not surfaced by outside critics.101214
- The Git backend's corruption bug and the same-change rule's data loss are both still open, not fixed.1113
- Who is actually paying for the maintainers' time is not the same story in the README and in the paid-contributors list.2019
- A pre-1.0 tool by the project's own admission: workflows and on-disk formats can still change in backward-incompatible ways.20
The operation log's safety net held up under a direct test: the byte-identical restore above came from a live command sequence, not from the documentation's own examples.2 Each of the four costs above is tracked in the project's own open issue log, not left for a user to discover firsthand.1113 Who is actually paying for that work is the one question this record cannot close. The README calls it Google's project.20 The paid-contributors list pays three of its own maintainers, the founder among them, through a different company.19 Trust the first two findings on the evidence above. Leave the third open until a source says more than either document does today.
Sources
- Jujutsu Docs · Working copy
- Jujutsu Docs · Operation log
- Jujutsu Docs · Glossary
- Jujutsu Docs · Tutorial
- Jujutsu Docs · Git comparison
- Jujutsu · CHANGELOG.md
- Hacker News · Comment by mkeeter
- Jujutsu Docs · Bookmarks
- Jujutsu Docs · Conflicts
- Jujutsu Docs · Technical: Concurrency
- GitHub · jj-vcs/jj Issue #2193
- Jujutsu Docs · Technical: Conflicts
- GitHub · jj-vcs/jj Issue #6369
- Jujutsu Docs · Git compatibility
- Minsoo Choo · My experience with Jujutsu: a pragmatic review
- Jujutsu Docs · Git experts
- Arun · Jujutsu VCS
- GitHub · jj-vcs/jj GOVERNANCE.md
- GitHub · jj-vcs/jj paid_contributors.md
- GitHub · jj-vcs/jj README.md
- East River Source Control · ersc.io
- Jujutsu Docs · Roadmap