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 jj repositories directly from existing Git projects.
  • Fetch and pull changes from Git remotes into your jj workspace.
  • Push your jj history, 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.

flowchart LR User[Developer] -->|Local Workflow| Jujutsu[Jujutsu Interface] Jujutsu -->|Manages internally| InternalGit[Internal Git Repository] InternalGit -->|Syncs with| RemoteGit[Remote Git Server] RemoteGit -->|Collaborates with| OtherUsers[Other Developers]

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.

  1. Create a dummy Git remote (outside your jj project):

    mkdir -p ../remote_repo_dummy
    cd ../remote_repo_dummy
    git init --bare
    cd ../my-jj-git-project
    
  2. Add the dummy remote to your jj repo:

    jj git remote add dummy ../remote_repo_dummy
    
  3. Simulate 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-project
    
  4. Now, pull from the dummy remote in your jj repo:

    jj git pull dummy main
    

    You should see jj fetching and then potentially rebasing. Even if you had no local changes, jj updates its dummy/main reference 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>
    

    jj has successfully pulled the changes from your simulated remote! You can verify with jj 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

  1. Make a new commit:
    echo "temporary log addition" >> logs.txt
    jj commit -m "chore: Add temporary logging"
    
  2. Amend the previous commit (simulate a quick fix or refinement):
    echo "another log line" >> logs.txt
    jj amend @-
    
    Edit the commit message to reflect the combined change (e.g., “chore: Refine logging setup”).
  3. Push to your dummy remote (created earlier):
    jj git push dummy main
    
    This should succeed because the amend only changed a commit that hadn’t been pushed to the dummy remote yet. If you had tried to amend a commit already on dummy/main and then pushed, jj would 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 for jj’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:

  1. Introduce a local change in your jj repo:

    echo "my important local change" >> conflict_file.txt
    jj commit -m "feat: Add local conflict line"
    
  2. 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-project
    
  3. Now, pull the remote changes into your jj repo:

    jj git pull dummy main
    

    jj will 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 --conflicts
    

    This command shows you the conflicted file(s) with standard Git conflict markers (<<<<<<<, =======, >>>>>>>).

  • Manually edit the file: Open conflict_file.txt in 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 change
    

    Or 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:

  1. Start from your main branch.
  2. Create two distinct jj commits 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.
  3. Simulate a conflict: have the “remote” modify a file (e.g., shared_config.txt) that your first UI commit also touched.
  4. Pull the remote changes into your jj repository, and resolve any conflicts that arise.
  5. 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”).
  6. Finally, push your single, clean feature commit to the dummy remote’s main branch.

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, or squashed locally) without explicitly using --force. jj will 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 (like main or develop). If you must force push (e.g., you’re the sole developer on a private feature branch, or coordinating with your team), use jj 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 jj to 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 about jj commit and jj amend.
  • Troubleshooting: Remember, in jj, your working copy is a commit (the “working-copy commit”). When you jj 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 (like amend or squash) 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/hooks directory. When jj interacts with its internal Git repository, these hooks might not always fire as expected or might require specific configuration.
  • Troubleshooting: jj has its own hook mechanism for jj-specific operations. For jj specific hooks, consult the official jj documentation on jj-hooks for the most current setup (as of 2026-05-19). For Git hooks that need to run when jj git push or jj git pull is executed, jj generally aims to run them where appropriate. If you encounter issues, verify the hook configuration within the internal .git directory jj uses (usually located under .jj/repo/store/git within your jj repository, though jj handles this mostly transparently). Ensure your Git hooks are compatible with how jj orchestrates 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:

  • jj as a Git Frontend: jj acts as a powerful, user-friendly frontend to Git, managing an internal Git repository and translating jj commands into Git operations.
  • Cloning Git Repos: Use jj clone to easily start jj projects from existing Git repositories.
  • Synchronization: jj git fetch retrieves remote changes, and jj git pull (which automatically rebases) integrates them into your local history.
  • Contributing: Use jj git push to send your local jj commits to Git remotes. Remember to clean up your history with jj squash or jj rebase for a linear, easy-to-review Git history.
  • Branching Models: Understand the distinction: jj branches are for Git interoperability, while jj bookmarks offer flexible, local, branchless workflow management.
  • Conflict Resolution: jj provides a streamlined workflow with jj diff --conflicts and jj resolve to 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

  1. Jujutsu Official GitHub Repository
  2. Jujutsu User Manual: Git Integration (Checked 2026-05-19)
  3. Jujutsu User Manual: Bookmarks (Checked 2026-05-19)
  4. Jujutsu User Manual: Conflicts (Checked 2026-05-19)
  5. Git Official Documentation

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.