Resolving conflicts from a Git rebase can be tricky. But don’t worry – here’s a comprehensive guide to how to resolve them.
There’s three phases:
- Which commit of mine is conflicting?
- What changes were made in the target branch that conflict with my commit?
- Resolve conflicts safely
These are accurate as of Git v2.23 and are for resolving conflicts using the command line.
Let’s walk through the process.
A conflict happens
On your working branch, you run:
git rebase origin/master
and are faced with a wall-of-text:
Applying: Improve naming around create-import-process end-point
Recorded resolution for 'src/octoenergy/interfaces/apisite/data_import/views.py'.
Applying: Extract serializer creation into own method
Using index info to reconstruct a base tree...
M src/octoenergy/interfaces/apisite/data_import/views.py
Falling back to patching base and 3-way merge...
Auto-merging src/octoenergy/interfaces/apisite/data_import/views.py
Applying: Group tests into classes for Account data-import serializer
Using index info to reconstruct a base tree...
M src/tests/unit/common/domain/data_import/validation/test_accounts.py
Falling back to patching base and 3-way merge...
Auto-merging src/tests/unit/common/domain/data_import/validation/test_accounts.py
CONFLICT (content): Merge conflict in src/tests/unit/common/domain/data_import/validation/test_accounts.py
Recorded preimage for 'src/tests/unit/common/domain/data_import/validation/test_accounts.py'
error: Failed to merge in the changes.
Patch failed at 0003 Group tests into classes for Account data-import serializer
hint: Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Conflicts? Conflicts!
Often the conflicts are simple and easily resolved by eye-balling the files in question. If that’s true, good for you: resolve the conflicts using your favourite editor and move on via:
git rebase --continue
However, if the conflicts are not trivially resolved, start by asking yourself this:
Which commit of mine is conflicting?
We can identify the conflicting commit in several ways:
-
Look for the
Patch failed at $NUMBER $SUBJECT
line in the rebase output. This prints the subject of the commit that couldn’t be applied. In the above example, the offending commit has subjectGroup tests into class for Account data-import serializer
. -
Alternatively, follow the advice in the rebase output and run:
git am --show-current-patch
which is equivalent to running
git show
on the conflicting commit. -
As of Git v2.17, this option can be used with
git rebase
too:git rebase --show-current-patch
-
Best of all, there is a
REBASE_HEAD
pseudo-ref that points to the conflicting commit, so you can do:git show REBASE_HEAD
to view the commit, or:
git rev-parse REBASE_HEAD
to see the commit SHA.
It can be useful to open the Github detail page for the offending commit to
allow a quick glance at the diff in another window. If you use hub
(and
Github), this can done with:
hub browse -- commit/$(git rev-parse REBASE_HEAD)
which, if you find it useful, could be alised as:
[alias]
openconflict = "!f() { hub browse -- commit/$(git rev-parse REBASE_HEAD); }; f"
in ~/.gitconfig
.
Now for the trickier question.
What changes were made in the target branch that conflict with my commit?
We understand what we were trying to do, but we need to understand what changes were made in the target branch that conflict. Two tips:
Use the diff3
format to see common ancestor code in conflict blocks
Globally enable this with:
git config --global merge.conflictstyle diff3
and then conflict blocks will be formatted like:
<<<<<<<< HEAD:path/to/file
content from target branch
|||||||| merged common ancestors:path/to/file
common ancestor content
========
content from your working branch
>>>>>>> Commit message:path/to/file
where the default conflict block has been extended with a new section, delimited
by ||||||||
and ========
, which reveals the common ancestor code.
Comparing the HEAD
block to the common ancestor block will often reveal the
nature of the target-branch changes, allowing a straight-forward resolution.
For instance, breath easy if the common ancestor block is empty:
<<<<<<<< HEAD:path/to/file
content from target branch
|||||||| merged common ancestors:path/to/file
========
content from your working branch
>>>>>>> Commit message:path/to/file
as this means both branches have added lines; they haven’t tried to update the same lines. You can simply delete the merge conflict markers to resolve.
If eyeballing the conflicts is not sufficient to safely resolve, we need to dig deeper.
Examine the target branch changes in detail
Sometimes the conflicted blocks are large and you can’t understand at a glance
what changes have been made in the target branch and why they were made. In this
situation, we may need to examine the individual changes made to each conflicted
$FILEPATH
in order to understand how to resolve safely.
We can examine the overall diff:
git diff REBASE_HEAD...origin/master $FILEPATH
or list the commits from the target branch that updated $FILEPATH
:
git log REBASE_HEAD..origin/master $FILEPATH
and review how each modified $FILEPATH
with:
git show $COMMIT_SHA -- $FILEPATH
Note the git diff
command uses three dots while the git log
command uses
two.
If there are lots of commits that modified $FILEPATH
, it can be helpful to run
git blame
and see which commits updated the conflicting block and focus your
attention on those.
This should provide enough information to understand the changes from the target branch so you can resolve the conflicts.
Resolve conflicts safely
A couple of things:
Apply your changes to the target branch code
When manually editing conflicted files, always resolve conflicts by applying
your changes to the target branch block (labelled HEAD
) as you understand your
changes better and are less likely to inadvertently break something.
For example: in the following diff:
<<<<<<<< HEAD
I like apples and pears
|||||||| merged common ancestors
I like apples
========
I love apples
>>>>>>> branch-a
Apply your change (replacing “like” with “love”) to the HEAD
block to give:
<<<<<<<< HEAD
I love apples and pears
|||||||| merged common ancestors
I like apples
========
I love apples
>>>>>>> working-branch
then remove the superseded lines and merge conflict markers to give:
I love apples and pears
Wholesale accept changes
Occasionally, you might know that the changes from one branch should be
accepted. This can be done using git checkout
with a merge “strategy-option”.
Beware that, when rebasing, the terminology is counter-intuitive.
To accept the changes from the target branch, use:
git checkout --ours -- $FILEPATH
To accept the changes made on your working branch, use:
git checkout --theirs -- $FILEPATH
As a rebase involves replaying your commits to the tip of the target branch, each replayed commit is treated as “theirs” (even though you are the author) while the existing target branch commits are “ours”.
Even more sweepingly, you can auto-resolve conflicts using a specified strategy when doing the rebase. Eg:
git rebase -Xtheirs origin/master
I’ve never used this much in practice though.
Re-use recorded resolutions (aka rerere
)
If you set:
git config --global rerere.enabled 1
then Git will record how you resolve conflicts and, if it sees the same conflict
during a future rebase (eg if you --abort
then retry), it will automatically
resolve the conflict for you.
You can see evidence of rerere
in action in the git rebase
output. You’ll
see:
Recorded preimage for '...'
when Git detects a conflicted file, then:
Recorded resolution for '...'
when Git records the resolution (to .git/rr-cache/
), and finally:
Resolved '...' using previous resolution.
when Git re-uses the saved resolution.
You should enable this – there’s no downside.
Summary
Hopefully the above is useful.
Resolving rebase conflicts is much easier if commits are “atomic”, with each change motivated by a single reason (similar to the Single Responsibility Principle). For instance, never mix file-system changes (ie moving files around) with core logic changes. Such commits are likely to attract conflicts and are hard to resolve.
Don’t worry if the rebase gets away from you; you can always abort with:
git rebase --abort
if things become too hairy.
Further reading
Here’s some additional resources on the topic:
-
Fix conflicts once with git rerere by Christophe Porteneuve (2014). Good detailed examination of how to use
git rerere
.