Dealing with conflicts
When you're merging or rebasing, if your changes are incompatible with each
other, you may introduce a conflict. Conflicts are often regarded as painful
by users of version control systems. jj
can't make that pain go away entirely,
but it can help a lot.
Let's deliberately introduce a conflict. First, we make a new change:
$ jj new -m "remove goodbye message"
Working copy now at: povouosx e2c9628c (empty) remove goodbye message
Parent commit : yykpmnuq 2b93da0c (empty) add better documentation
And then update src/main.rs
appropriately:
/// A "Hello, world!" program. /// /// This is the best implementation of this program to ever exist. fn main() { print_hello(); } fn print_hello() { println!("Hello, world!"); }
Let's also make a new change off of the previous head:
$ jj new yykpmnuq -m "refactor printing"
Working copy now at: vvmrvwuz 44205653 (empty) refactor printing
Parent commit : yykpmnuq 2b93da0c (empty) add better documentation
Added 0 files, modified 1 files, removed 0 files
And if we open src/main.rs
again, we'll see that of course, it's back to the
state it was before we made our other change:
/// A "Hello, world!" program. /// /// This is the best implementation of this program to ever exist. fn main() { print_hello(); print_goodbye(); } fn print_hello() { println!("Hello, world!"); } fn print_goodbye() { println!("Goodbye, world!"); }
Let's make a very silly change: our own print function. Edit src/main.rs
to
look like this:
/// A "Hello, world!" program. /// /// This is the best implementation of this program to ever exist. fn main() { print("Hello, world!"); print("Goodbye, world!"); } fn print(m: &str) { println!("{m}") }
Excellent:
$ jj log --limit 3
@ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:29:12.000 -06:00 5f858c15
│ refactor printing
│ ◉ povouosx steve@steveklabnik.com 2024-03-01 17:27:14.000 -06:00 28010506
├─╯ remove goodbye message
◉ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36.000 -06:00 2b93da0c
│ (empty) add better documentation
Everything looks to be in order. Let's rebase our goodbye message change onto our refactor printing change:
#![allow(unused)] fn main() { $ jj rebase -r povouosx -d @ New conflicts appeared in these commits: povouosx 793ce8e0 (conflict) remove goodbye message To resolve the conflicts, start by updating to it: jj new povouosxlror Then use `jj resolve`, or edit the conflict markers in the file directly. Once the conflicts are resolved, you may want inspect the result with `jj diff`. Then run `jj squash` to move the resolution into the conflicted commit. }
Wait a minute, I thought I told you that rebases always succeed. Well... it did:
> jj log --limit 3
◉ povouosx steve@steveklabnik.com 2024-03-01 17:30:32.000 -06:00 793ce8e0 conflict
│ remove goodbye message
@ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:29:12.000 -06:00 5f858c15
│ refactor printing
◉ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36.000 -06:00 2b93da0c
│ (empty) add better documentation
Remember, @
stays where it is, and we moved a commit ahead of us, so we're
good. Go ahead, check out src/main.rs
, you'll see that it's still just like
it was before, with our printer refactoring.
However, you'll notice that in our log output, it says that povouosx
is now
conflicted. This is why rebases always succeed in jj
: if there's a conflict,
it doesn't make you stop and fix it, it records that there's a conflict and
still performs the rest of the rebase. This is very powerful. Even in this
case with one change, it lets us handle the conflict when we're ready. We can
keep making changes to our current change if we want to:
/// A "Hello, world!" program. /// /// This is the best implementation of this program to ever exist. fn main() { print("Hello, world!"); print("Goodbye, world!"); } // a function that prints a message fn print(m: &str) { println!("{m}") }
And then we check our log again:
$ jj log --limit 3
Rebased 1 descendant commits onto updated working copy
◉ povouosx steve@steveklabnik.com 2024-03-01 17:49:07.000 -06:00 a912c809 conflict
│ remove goodbye message
@ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07.000 -06:00 d41c079b
│ refactor printing
◉ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36.000 -06:00 2b93da0c
│ (empty) add better documentation
jj
automatically rebased povouosx
again. It's still in conflict. But that's
totally okay. We only need to handle it when we're ready. This automatic
rebasing behavior only works because jj
is okay with commits being in
conflict. And if we had even more children commits, they'd all be rebased,
automatically.
Resolving the conflict
The output we got back when the conflict was created gave us some advice:
To resolve the conflicts, start by updating to it:
jj new povouosxlror
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you may want inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.
This advice is good, but also more complex than we need to do right now. Doing
this is a great way to handle a complex resolution, where you want to double
check what you've done before you apply the changes. But we are just using a
small example to make a point. Therefore, we can just edit povouosxlror
and
remove the conflict markers directly:
> jj edit povouosx
Working copy now at: povouosx a912c809 (conflict) remove goodbye message
Parent commit : vvmrvwuz d41c079b refactor printing
Added 0 files, modified 1 files, removed 0 files
Here's src/main.rs
:
/// A "Hello, world!" program. /// /// This is the best implementation of this program to ever exist. fn main() { <<<<<<< +++++++ print("Hello, world!"); print("Goodbye, world!"); %%%%%%% print_hello(); - print_goodbye(); >>>>>>> } // a function that prints a message fn print(m: &str) { println!("{m}") }
git
uses a combination of <<<<
, =====
, and >>>>
to mark conflicts. jj
has more rich conflict markers. It still uses the >>>
and <<<
s to indicate
the start and end, but has two other markers: +++++++
and %%%%%%%
. The +
s
indicate the start of a snapshot, and %
s mark the start of a diff. So we can
see that we have two print
lines, but our changes wanted to remove one of them,
but since that's not the same thing, conflict. To resolve this, we apply our own
take on the diff to the snapshot:
/// A "Hello, world!" program. /// /// This is the best implementation of this program to ever exist. fn main() { print("Hello, world!"); } // a function that prints a message fn print(m: &str) { println!("{m}") }
Let's take a look:
$ jj st
Working copy changes:
M src\main.rs
Working copy : povouosx 7647f7a0 remove goodbye message
Parent commit: vvmrvwuz d41c079b refactor printing
$ jj log --limit 3
@ povouosx steve@steveklabnik.com 2024-03-01 18:08:23.000 -06:00 7647f7a0
│ remove goodbye message
◉ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07.000 -06:00 d41c079b
│ refactor printing
◉ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36.000 -06:00 2b93da0c
│ (empty) add better documentation
Conflict resolved!
Automatic rebasing conflict resolution
A wild thing about this though is the combination of conflicted changes and automatic rebasing. Here, I'll show you. First we need to undo our resolution, and then we'll make a new change on top of our conflicted change:
> jj undo
New conflicts appeared in these commits:
povouosx a912c809 (conflict) remove goodbye message
To resolve the conflicts, start by updating to it:
jj new povouosxlror
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you may want inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.
Working copy now at: povouosx a912c809 (conflict) remove goodbye message
Parent commit : vvmrvwuz d41c079b refactor printing
Added 0 files, modified 1 files, removed 0 files
> jj new povouosxlror --no-edit
Created new commit mlzwmxzs 07bb727d (conflict) (empty) (no description set)
> jj log --limit 4
◉ mlzwmxzs steve@steveklabnik.com 2024-03-01 18:10:08.000 -06:00 07bb727d conflict
│ (empty) (no description set)
@ povouosx steve@steveklabnik.com 2024-03-01 17:49:07.000 -06:00 a912c809 conflict
│ remove goodbye message
◉ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07.000 -06:00 d41c079b
│ refactor printing
◉ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36.000 -06:00 2b93da0c
│ (empty) add better documentation
We have our conflict, and then our new change, mlzwmxzs
, is also in conflict.
So fix the conflict in main.rs
again, and then let's see what happens:
> jj log --limit 4
Rebased 1 descendant commits onto updated working copy
◉ mlzwmxzs steve@steveklabnik.com 2024-03-01 18:12:43.000 -06:00 9a4ad229
│ (empty) (no description set)
@ povouosx steve@steveklabnik.com 2024-03-01 18:12:43.000 -06:00 f68d1623
│ remove goodbye message
◉ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07.000 -06:00 d41c079b
│ refactor printing
◉ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36.000 -06:00 2b93da0c
│ (empty) add better documentation
Not only did we fix our issue, but after we did, jj
automatically rebased
mlzwmxzs
, and the fix propagated correctly. mlzwmxzs
is no longer in
conflict.
By the way, let's abandon that change, as we don't intend to use it for anything right now:
$ jj abandon mlzwmxzs
Abandoned commit mlzwmxzs 9a4ad229 (empty) (no description set)
Great, we've cleaned that up.
These behaviors, namely recording conflicts and automatic rebasing, form the
behaviors necessary for a very cool jj
workflow, and that's stacking pull
requests. Before we talk about that though, we have to talk about how to
use jj
with GitHub in the first place! Let's go over that next.