Chapters

Hide chapters

Advanced Git

First Edition · Git 2.28 · Console

Section I: Advanced Git

Section 1: 7 chapters
Show chapters Hide chapters

4. Demystifying Rebasing
Written by Chris Belanger

Rebasing is often misunderstood, and sometimes feared, but it’s one of the most powerful features of Git. Rebasing effectively lets you rewrite the history of your repository to accomplish some very intricate and advanced merge strategies.

Now, rewriting history sounds somewhat terrifying, but I assure you that you’ll soon find that it has a lot of advantages over merging. You just have to be sure to rebase responsibly.

Why would you rebase?

Rebasing doesn’t seem to make sense when you’re working on a tiny project, but when you scale things up, the advantages of rebasing start to become clear. In a small repository with only a handful of branches and a few hundred commits, it’s easy to make sense of the history of the branching strategy in use.

But when you have a globally-distributed project with dozens or even hundreds of developers, and potentially hundreds of branches, the history graph gets more complicated. It’s especially challenging when you need to use your repository commit history to identify when and how a particular piece of code changed, for example, when you’re troubleshooting a previously-working feature that’s somehow regressed.

Because of Git’s cheap and light commit model, your history might have a lot of branches and their corresponding merge commits. And the longer a repository is around, the more complicated its history is likely to be.

The issue with merge commits becomes more apparent as the number of branches off of a feature branch grows. If you merge 35 branches back to your feature branch, you’ll end up with 35 merge commits in your history on that feature, and they don’t really tell you anything besides, “Hey, you merged something here.”

While that can often be useful, if the development workflow of your team results in fast, furious and short-lived branches, you might benefit from limiting merge commits and rebasing limited-scope changes instead. Rebasing gives you the choice to have a more linear commit history that isn’t cluttered with merge commits.

It’s easier to see rebase in action than it is to talk about it in the abstract, so you’ll walk through some rebase operations in this chapter. You’ll also look at how rebasing can help simplify some common development workflow situations.

What is rebasing?

Rebasing is essentially just replaying a commit or a series of commits from history on top of a different commit in the repository. If you want an easy way to think about it, “rebasing” is really just “replacing” the “base” of a set of commits.

Take a look at the following scenario: The f commits denote a random feature branch, and the b commits denote a bugfix branch you created in order to correct or improve something inside the feature branch, without impeding work on the feature development. You’ve made a few commits along the way, and now it’s time to merge the work in the bugfix branch back to feature.

A simple branch (b) off of feature (f).
A simple branch (b) off of feature (f).

If you were to simply merge the bugfix branch back to feature, as you would normally tend to do, then the resulting history graph would look like this:

A simple branch (b) off of feature (f), merged back to feature with merge commit mc5.
A simple branch (b) off of feature (f), merged back to feature with merge commit mc5.

The mc5 commit is your merge commit. Merge commits are a familiar sight, and merging is a mechanism that you and most everyone else who uses Git understands well. But as the size and activity of your repository grow, you can end up with a very complicated graph.

A more complex set of multiple branches and merge commits, with merge commits mc5, mc6 and mc7.
A more complex set of multiple branches and merge commits, with merge commits mc5, mc6 and mc7.

Depending on the kind of work you’re doing, you may not want to have your repository history show that you branched off, did some work and merged the changes back in. That little bit of extra cognitive overhead starts to add up as you try to make sense of months or even years of history graphs. Especially with small or trivial changes, you might prefer a linear history over seeing the code branched off and merged in again.

Rebasing gives you the freedom to avoid retaining branch history and merge commits. Instead, you can recreate your work as a linear commit progression.

Go back to the original branching scenario, with your bugfix branch off of feature:

A simple branch (b) off of feature (f).
A simple branch (b) off of feature (f).

Rebasing uses a series of standard Git operations under the hood to accomplish rebasing. It isn’t quite as straightforward as simply moving commits around as you’d move nodes in a tree data structure, for instance.

Let’s presume you wanted it to appear as though the work performed on feature followed the work you did on bugfix. In Git parlance, you’d be rebasing feature on top of bugfix.

Git first rewinds the branch that you’re rebasing – in this case, feature – back to its common ancestor with bugfix. The common ancestor is commit f:

