Git provides two commands for integrating changes from one branch into another, merge
and rebase
. In this post I am going to slightly explain both commands but mainly focus on rebasing and how to use it.
Introduction
When developing software in a team it is common to use a version control and an associated workflow. This allows developers to work independently on different features and bring them together at the end of a development cycle.
Let’s say we have this initial situation:
We have a main
and a feature
branch. main
is the default branch in which every feature
gets merged when finished.
A---B---C <- main
\
D <- feature
When working together with other developers it occurs that main
evolves and is ahead of feature
:
A---B---C---E <- main
\
D <- feature
As commit E
introduces a very cool functionality you might want to use it in your feature
, therefore you have to update it with the current main
. How can we do this?
merge
For this purpose we could use merge
:
$ git checkout feature
$ git merge main
This command will replay all commits inside main
on top of feature
and create a merge commit F
:
A---B---C---E <- main
\ \
D---F <- feature
Merging is quite straight forward and the easiest way to integrate changes. The downside is that it creates a merge commit which is shown in the history.
rebase
As an alternative to merging, you can rebase
the main
branch onto feature
:
$ git checkout feature
$ git rebase main
The entire feature
branch is moved to the top of main
:
A---B---C---E <- main
\
D' <- feature
As rebasing does not create a merge commit, it leads to a clearer history. But it should be used with caution. Rebasing is a powerful tool and should only be used if you know what you are doing, as it allows you to modify the history of your current branch.
It also needs be mentioned that only local branches should be rebased. As soon as a branch gets pushed to a remote repository and someone else might depend on published code, rebasing should be avoided as you potentially overwrite other developers work.
Pushing to a published branch after rebase
In my experience the following is the most common problem someone will encounter when working with rebase
. You rebased main
onto your feature
:
A---B---C---E <- main
\
D' <- feature
feature
is already published to a remote repository and now you want to push
your changes. When executing push
you will get an error:
! [rejected] HEAD -> feature-1 (non-fast-forward)
As the commit D
is applied to a new base (E
instead of C
), git can not fast-forward the remote branch. Therefore a normal push
is not sufficient. What you have to do in this case is to execute a forceful push. I can not emphasize this enough: Make sure you know what you are doing!
To forcefully push use the --force-with-lease
flag. This will check the upstream branch for changes and refuse to update if you are about to damage others work:
$ git push --force-with-lease
In case the upstream branch changed, you will receive this error:
! [rejected] HEAD -> feature (stale info)
Perform a pull
to update your local branch before pushing forcefully again:
$ git pull --rebase
In addition to the danger of unintentionally deleting code, you might also encounter some problems using git management tools such as Bitbucket. If you forcefully push a rebased branch with an open pull request, comments in the pull request might be gone. Those comments are always bound to specific commits. As rebase
changes the history, and thereby also the commit hashes, comments can no longer be displayed correctly.
Conclusion
rebase
is very handy for maintaining a clean history on local environments. But it should be used with caution (or completely avoided) on published branches.
If a clear history is not that important for you, go with merge
.
Whether or not to use rebase
sometimes also depends on the company or team you are currently working for. I have worked for several bigger and smaller companies and different teams and none of them ever looked at the history. Some even explicitly agreed on using merge
over rebase
because for them a clear history was not in proportion to the risk of something being deleted via a forceful push.