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 5f858c15
│ refactor printing
│ ○ povouosx steve@steveklabnik.com 2024-03-01 17:27:14 28010506
├─╯ remove goodbye message
○ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 2b93da0c
│ (empty) add better documentation
Everything looks to be in order. Let’s rebase our goodbye message change onto our refactor printing change:
$ jj rebase -r povouosx -o @
Rebased 1 commits to destination.
New conflicts appeared in 1 commits:
povouosx 793ce8e0 (conflict) remove goodbye message
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
jj new povouosx
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can 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 793ce8e0 (conflict)
│ remove goodbye message
@ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:29:12 5f858c15
│ refactor printing
○ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 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 a912c809 (conflict)
│ remove goodbye message
@ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07 d41c079b
│ refactor printing
○ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 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:
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
jj new povouosx
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can 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 povouosx 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
Warning: There are unresolved conflicts at these paths:
src/main.rs 2-sided conflict
Here’s src/main.rs:
/// A "Hello, world!" program.
///
/// This is the best implementation of this program to ever exist.
fn main() {
<<<<<<< conflict 1 of 1
+++++++ vvmrvwuz d41c079b "refactor printing" (rebase destination)
print("Hello, world!");
print("Goodbye, world!");
}
// a function that prints a message
fn print(m: &str) {
println!("{m}")
}
%%%%%%% diff from: yykpmnuq 2b93da0c "add better documentation" (parents of rebased revision)
\\\\\\\ to: povouosx 28010506 "remove goodbye message" (rebased revision)
print_hello();
- print_goodbye();
}
fn print_hello() {
println!("Hello, world!");
}
-
-fn print_goodbye() {
- println!("Goodbye, world!");
-}
>>>>>>> conflict 1 of 1 ends
git uses a combination of <<<<, =====, and >>>> to mark conflicts. jj
has richer conflict markers. It still uses the >>> and <<<s to indicate the
start and end, but has two others: +++++++ marks a snapshot, and %%%%%%%
marks a diff. Each one is labelled with the commit it came from, which is what
makes them readable.
So: the snapshot is the destination we rebased onto, vvmrvwuz, verbatim. Then
the diff is our own change, expressed as what povouosx did to its old parent
yykpmnuq — it dropped the print_goodbye() call and the whole
print_goodbye function. jj couldn’t apply that diff to that snapshot,
because the lines it wanted to remove aren’t there any more; vvmrvwuz rewrote
them into print(...) calls. So it hands you both halves and lets you do it.
Notice how much of the file is inside the conflict region. jj isn’t
conflicting on one line — the diff and the snapshot overlap across most of
main() and the functions below it, so the whole span gets marked.
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 7647f7a0
│ remove goodbye message
○ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07 d41c079b
│ refactor printing
○ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 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
Undid operation: 3c9a71fd50e2 (2024-03-01 18:08:23) snapshot working copy
Restored to operation: 8b40cc21ae77 (2024-03-01 17:49:07) snapshot working copy
New conflicts appeared in 1 commits:
povouosx a912c809 (conflict) remove goodbye message
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
jj new povouosx
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can 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 povouosx --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 07bb727d (conflict)
│ (empty) (no description set)
@ povouosx steve@steveklabnik.com 2024-03-01 17:49:07 a912c809 (conflict)
│ remove goodbye message
○ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07 d41c079b
│ refactor printing
○ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 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 9a4ad229
│ (empty) (no description set)
@ povouosx steve@steveklabnik.com 2024-03-01 18:12:43 f68d1623
│ remove goodbye message
○ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07 d41c079b
│ refactor printing
○ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 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 1 commits:
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. We’ll get to it in the “More advanced workflows” section.
We fixed this conflict by editing the markers out of the file ourselves. Next,
the other way of going about it: handing the file to a merge tool with
jj resolve.