Git fast forward VS no fast forward merge

The --no-ff option is useful when you want to have a clear notion of your feature branch. So even if in the meantime no commits were made, FF is possible - you still want sometimes to have each commit in the mainline correspond to one feature. So you treat a feature branch with a bunch of commits as a single unit, and merge them as a single unit. It is clear from your history when you do feature branch merging with --no-ff.

If you do not care about such thing - you could probably get away with FF whenever it is possible. Thus you will have more svn-like feeling of workflow.

For example, the author of this article thinks that --no-ff option should be default and his reasoning is close to that I outlined above:

Consider the situation where a series of minor commits on the "feature" branch collectively make up one new feature: If you just do "git merge feature_branch" without --no-ff, "it is impossible to see from the Git history which of the commit objects together have implemented a feature—you would have to manually read all the log messages. Reverting a whole feature (i.e. a group of commits), is a true headache [if --no-ff is not used], whereas it is easily done if the --no-ff flag was used [because it's just one commit]."

Graphic showing how --no-ff groups together all commits from feature branch into one commit on master branch


When we work on development environment and merge our code to staging/production branch then Git no fast forward can be a better option. Usually when we work in development branch for a single feature we tend to have multiple commits. Tracking changes with multiple commits can be inconvenient later on. If we merge with staging/production branch using Git no fast forward then it will have only 1 commit. Now anytime we want to revert the feature, just revert that commit. Life is easy.


I can give an example commonly seen in project.

Here, option --no-ff (i.e. true merge) creates a new commit with multiple parents, and provides a better history tracking. Otherwise, --ff (i.e. fast-forward merge) is by default.

$ git checkout master
$ git checkout -b newFeature
$ ...
$ git commit -m 'work from day 1'
$ ...
$ git commit -m 'work from day 2'
$ ...
$ git commit -m 'finish the feature'
$ git checkout master
$ git merge --no-ff newFeature -m 'add new feature'
$ git log
// something like below
commit 'add new feature'         // => commit created at merge with proper message
commit 'finish the feature'
commit 'work from day 2'
commit 'work from day 1'
$ gitk                           // => see details with graph

$ git checkout -b anotherFeature        // => create a new branch (*)
$ ...
$ git commit -m 'work from day 3'
$ ...
$ git commit -m 'work from day 4'
$ ...
$ git commit -m 'finish another feature'
$ git checkout master
$ git merge anotherFeature       // --ff is by default, message will be ignored
$ git log
// something like below
commit 'work from day 4'
commit 'work from day 3'
commit 'add new feature'
commit 'finish the feature'
commit ...
$ gitk                           // => see details with graph

(*) Note that here if the newFeature branch is re-used, instead of creating a new branch, git will have to do a --no-ff merge anyway. This means fast forward merge is not always eligible.


Git visualization

Here are the git log visualizations with the differences. These are what the trees looked like creating three branches from dev called 1 2 3, then merging with and without fast-forward. I'll put the setup code at the bottom. You can paste a paragraph of commands into your terminal to quickly setup and reset different git scenarios, which was very helpful in learning git.

Notice that with fast-forward, git doesn't even indicate a merge.

--no-ff         --ff (default)

* Merge 3
| * 3
* | Merge 2     * Merge 3
| | * 2         | * 3
| |/            * | Merge 2
* / Merge 1     | | * 2
|/              | |/
| * 1           * / 1
|/              |/
* main          * main

It's worth comparing this approach with rebasing.

Setup/teardown

You can run this repeatedly and it'll delete and reinitialize the repo. It's for windows, so I think you'll just have to change the filepath and the rd remove directory commands if you're on *nix.

To see the behavior with fast-forward, remove --no-ff from the merge commands at the end. Remove the --pretty piece if you want to see the commit IDs.

cd \user\docs\code\learning\github\sandbox
rd /s /q 0.git 1 2 3
git init --bare 0.git
git clone 0.git 1
cd 1
git config user.name "user"
git config user.email "[email protected]"
git commit --allow-empty -m main
git switch -c 1 main
git commit --allow-empty -m 1
git switch -c 2 main
git commit --allow-empty -m 2
git switch -c 3 main
git commit --allow-empty -m 3
git switch main
git merge --no-ff 1
git merge --no-ff 2
git merge --no-ff 3
git log --graph --oneline --first-parent --all --pretty=%s

Tags:

Git