Rewinding HEAD to the common ancestor of the feature and bugfix branches
Rewinding HEAD to the common ancestor of the feature and bugfix branches

Git then replays the patch of each commit from the branch you’re rebasing on top of, in this case, bugfix, and moves the HEAD and bugfix labels along:

Applying b1, b2 and b3 patches on top of the common ancestor and moving labels along.
Applying b1, b2 and b3 patches on top of the common ancestor and moving labels along.

Finally, one at a time, Git applies the patch of each commit from the branch you’re rebasing, in this case, feature, and moves the HEAD and feature labels along:

Applying f1, and f2 patches on top of the new base branch and moving labels along
Applying f1, and f2 patches on top of the new base branch and moving labels along

At this point, you no longer have any real reference to those original commits from the feature branch. Git will eventually just collect those orphaned commits (or “loose” commits, as they’re known) and clean them up in a regular garbage collection process. Although the loose commits still “know” who their parent is, you won’t see these commits show up in the history graph.

It’s like those commits never even existed. That’s where the whole idea comes from that Git rebase is, quite literally, rewriting history. For all intents and purposes, anyone looking at the repository has no reason to believe that you didn’t just make those commits on top of feature in the first place.

It’s important to understand that Git is not just moving commits here; it’s actually creating a brand new commit based on the contents of the patch it calculated at each commit in your branch.

Note: Choose to rebase only when the branch you’re on is not shared with anyone else because, once again, you’re rewriting the history of the repository. If you must rebase a shared branch, you’ll have to coordinate with your team to make sure that everyone has pushed any and all changes to the branch and deleted it locally before you begin your work. Otherwise, you’re gonna have a bad time.

Creating your first rebase operation

To start, find the starting repository for this chapter in the starter folder and unzip it to a working location.

You’ll create an extremely trivial branch off of wValidator, make a change on that branch, and then rebase wValidator on top of your branch.

First, check that you’re on the correct branch for your repo:

git branch

You should be on the wValidator branch.

Create a new branch named cValidator from wValidator:

git checkout -b cValidator

Next, open up README.md and add your name to the end of the # Maintainers section:

# Maintainers

This project is maintained by teamWYXZ:
- Will
- Yasmin
- Xanthe
- Zack
- Chris

Save your changes and exit the editor.

Add your changes:

git add .

Commit your changes with an appropriate message:

git commit -m "Added new maintainer to README.md"

At this point, you have a branch cValidator with a commit containing changes to README.md. Now, you want to simulate someone creating more commits on the wValidator branch.

Switch back to the wValidator branch:

git checkout wValidator

Open README.md and add your initial to the end of the team name in the # Maintainers section:

# Maintainers

This project is maintained by teamWYXZC:

(A bit of alphabet soup, that is: “teamWXYZC”. You should petition the team to get a really cool name someday. But that’s for later.)

Save your changes, exit the editor and stage your changes:

git add .

Now create a commit with that change, using an appropriate message:

git commit -m "Updated team acronym"

Take a quick look at the current state of the repository in graphical form:

git log --all --decorate --oneline --graph

The top three lines show you what’s what:

* c628929 (HEAD -> wValidator) Updated team acronym
| * 2eb17a2 (cValidator) Added new maintainer to README.md
|/
* 3574ab3 Whoops — didn't need to call that one twice

You have a commit in a separate branch, cValidator, that you’d like to rebase wValidator on top of. While you could merge this in as usual with a merge commit, there’s really no need, since the change is so small and the changes in each branch are trivial and related to each other.

To rebase wValidator on top of cValidator, you need to be on the wValidator branch (you’re there now), and tell Git to execute the rebase with the following command:

git rebase cValidator

Git shows a bit of output, telling you what it’s doing:

Successfully rebased and updated refs/heads/wValidator.

As expected, Git rewinds HEAD to the common ancestor — commit 3574ab3 in the graph shown above. It then applies each commit from the branch you are on — i.e., the branch that’s being rebased — on top of the end of the branch you are rebasing onto. In this case, the only commit from wValidator Git has to apply is 78c60c3 - Updated team acronym.

Take a look at the history graph to see the end result by executing the following:

git log --all --decorate --oneline --graph

You’ll see the following linear activity at the top of the graph:

