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
Now we will start branch with our new feature implementation. We will call this
# 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,
How Git simplifies history
Now it’s time to merge our feature branch
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
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
Preventing Git fast-forward merges
We can prevent Git from doing fast-forward when we merge
--no-ff (“no fast-forward”) flag. Let’s recreate the same situation in the repository, this time with a branch
# 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'
When we now do a merge
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.
Moreover, in the merge commit default message (that we didn’t edit, thanks
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
[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.