Git fast-forward merge - why you should turn it off

Published on September 9, 2020

Git is a standard version control tool. You should definitely use it even for small personal projects. And when it comes to any teamwork, it’s mandatory.

Unfortunately, with default Git configuration we will not always see our work history true. Here we will investigate what is Git fast-forward merge mode behavior, how does it affect repository history, and why one should think about disabling it.

Usual work on branches

In a standard, multi-person work on a single project it’s normal every task is done on a separate branch. Usually, teams and companies follow some version of the Git flow. It:

  • prevents people from rewriting each other changes,
  • minimizes code conflicts (and personal ones too 😉),
  • provides a stable branch with an always-working version of a product,
  • and is generally a good idea.

Also, the most common (and simplest) way for merging branches is to do, well, merges (as opposed to rebasing, another possible strategy).

To simulate this let’s create a simple repository with two commits in history.

# create new project
mkdir great-project
cd great-project
git init

# add first commit
echo "Hello" > README.md
git add README.md
git commit -m 'Initial commit'

# add second commit
echo "Hello World" > README.md
git add README.md
git commit -m 'Changed "Hello" to "Hello World"'

Here is how our repository looks right now. It contains two commits on the master branch.

Repository tree after 2 first commits

Now we will start branch with our new feature implementation. We will call this branch awesome-feature and will create 2 more commits on it.

# create new branch
git checkout -b awesome-feature

# create first commit on branch
echo "lorem" > test.txt
git add test.txt
git commit -m 'Added "lorem" to test.txt'

# create second commit on branch
echo "ipsum" >> test.txt
git add test.txt
git commit -m 'Added "ipsum" to test.txt'

Right now we have two branches, master and awesome-feature. We’ve created two commits on master, then started branch awesome-feature and created two more commits there.

Repository tree with new "awesome-feature" branch, before the merge

How Git simplifies history

Now it’s time to merge our feature branch into master. No other person did anything on the master, there are no new commits created after we switched to awesome-feature. It’s as simple as it may be.

git checkout master
git merge awesome-feature

If we look at the Git history graph right now, it doesn’t tell us in any way that two of the last four commits were done on a separate branch. Git flattened history, deciding that since no other commits were made on master in the meantime, our changes could be as well done directly on master. This is called “fast-forward” mode and is a default Git behavior.

Repository tree after "fast-forward" merge – no info about what was done on the branch

This is problematic out of at least two reasons. Firstly, we don’t see that some of the commits were done in the scope of a common branch, and how was it named. We lose the greater context of those commits. Secondly, if we discover that this feature shouldn’t be merged yet, undoing it will be complicated, even if we determine which commits were in the scope of that branch.

Preventing Git fast-forward merges

We can prevent Git from doing fast-forward when we merge branches with --no-ff (“no fast-forward”) flag. Let’s recreate the same situation in the repository, this time with a branch called next-feature.

# create new branch
git checkout -b next-feature

# create first commit on branch
echo "dolor" > new.txt
git add new.txt
git commit -m 'Added "dolor" to new.txt'

# create second commit on branch
echo "sit" >> new.txt
git add new.txt
git commit -m 'Added "sit" to new.txt'
Repository tree with new "next-feature" branch

When we now do a merge to master, we will tell Git explicitly not to do it in fast-forward mode.

git checkout master
git merge --no-ff --no-edit next-feature

This time our Git history looks different. When we did a merge, Git created a merge commit. We can clearly see, even after the merge, that those two commits were done on a separate branch.

Repository tree with merge commit and preserved info about the work on the branch

Moreover, in the merge commit default message (that we didn’t edit, thanks to --no-edit flag) we can see the name of the merged branch. This allows us to get full and true info on past work in the repository.

Disabling fast-forward permanently

To prevent Git fast-forward mode permanently we can disable fast-forward globally. Then we don’t have to remember to use --no-ff flag for every merge operation.

One important thing to know is that when we pull new changes from the remote repository, Git in fact does a merge operation with the remote branch. So to prevent it from creating a new commit every time we pull changes, which is totally redundant, we have to set two Git configuration parameters.

git config --global merge.ff false
git config --global pull.ff true

This will add configuration parameters to ~/.gitconfig file. If we want, we can also edit this file directly and put our config there.

[merge]
	ff = false
[pull]
	ff = true

Fast-forward on GitHub and GitLab

Worth noticing is that when we merge branches from pull requests on GitHub or GitLab, they are also done in no fast-forward mode. This ensures we have true repository history preserved.

Project-wide merge policy

To have Git history truthful and consistent it requires that all team members are following the same policy and use the same configuration. Various GUI for Git handle this differently, but usually can be configured to follow the same strategy as we discussed. A little bit of team effort at first helps to keep the repository clean, which often benefits on later stages.

Category: Tools