Of all the commands git has to offer, rebase gets the most negative attention for being something that can torpedo your workflow. I want to make the argument for positive uses of rebase, and show how it can make your life and general workflow better.
This advice is given in the context of a feature branch where you are the sole committer. If you are working against a shared branch or on someone else’s commits, using rebase will cause much confusion when used on public commits (see "May the --force be with you" to understand why this is the case).
What is rebase?
git rebase lets you play back a series of commits and, if desired, interactively alter them. The two common usages are:
- as an alternative to
git mergewhen pulling in changes from a base branch likemain - as a way to go back in time and massage your commits on a branch, combining or altering the commit history
The git documentation does a better job than I could of describing how this works in depth, so give that a read first if you’d like more detail.
Rebase instead of merge
Using git merge is one way to integrate changes into your branch from a base branch - most commonly seen when bringing your branch up to speed with new commits from main. The way that merge works is to take any differing commits from the base branch, and apply them on top of your feature branch with an additional merge commit.
A---B---C topic
\
--F---G---H main
This is fine, and does the job, but results in a very messy branch history if you make some changes, merge from main make some more changes, etc.
As an alternative, rebase will still find the divergent commits from the base branch, but instead of applying them on top of your changes, it will start from the point the branches diverged and pull in the base commits from main first, then replay your changes on top.
A'--B'--C' topic
/
D---E---F---G main
The branch history is then much cleaner - operations like git diff or git log will have a nice linear series of commits, where your changes are on top.
The reason this is discouraged for shared branches is that the commits are now different - replaying A, B, and C onto the branch effectively re-commits them, so their hashes will be different from the public history and can’t be reconciled, causing problems if someone else tries to pull the branch.
If you do try and re-fetch someone else’s branch which has been rebased and get an error, the simplest thing to do (if you don’t have your own commits on the local copy) is switch back to main, remove your local copy of the branch with git branch -D <branchname>, then pull a new local copy with git checkout <branchname>.
Rewriting your branch history
The other main use of rebase is to let you rewrite the history of your branch. If you’re a reflexive committer like me, you probably end up with a feature branch with a bunch of small commits, undos, and fixes as you’re working on a feature.
This makes it a bit of a nightmare for someone reviewing your branch since there’s no logical grouping of changes; and it looks messy, which doesn’t inspire confidence. What we can do here is use an interactive rebase. Give it a starting reference and you can interactively alter the commits that it applies.
The simplest way to do this is by using your base branch as the starting point, e.g. git rebase main -i. However, you can also be more discerning by picking a specific commit to start from - if I know I need to rebase a certain number of commits I’ll often use something like git rebase HEAD~3 -i, which lets me rebase the last three commits (for more detail on what that construction means, see the git documentation).
Once you’ve kicked off the rebase you’ll be presented with an editor window where each commit is on its own line with a command, SHA, and commit message (as a comment). Note that you can’t edit the commit message in this window, but you have a range of actions available: pick, reword, edit, squash, fixup, exec, break, drop, label, reset, merge, and update-ref.
Frankly, I only use a small subset of these, so for more comprehensive information check out the git documentation. The ones that are of general interest are:
pick- the default, which means “use this commit as-is”reword- drops you into an editor so that you can edit the commit messageedit- stops during the rebase after this commit, so you can make changes before continuingsquash- combine the contents of this commit with the previous, allowing you to interactively edit the commit messagesfixup- combine the contents of this commit with the previous, discarding the commit messagedrop- throw away this commit
Most of my interactive rebasing is a combination of fixup and reword , but the other actions are also handy. Simply edit the lines to change the action to the one you’d like to perform, save and exit the rebase editor, and git will take care of the rest.
You can also re-order the lines in the editor to re-order the commits, but beware that if you rearrange commits which depend on each other you will get merge conflicts, so better to squash them together first.
May the --force be with you
You will have to make a change to how you push your changes if you start using rebase - a regular git push will no longer work, so you’ll have to git push --force (or gitp -f if you have shortcuts set up) instead. Rather than just pushing up any new commits to the branch this will instead remove and overwrite the existing commits with your new ones, something git push on its own doesn’t allow.
For a safer, if more verbose, option, use git push --force-with-lease --force-if-includes (or create an alias for same). This will prevent you from pushing to the branch if anyone else has pushed commits in the interim, forcing you to either integrate those commits or make a conscious decision to overwrite them.
Being a good neighbour
One case where you may choose to proactively not rebase is when you’re pushing review changes to a branch - the downside of rebasing at this point is that the reviewer has no visibility over what has changed since it’s absorbed into your previous commits.
Instead, push your changes into a new commit without rebasing the old commits - when it’s merged they all still get squashed into one commit so this makes little difference to how it lands in main. Your reviewer will thank you.
Something has gone catastrophically wrong
A common concern with rebase is that you’ll get into a state where you have started an operation and gotten stuck somewhere, either in the middle of a set of changes or after the rebase has completed. This is a legitimate concern, but git provides you with the tools to deal with this.
If you’re stuck in the middle of a rebase, you can get out of it and return to where you started at any time with git rebase --abort. This will take you back to where you initiated the git rebase, and you can choose what to do from there.
If you’ve completed the rebase (or multiple rebases) and you’re unhappy with where you’ve ended up this won’t work. However, at this point you can make use of another magical command - git reflog. This stores the history of all of the changes that have happened in your local repository, so you can simply use this history to pick a commit before things went wrong and rewind to it with git reset --hard <commit>. Now you’re again back to a safe spot, and you can do what you want from there.
Helpful commands
I use the following command a lot; it automates the dance required to update your main branch and rebase your current branch against it (requires the zsh git helper functions):
# Rebase the current branch from main branch
function grmb() {
CURRENT_BRANCH="$(git_current_branch)"
if [[ "$CURRENT_BRANCH" == "$(git_main_branch)" ]]; then
echo "Can't run this on the main branch"
return 1
fi
git checkout "$(git_main_branch)" && \
git pull && \
git checkout "$CURRENT_BRANCH" && \
git rebase "$(git_main_branch)"
}