This one is going to be a bit different. I'll take most of this from a StackOverflow answer I found that did an excellent job of explaining this, and I'll probably mess it all up before I'm done. Once again, this post is meant to anchor this information in a place that I can get to easily, but I hope that it serves to help direct other people to an answer as well.
Thusly, and without further ado, the StackOverflow question I am referencing is located here: https://stackoverflow.com/questions/29914052/how-to-git-rebase-a-branch-with-the-onto-command
And the answer from which I am attempting to derive the rest of this post is, at the time of writing, the first listed answer when sorted by Highest score. I would be surprised if another answer floated above that as I am seeing 785 upvotes currently with the next runner up at 141. The other answers aren't necessarily bad, but I found this answer to be particularly helpful. Additionally, it starts off with a nice bold tl;dr at the top.
BLUF: there are three forms of git rebase
that we cover here:
# Simple rebase of HEAD to the most recent commit of a branch
git rebase <branch>
# Advanced rebase of HEAD to a specific commit of branch
# The old parent will be removed
git rebase --onto <newparent> <oldparent>
# Super advance rebase of a given range of commits reachable
# from HEAD to a specific commit of branch
# The old parent and any commits *after* the 'end' commit
# will be removed
git rebase --onto <newparent> <oldparent> <end>
Firstly, and as I understand it, a git rebase essentially "replays" a selected set of commits starting at a different point. We can choose how/if those commits are replayed.
Lets begin with the following scenario; a project with a clean and irrelevant commit history to date. For a reference point, we will start with the current commit A
on the main
branch. Having made a simple change to a file, we will make a commit directly to the main
branch, B
, and repeat once again for C
. Our commit history now looks like this:
A---B---C (main)
Great, we have our frame of reference. Let's screw some stuff up.
You can see we are committing directly to main
, which is typically a no-no. We've seen the error of our ways, and we create a topic branch, feat-widget
, to develop some new widget and we make a few commits to that branch. But let's also assume that there was a big issue with the project, and we had to make a quick fix. Maybe we did the right thing and made an issue branch and merged it back into main
, but let's say for now that the fix was committed directly to main
again. Probably more realistic. Here's our commit history now:
A---B---C---F---G (main)
\
D---E (feat-widget)
Well, crap! How are we going to merge feat-widget
into main
later once we are ready to submit a pull request? Sure, we can roll the dice and hope that the merge works, and it very well could. But, let's also consider whether our cool widget we are building in this branch will work correctly with the patches we applied to the main
branch. Our branch currently does not contain those patches as well, and so the issue is still present there. We need to rebase so we can integrate commits F-G
into our branch. We can do that using the simplest form of rebase, git rebase <branch>
. This will essentially shift our topic branch forward to base from G
instead of C
, or in other words, change the parent of D
from C
to G
. After this, the changes in F
and G
will be present in our working tree at E
.
A---B---C---F---G (main)
\
D---E (feat-widget)
Now we have the patches from F
and G
and we can test everything knowing that it will work when we merge. That wasn't so bad.
Now let's rewind a little bit and say that we need to include those same security patches, but only up to F
. This is a little more tricky, but we can achieve it using the --onto
parameter. The syntax is git rebase --onto <newparent> <oldparent>
where newparent
is the commit on the main
branch that we want to target, and oldparent
is the commit prior to the one that will be moved to the newparent
. So in our example, we would run git rebase --onto F D
.
A---B---C---F---G (main)
\
E (feat-widget)
Notice that D
is missing. It has been effectively deleted because essentially it was a copy of C
, which is no longer where our topic branch diverges from main
. This also means we can use the same form of git rebase
to delete commits from a branch. Let's consider a linear line of commits:
A---B---C---D---E (HEAD)
We want to remove C
and D
, but keep the rest. We could do an interactive rebase and cherry-pick all but those. Or we could use git rebase --onto B D
to rebase HEAD
on top of B
where the old parent was D
. This should result in the following:
A---B---E (HEAD)
We can pass 3 arguments to our rebase command to get absolutely insane. Buckle up.
Back to our original example, let's recall our starting point, but we've done a bit more work on our topic branch:
A---B---C---F---G (main)
\
D---E---H---I (feat-widget)
Let's say that we need to take E
and H
and incorporate F
. At the same time, for whatever reason, we do not need I
anymore. Maybe it was included in F
or something. This one is pretty granular but we can do it! git rebase --onto <newparent> <oldparent> <end>
is the syntax we will use. The newparent
and oldparent
are familiar and essentially the same, however, the end
value is the commit we will stop at. So we can think of this as rebasing the range of commits starting with the one whose parent is oldparent
and ending with end
. So until
is included in the range, but oldparent
is not. In our example situation, we need to keep E
and H
, so oldparent = D
, end = H
, and newparent = F
leaving us with the following: git rebase --onto F D H
. Now we have the following tree:
A---B---C---F---G (main)
\
E---H (feat-widget)
Bask in the glory of the insanity you have conquered with a single git command. Now go break something, but do it with excellence.
0 comments