* 17771e6 (HEAD -> wValidator) Updated team acronym
* 2eb17a2 (cValidator) Added new maintainer to README.md
* 3574ab3 Whoops — didn't need to call that one twice

For a bit of perspective, you can look at the simple graphs at the start of the chapter for a visual reference to what’s happened here. But here’s the play-by-play to show you each of the steps:

  • Git rewound back to the common ancestor (3574ab3).
  • Git then replayed the cValidator branch commits (in this case, just 3f7969b) on top of the common ancestor.
  • Git left the branch label cValidator attached to 3f7969b.
  • Git then replayed the patches from each commit in wValidator on top of the commits from cValidator and moved the HEAD and wValidator labels to the tip of this branch.

You don’t need that cValidator branch anymore, nor is instructive to keep that label hanging around in the repository, so clean up after yourself with the following command:

git branch -d cValidator

As an aside, did you notice the difference in the commit hashes?

  • Old commit for Updated team acronym: 78c60c3
  • New commit for Updated team acronym: f76b62c

They’re different because what you have at the tip of wValidator is a brand-new commit — not just the old commit tacked onto the end of the branch.

You may be wondering where that old commit went, and you’ll dig into those details just a little further into this chapter as you investigate a more common scenario where you’ll encounter and resolve rebase conflicts.

A more complex rebase

Let’s go back to our Magic Square development team. Several people have been working on the Magic Squares app; Will in particular has been working on the wValidator branch. Xanthe has also been busy refactoring on the xValidator branch.

Here’s what the repository history looks like at this point:

The partial GitUp view of the repository, including the branches wValidator and xValidator.
The partial GitUp view of the repository, including the branches wValidator and xValidator.

Xanthe has branched off of Will’s original branch to work on some refactoring, and it’s now time to bring everything back into the wValidator branch. Because branching is cheap and easy in Git, these types of scenarios where developers branch off of existing branches is fairly common. Again, there’s nothing saying that you always have to branch off of master or main — you can support any branching scheme you like, as long as you can keep track of things!

Although you could just merge all of Xanthe’s work into Will’s branch, you’d end up with a merge commit and clutter the history a little. And, conceptually, it makes sense to rebase in this situation, because the refactoring that Xanthe has done is within the logical context of Will’s work, so you might as well make it appear that the work has all taken place on a common branch.

First, check out the commits made since the common ancestor of wValidator and xValidator:

git log --oneline bf3753e~..

That last bit is new. What bf3753e~.. means is: “limit git log to just this particular commit (inclusive) up to HEAD.” Not providing the end commit hash indicates HEAD.

You’ll see the following:

f76b62c (HEAD -> wValidator) Updated team acronym
3f7969b Added new maintainer to README.md
3574ab3 Whoops — didn't need to call that one twice
43d6f24 check05: Finally, we can return true
bf3753e check04: Checking diagonal sums

Those are the most recent commits on wValidator. Now, you know that xValidator branched from wValidator, so is it possible to view just what’s changed on xValidator?

Absolutely. Execute the following to see what’s happened since you branched xValidator from wValidator:

git log --oneline bf3753e~..xValidator

You’ll see the following:

8ef01ac (xValidator) Refactoring the main check function
5fea71e Removing TODO
bf3753e check04: Checking diagonal sums

Your goal is to rebase the changes from wValidator on top of xValidator.

To start, ensure you’ve checked out the branch you want to merge your changes into with the following command:

git checkout wValidator

Git tells you you’re already on that branch — no worries. It always pays to be sure.

Now, begin the rebase operation with the git rebase command, where you indicate which branch you want to rebase on top of your current branch:

git rebase xValidator

Resolving errors

git rebase provides quite a lot of verbose output, but if you look carefully through the output of your command, you’ll see that there’s a conflict you have to resolve in js/magic_square/validator.js.

Open up js/magic_square/validator.js and you’ll see the conflict that you need to resolve. In this case, you want to keep the bits marked as <<< HEAD, since these are Xanthe’s refactored changes that you want to keep.

Note: You might be confused here. Why are you keeping the HEAD changes, if HEAD is the tip of the branch you’re on — in this case, wValidator?

In a rebase situation, HEAD refers to the tip of the branch you’re rebasing on top of. As Git replays each commit onto this branch, HEAD moves along with each replayed commit.

