[Do not accept this answer. It’s just a sort of commentary on torek’s answer.]
The way to look at this, in my opinion, is to understand that the full form of git rebase
is with onto
and three parameters:
git rebase --onto x y z
To read that, in your mind clump y
and z
together, and swap --onto x
to the end (because that’s the natural English order for direct object and prepositional phrase), so that the whole thing parses something like this (pseudo-code):
rebase [y z] onto x
In that pseudo-code, the expression [y z]
means “starting right after y
and continuing all the way to z
.” Git calculates what “starting right after y
” means by working backwards from z
, not forward from y
, but the effect is generally the same.
So git rebase --onto x y z
means: “Grab all the commits starting right after y
and continuing all the way to z
, and append them to x
.”
Very well. That’s the full form of git rebase
. When you omit any of the parameters, Git fills them in for you. And the way it does that is surprising. That’s the reason for the results you’re seeing.
So let’s take a real example. Here’s our starting position:
* f8696e6 (HEAD -> dev) z
* 103333e (origin/dev) y
* 559ad1f x
| * 8032a5d (origin/main, main) c
| * 2caa1e9 b
|/
* 06c7439 a
Look carefully at the graph. We are on dev
. We have a remote origin
, and we are one ahead of our remote-tracking branch origin/dev
. dev
split off from main
at a
, and after that it goes
x y z
Meanwhile, main
goes
a b c
Now let’s try
git rebase main
We are on dev
, so that means
git rebase main main dev
Which means “grab the commits start from after main
and continuing to dev
— namely, x
y
z
— and attach them to main
.” Here we go… Here’s what we get:
* f6b903e (HEAD -> dev) z
* 4adb109 y
* e9cc7fd x
* 8032a5d (origin/main, main) c
* 2caa1e9 b
* 06c7439 a
Yup, just as I said. This, I think, is what most people expect when they use a one-parameter git rebase
.
Okay, now start over. This time we’ll say
git rebase --onto main
That, as torek’s answer tells you, means
git rebase --onto main origin/dev dev
So that means “grab everything from origin/dev
to dev
— that’s just z
— and attach it to main
. That’s extremely surprising! We never said anything about origin/dev
, but that’s where Git is going to snip our branch as we rebase. Here we go… Here’s what we get:
* 0dccc25 (HEAD -> dev) z
* 8032a5d (origin/main, main) c
* 2caa1e9 b
| * 103333e (origin/dev) y
| * 559ad1f x
|/
* 06c7439 a
That’s probably the kind of thing happened to you (the OP). And it’s easy to see why you found it surprising!
So in my opinion the main takeaway is that if you leave out any of the three parameters, you may be surprised by what Git chooses for them. Therefore, also in my opinion, you should not leave out any of them! You just don’t know what will happen if you do.
Final note: Okay, I lied a little. Remember the first result?
* f6b903e (HEAD -> dev) z
* 4adb109 y
* e9cc7fd x
* 8032a5d (origin/main, main) c
* 2caa1e9 b
* 06c7439 a
I left out origin/dev
from that diagram. In reality, this what we now have:
* f6b903e (HEAD -> dev) z
* 4adb109 y
* e9cc7fd x
* 8032a5d (origin/main, main) c
* 2caa1e9 b
| * 103333e (origin/dev) y
| * 559ad1f x
|/
* 06c7439 a
Notice the duplication of the x
and y
commits. That’s what git rebase
does: it copies commits. This is a tricky situation if we intend to push dev
, because we will be asking the remote origin
to forget about the y
and x
currently pointed to by origin/dev
, and it isn’t going to be happy about that.