Rewriting Git history with Rebase: Part I
This is part one of a two part deep dive into Git Rebase - so deep we had to split into two parts. Keep your eyes peeled for part two, coming soon! - Ed.
How and when to rebase
As developers we all have our preferred work flows and Git is no exception. Effective Git workflows will look very similar for most people but there is definitely room for making it your own. Once you start going beyond Git basics, however, it can quickly become overwhelming, making it tempting to just stick to what you know. Whether you are new to git rebase or a rebase expert, this article is here to share a different perspective. Hopefully it will be a welcome to addition to your git flow.
What is rebase and when do we use it at TextNow?
The most common way to to integrate changes from one branch to another is through git merge, which most developers are very familiar with. But there is another way: git rebase.
Both git merge and rebase accomplish the same goal but they do so in different ways. During a merge we take the contents of the feature branch and integrate in with main preserving commit order leaving the feature branch history unchanged. When rebasing a branch on to main, on the other hand, we move the base of the feature branch to the tip of main.
When rebasing, git actually makes a new branch behind the scenes from main and moves all the commits from the feature branch to the tip of the new branch, one at time. This is a destructive operation that rewrites history and will have to be force pushed to the remote. Merging on the other hand, is non-destructive.
Merge preserves history while rebase rewrites it.
Rebasing makes sense for repositories with frequent releases with multiple collaborators. It creates a cleaner, more concise, and easier to read git history because commits from a single pull request are grouped together and placed at the tip of main branch.
Rebasing allows us to easily find all the commits belonging to a particular feature or a bug fix, since they are grouped together. This is a practice we adopted at TextNow. In addition to rebasing we also squash most changes down to a single commit, resulting in one-to-one relationship between a commit and a particular bug fix or feature. Squashing the commits makes the git history more concise, allowing us to revert a change easily, if needed, since we would only have a single commit to revert rather than finding all the commits pertaining to the change we want to revert.
Git rebase alone, however, does help with this by making commits easier to find in git history, since they are grouped together. A big benefit of rebase vs merge is that when conflicts occur during rebase, they are presented one commit at time, which often makes them easier to resolve as compared to merge, which presents all conflicts at once.
While rebase is a powerful tool for a streamlined git history it is important to remember that rebase only makes sense for private branches or branches where very few developers are collaborating very closely. This is why at TextNow while we always use rebase to incorporate changes from main into private or feature branches we always merge the changes into main.
NOTE: You can pull down changes from remote using merge (default) or rebaseIt is important to keep in mind that git pull will also perform a merge for the remote branch unless you explicitly set your pull configuration to rebase by default using global config. In this workflow, when there are new changes on main and we’ve fetched and pulled, we would then bring them into the feature branch using git rebase main instead of git merge main, as shown in the previous example.
The merge option
You’re probably most familiar with the merge operation where a simple workflow might look something likes this:
Here we would incorporate the new relevant changes into our feature branch using git merge. Then once we have finished with the fourth and final commit, we merge the feature branch into main using merge once again. Unlike rebase, this results in a merge commit, with the commits arranged as they were created.
An important distinction of merging is that it is non-destructive, and leaves the feature branch history intact. Merging takes the feature branch changes and adds them to main git history. The downside is if the main branch is very active, history gets polluted with a lot of merge commits and becomes harder to read. This is one of the reasons we have opted to use rebase rather than merge at TextNow.
The rebase option
The rebase operation incorporates the feature branch changes quite differently by rewriting the history of the feature branch. If we choose to follow a rebase-type workflow, and we are working with remote repositories, we should not be using git merge on the same branch. Then once the final commit is done we are ready to merge the feature branch into main.
At this point, it's good practice to make sure there aren’t any more changes available in main. If there are we will want to pull and rebase again to make sure there are no conflicts. It's cleaner for git history to address them locally in your feature branch rather than part of a merge commit.
Another potential benefit of using rebase to resolve conflicts is rebase does so one commit at a time, while merge operation does all commits at once. Why? Because behind the scenes, git rebase actually creates a brand new branch and reapplies each commit one-by-one at the tip of the main branch.
The last step of this workflow will still be to merge the feature branch changes using git merge. because by merging you have effectively rewritten git history. You are going to have to force push to the remote to override it, so be sure your git settings allow overrides on your feature branch.
At TextNow we have agreed to do merge using the Github’s rebase and merge option on a pull request. The resulting git history is linear and sequential where the commits from one feature branch are grouped together, rather than interspersed throughout main branch. While this is already a great improvement for the history compared to merge, we can take it a step further and rebase & squash commits into a single commit corresponding to a particular feature or bug fix.