Image generated by FLUX.1 [dev] with prompt “abstract illustration of a minimal simple Sankey diagram of a timeline from left to right with a couple of lines splitting off and merging back into each other”

How-To Ban Git Merge Commits

Banning git merge commits in closed-source projects is all the rage. How can you actually enforce it?

I hate noise in my git history and useless git blame identifiers with the heat of a thousand suns. I’ve felt somewhat embarassed and self-conscious about this fact, because it is yet another symptom of my overbearing perfectionist inclinations. However, I then came across a blog post called “Why large companies and fast-moving startups are banning merge commits” by Greg Foster, and saw that this is a more common practice than I had thought.

A quick summary of why: banning merge commits creates a cleaner, more understandable history, especially when you also adopt atomic commits. The symbiosis between banning merge commits and practicing atomic commits is very powerful: as Frederick Vanbrabant argues above, atomic commits are about “Telling stories with Git”. Why was this change made, and what was being considered at the time? If it is causing a bug for customers, will changing it break something else and cause a regression? Did the original commit reference any issues I can pass on to QA for regression when I create my PR? This is invaluable data when maintaining and improving a codebase, so I strive to preserve it and make it as accessible as possible.

So, how do you actually enforce this policy? There are a few git config settings that make this possible. First, make sure that when you pull updates to a branch, you tell git to rebase your un-pushed commits on top of any commits pulled down from the remote that you didn’t already have locally:

git config --global pull.rebase true

I personally believe that anyone who is generally comfortable with the notion of rebasing should set this option, not just people who want to ban merge commits in their codebase. Collaborating on a branch without this setting quickly becomes comically messy and noisy. Note that in older versions of git (pre v1.7.9), you could accomplish something similar with git config --global branch.autosetuprebase always, though that option only applies to newly created branches.

The next config is a little more opinionated and has the potential to cause frustration, especially for those with poor git hygiene:

git config --global merge.ff only

That’s because it tells git to only perform fast-forward merges, which means that when doing a merge, if the branch you are merging into has commits that you don’t have in your branch, git will refuse to do the merge. This is a good thing, because it forces you to rebase your branch on top of the branch you’re trying to merge into, which keeps the history clean and linear. However, I could imagine this being annoying if you’re not in the habit of rebasing, especially if you aren’t comfortable with the process.

The merge.ff only config option has a sibling for the git pull command:

git config --global pull.ff only

Do not use this config option! This blocks any pull command that can’t be done as a fast-forward (e.g. if there are remote commits you don’t have locally and you have local commits that haven’t been pushed up). In that situation, the pull.rebase true config option is there to ensure that your local commits are applied after the remote commits, which is what we want. However, this setting will make the pull command fail before the pull.rebase setting can take effect.

So that’s it! Just a couple of git config settings and you will avoid merge commits in your repo. The only issue is that there’s no way to ensure that anyone who clones your repo also gets those config settings, which is a limitation that exists for security reasons; gitconfig files can run arbitrary CLI commands, so you might unwittingly kick-off a key logger or any other malicious code when running, say, git commit, if the repo owner set up the gitconfig to do so. As a result, you have to add instructions to your README or other documentation and rely on socialization to ensure that everyone on your team is on the same page.

If you know any better way to enforce this on the repo level, please let me know here in the comments or on Twitter (⤬)! I’d love to update this post with a better solution.