Rebase — One of the Most Powerful Git Commands
W hen a programmer is first learning git
the typical commands that are picked up include add
, commit
, push
, pull
, status
, branch
, checkout
and merge
. After those foundational commands are learned, I think that the rebase
command should be understood.
Rebasing is often used as an alternative to merging. Rebasing a branch updates one branch with another by applying the commits of one branch on top of the commits of another branch. For example, if working on a feature
branch that is out of date with a dev
branch, rebasing the feature
branch onto dev
will allow all the new commits from dev
to be included in feature
. Here’s what this looks like visually:
For the above example, this is what it would look like from the command line:
git rebase feature dev
However, it is more common to first checkout a branch and then run the rebase command with the name of the branch you wish to rebase on to:
git checkout feature
git rebase dev
Typical Rebase Use Cases
Updating a Feature Branch
Lets say you’re working away on a feature
branch, minding your own business.
Then you notice some new commits on dev
that you’d like to have in your feature branch, since the new commits may affect how you implement the feature.
You decide to run git rebase dev
from your feature
branch to get up-to-date with dev
.
However when you run the rebase command, there are some conflicts between the changes you made on feature
and the new commits on dev
. Thankfully, the rebase process goes through each commit one at a time and so as soon as it notices a conflict on a commit, git will provide a message in the terminal outlining what files need to be resolved. Once you’ve resolved the conflict, you git add
your changes to the commit and run git rebase --continue
to continue the rebase process. If there are no more conflicts, you will have successfully rebased your feature
branch onto dev
.
Now you can continue working on your feature with the latest commits from dev
included in feature
and all is well again in the world. This process can repeat itself if the dev
branch is updated with additional commits.
Updating a Feature Branch Prior to Merge
Another popular use for rebasing is to rebase a feature
branch, just prior to merging into a dev
branch. Let’s say that the current state of a feature
and dev
branch is as follows:
Once thefeature
is complete, it should be merged into the dev
branch. First, thefeature
branch should be updated with dev
using git rebase dev
.
Next, merge feature
into dev
. This is usually done through a Pull Request, so others can review the work done in the feature
branch. Once this is done, the result is as follows:
Notice how all the commits from the feature
branch are added to the end of the dev
branch. This will always be the case if rebasing is immediately done prior to merging. The reason this happens is that rebasing rewrites the git history and so the timestamp of the commits on the rebased branch will all be the moment at which the rebase command was run. To be clear, rewriting history with rebase recreates commits that contain the same changes and commit message, but with a new hash and timestamp. Therefore, when merging a recently rebased feature
branch into dev
, all the feature
branch commits will be added at the end of dev
since merging orders commits in chronological order.
Github actually has an option on Pull Requests to rebase a branch prior to merging.
Renaming & Consolidating Commits in Feature Branches
Odds are, when nearing the completion of a feature in a feature
branch, the commit history could be improved upon by renaming some commits to something more appropriate, combining closely related commits, and/or reordering commits in a more logical order. Rebasing allows for all of this!
Since rebasing rewrites the git history, it provides some tools to specify exactly how to rewrite the history. The best way to take advantage of these extra rebase features is by running rebase in interactive mode by passing the -i
flag with the command (i.e. git rebase -i feature dev
).
When running an interactive rebase, a list of commits that will be rebased along with the word pick
at the beginning of each commit message is presented in the default git editor configured on your machine (in my case it’s Visual Studio Code). The word pick
can be changed with any of the following options (all of which are listed when running an interactive rebase):
pick = use commit
reword = use commit, but edit the commit message
edit = use commit, but stop for amending
squash = use commit, but meld into previous commit
fixup = like "squash", but discard this commit's log message
exec = run command (the rest of the line) using shell
drop = remove commit
For the examples mentioned earlier here’s how you would tackle them:
- A commit message can be reworded using the
reword
option for a commit. - Combining commits is done with the
squash
orfixup
option on a commit that should be combined with another commit. Ensure that the commit specified withsquash
/fixup
immediately follows the commit with which it should be combined. - Reordering commits is as simple as moving around the commit messages in the desired order.
What’s So Great About Rebasing?
No Merge Commits
Merging two branches together always requires a merge commit. If a feature branch is continually being updated with a dev branch using a merge, there will be several merge commits resulting in a less clear git history.
These merge commits can be completely mitigated if rebase is used to update the feature
branch instead of a merge.
Linear & Sequential Git History
When rebasing a branch prior to a merge, all the the feature
branch commits are grouped together at the end of the dev
branch whereas the feature
branch commits are interspersed in the dev
branch when a rebase is not used prior to merge. This is because merging orders commits chronologically. Grouping feature commits together, as is the case with a rebase, makes it simple to understand/analyze the git history afterwards.
Simpler Time Resolving Conflicts
When rebasing, git applies each commit one at a time and so conflicts can be resolved progressively. When resolving conflicts for a merge, all conflicts must be resolved at once which can make it a bit more difficult to handle.
If a conflict is encountered while rebasing, git will indicate which files are conflicting and need to be modified. After changes have been made, the changes need to be staged to the commit and then the rebase can resume using git rebase --continue
. There is also the option of running git rebase --abort
while resolving conflicts in a rebase, which will cancel the rebase and leave the branch unchanged.
Well Organized and Clean Git History
With all the benefits above in addition to the ability to change commit messages, combine commits, and reorder commits, rebasing makes for a very clean git history that is easy to understand. Having an understandable git history is great when trying to pinpoint when a certain feature or bug was introduced in your codebase.
When Not To Rebase
Ok, rebasing is pretty sweet, but there are certainly cases where you should not rebase. The golden rule of rebasing is to not rebase public branches. The reason you shouldn’t rebase a public branch is that you are rewriting the git history for that branch and this will result in your version of the public branch to differ from the version other users are working with. This will result in a bunch of conflicts and can be a tedious problem to resolve once it’s done, so make sure to only rebase branches you, and only you, are working on.
Adding git rebase
to your git workflow can be an extremely valuable way of cleaning up your git history and making the overall git workflow easier to work with. The way rebase
works may not be completely intuitive at first, but after using the command a few times on feature branches to better familiarize yourself with it, you’ll hopefully end up seeing some of the benefits mentioned in this article.