DEV Community

Mark
Mark

Posted on

When Git merge goes quietly wrong or the case of missing commits

Working with git never stops to amaze me.
The simplicity on the frontend and the complexity behind the scenes.

Here is an example of such case with git merge.

The mystery: why are commits missing?

Recently we faced a subtle but serious problem while working on a long-lived feature branch in git.

Periodically, we merge code that is introduced on the master branch to the feature branch, so that we will work on the most recent code base and avoid last minute conflicts or face additional work when the feature is complete.

Following one of the typical merges from the master branch to the feature branch, we noticed that some commits we expected on the target branch were in fact missing.

The only reason we noticed it, is due to the CI pipelines started to fail without obvious reason.

Think about it, the pipelines were completely fine just minutes ago when they ran on the master branch.

At first, we suspected that the merge was unsuccessful, maybe some incorrect conflict resolution reverted something. Or the source branch was not up to date.

After deep investigation, we noticed that the problem was rooted in how git determines the common ancestor (merge base) and how merges behave when they are not finalized correctly.

Let's examine our special workflow

  1. We had a long-lived feature branch that periodically is merged with updates from the master branch to stay up-to-date.
  2. In our policy, we cannot directly push to the feature branch, hence we would create an intermediate branch for integration.
git checkout -b merge-from-master
git merge --no-commit --no-ff
# make a PR or several PRs based on external code dependencies, back to feature
Enter fullscreen mode Exit fullscreen mode
  1. Merge the PRs using squash merge policy
  2. Merge is completed and we expect all the changes from master branch to be present on the feature branch.

Here is the funny thing. The weren't.

The problem - merge base was not updated.

As you might be aware, git uses a 3-way merge algorithm based on the merge base, which is the last common ancestor of both branches. If you merge master branch into the feature branch but:

Use --no-commit and never finalize the merge, or use squash merges.
You basically neglect to update the merge base ancestor.

In which case, git doesn’t consider those changes as "merged."
So when you later merge the feature branch back into the master branch, git will use an outdated merge base.
And assumes that reverted or neutralized files are "unchanged"

Which will eventually lead to silently skipping commits.

In our case, this led to code files and business logic being missing in our branch.

Consider the case below.

  1. You wrote a function func() and committed it to the master branch.
  2. You create a branch from master to work on a long-lived feature. You end up getting the func() to your branch.
  3. Your manager asked you to add a certain change to the product release. This change involves modifying the function you've recently added. So you make the necessary changes. Satisfied, you commit to master.
  4. You then merge from master to your feature branch to get it up-to-date. You end up getting the updated function on your branch.
  5. Later, one of the security scanning tools detected a security vulnerability in your function. As this is quite close to release to production, the manager agrees to postpone the changes and you end up reverting your code to the function func(). Now happy, you commit to the master branch and end your working day.
  6. Next morning, you merge the master to your feature branch. As the merge base was never updated. And we already know that git merge considers states.

Considering that the code file at the beginning had the function func().
Then it was modified, but eventually the changes were reverted. Hence, if we to look at the code at the beginning of the process and at the end of it. We would find that the file remaind the exact same.
Hence, by git design, nothing changed and thus nothing to merge.
Hence forth, our feature branch will stay with vulnerable function func() without us even realizing it.
And we can end being merging it back to our code base on the master branch when the feature is completed.

How we detected the issue?

We started by noticing that some file changes on a master branch were not present in the feature branch after merging.

To trace it, we ran:
git log feature -- path/to/file
git log master -- path/to/file

We compared the file contents
git diff $(git merge-base feature master) feature -- path/to/file
git diff $(git merge-base feature master) master -- path/to/file

We realized that the file had been changed and later reverted on master, and since the feature branch still had a different version, git had skipped the file silently during the final merge.

The fun part? The merge base hadn't updated because we never committed the earlier merge properly.

How to avoid the issue?

  • Always Commit Merges

If you run:
git merge --no-commit master

Finish with:
git commit -m "Merge master into feature"

Even if you selectively commit changes, finalize the merge with a proper commit.

And if you do partial commits from a --no-commit merge, conclude it with an empty commit:

git merge --no-commit master  
# to restore metadata
# Unstage/restore remaining files
git commit --allow-empty -m "Finalize merge from master (selective)"
Enter fullscreen mode Exit fullscreen mode

This updates the merge base without merging unwanted files.

  • Avoid Squash Merges

Squashing is fine for final cleanup branches, but for merging master into feature or doing intermediate merges:

Use a regular merge commit
Keep history and ancestry intact

Conclusions

Git is very powerful tool and sometimes we naively think that it should do what we think it should do.
However, it is not the case, it does everything very technically.
If you don’t commit a merge, Git acts like it never happened, which can lead to skipped files, duplicate changes, and messy conflicts.

Understanding how the merge base, reverts, and partial merges work is key to avoiding hidden bugs in branches.

Hopefully, this post can save you some time for find it.

Top comments (0)