{"slug": "how-jujutsu-rethinks-git-s-working-copy-and-conflict-model", "title": "How Jujutsu Rethinks Git's Working Copy and Conflict Model", "summary": "Jujutsu (jj) is a version control system that rethinks Git's working copy and conflict model by eliminating the staging area and treating the working copy as an always-committed state. It uses Git repositories as a storage backend, offering full compatibility while providing a cleaner foundation for modern workflows, including AI-assisted coding. The tool aims to reduce friction in common Git operations like switching branches and resolving merge conflicts.", "body_md": "Git is so deeply embedded in how we work that its rough edges start to feel like facts of life. The staging area, the fear of rewriting history, the stash dance before switching branches, the merge conflict that grinds everything to a halt. [Jujutsu](https://github.com/jj-vcs/jj) — or jj — is a version control system that takes a hard look at those rough edges and asks: does it have to be this way?\n\nThe answer, it turns out, is no. jj isn't a radical reinvention — it's a disciplined redesign. It keeps what's great about Git (the distributed model, the commit graph, full GitHub/GitLab compatibility) and replaces the parts that have always been awkward with something more coherent. And as our workflows grow more demanding — particularly with AI agents now writing and managing code alongside us — that cleaner foundation is starting to matter a great deal.\n\nThis article is for developers who already know Git and want to understand what jj actually offers. Not a command cheatsheet — a genuine look at where jj's model pays off. Think of it as your white belt.\n\n[Getting Started: jj Layers on Top of Git](#getting-started-jj-layers-on-top-of-git)\n\nThe first thing to know: jj is not a replacement ecosystem. It's a separate CLI tool that uses a Git repository as its storage backend. Your history lives in Git objects. Your remotes are Git remotes. Your colleagues using plain Git are unaffected.\n\nYou can install jj via Homebrew on macOS, Cargo if you're in the Rust ecosystem, or download a binary directly from the [jj releases page](https://github.com/jj-vcs/jj/releases). On macOS:\n\n```\nbrew install jj\n```\n\nAdopting it in an existing repo is then a single command:\n\n```\njj git init --colocate\n```\n\nThis adds a `.jj`\n\nfolder alongside your existing `.git`\n\nfolder. That's it. The `.jj`\n\nfolder holds jj's own metadata — its operation log, working copy state, and repo-level config — but the actual content stays in `.git`\n\n. Crucially, jj keeps `.jj`\n\nout of Git's way by writing a `.gitignore`\n\ninside the folder itself, rather than touching your project's root `.gitignore`\n\n, so your ignore file stays clean and your teammates never see a thing.\n\nFrom this point on, you can use jj commands alongside or instead of Git commands. Both work on the same repository.\n\n[The Log: Your First Window Into jj](#the-log-your-first-window-into-jj)\n\nBefore diving into workflow, it's worth seeing how jj presents your history. Running `jj log`\n\nout of the box gives you something like:\n\n```\n@  mykqwroo bruno@git-tower.com 2026-06-08 15:07:41 my-feature a1b2c3d4\n│  add login validation\n○  kkzrwqpo bruno@git-tower.com 2026-06-07 09:14:22 main b2c3d4e5\n│  update README\n○  nnpqvstu bruno@git-tower.com 2026-06-06 16:33:01 c3d4e5f6\n│  initial commit\n~\n```\n\nThe `@`\n\nmarks your current working-copy commit. No flags needed — jj's default log template is already more readable than `git log --oneline --graph`\n\n. You see the relevant context immediately.\n\nNotice those short alphabetic strings like `mykqwroo`\n\n. Those are **change IDs** — jj's own commit identifiers, entirely separate from Git's SHA hashes. They matter a lot, and we'll come back to them.\n\n[A New Mental Model](#a-new-mental-model)\n\njj keeps everything structural about Git — the commit graph, the distributed model, the remotes — and rethinks two fundamentals instead: what your working copy actually *is*, and how commits are identified. Almost everything that feels different downstream traces back to these two ideas.\n\n[The Working Copy Is Always a Commit](#the-working-copy-is-always-a-commit)\n\nIn Git, there's a three-way distinction between your working directory, the staging area (index), and a commit. You edit files in the working directory, selectively add them to the index with `git add`\n\n, and then commit what's staged.\n\njj collapses this. There is no staging area. Your working copy is always, automatically, a commit. As you edit files, jj snapshots them continuously. You're never in an uncommitted state — you're always on a commit, it just might not have a description yet.\n\nThis means there is no `jj add`\n\n. You simply edit files and jj tracks the changes. When you're ready to describe what you've done:\n\n```\njj describe -m \"add login validation\"\n```\n\nOr, if you want to describe the current change and immediately start a new empty one — the closest equivalent to Git's commit-and-move-on:\n\n```\njj commit -m \"add login validation\"\n# shorthand for: jj describe -m \"...\" && jj new\n```\n\nAnd if you want to start fresh work without describing first — say, you're mid-feature and want to capture a checkpoint:\n\n```\njj new\n```\n\nThis seals the current commit and starts a new working-copy commit on top. If no description was provided, jj marks it as `(no description set)`\n\nin the log — it's still a fully tracked commit, just unnamed. Your in-progress work is never lost — it's just a commit without a name yet.\n\nThe practical impact of this model is hard to overstate. The staging area was always a source of subtle bugs (`git add -A`\n\nvs `git add .`\n\n, forgetting to stage a file before committing, the confusion of what's staged vs what's not). In jj, that entire class of problem simply doesn't exist.\n\n[Change IDs: References That Survive Rewrites](#change-ids-references-that-survive-rewrites)\n\nIn Git, every commit has a SHA hash derived from its content and its parent. Amend a commit, and you get a new hash. Rebase a branch, and every commit in it gets a new hash. This makes scripting and referencing commits fragile — the identity of a piece of work changes every time you reshape it.\n\njj introduces **change IDs**: stable, alphabetic identifiers assigned when a commit is first created and kept constant through any number of rewrites. Amend, rebase, squash — the change ID stays the same.\n\n```\njj edit mykqwroo    # works before and after a rebase\njj edit @-          # relative: parent of current commit\njj edit @--         # grandparent\njj edit main        # by bookmark name\n```\n\nThe `@`\n\nsymbol means \"current working-copy commit,\" and relative navigation with `-`\n\nsuffixes lets you move through history without looking up identifiers at all.\n\nGit hashes still exist — jj shows both in `jj show`\n\n— but they're treated as secondary. Day-to-day, you reference commits by change ID, bookmark name, or relative position.\n\n[A Calmer Everyday Workflow](#a-calmer-everyday-workflow)\n\nThose foundations quietly reshape the day-to-day. Branching, switching tasks, and undoing mistakes — the everyday moments where Git tends to introduce ceremony or risk — all get noticeably calmer.\n\n[Bookmarks: Branches Without the Obligation](#bookmarks-branches-without-the-obligation)\n\nIn Git, you're always on a branch. HEAD points to a branch, the branch moves with every commit, and switching context means committing, stashing, or losing work.\n\njj has **bookmarks** — pointers that work like Git branches — but you're not required to be on one. Commits exist freely in the graph, and you attach a bookmark name when you actually need one, typically when you're ready to push.\n\nThe workflow will feel familiar, with one key difference: in Git you declare a branch before you start working; in jj you name it at the end, when you're ready to push. Everything in between looks much the same:\n\n```\n# ...edit files...\njj commit -m \"add login validation\"\n# ...edit more files...\njj commit -m \"fix edge case\"\njj bookmark create my-feature -r @-   # name the branch when ready to push\njj git push --bookmark my-feature\n```\n\nNote that `jj commit`\n\nhandles staging and committing in one step — there is no `git add`\n\n. We point the bookmark at `@-`\n\n(the parent) because after a `jj commit`\n\nyour working copy is a fresh, empty commit sitting on top — `@-`\n\nis the last commit that actually holds work. Otherwise the flow is the same: a stack of commits, a named branch, a push. The PR workflow on GitHub works identically from here.\n\nWhen your branch is ready to merge locally, you have two options. For a fast-forward:\n\n```\njj rebase -b my-feature -d main        # rebase onto latest main\njj bookmark set main -r my-feature     # move main pointer forward\njj git push --bookmark main\n```\n\nOr for a proper merge commit:\n\n```\njj new main my-feature    # new commit with two parents = merge commit\njj bookmark set main -r @ # move main to the merge commit\n```\n\nNote that `jj new`\n\naccepts multiple parents — that's how merges are created. No dedicated `merge`\n\ncommand.\n\n[Switching Context Without the Stash Dance](#switching-context-without-the-stash-dance)\n\nHere's where the \"working copy is always a commit\" model pays off most visibly day-to-day.\n\nIn Git, switching branches with uncommitted work is a problem. You stash, switch, do your work, switch back, pop the stash, hope for no conflicts. It's tedious and error-prone.\n\nIn jj, there is no dirty working copy. Everything is always committed. If you need to switch to something else:\n\n```\njj edit kkzrwqpo     # jump to any commit in the graph\n```\n\nYour in-progress work stays exactly where it was — a commit sitting in the graph. Come back to it with another `jj edit`\n\n. No stash, no ceremony.\n\nYou can also maintain multiple in-flight workstreams simultaneously using **workspaces** — jj's equivalent of Git worktrees:\n\n```\njj workspace add ../hotfix-workspace\n```\n\nEach workspace gets its own working-copy commit, completely isolated. Switch between them freely. Nothing bleeds across.\n\n[Undo Everything](#undo-everything)\n\nIf you use [Tower](https://www.git-tower.com), you already know the comfort of ⌘ + Z (orCTRL + Z on Windows) to [undo your last Git action](https://www.git-tower.com/features/undo). jj takes this idea further, building undo into the version control model itself.\n\nEvery jj action — rebase, amend, describe, restore, even a failed command — is recorded in the **operation log**:\n\n```\njj op log\n@  abc123def bruno@bruno-mbp 2026-06-08 10:42:05 - 2026-06-08 10:42:05\n│  rebase commits\n│  args: jj rebase -b my-feature -d main\n○  def456abc bruno@bruno-mbp 2026-06-08 10:41:30 - 2026-06-08 10:41:30\n│  describe commit mykqwroo\n│  args: jj describe -m \"add login validation\"\n○  ghi789def bruno@bruno-mbp 2026-06-08 10:40:12 - 2026-06-08 10:40:12\n│  new empty commit\n~\n```\n\nMade a mistake?\n\n```\njj undo          # undo the last operation\njj op restore abc123   # go back to any specific point in op history\n```\n\nThis is meaningfully different from `git reflog`\n\n. The reflog tracks commit pointer movement. The operation log tracks every structural action — including rebases, squashes, and workspace changes — and makes all of them reversible. It's a safety net that covers ground no Git command reaches.\n\n[Reshaping History Without Fear](#reshaping-history-without-fear)\n\nGit's interactive rebase is powerful but intimidating. A text editor opens with a list of cryptic commands, one wrong edit can corrupt your history, and there's no obvious way to recover if you make a mistake. As a result, many developers avoid history editing entirely.\n\njj replaces the interactive rebase ceremony with dedicated, composable commands — each reversible with `jj undo`\n\n.\n\n[Splitting a Commit](#splitting-a-commit)\n\nYou've been heads-down and realize your commit is doing two unrelated things. In Git, this means `git rebase -i`\n\nand a careful dance of `edit`\n\n, reset, and re-commit. In jj:\n\n```\njj split\n```\n\nAn interactive hunk selector opens. You pick which changes belong in the first commit; the rest automatically become a second commit on top. What makes this particularly powerful: you can split *any* commit in history, not just the most recent one:\n\n```\njj split -r mykqwroo\n```\n\njj rebases any descendants automatically.\n\n[Squashing Commits](#squashing-commits)\n\nThe inverse — folding one commit into its parent:\n\n```\njj squash              # squash working copy into parent\njj squash -r mykqwroo  # squash a specific commit into its parent\njj squash --interactive  # pick specific hunks to move, leave the rest\n```\n\nThe `--interactive`\n\nflag uses the same hunk selector as `jj split`\n\n. You can move parts of a commit up and leave the rest in place — something that requires several steps in Git.\n\n[Abandoning Commits](#abandoning-commits)\n\nTo discard a commit entirely:\n\n```\njj abandon mykqwroo\n```\n\nIf the abandoned commit has descendants, jj automatically rebases them onto the abandoned commit's parent. Nothing is left dangling. And since it's an operation, `jj undo`\n\nbrings it straight back.\n\n[Conflicts as a State, Not a Blocker](#conflicts-as-a-state-not-a-blocker)\n\nThis is perhaps the most fundamental departure from Git's model.\n\nIn Git, a merge conflict is a hard stop. The operation halts, you must resolve every conflict before you can continue, and until you do, your repository is in a suspended state. If you're mid-rebase with ten commits and hit a conflict on the third, you're stuck until it's resolved.\n\njj treats conflicts differently: they're a state a commit can be in, not a blocker. When a conflict occurs during rebase or merge, jj records it *inside the commit* and keeps going:\n\n```\njj rebase -b my-feature -d main\n# jj notes conflicts but does not stop\n# all commits land in the graph, some marked as conflicted\n```\n\nYou can inspect which files have conflicts and resolve them when you're ready:\n\n```\njj resolve --list    # list all conflicted files\njj resolve           # open your configured merge tool (VS Code, vimdiff, etc.)\n```\n\nOnce you save in the merge tool, jj detects the resolution automatically. No `git add`\n\nto mark files resolved, no `git rebase --continue`\n\nto type.\n\nThe deeper implication: you can rebase an entire stack of commits through a conflict, keep all of them in the graph, and resolve everything in one session later. This is not just convenient — it changes how you think about integrating parallel work.\n\n[jj in the Age of AI Agents](#jj-in-the-age-of-ai-agents)\n\nThe properties that make jj better for individual developers turn out to make it particularly well-suited for the agentic workflows that are becoming standard today.\n\n[Parallel Agents, Isolated Workspaces](#parallel-agents-isolated-workspaces)\n\nWhen running multiple AI coding agents simultaneously — each working on a different feature or task — the standard Git approach is worktrees: separate directories checked out from the same repo. jj's workspaces work the same way, but with one key advantage. In Git, two worktrees cannot share the same branch. In jj, since commits are independent of bookmarks, multiple workspaces coexist freely with no restrictions:\n\n```\njj workspace add ../agent-auth\njj workspace add ../agent-ui\njj workspace add ../agent-tests\n```\n\nEach agent operates in its own workspace, editing its own working-copy commit, with no risk of collision.\n\n[Stable References for Orchestrators](#stable-references-for-orchestrators)\n\nA recurring problem in AI-driven workflows: an orchestrator or script tracks a commit by its hash, a rebase or amend happens, and the reference breaks. You're left chasing a commit that has technically vanished.\n\njj's change IDs solve this. An orchestrator can record the change ID of a piece of work at the start and reference it reliably through any number of rewrites — squashes, rebases, amends — without ever losing track:\n\n```\n# record the change ID when the agent starts work\nCHANGE_ID=$(jj log -r @ --no-graph -T 'change_id')\n\n# later, after multiple rewrites, still works\njj show $CHANGE_ID\n```\n\n[Async Conflict Resolution](#async-conflict-resolution)\n\nWhen multiple agents produce conflicting changes, Git's model requires one agent to block and resolve before the other can continue. jj's first-class conflict handling changes this dynamic entirely.\n\nConflicting agent outputs can land in the graph as conflicted commits, without halting either agent's workflow. An orchestrator — or a human — can then inspect the conflicts at a natural pause point and resolve them in batch:\n\n```\njj resolve --list    # see everything that needs attention\njj resolve           # work through them one by one\n```\n\nThis maps much more naturally onto asynchronous, parallelized workflows than Git's synchronous conflict model.\n\n[Caveats & Closing Thoughts](#caveats-amp-closing-thoughts)\n\njj is genuinely impressive, but it's worth being clear-eyed about where it stands today. After all, jj has a bit of a learning curve — but so does any martial art.\n\n[A Few Honest Caveats](#a-few-honest-caveats)\n\n**The CLI is not yet stable.** Command names and flags can change between versions. A notable example: what was previously called `branches`\n\nwas renamed to `bookmarks`\n\n, turning `jj branch create`\n\ninto `jj bookmark create`\n\novernight — breaking existing guides, scripts, and muscle memory in one go. The jj team is thoughtful about this — changes tend to be improvements — but it means you may occasionally need to update muscle memory or scripts.\n\n**The GUI ecosystem is thin.** jj's terminal output is excellent, and a handful of dedicated tools have appeared — GG (a cross-platform GUI with a visual graph), terminal UIs like Lazyjj and jjui, and a VS Code extension — but the tooling is nowhere near as mature as Git's. Since jj uses a Git backend, Git GUIs can read the repository, though they won't understand jj-specific concepts like the operation log.\n\n**AI coding tools are Git-first.** Cursor, Copilot, and most agentic tools are built around Git commands. jj's theoretical advantages for agentic workflows are real, but the ecosystem hasn't caught up yet. You'd be working around some tooling assumptions.\n\nNone of this should be dealbreakers, especially given how easy adoption is. The colocated setup means you can try jj without any commitment — use it on your own machine, keep your team on Git, and flip between the two whenever you need.\n\n[Closing Thoughts](#closing-thoughts)\n\njj doesn't ask you to abandon what you know about Git. The concepts translate — commits, branches, remotes, merges — and the underlying storage is Git itself. What changes is the model: a working copy that's always a commit, change IDs that survive rewrites, conflicts that don't stop the world, history that's safe to reshape.\n\nFor developers who've ever stashed work to switch branches, lost a rebase to a bad conflict resolution, or hesitated before amending a commit in shared history — jj makes those specific moments noticeably less stressful. Not through new features, but through cleaner foundations.\n\nIt's worth an afternoon. The install is one command, the undo is always there if you need it, and you might find yourself wondering why we ever settled for the staging area in the first place. Give it a try — you might not look back.\n\nFor more on version control workflows and Git best practices, sign up for our newsletter below and follow Tower on [Twitter / X](https://twitter.com/gittower) and [LinkedIn](https://www.linkedin.com/company/gittower/)! ✌️\n\n## Join Over 100,000 Developers & Designers\n\nBe the first to know about new content from the Tower blog as well as giveaways and freebies via email.", "url": "https://wpnews.pro/news/how-jujutsu-rethinks-git-s-working-copy-and-conflict-model", "canonical_source": "https://www.git-tower.com/blog/jujutsu", "published_at": "2026-07-01 11:49:27+00:00", "updated_at": "2026-07-01 12:20:15.902943+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Jujutsu", "Git", "GitHub", "GitLab", "Homebrew", "Cargo"], "alternates": {"html": "https://wpnews.pro/news/how-jujutsu-rethinks-git-s-working-copy-and-conflict-model", "markdown": "https://wpnews.pro/news/how-jujutsu-rethinks-git-s-working-copy-and-conflict-model.md", "text": "https://wpnews.pro/news/how-jujutsu-rethinks-git-s-working-copy-and-conflict-model.txt", "jsonld": "https://wpnews.pro/news/how-jujutsu-rethinks-git-s-working-copy-and-conflict-model.jsonld"}}