Introduction
Like many developers, I use Git for source control. Normally I use a short-lived branch for each feature I code. I create it from the master branch and rebase it as required. But sometimes I have to branch off of a branch and in this situation rebasing can be problematic. One solution is to use the --onto
option when rebasing, and this post details how the process works.
A feature on a feature
Let us say that I start developing a new feature. I create branch-a off of master and I push a couple of commits to it (B and C):
My aim with this feature is to submit the work as a few smaller pull requests rather than as one large pull request. This makes each pull request easier to review. I complete the work on branch-a and submit a pull request for it. But while that pull request is being reviewed I want to continue working on the next part of the feature. To do this I create branch-b, a branch off of branch-a. I now continue to work on this new branch, adding commits D and E:
A problem arises
A short while passes and the review of my pull request is complete. The result is that I need to make a change, so I add commit F to branch-a:
But before I can merge my pull request, another developer merges their own pull request to master. This is commit G:
I now need to rebase branch-a before I can merge it. Rebasing branch-a creates new commits for B, C and F, here labelled B1, C1 and F1:
This is because rebasing in Git rewrites Git history. Note that commits B and C still exist, but they are now considered part of branch-b.
I merge my pull request using the 'squash and merge' option. This collapses the branch-a commits into a single new commit (H). This commit gets appended to the tip of master:
Now I want to continue working on branch-b, but I want to be working on top of the latest changes from master. This should include the pull request commit. If branch-b had been branched from master, then this would be simple. I could do a regular rebase of branch-b against master. But if I do that here, I will re-apply the changes from commits B and C that are already included in commit H:
The rebase affects commits B, C, D and E, but I am only interested in commits D and E. Depending on the exact changes involved, this can result in a very confusing rebase:
- There can be merge conflicts between the changes in the original commits on branch-a (commits B and C) and the final combined commit on master.
- There can be no-op commits when the changes in an original commit match those already on master, indicated by the message "No changes - did you forget to use 'git add'?".
What I need is more control over the result of the rebase.
A more precise rebase
One answer is to use the --onto
option with Git rebase as documented here. This allows you to specify a range of commits to rebase; no other commits get included in the rebase. You need to specify both the branch to rebase onto and the range of commits to rebase. There are many ways to specify that range. I find the simplest is the HEAD~x
syntax, where x
is the number of commits to count back from the head commit. (The head commit is the current commit.HEAD
is Git's shorthand reference for that commit.) With this syntax you specify the first commit you do not want to include in the rebase, working back in time. To only include the most recent commit you would specify HEAD~1
. To include the two most recent two you would specify HEAD~2
. In the example in this post I enter the following commands in turn:
# Check out the branch to be rebased (branch-b):
git checkout branch-b
# Rebase the desired range of commits:
git rebase --onto master HEAD~2
# Check things look right:
git log --oneline --graph --decorate
# Force push the rebased branch-b to the origin repo:
git push --force-with-lease origin HEAD
This results in the following state, and I am now able to continue working on branch-b:
Note that there are other ways to achieve the same result. I could perform an interactive rebase of branch-b in which I remove the lines for outdated commits B and C.
Conclusion
Having learned the git rebase --onto
technique, I now feel more in control of Git. I have the power to move ranges of commits around, without the merge conflicts I would have had in the past.
Changelog
- 2020-04-25 Initial version
- 2020-06-27 Minor formatting changes
- 2020-06-28 Added section headings and formatting changes
- 2020-08-27 Plain English improvements
# Comments
Comments on this site are implemented using GitHub Issues. To add your comment, please add it to this GitHub Issue. It will then appear below.