When Git Fails Silently

Learn how Git's three-way merge can unexpectedly change your code, and explore strategies to avoid these pitfalls by always updating your branch before merging pull requests.

The examples shown in post are available as Git repository here.

Imagine your codebase looks like this:

line 1
line 2

Your coworkers, Alice and Bob, each submit a pull request on their respective branch.

Alice

The PR of Alice includes two commits:

  • In the first commit, she adds her own name to the top of the file:
+ alice
line 1
line 2
  • In the second commit, she copies line 1 and line 2 above the existing code:
+ line 1
+ line 2
alice
line 1
line 2

Bob

Bob’s PR consists of a single commit where he adds his name in between the existing code:

line 1
+ bob
line 2

Code Review

You are tasked with reviewing the PRs from Alice and Bob.

First you inspect the two diffs (#1 and #2) of Alice’s commits and then merge her PR. Then, you proceed to review the diff of Bob’s PR.

After merging both PRs, you anticipate the code to appear as follows:

line 1
line 2
alice
line 1
bob
line 2

However, contrary to your expectations, the code looks like this:

line 1
bob
line 2
alice
line 1
line 2

What Happened?

Git uses a three-way-merge strategy (more precisely the ort strategy), which does not consider intermediate commits. It analyzes the differences between the latest commits of the two commit histories and their common ancestor commit.

Loading graph...

So in our case, it will only compare the 2nd commit of Alice with the one of Bob and doesn’t take into consideration the first commit of Alice. The information on how she arrived at a specific version of the code is lost.

Specifically, both Alice’s 2nd commit and the common ancestor begin with:

line 1
line 2

Meanwhile, Bob’s file starts with:

line 1
bob
line 2

Consequently, Git opts for Bob’s section of the code. During the subsequent step, while Bob and the common ancestor share an empty section, Alice’s version contains:

alice
line 1
line 2

Hence, Git selects Alice’s section in this instance. This results in the final code looking as follows:

line 1
bob
line 2
alice
line 1
line 2

How Can You Prevent This?

To avoid this issue, use GitHub’s options to always update your branch before merging:

Settings > General > Pull Requests

Always suggest updating pull request branches

Settings > Branches > Protection Rules

Require branches to be up to date before merging

Note While this doesn’t prevent Git from merging “incorrectly,” this method allows you to identify errors before merging into your main branch. Therefore, you can notice the error when inspecting the diff (Files changed) and your CI would be capable of detecting them.

References

This entire repository is based on an example presented in this talk.