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.
Here is how our repository looks right now.
It contains two commits on the master
branch.
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.
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.
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.
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.
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
.
When we now do a merge to master
, we will tell Git explicitly not to do it in fast-forward mode.
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 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.
This will add configuration parameters to ~/.gitconfig
file.
If we want, we can also edit this file directly and put our config there.
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.