Git has established itself as the leader in source control management and for good reason. It’s an extremely powerful and robust tool. I often joke you could spend your entire career striving for complete Git understanding and never achieve it because there is simply too much to learn in a lifetime.
That said, one aspect of Git that is crucial to know how to use is rebase. Rebasing your commits leaves behind an organized and detailed history from which others can learn. The best engineers, after all, empower others to pick up where they left off and be successful long after they are gone. Git Rebase achieves this by clearly explaining what you worked on, your thought processes, and your approach. Git Rebase leaves behind a logical history — or as close as possible to one — of how the implementation formed.
The purpose of this article is to bootstrap your knowledge so you can leverage the power of Git Rebase in your daily work.
Merge versus Rebase
Rebasing, unfortunately, is not enabled by default. Instead, the default behavior is merging which
is essentially the opposite. Commands like
git pull or
git merge exacerbate the merge problem.
To best understand a rebase workflow, we can start by comparing/contrasting merge and rebase. Take this commit history of a repository that constantly merges, for example:
I don’t know about you, but I’d be hard pressed to figure out which author (i.e. which color coded line) did what and when in the history of this repository. The following fun-yet-sad scene often comes to mind when I see repositories in this state:
You can avoid such a tangled mess of commits by using
git pull --rebase and
--interactive instead of merging when committing code. We’ll dig into the details of these commands
more later in this article, but for now, notice how the Git log changes when a project has been
properly rebased over the course of many years:
Compared, with the repository full of merges seen earlier, it’s hard to deny that the above is much easier to understand. 🎉
💡 In case you were curious, the command line code for rendering the Git logs shown in the videos
git log --graph --pretty=format:"%C(yellow)%h%C(reset) %C(green)%cr.%C(reset)". To learn
more about formatting useful Git logs, check out the Git Log
Pretty screencast for details.
Switching from a merge to a rebase workflow starts with a single configuration change. Open up your terminal and run the following command to enable:
git config --global pull.rebase true
If you were to look at your
~/.gitconfig, you’d now see the following entry:
[pull] rebase = true
💡 As an advanced technique, I’d recommend switching
merges in the command above. Doing
so will make it easier to preserve merge bubbles when rebasing so you can work in repositories where
teams don’t practice a rebase workflow and/or use a hybrid of merges and rebases. This means the
modified version of the command above would look like this:
git config --global pull.rebase
merges. For more information, see
Documentation for details.
A typical rebase workflow toggles between working on new branches, switching to existing branches, and handling any conflicts. The following delves into each workflow and includes additional hints and tips to make your working experience a joyful one.
By the way,
--rebase is optional in the workflows below if you’ve already performed the setup
above. Use of the
--rebase option is meant to emphasize the rebase workflow or allow you to
manually rebase at will should you not want the aforementioned setup.
In order to keep all of your work grouped together, new development requires creating a new branch. Here is what that workflow might look like:
git pull --rebase # Rebase on top of new changes from `main`. git switch --create tutorial --track # Create a `tutorial` branch and track changes. git commit # Commit changes -- Repeat until done. git rebase --interactive # Clean up your work -- Repeat as necessary. git push --force-with-lease --force-if-includes # Safely push changes to the `tutorial` branch.
During the course of a day or week, you’ll often switch between different feature branches. Managing existing branches is nearly identical to new branches except you’ll want to rebase on top of upstream chagnes before resuming work in order to ensure you haven’t fallen too far behind the work of your team.
git switch tutorial # Switch to `tutorial` branch to resume work. git pull --rebase origin main # Rebase feature branch on top of `main` changes. git commit # Commit changes -- Repeat until done. git rebase --interactive # Clean up your work -- Repeat as necessary. git push --force-with-lease --force-if-includes # Safely push changes to the `tutorial` branch.
git rebase --interactive uses the
main branch to build a list of commits since
branch creation. Sometimes you don’t want this behavior, though. Here are other use cases that might
be of interest:
git rebase --interactive HEAD~<number>- Allows you to rebase back to a certain number of commits. For instance,
HEAD~1would go back one commit while
HEAD~3would go back three commits. 💡 If you don’t want to type
HEADeach time, you can use
@as shorthand for less typing.
git rebase --interactive <branch>- Allows you to rebase on top of a specific branch. This is handy when basing your changes off of an existing branch or when nesting branches. The latter is especially important as this command allows you to push your first branch up for code review while you make a new branch from that first branch in order to continue working, unblocked, while feedback rolls in.
git rebase --interactive <tag>- Allows you to rebase off of a specific tag, which comes in handy when working on a patch to a previously released version.
git rebase --interactive <SHA>- Allows you to rebase to a specific commit SHA. This works a lot like
HEAD~<number>except you can skip counting the number of commits you want to rebase back to and supply the SHA instead.
git rebase --interactive @<upstream>- Allows you to rebase on top of upstream changes. While you can specify different branches,
<upstream>will default to whatever name you used when creating/pushing the branch upstream.
git rebase --interactive --root- Allows you to rebase back to the first commit made to the repository. This is my personal favorite. I use
--rootoften when creating and sharing a new repository. One word of caution, though. Never use this on an existing repository shared amongst colleagues! Rebasing like this will change all of the commit SHAs, causing incompatibilities with everyone’s local copy and ultimately forcing everyone to reclone the repository.
Resolving upstream conflicts with code is a normal part of the job when working in a team. With luck, it won’t happen often but when it does you’ll need to know how to resolve the situation so you can continue. Dealing with conflicts isn’t unique to the rebase workflow, of course. You’ll encounter conflicts with the merge workflow too. Luckily, Git is there to help you out during these conflicts. Here are the commands you’ll end up using to resolve rebase conflicts:
git rebase --abort- Allows you to abort the current rebase and start over. This is your escape hatch should you realize you made a mistake, are confused on what to do next, or just need to bail quickly without applying any changes.
git rebase --continue- Allows you to resume rebasing once you have resolved a conflict or if the rebase has halted and is waiting for you to continue.
git rebase --skip- Allows you to skip what is currently being rebased. This can happen when the rebase is halted but has nothing to do or when you want to skip because there is nothing to change.
git rebase --show-current-patch- Allows you to inspect where you are when halted during a rebase. You can also use
I wish this feature was enabled by default because it’s a powerful tool that’s often overlooked. Whether you are resolving conflicts during a rebase or a merge, you’ll want rerere enabled in order to record conflict resolutions and speed up your workflow. Think of rerere as a way to teach Git how to detect and automatically apply previously resolved conflicts so you don’t have to do the same work multiple times. Trust me, you want this enabled! Here’s how:
git config --global rerere.enabled true git config --global rerere.autoUpdate true
Running the above will result in the following being added to your
[rerere] enabled = true autoUpdate = true
The first line enables rerere while the second line informs rerere to auto update an existing conflict resolution should you have to make modifications.
This is all you need to leverage the power of rerere. Rerere stores it’s cache in the
.git/rr-cache folder and you can use commands like
git rerere forget <path>,
git rerere clear,
etc to manipulate the cache. To learn more, check out the
Git Rerere Documentation.
The following screencast tutorials walk you through how to configure Git so more of the rebase workflow is automated. Hopefully, they help you understand how to interact with Git and provide advanced techniques for speeding up a rebase all together.
Git Rebase Abbreviations - Learn how to configure Git so all rebase commands are single characters in order to type less.
Git Rebase AutoSquash - Learn how to configure Git so
squash!commits are automatically associated with the commits you want to fix or squash.
Git Rebase AutoStash - Learn how to configure Git to temporarily store untracked, staged, and/or modified local changes while rebasing so you can easily return to what you were working on after the rebase.
git rebase --interactive from the command line to rebase your work, you’ll be
greeted with the following documentation within your default editor:
# Rebase 8c0b6dea7bb4 onto d32bf0d64bcd (7 commands) # # Commands: # p, pick <commit> = use commit # r, reword <commit> = use commit, but edit the commit message # e, edit <commit> = use commit, but stop for amending # s, squash <commit> = use commit, but meld into previous commit # f, fixup <commit> = like "squash", but discard this commit's log message # x, exec <command> = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop <commit> = remove commit # l, label <label> = label current HEAD with a name # t, reset <label> = reset HEAD to a label # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] # . create a merge commit using the original merge commit's # . message (or the oneline, if no original merge commit was # . specified). Use -c <commit> to reword the commit message. # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted.
The following walks you through understanding what the above commands are and how to use them:
Git Rebase Pick - Learn how to choose the commits you want to rebase and/or reorder for them improved readability.
Git Rebase Reword - Learn how to reword commits so the subject (what) and body (why) properly explain the reasoning behind your work.
Git Rebase Edit - Learn how to edit an existing commit’s implementation and/or commit message.
Git Rebase Squash - Learn how to combine several commits into one while massaging the commit message into a single, cohesive, thought.
Git Rebase Fixup - Learn how to fix an existing commit’s implementation while bypassing the need to update the commit message.
Git Rebase Exec - Learn how to execute code against single or multiple commits while rebasing.
Git Rebase Break - Learn how to halt the rebase after single or multiple commits for debugging purposes.
Git Rebase Drop - Learn how to delete commits you no longer need.
Git Commit Amend - Learn how to quickly amend a previous commit via the command line.
Git Commit Fixup - Learn how to fix a commit via the command line without having to open the rebase editor.
Git Commit Squash - Learn how to squash a commit from the command line without having to open the rebase editor.
Git Lint - A coding buddy which can automatically ensure your Git commits are consistent and of high quality. Git Lint can be configured locally as a Git Hook, added to your code review process, and/or used to validate continuous integration builds. Git Lint is used to check all of the projects on this site too.
Kaleidoscope - A native macOS application, with an excellent UI, for visualizing the difference between commits. Kaleidoscope can also be configured to run from the command line via the ksdiff program too. Here’s my .gitconfig should you need an example of how to configure Kaleidoscope with Git.
Git Delta - A side-by-side diff complete with line numbers. Git Delta is my favorite tool for comparing differences from the command line. Here’s my .gitconfig should you need examples for further customization.
Diff So Fancy - Another handy tool for the command line that makes parsing differences much easier.
Before wrapping up, I would be remiss not to highlight the importance of well written commit messages. Not only should your history be clean, logical, and well organized, being able to explain each commit message in the evolution of a project maximizes the value of rebasing. The elements of a communicative commit message have been documented in the Git Commit Anatomy article posted earlier on this site. Now would be a great opportunity to delve into the details of a commit message if you haven’t already.
Using a rebase workflow is an essential part of the craft of building quality software but also a powerful tool with which to communicate with others. Whether you still work in the same repository or have moved on, it’s important to leave behind a legacy of information which empowers others to pick up where you left of and even learn from your insights. Who knows? Reading and/or searching through earlier commits could even become your past self reeducating your future self — I know I’ve been grateful many times for my own past wisdom.
Hopefully, this article has empowered you to lead by example and put your best foot forward. Enjoy and have fun!