A feature or topic branch based workflow is an essential part of modern software development. A topic branch gives a developer an isolated place to do work. And once that work is done, it provides a basis for creating a pull request.
Once a feature is completed and pushed, sometimes it makes sense to create a second topic branch, based on the first. These are often called sub branches. This might happen for several reasons, including:
- You’re not ready to merge the first branch
- You’re holding off merging, to keep the new feature out of
master, for reasons
- PRs might take a while to get approved
You probably know that creating one topic branch off of another is easy. Merging this work back to master can also be pretty straightforward. Rebasing, however, takes a little more care.
git rebase --onto to transplant your topic branches in the order they were created, but take care if your branch is used in a pull request.
The following examples are based on this hypothetical set of topic branches. Each consecutive topic branch a sub-branch of the earlier branch.
A---B---C master \ D branch-one \ E---F---G branch-two \ H---I branch-three
branch-one has a single commit,
branch-two has three commits, and
branch-three has two commits—but it starts halfway along the
Meanwhile, two other commits have occurred on
If you’re not rebasing, these branches can be easily merged to
master, in order, with one merge commit each.
The merge-commit approach would look like this.
A---B---C---D'---G'--I' master \ / / / D------' / / \ / / E---F---G / \ / H---I
One way to merge these branches with rebasing is to build them up gradually, starting with rebasing
branch-two, and so on.
This requires rewriting each sub-branch multiple times.
As a consequence, getting PRs to track is difficult.
In this post, we take the approach of rebasing onto
master, in the order the branches were created.
Merging branch one
The first case is easy, since it’s branched directly off of
master. You can use standard rebase.
git checkout branch-one git rebase master
We get the expected result, with a new commit,
D', containing the changes from commit
D replayed on master.
The branch reference was also moved as expected, leaving the original commit,
D without one.
+-------------- master | v A---B---C---D' <------- branch-one \ D \ E---F---G branch-two \ H---I branch-three
If you use GitHub, and you had PRs outstanding for these branches, at this point, GitHub might become confused.
It would see that the merge target of
branch-two had changed and it would close the PR associated with
To avoid this, you can edit the PR and set the target to be
master instead of
branch-one before rebasing
This might cause your PR changes to look a little crazy in the interim, but they return to normal afterwards.
branch-one, be sure to update the PR to reflect the final state of the branch.
git checkout branch-one git push -f
master and push so the remote repo is up to date.
git checkout master git merge branch-one git push
This will be a fast-forward merge if your local
master branch was up to date prior to rebasing.
Finally, you’ll want to perform the normal PR cleanup:
git push origin -d branch-one# to delete the remote branch
git branch -d branch-one# to delete your local branch
Merging branch two
Before we get started, we want to make sure
branch-three’s PR isn’t clobbered when we rebase.
Again, if we’re using GitHub, we will edit the PR for
branch-three and change the merge target from
Here’s where things change from the normal rebase flow.
We want to rebase
master, per normal.
If we do a normal rebase, git will look all the way back to master, which includes the moribund commit,
In simple cases, this won’t be a problem, as the replay will be a no-op.
It can cause conflicts, though, if changes have been made in the interim on
Fortunately, rebase provides the
--onto flag for exactly this situation.
We need to tell git to rebase only the commits from the topic branch that we care about—that is, commits
If we wanted to move
branch-two earlier, while
branch-one was still in its original place, we could have run.
git rebase --onto master branch-one branch-two
Indeed, that’s the canonical case.
We want to rebase
branch-two, starting from the upstream branch,
branch-one is no longer around.
So we need to get the hash for commit
D as a substitute for
branch-one for the upstream argument.
You can get it by checking out
branch-two and running
git log to find the commit just before
If you plan ahead, you can run
git log --oneline from each branch to get a list of all of the relevant branch refs and their commit hashes prior to starting.
e61e988 (HEAD -> branch-three) I 37dcec4 H 61c2db4 F 32c9e2c E 2faee42 (branch-one) D 1e10333 B 0e58a1e A
5f45eae (HEAD -> branch-two) G 61c2db4 F 32c9e2c E 2faee42 (branch-one) D 1e10333 B 0e58a1e A
Once we have the hash for
D, we can rebase
git rebase --onto master 2faee42 branch-two
+------------------ master | v A---B---C---D'--E'--F'--G' <--- branch-two \ D \ E---F---G \ H---I branch-three
Now, perform the remaining steps to wrap up
# Update the PR git checkout branch-two git push -f # Fast-forward merge git checkout master git merge branch-two git push # Delete the branch git push origin -d branch-two git branch -d branch-two
Interlude: merging branch two after squashing
Some teams routinely squash commits prior to merging or rebasing. This can make it easier to see where some work ends and other work begins, especially if rebasing onto master.
To squash, just use interactive rebase as you normally would.
In this case, we would like to squash commits
G into one, say
git checkout branch-two git rebase -i HEAD~3
In the invoked editor, we would squash (or fixup) two commits.
pick 5cc6ba4cd E squash eda9daade F squash c4f181c1b G
This would leave us with a tree that looks like this:
A---B---C---D' <------- branch-one \ D |\ | E' branch-two \ E---F---G \ H---I branch-three
The rebase would then continue as before, with the
Merging branch three
The process continues for
branch-three as it was performed for
Note that in this case,
branch-three was created partway through the work on
This doesn’t create any special considerations.
It just means that you will need to select a hash for commit
G rather than
E for the rebase. When running rebase with branch names, git looks at the common ancestor of each branch to determine where they diverge. The same goes for commit hashes, so the choice of
G isn’t important.
The final state will look like this.
A---B---C---D'--E'--G'--H'--I' master \ D \ E---F---G \ H---I
Now that there are no references to the original commits, they will eventually be garbage collected.
Prior to starting
- Check out each branch
git log --onelineto get hashes associated with original branch names
For each branch
- Prepare relevant PRs (if using GitHub)
- Update the merge target of the descendent PR to refer to
- Update the merge target of the descendent PR to refer to
- Squash (optional)
git rebase -i <upstream> <branch>
- Rebase onto
git rebase --onto <newbase> <upstream> <branch>
- Update the PR
git checkout <branch>
git push -f
- Merge the branch to
git checkout master
git merge <branch>
- Delete the branch
git push origin -d <branch>
git branch -d <branch>
When newcomers first learn about rebasing, it can seem confusing.
Part of this confusion arises, in my opinion, because rebasing is used to do different things—much like merge.
(This relates to complaints sometimes heard about git’s porcelain.)
In this post, I referenced two different modes for using rebase.
The first (and the point of the article), is rebasing to move commits rather than using merge.
The second mode is rebasing to combine commits, often called squashing.
This is usually done using interactive rebase, with the
The fact that they’re often done in the same workflow can add to the confusion.
Whether to merge or rebase is a question each organization needs to answer for themselves. The debate over which approach to take can sometimes result in levels of vehemence only seen in arguments over tabs vs. spaces, and similar important questions.
In our organization, we take a compromised approach. If a PR has one commit, then the answer is easy: rebase and avoid a merge commit. The PR and comment are self-contained, so avoid the clutter.
If a PR has several significant commits, then just do a merge commit. This preserves the branch structure. The progression of work can be seen in isolation. The merge commit can also contain the original branch name in the comment, which is lost after a merge when using git, due to the ephemeral nature of branches. This can be a plus for some people.
Finally, if the PR has more than one commit, but most are tiny or inconsequential, then the history of those commits isn’t that important. In this case, it makes sense to squash down to one commit on the branch and then rebase onto master.
This approach has worked well for us. It combines the best of both philosophies—preserving history and branch structure when it’s important, and cleaning up when it’s not.