Welcome back, fellow version control enthusiast! In the previous chapters, we’ve explored the foundational concepts of Jujutsu (jj), from its unique working-copy-as-a-commit model to the power of mutable history and the operation log. You’re now comfortable with jj’s core philosophy and its local development superpowers.
However, the reality of modern software development is that Git remains the dominant version control system. How do we reconcile jj’s innovative approach with the pervasive need to collaborate within a Git-centric ecosystem? This chapter is your bridge, showing you how jj and Git don’t just coexist, but work together beautifully.
Why This Matters
The ability to seamlessly interact with Git repositories is not just a convenience; it’s a necessity for jj users. Whether you’re contributing to a large open-source project, working on a team that exclusively uses Git, or migrating an existing Git codebase, jj’s interoperability features allow you to leverage its power without forcing your entire team to switch. You get the best of both worlds: jj’s superior local workflow and Git’s ubiquitous remote collaboration.
By the end of this chapter, you’ll be able to:
- Initialize
jjrepositories directly from existing Git projects. - Fetch and pull changes from Git remotes into your
jjworkspace. - Push your
jjhistory, including carefully crafted stacked changes, to Git remotes. - Understand the nuances of
jj’s branch concepts when interacting with Git. - Effectively resolve conflicts that arise during Git synchronization, making merges less painful.
Let’s dive into making jj and Git the best of friends, enabling you to contribute to any Git project with jj’s power!
Jujutsu’s Git Integration: A Symbiotic Relationship
At its core, jj isn’t trying to replace Git entirely; it’s designed to be a more powerful, user-friendly frontend to Git. When you use jj with a Git repository, jj actually maintains a full Git repository internally, alongside its own object store. This means jj understands Git objects, references, and protocols natively, acting as a sophisticated translator.
📌 Key Idea: Think of jj as a “smart wrapper” or an “enhanced interface” around a standard Git repository. It translates your jj commands and concepts into native Git operations when you interact with remotes, providing a superior local experience without disrupting external Git workflows.
The Mental Model: jj as Your Local Powerhouse
When you’re working locally, jj gives you superpowers: mutable history, easy stacking of changes, and the invaluable operation log. These features streamline your development process. When it’s time to share your work or incorporate others’ changes, jj gracefully handles the translation to and from Git, ensuring compatibility.
This diagram illustrates how jj acts as your primary interface, managing its own internal representation while seamlessly synchronizing with external Git repositories. This setup means jj isn’t just a separate tool; it’s deeply integrated with Git’s underlying mechanics.
Step-by-Step: Interacting with Git Repositories
Now let’s get hands-on with the essential commands for jj-Git interoperability.
1. Initializing a Jujutsu Repository from Git
The most common way to start using jj with an existing Git project is to clone it. jj provides a dedicated command that handles this transparently.
Action: Clone a Git Repository
Instead of git clone, you’ll use jj clone. Let’s set up a new directory for our work.
First, navigate to your desired parent directory. For example:
cd ~/projects
Now, let’s clone a hypothetical Git repository. For this exercise, you can use a public example like https://github.com/jj-vcs/jj.git for demonstration, or replace it with any Git repository URL you have access to.
jj clone https://github.com/jj-vcs/jj.git my-jj-git-project
After cloning, jj will provide feedback on the new repository and its current state.
Cloned Git repository into "my-jj-git-project".
Working copy now at node 61a1a7b3c2e3 (empty commit)
Notice the output: jj has cloned the Git repository and immediately created a jj working copy. The jj working copy always points to a commit, even if it’s an “empty commit” in terms of changes you’ve made directly. This is jj’s “working-copy-as-a-commit” model in action.
Action: Explore the Cloned Repository
Navigate into your new jj project directory:
cd my-jj-git-project
Now, let’s inspect the history using jj log:
jj log
You’ll see a history that mirrors the Git repository’s history, but presented in jj’s intuitive format. You might also see commits labeled with origin/main (or origin/master), indicating the remote’s default branch. This shows jj is aware of the remote state.
⚡ Quick Note: jj automatically sets up the remote origin for you, just like Git would. You can verify this by listing the Git remotes jj is aware of:
jj git remote list
origin (https://github.com/jj-vcs/jj.git)
This command confirms the Git remotes configured for jj’s internal Git repository.
2. Synchronizing Changes: Fetching and Pulling
Keeping your local jj repository in sync with the remote Git repository is crucial for collaboration. jj provides commands that mirror git fetch and git pull, but with jj’s mutable history advantages.
Action: Fetch Remote Changes with jj git fetch
jj git fetch does exactly what git fetch does: it downloads objects and references from the remote Git repository into your local jj’s internal Git repository. Crucially, it does not modify your local working copy or jj’s commit history directly. It only updates the remote-tracking branches (e.g., origin/main).
Let’s simulate fetching from a remote. If your cloned repository has new commits pushed to origin/main by other developers, jj git fetch will retrieve them.
jj git fetch origin
Fetching into "my-jj-git-project"...
[2026-05-19 10:00:00] Fetched from origin.
After fetching, you can see the new remote changes using jj log -r 'remote_branches()' or by comparing your local main with origin/main.
jj log -r 'main..origin/main'
This command uses a revset (which we’ll explore more deeply in a later chapter) to show commits that are on origin/main but are not yet part of your local main branch’s history.
Action: Pulling Changes with jj git pull
jj git pull is the equivalent of git pull --rebase. It first fetches changes from the remote and then automatically rebases your local changes on top of the newly fetched remote commits. This is jj’s default and recommended way to integrate changes, promoting a clean, linear history.
Let’s assume there are new commits on origin/main that you want to integrate into your local jj repository.
jj git pull origin main
Fetching into "my-jj-git-project"...
Rebasing 1 commits onto 87654321fedc...
Working copy now at node 987654321abc
🧠 Important: jj git pull automatically performs a rebase, aligning perfectly with jj’s philosophy of linear history and mutable changes. If you had local changes (commits not yet pushed to origin/main), jj would intelligently rebase them on top of the newly fetched remote commits. This is where jj shines, as its rebase operations are fast, reliable, and often conflict-free, even with complex stacks of changes.
Hands-on Exercise: Simulating a Remote Update and Pull
Let’s create a scenario where a “remote” changes, and you pull those changes into your jj repository. We’ll use a temporary Git repository to simulate this.
Create a dummy Git remote (outside your
jjproject):mkdir -p ../remote_repo_dummy cd ../remote_repo_dummy git init --bare cd ../my-jj-git-projectAdd the dummy remote to your
jjrepo:jj git remote add dummy ../remote_repo_dummySimulate another developer making a commit directly to the dummy remote:
cd ../remote_repo_dummy git clone . ../temp_clone_for_remote_commit cd ../temp_clone_for_remote_commit echo "Initial content for dummy remote" > remote_file.txt git add remote_file.txt git commit -m "feat: Initial commit on dummy remote main branch" git push origin main cd ../my-jj-git-projectNow, pull from the dummy remote in your
jjrepo:jj git pull dummy mainYou should see
jjfetching and then potentially rebasing. Even if you had no local changes,jjupdates itsdummy/mainreference and potentially moves your working copy to track the latest remote commit.Fetching into "my-jj-git-project"... Rebasing 0 commits onto <new_commit_id>... Working copy now at node <new_commit_id>jjhas successfully pulled the changes from your simulated remote! You can verify withjj log.
3. Contributing to Git: Pushing Your Changes
When you’ve made changes and commits in jj and are ready to share them with a Git remote, you use jj git push. This command pushes your local jj commits to a specified Git branch on a remote.
Action: Make Local Changes in jj
Let’s create a couple of stacked commits in jj.
echo "feature A line 1" >> feature-a.txt
jj commit -m "feat: Add feature A initial UI"
echo "feature A line 2" >> feature-a.txt
jj commit -m "feat: Implement feature A backend logic"
Now, your jj log will show two new commits on top of your main branch.
jj log -r 'main..'
This revset shows commits reachable from your working copy that are not reachable from main.
Action: Prepare for Push: Clean History
Git’s model generally prefers a linear, non-rewritten history for shared branches. While jj thrives on mutable history locally, it’s a best practice to clean up your local jj history (e.g., by squashing small commits or rebasing for a cleaner narrative) before pushing to a shared Git remote. This makes your contributions easier for others to review and merge.
Let’s squash our two feature commits into one, presenting a single, cohesive change to the remote.
jj squash @-
This command squashes the working copy’s parent commit (@-) into the working copy itself, effectively combining the two feature commits. You’ll be prompted to edit the commit message. Combine them into a single, concise message like “feat: Implement complete user profile editing feature”.
Rebased 1 commits onto <parent_of_squashed_commit_id>...
Working copy now at node <new_squashed_commit_id>
Now, jj log -r 'main..' should show just one clean, well-described commit ready for public consumption.
Action: Push with jj git push
Now that your history is clean and linear, you can push it to the remote.
jj git push origin main
Pushing to origin...
If successful, your local main branch will now be synchronized with origin/main.
⚠️ What can go wrong: If you try to push rewritten history (e.g., you jj rebase or jj amend a commit that’s already been pushed to Git and shared), jj git push will fail, just like git push would without --force. jj is smart enough to detect this and prevent accidental force pushes to shared branches. To force a push, you would use jj git push --force origin main, but this should be done with extreme caution and only when you understand the implications for other collaborators (as it rewrites shared history).
Hands-on Exercise: Push a Refactored Commit
- Make a new commit:
echo "temporary log addition" >> logs.txt jj commit -m "chore: Add temporary logging" - Amend the previous commit (simulate a quick fix or refinement):Edit the commit message to reflect the combined change (e.g., “chore: Refine logging setup”).
echo "another log line" >> logs.txt jj amend @- - Push to your dummy remote (created earlier):This should succeed because the
jj git push dummy mainamendonly changed a commit that hadn’t been pushed to thedummyremote yet. If you had tried to amend a commit already ondummy/mainand then pushed,jjwould prevent it without--force.
4. Branch Management: jj bookmarks vs. Git Branches
This is a critical area where jj’s philosophy diverges from Git’s, yet jj provides tools to bridge the gap effectively.
Git Branches in jj
When you clone a Git repository, jj automatically tracks Git branches (like main, develop, feature/x) as special jj branches. These jj branches behave like Git branches: they point to a specific commit, and when you push or pull, jj updates them accordingly, making them suitable for remote synchronization.
You can list these jj branches using jj branch list:
jj branch list
main: 61a1a7b3c2e3 (feat: Implement complete user profile editing feature)
@ 4b3d8c1e2f3a (chore: Refine logging setup)
The main entry here represents the main branch that jj manages and synchronizes with origin/main.
jj bookmarks: Your Local, Branchless Superpower
jj introduces bookmarks as a more flexible, local alternative to traditional Git branches. Bookmarks are simply named pointers to commits, but unlike jj branches (which are designed for Git interoperability), bookmarks are purely local to your jj repository and are not pushed to Git remotes.
🔥 Pro tip: For most of your local development in jj, especially when working with stacked changes, you’ll find yourself relying on jj’s “branchless” workflow. This means you just keep committing on top of your working copy, and jj handles the commit graph. Bookmarks become useful when you want to easily jump back to a specific point, mark a significant commit for later reference, or share a commit reference with another jj user locally.
You can create a bookmark like this:
jj bookmark my-experimental-feature
Now, jj log will show your bookmark alongside your commits:
...
@ 4b3d8c1e2f3a (chore: Refine logging setup)
my-experimental-feature
...
You can switch your working copy to a bookmark using jj edit my-experimental-feature.
When to use jj branch vs. jj bookmark:
jj branch: Use when you need to interact with a Git remote. These branches are synchronized with Git and are suitable for shared, long-lived branches.jj bookmark: Use for purely local navigation, temporary pointers, or when you don’t want to expose a branch to Git. They are perfect forjj’s branchless workflow, allowing you to easily manage multiple lines of work without the overhead of Git branches.
5. Resolving Conflicts with jj and Git
Conflicts are an inevitable part of collaborative development, especially when integrating changes from Git. jj provides a streamlined workflow for resolving them, making the process less daunting.
Action: Trigger a Conflict
A conflict typically occurs when you jj git pull (which rebases your changes) and both you and the remote have modified the same lines in a file.
Let’s simulate a conflict:
Introduce a local change in your
jjrepo:echo "my important local change" >> conflict_file.txt jj commit -m "feat: Add local conflict line"Simulate a remote change on the same line (using our dummy remote):
cd ../remote_repo_dummy git clone . ../temp_clone_2 cd ../temp_clone_2 echo "remote important change" >> conflict_file.txt git add conflict_file.txt git commit -m "feat: Add remote conflict line" git push origin main cd ../my-jj-git-projectNow, pull the remote changes into your
jjrepo:jj git pull dummy mainjjwill detect the conflict during the rebase operation and pause, indicating a conflicted state.Rebasing 1 commits onto <remote_commit_id>... Conflict: 'conflict_file.txt' was modified in parallel. Working copy now at node <conflict_commit_id> (conflict)Your working copy is now in a conflicted state, waiting for your intervention.
Action: Identify and Resolve Conflicts
jj provides commands to help you see and resolve conflicts.
View conflicting files:
jj diff --conflictsThis command shows you the conflicted file(s) with standard Git conflict markers (
<<<<<<<,=======,>>>>>>>).Manually edit the file: Open
conflict_file.txtin your favorite text editor. You’ll see content similar to this:# Before editing conflict_file.txt <<<<<<< my important local change ======= remote important change >>>>>>>Resolve the conflict by choosing which changes to keep, or by merging them. Then, remove the conflict markers. For example, if you decide to keep the remote change:
# After editing conflict_file.txt (e.g., choosing remote) remote important changeOr if you want to keep both:
# Example of keeping both changes my important local change remote important change
Action: Mark Conflicts as Resolved
Once you’ve manually edited the file and removed all conflict markers, you need to tell jj that the conflict is resolved.
jj resolve conflict_file.txt
You can resolve multiple files at once by listing them or using jj resolve --all. After resolving all conflicts, jj will automatically complete the paused rebase operation.
Resolved conflict in 'conflict_file.txt'.
Rebased 1 commits onto <remote_commit_id>...
Working copy now at node <resolved_commit_id>
Your working copy is now clean, and the rebase is complete.
Hands-on Exercise: Resolve a Conflict
Follow the steps above to create and resolve a conflict. Pay close attention to the output of jj diff --conflicts and how jj resolve completes the rebase. Experiment with different resolution strategies (keeping local, keeping remote, or merging both) to get comfortable with the editing process. This practice will build your confidence for real-world scenarios.
Mini-Challenge: Feature Development and Git Integration
Let’s put your jj and Git interoperability skills to the test with a more comprehensive scenario.
Challenge: You are tasked with developing a new feature, user-profile-editing. While you’re working, a critical bug fix is pushed to the remote. You should:
- Start from your
mainbranch. - Create two distinct
jjcommits for your feature:- One commit for adding the basic UI elements for profile editing.
- Another commit for implementing the backend logic for saving profile data.
- Simulate a conflict: have the “remote” modify a file (e.g.,
shared_config.txt) that your first UI commit also touched. - Pull the remote changes into your
jjrepository, and resolve any conflicts that arise. - After resolving, squash your two feature commits into a single, cohesive commit with a clear, descriptive message (e.g., “feat: Implement full user profile editing”).
- Finally, push your single, clean feature commit to the
dummyremote’smainbranch.
Hint: Remember to use jj squash to clean up your history before pushing to present a polished commit to the remote. For the conflict simulation, make sure the “remote” change to shared_config.txt overlaps with your jj UI commit’s change.
What to observe/learn: This challenge will reinforce the entire workflow: local jj development, handling unexpected remote updates, conflict resolution, history cleaning, and finally, pushing your polished work to Git. You’ll appreciate how jj simplifies the rebase and squash operations that are often cumbersome in raw Git, making your development process smoother and more efficient.
Common Pitfalls & Troubleshooting
Even with jj’s excellent design, integrating with Git can have its nuances. Being aware of these common pitfalls can save you time and frustration.
1. Pushing Rewritten History to a Shared Git Branch
- Pitfall: Accidentally trying to push a rewritten commit (one that’s already on the remote but you’ve
amended,rebased, orsquashedlocally) without explicitly using--force.jjwill correctly block this to protect shared history. - Troubleshooting: Understand that
jj’s mutable history is incredibly powerful locally. When interacting with shared Git remotes, aim for linear, non-rewritten history on shared branches (likemainordevelop). If you must force push (e.g., you’re the sole developer on a private feature branch, or coordinating with your team), usejj git push --force origin my-feature-branch. Always be extremely cautious with force pushes on shared branches, as they can cause significant headaches for other collaborators.
2. Misunderstanding jj’s Working Copy as a Commit Model
- Pitfall: Expecting
jjto behave exactly like Git, where the working directory is separate from a “staging area” and the “last commit.” This mental model can lead to confusion aboutjj commitandjj amend. - Troubleshooting: Remember, in
jj, your working copy is a commit (the “working-copy commit”). When youjj commit, you’re essentially creating a new commit on top of your current working copy’s state, and your working copy then points to this new commit. Changes in your working directory are uncommitted changes that will become part of the next commit (or amend the current one). This model simplifies many operations (likeamendorsquash) but requires a slight mental shift initially.
3. Issues with Git Hooks and jj
- Pitfall: Git hooks (like
pre-commit,pre-push) are often configured in the.git/hooksdirectory. Whenjjinteracts with its internal Git repository, these hooks might not always fire as expected or might require specific configuration. - Troubleshooting:
jjhas its own hook mechanism forjj-specific operations. Forjjspecific hooks, consult the officialjjdocumentation onjj-hooksfor the most current setup (as of 2026-05-19). For Git hooks that need to run whenjj git pushorjj git pullis executed,jjgenerally aims to run them where appropriate. If you encounter issues, verify the hook configuration within the internal.gitdirectoryjjuses (usually located under.jj/repo/store/gitwithin yourjjrepository, thoughjjhandles this mostly transparently). Ensure your Git hooks are compatible with howjjorchestrates Git operations.
Summary
Congratulations! You’ve successfully navigated the exciting world of jj and Git interoperability. You now possess the knowledge to integrate jj into virtually any Git-based workflow, leveraging jj’s local power while seamlessly collaborating with the wider Git ecosystem.
Here are the key takeaways from this chapter:
jjas a Git Frontend:jjacts as a powerful, user-friendly frontend to Git, managing an internal Git repository and translatingjjcommands into Git operations.- Cloning Git Repos: Use
jj cloneto easily startjjprojects from existing Git repositories. - Synchronization:
jj git fetchretrieves remote changes, andjj git pull(which automatically rebases) integrates them into your local history. - Contributing: Use
jj git pushto send your localjjcommits to Git remotes. Remember to clean up your history withjj squashorjj rebasefor a linear, easy-to-review Git history. - Branching Models: Understand the distinction:
jjbranches are for Git interoperability, whilejjbookmarks offer flexible, local, branchless workflow management. - Conflict Resolution:
jjprovides a streamlined workflow withjj diff --conflictsandjj resolveto handle merge conflicts efficiently.
You’re now equipped to enjoy jj’s superior local development experience while seamlessly collaborating with the wider Git world. This blend of local efficiency and global compatibility is what makes jj such a compelling tool for modern developers.
What’s next? In the upcoming chapters, we’ll dive deeper into advanced topics like revsets for powerful history manipulation, customizing jj for peak productivity, and adopting truly branchless workflows for large projects. Get ready to master jj!
References
- Jujutsu Official GitHub Repository
- Jujutsu User Manual: Git Integration (Checked 2026-05-19)
- Jujutsu User Manual: Bookmarks (Checked 2026-05-19)
- Jujutsu User Manual: Conflicts (Checked 2026-05-19)
- Git Official Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.