Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Resolving merge conflicts

Reset your progress

To reset your progress to the start of this chapter, run the following command:

curl https://jj-for-everyone.github.io/reset.sh | bash -s conflict
cd ~/jj-tutorial/repo

Alice & Bob just attended the next programming lecture. Among other things, they finally learned about loops in Python. Alice now wants to go back to her experiment to fix it:

jj new 'description("WIP: Add for loop")'

Her code currently looks like this:

for (i = 0; i < 10; i = i + 1):
    print('Hello, world!')

This syntax is wrong, so Alice fixes it with the syntax she learned during the lecture:

for _ in range(10):
    print('Hello, world!')

You can edit the file manually or run this command:

echo "for _ in range(10):
    print('Hello, world!')" > hello.py

Let's commit that correction:

jj commit -m "Fix loop syntax"
@  zzlzryut alice@local 2025-08-23 21:21:54 5b7e967e(empty) (no description set)oxnuoryz alice@local 2025-08-23 21:21:54 git_head() c6042124
│  Fix loop syntax
○  tvrzpsvy alice@local 2025-08-23 21:21:53 push-tvrzpsvypkqk 6e468237
│  WIP: Add for loop (need to fix syntax)
│   quvtvrzl alice@local 2025-08-23 21:21:54 main d0c3efcd
│ │  Print German and French greetings as well
│ ~  (elided revisions)
├─╯
  krymmwqm alice@local 2025-08-23 21:21:53 1581674f(empty) Merge code and documentation for hello-world
~

Creating a conflict

Alice now wants to push her new code with the loop to main. She decides to do so with an explicit merge commit:

jj new main @-
Working copy  (@) now at: srpklysn 0f64cf5a (conflict) (empty) (no description set)
Parent commit (@-)      : quvtvrzl d0c3efcd main | Print German and French greetings as well
Parent commit (@-)      : oxnuoryz c6042124 Fix loop syntax
Added 1 files, modified 2 files, removed 0 files
Warning: There are unresolved conflicts at these paths:
hello.py    2-sided conflict

Oh no! There's a scary red (conflict) marker on the newly created merge commit and a warning telling is about a conflict in the file hello.py. The conflict can also be seen in the log:

@    lozqokkt alice@local 2025-08-23 21:34:35 fe21ff1e conflict
├─╮  (empty) (no description set)
│ ○  oxnuoryz alice@local 2025-08-23 21:21:54 c6042124
│ │  Fix loop syntax
│ ○  tvrzpsvy alice@local 2025-08-23 21:21:53 push-tvrzpsvypkqk 6e468237
│ │  WIP: Add for loop (need to fix syntax)
quvtvrzl alice@local 2025-08-23 21:21:54 main git_head() d0c3efcd
│ │  Print German and French greetings as well
~(elided revisions)
├─╯
  krymmwqm alice@local 2025-08-23 21:21:53 1581674f(empty) Merge code and documentation for hello-world
~

You may have already guessed how this happened. One side of the merge, the main branch, changed the file hello.py in one way (adding more languages). The other side added the loop. Jujutsu doesn't know how to combine these changes. This situation is called a conflict.

Reading conflict markers

Open the file hello.py in a text editor or run cat hello.py to see the content of the conflicted file:

<<<<<<< Conflict 1 of 1
%%%%%%% Changes from base to side #1
 print('Hello, world!')
+print('Hallo, Welt!')
+print('Bonjour, le monde!')
+++++++ Contents of side #2
for _ in range(10):
    print('Hello, world!')
>>>>>>> Conflict 1 of 1 ends

Yikes, that looks gnarly. Let's try to untangle what we're seeing so we can understand the conflict step-by-step.

Focus on the first and last lines:

<<<<<<< Conflict 1 of 1
...
>>>>>>> Conflict 1 of 1 ends

These two lines indicate the start and end of a conflict. In bigger files, it's often the case that most of the file can be merged without conflict. Only when both sides of a merge modify the same lines does Jujutsu report a conflict. These chunks of conflicted lines are then manifested as numbered sections, with start and end markers like the above. In our example, the file is so small that the entire thing is a single conflict.

Next, focus on the first section within the conflict:

%%%%%%% Changes from base to side #1
 print('Hello, world!')
+print('Hallo, Welt!')
+print('Bonjour, le monde!')

The "heading" of this section says "Changes from base to side #1". What are "base" and "side #1"? Think of the "base" as the commit where the two branches of a merge started to fork. This commit is visible in the log above, it's the one with the description: "Merge code and documentation for hello-world".