Resolve the commit manually, removing the bits from the common ancestors and the original bits from Will’s code. Save your work when you’re done.

Note: Did you notice the final separator line of the conflict?

>>>>>>> 43d6f24... check05: Finally, we can return true

Because rebasing works by replaying the commits of the other branch one by one on the current branch, Git helpfully tells you in which commit the conflict occurred. For complex merge conflicts, this little bit of extra information can be quite useful.

When you’re done, return to the command line and continue the rebase with the following command:

git rebase --continue

Oh, but Git won’t let you continue. It gives you the following message:

js/magic_square/validator.js: needs merge
You must edit all merge conflicts and then
mark them as resolved using git add

Again, because you’re working within the context of a single commit, you need to stage those changes. Git rebases each of the original commits one at a time, so you need to deal with and add the changes from each commit resolution one at a time.

Execute the following command to stage those changes to continue:

git add .

Then continue with the rebase:

git rebase --continue

But, frustratingly, Git still won’t let you continue:

Auto-merging js/magic_square/validator.js
CONFLICT (content): Merge conflict in js/magic_square/validator.js
error: could not apply 3574ab3... Whoops—didn't need to call that one twice
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 3574ab3... Whoops—didn't need to call that one twice

This is one of those instances where Git can appear to be completely dense. Doesn’t Git know that you just ran git add? Doesn’t it see that you just resolved those commits? Gitdammit.

Feel free to vent for a second, and then consider the situation from Git’s perspective.

What you did above was to keep everything from the commit you are rebasing from xValidator. But Git is effectively expecting that there should be some change in the commit you’re rebasing from xValidator, as this is the most likely case you’ll encounter when rebasing work.

Imagine if Git just assumed that taking the commit verbatim was a completely normal situation; if you weren’t paying attention to your commit resolution, you’d likely hit a point where you’d unwittingly clobber some work on the branch on which you’re rebasing on top of. And then you’d spend countless hours, late at night, with too much coffee, trying to figure out where your rebase went so horribly wrong. In this situation, Git’s error message would actually help you out.

But in this case, since you are taking the commit verbatim with no changes, you can simply execute the following command to carry on:

git rebase --skip

Note: This may or may not be a great time to tell you that you didn’t actually need to perform that conflict resolution in validator.js, since you took that commit as-is from xValidator. You could’ve just executed git rebase --skip straight away to tell Git that you had no interest in resolving the commit and to rebase the commit unchanged.

Git then carries on and attempts to apply the second commit:

Successfully rebased and updated refs/heads/wValidator.

And you’re done that frustrating, yet enlightening journey through Git rebasing.

To see the result of your work from the perspective of Git, have a look at your history graph again since that common ancestor:

git log --oneline bf3753e~..

You’ll see that the two original commits Will made at the end of wValidator are gone (the commits with short hash 3574ab3 and 43d6f24), and Xanthe’s commits are now neatly tucked in between the common ancestor and your updates to README.md, the xValidator branch label points to what was the tip of xValidator, and the wValidator branch label points to the tip as expected:

57f62b0 (HEAD -> wValidator) Updated team acronym
b14948d Added new maintainer to README.md
8ef01ac (xValidator) Refactoring the main check function
5fea71e Removing TODO
bf3753e check04: Checking diagonal sums

Stop just for a moment and consider what you’ve done here. Where did those commits from Will go? If you’d just done a simple merge, as you’re used to doing, you would have still seen them in the history of the repo.

Even more confusingly, you can still find these commits in the logs. Execute the following command to see the three logged commits, starting at the “Whoops — didn’t need to call that one twice” commit of 3574ab3:

git log --oneline -3 3574ab3

That shows the history of the wValidator branch, from 3574ab3 back, as you understood it before you started rebasing:

3574ab3 Whoops — didn't need to call that one twice
43d6f24 check05: Finally, we can return true
bf3753e check04: Checking diagonal sums

But where are those commits? Essentially, those commits are orphaned, or “loose” as Git refers to them. They are no longer referenced from any part of the repository tree, except for their mention in the Git internal logs.

You can see that the object still exists inside the .git directory:

git cat-file -p 3574ab3

Git returns with the commit metadata:

