Git Fast-Forward VS Non-Fast-Forward
Introduction
When we run git push
, sometimes we will encounter rejections because the remote origin branch cannot be fast-forward updated. It’s because we might be doing something sloppy and a git mechanism was triggered for preventing losing commits.
1 | $ git push |
In this blog post, I would like to discuss git fast-forward versus non-fast-forward, understanding the rationale behind the differences, and using git push --force
correctly and carefully.
Fast-Forward
When an update changes a branch (or more in general, a ref) that used to point at commit A to point at another commit B, it is called a fast-forward update if and only if B is a descendant of A.
In a fast-forward update from A to B, the set of commits that the original commit A built on top of is a subset of the commits the new commit B builds on top of. Hence, it does not lose any history.
Example 1
This is the most common example we will encounter during our daily git usages. We created a local branch, created a new commit, and want to update the origin branch from the local branch.
The commit B is a descendant of the commit A so the origin branch can be fast-forward updated from the local branch via git push
.
Before fast-forward updating the origin branch, the diagram looks like this.
1 | B local branch |
After fast-forward updating the origin branch, the consequent diagram looks like this.
1 | ---A---B origin branch and local branch |
Example 2
This example is extended from the example 1 that we created multiple commits in the local branch, and we want to update the origin branch from the local branch.
The commit C is a descendant of the commit A so this branch can be fast-forward updated via git push
.
Before fast-forward updating the origin branch, the diagram looks like this.
1 | B---C local branch |
After fast-forward updating the origin branch, the consequent diagram looks like this.
1 | ---A---B---C origin branch and local branch |
This normally happens when the user created a branch and is the only user who works on the branch. There are multiple descendent commits being updated to the branch together.
Example 3
This example is extended from the example 1 that while we created commits in the local branch, the original branch got updated by other contributors, and we want to update the origin branch from the local branch.
1 | B local branch |
We will have to pull the commits from the origin branch to the local branch, resolve merge conflicts if there is any, resulting in a new commit C.
The commit C is now a descendant of A so this branch can be fast-forward updated via git push
.
Before fast-forward updating the origin branch, the diagram looks like this.
1 | B---C local branch |
After fast-forward updating the origin branch, the consequent diagram looks like this.
1 | ---X---A---C origin branch and local branch |
Non-Fast-Forward
In contrast, a non-fast-forward update will lose history. For example, suppose you and somebody else started at the same commit X, and you built a history leading to commit B while the other person built a history leading to commit A.
Git Rebase Example
It’s very common that the origin branch will fall behind the origin main branch in a collaborative repository. Therefore, when the user tries to merge the origin branch to the origin main branch, there can be merge conflicts.
1 | B origin branch |
A common habit is that the user will rebase the origin branch to the origin main branch from time to time and resolve conflicts if there is any during rebasing so that when the origin branch is about to be merged to the origin main branch, the diagram will look like this and the original main branch can be fast-forward updated.
1 | C origin branch |
However, in practice, after rebasing the local branch to the origin main, when the user tries to update the origin branch from the local branch, the origin branch cannot be fast-forward updated.
Before rebasing the local branch to the origin main branch, the diagram looks like this.
1 | B origin branch and local branch |
After rebasing the local branch to the origin main branch and resolving the rebasing conflicts if there is any,
1 | B origin branch |
To update the origin branch, we will have to force the non-fast-forward update using git push --force
. The consequent diagram will look like this.
1 | C origin branch and local branch |
Notice that if there is another commit D that is merged to the origin branch before git push --force
, such as
1 | B---D origin branch |
The commit D will be erased by the non-fast-forward update and the commit D will be lost. The consequent diagram will still look like this.
1 | C origin branch and local branch |
Git Commit Amend Example
Git operations that changes the git commit history, such as git commit --amend
, will sometimes disable fast-forward update.
Before fast-forward updating the origin branch, the diagram looks like this.
1 | A local branch |
After fast-forward updating the origin branch, the diagram looks like this.
1 | ---X---A origin branch and local branch |
Then we continue working on the local branch and somehow we used git commit --amend
that creates a new commit B changes the past git history. The diagram will now look like this.
1 | B local branch |
In this situation, even if we know that the commit B is a continuation of the the commit A, fast-forward updating the origin branch from the local branch becomes impossible, because the commit B from the local branch is not a descendent of the commit A from the origin branch. We will have to use git push --force
and the consequent diagram will look like this.
1 | ---X---B origin branch and local branch |
Conclusions
Therefore, git push --force
is “dangerous” and will cause losing commits. But it’s not an enemy. In many situations it’s inevitable to use. We should know what we are doing and communicate with collaborators well when we are working on a non-main branch. The origin main branch, however, should never be non-fast-forward updated as we usually cannot afford losing commits from the origin main branch.
References
Git Fast-Forward VS Non-Fast-Forward
https://leimao.github.io/blog/Git-Fast-Forward-VS-Non-Fast-Forward/