The number of the side corresponds to the order in which we specified the commits to be merged. The merge command was jj new main @-, so "side #1" corresponds to the main branch.

So, this section is showing us the changes that were made to the conflicted lines on the main branch. The format for showing the changes is the common "diff" format. In this format, all lines are prefixed with either a space, a minus - or a plus + sign. The space means the line was not changed. Minus means the line was removed and plus means the line was added. A line that was changed is represented as one removed and one added line.

In this case we have one unchanged line (the English greeting) and two added lines (the German and French ones).

Time to look at the second section of the conflict:

+++++++ Contents of side #2
for _ in range(10):
    print('Hello, world!')

This section is not showing us the "changes from base to side #2", it's just showing us the final state of side #2. The reason is that the state of base can be inferred from the previous section.

This explanation may have seemed overly verbose. But merge conflicts can get more complicated than this. Hopefully you are a little better prepared to face them from now on.

Fixing a conflict

Now that we understand the state of each side of the conflict, how do we fix it? There is no mechanical solution here! If there was, Jujutsu would've done it for you. The point of a merge conflict is that the correct resolution depends on the meaning of the conflicted changes.

In this case, the conflicted changes are:

  • Print the greeting ten times in a loop.
  • Also print a German and French greeting.

When merging these two changes, should the German and French greetings also be repeated ten times? If all languages should be repeated, should they be interleaved or neatly separated?

By example, there are three different reasonable merges of these changes:

  1. Only repeat the English greeting:

    for _ in range(10):
        print('Hello, world!')
    print('Hallo, Welt!')
    print('Bonjour, le monde!')
    
  2. Repeat all languages and interleave them:

    for _ in range(10):
        print('Hello, world!')
        print('Hallo, Welt!')
        print('Bonjour, le monde!')
    
  3. Repeat all languages but keep them separate:

    for _ in range(10):
        print('Hello, world!')
    for _ in range(10):
        print('Hallo, Welt!')
    for _ in range(10):
        print('Bonjour, le monde!')
    

I think you get the point. In order to resolve a merge conflict correctly, you usually have to work your brain a little.

Let's pick the second option as our conflict resolution. You can edit the conflict markers manually or simply replace the file with this command:

echo "for _ in range(10):
    print('Hello, world!')
    print('Hallo, Welt!')
    print('Bonjour, le monde!')" > hello.py

Let's check the log to confirm our merge commit is not marked as conflicted anymore:

@    kprnqvtk alice@local 2025-08-24 07:41:36 b46df36e
├─╮  (no description set)
│ ○  ttuuwuvl alice@local 2025-08-24 07:38:02 c8479848
│ │  Fix loop syntax
│ ○  uxpxzolo alice@local 2025-08-24 07:38:01 push-uxpxzoloznts b326e556
│ │  WIP: Add for loop (need to fix syntax)
wruksqvz alice@local 2025-08-24 07:38:02 main git_head() 118457e4
│ │  Print German and French greetings as well
~(elided revisions)
├─╯
  kropmvym alice@local 2025-08-24 07:38:01 8c90e9fc(empty) Merge code and documentation for hello-world
~

Fantastic! A conflict resolution is considered a change itself, so the commit isn't marked as "(empty)" like the first merge commit we made. Let's commit this and push to main:

jj commit -m "Merge repetition and translation of greeting"
jj bookmark move main --to @-
jj git push

Conflicts during a rebase

Conflicts can also occur when doing a rebase. Those are slightly more difficult to solve though. We will learn about it in the next level. If you run into a conflict while doing a rebase, consider aborting for now (with jj undo) and creating a merge commit instead.

Tools that help with conflict resolution

What I've taught you above is to solve the conflict manually by replacing the conflict markers with the desired outcome. That manual approach will always work. Conflicts can be tedious to solve manually, though.

There are various tools that can help you solve conflicts in certain situations. For example, the built-in tools :ours and :theirs let you discard one side of the conflict and only keep the other. Which side of the conflict is "ours" and which one is "theirs" may not be obvious. Just try one and run jj undo if needed. For example:

# keep "our" changes, throw away "theirs"
jj resolve --tool :ours

In addition to these simple built-in tools, Jujutsu can call more powerful, external ones. One such example is Mergiraf. It tries to automatically fix a conflict based on the syntax tree of the programming language of the conflicted file. Jujutsu doesn't ship with Mergiraf though, you have to install it separately if you want to use it. Once that's done, use it like this:

jj resolve --tool mergiraf

That works because Jujutsu has a default tool configuration for how to call Mergiraf. There are likely other tools that Jujutsu doesn't know about. If you want to use one of those, you can add a custom merge tool configuration.