tree 1b4c07023270ed26167d322c6e7d9b63125320ef
parent 43d6f24d140fa63721bd67fb3ad3aafa8232ca97
author Will <will@example.com> 1499074126 +0700
committer Sam Davies <sam@razeware.com> 1499074126 +0700

Whoops — didn't need to call that one twice

But as you saw from the repository history tree above, that actual commit is no longer referenced anywhere. It’s just sitting there until Git does its usual garbage collection, at which point Git will physically delete any loose objects that have been hanging around too long.

Note: Git generally tries to be as paranoid as possible when running garbage collection. It doesn’t clean up every single loose object it finds, because there might be a chance that you made a mistake and really need the code from that commit.

In fact, even though the commit isn’t referenced anywhere, as long as you know the hash of that commit from the logs, you can still check it out and work with the code inside. So Git, like any good developer, will keep those files hanging around for a while…juuuuust in case you need them later. Thanks, Git!

Just for comparison purposes, check out what this entire scenario would have looked like from a merge perspective, as opposed to a rebase perspective:

*   96f42e3 (HEAD -> wValidator) Merge branch 'xValidator' into wValidator
|\
| * 8ef01ac (xValidator) Refactoring the main check function
| * 5fea71e Removing TODO
* |   b567a15 Merge branch 'cValidator' into wValidator
|\ \
| * | 9443e8d (cValidator) Added new maintainer to README.md
* | | 76bacc5 Updated team acronym
|/ /
* | 3574ab3 Whoops — didn't need to call that one twice
* | 43d6f24 check05: Finally, we can return true
|/
* bf3753e check04: Checking diagonal sums

You can see that the merge commit would result in the branch actions remaining in the repository history. Instead, the rebase action streamlined the commit history and gathered those changes as a cohesive linear operation. This is, arguably, clearer to the casual observer of your repository’s history.

Although the politics and goals of your development team will dictate your approach to merging and rebasing, here are some pragmatic tips on when rebasing might be more appropriate over merging, and vice versa:

  • Choose to rebase when grouping the changes in a linear fashion makes contextual sense, such as Will’s and Xanthe’s work above that’s contained to the same file.
  • Choose to merge when you’ve created major changes, such as adding a new feature in a pull request, where the branching strategy will give context to the history graph. A merge commit will have the history of both common ancestors, while rebasing removes this bit of contextual information.
  • Choose to rebase when you have a messy local commit or local branching history and you want to clean things up before you push. This touches on what’s known as squashing, which you’ll cover in a later chapter.
  • Choose to merge when having a complex history graph doesn’t affect the day-to-day functions of your team.
  • Choose to rebase when your team frequently has to work through the history graph to figure out who changed what and when. Those merge commits add up over time!

There’s a long, political history surrounding rebasing in Git, but hopefully, you’ve seen that it’s simply another tool in your arsenal. Rebasing is most useful in your local, unpushed branches, to clean up the unavoidably messy business of coding.

But you’ve only begun your journey with rebasing. In the next chapter, you’ll learn about interactive rebasing, where you can literally rewrite the history of the entire repository, one commit at a time.

Challenge

Challenge: Rebase on top of another branch

You’ve discovered that Zach has also been doing a bit of refactoring on the zValidator branch with the range checking function:

| * 136dc26 (zValidator) Refactoring the range checking function
|/
* 665575c util02: Adding function to check the range of values

Your challenge is to rebase the work you’ve done on the wValidator branch on top of the zValidator branch. Again, the shared context here and the limited scope of the changes mean you don’t need a merge commit.

Once you’ve rebased wValidator on top of zValidator, delete both the zValidator and xValidator branches, as you’re done with them. Git might complain when you try to delete the branches. Explain why this is, and then figure out how to force Git to do it anyway.

As always, if you need help, or want to be sure that you’ve done it properly, you can always find the solution under the challenge folder for this chapter.

Key points

  • Rebasing “replays” commits from one branch on top of another.
  • Rebasing is a great technique over merging when you want to keep the repository history linear and as free from merge commits as possible.
  • To rebase your current branch on top of another one, execute git rebase <rebase-branch-name>.
  • You can resolve rebase conflicts just as you do merge conflicts.
  • To resume a rebase operation after resolving conflicts and staging your changes, execute git rebase --continue.
  • To skip rebasing a commit on top of the current branch, execute git rebase --skip.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.