Resolving conflicts during a Git rebase

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:

  1. Which commit of mine is conflicting?
  2. What changes were made in the target branch that conflict with my commit?
  3. 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 subject Group 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:

——————

Something wrong? Suggest an improvement or add a comment (see article history)
Tagged with: git
Filed in: guides

Previous: Software development tips – part 1
Next: Software development tips – part 2

Copyright © 2005-2024 David Winterbottom
Content licensed under CC BY-NC-SA 4.0.