Coming from Mercurial, you're expecting that a commit is permanently attached to a specific branch. That is, if you manage to extract some commit in isolation, you have something that says:
I am a commit on branch foo.
I change file bar.
Git does not work this way: a commit is independent of any branch (name), and in fact, branch names—labels, if you will—can be peeled away and stuck somewhere else willy-nilly. They have no use1 except to humans trying to interpret the mess.
In Mercurial, when you "rebase" some changeset(s), you (in effect at least) dump them out as diffs against their bases, then you change over to the other branch you want them on and make new commits on that other branch. Mercurial used to (maybe still does) call this first step, "grafting". These new commits are now permanently attached to (and only to) this other, different branch:
master: f1 - f2 - f3 - f4 - f7 - f8
\
feature/AAA-1: f5 - f6
becomes:
master: f1 - f2 - f3 - f4 - f7 - f8
\
feature/AAA-1: f5 - f6 - 9 - 10
At this point you can "safely undo" f7 and f8, taking them off the master
line, and your rebase is finished with the copies only on the other branch.
Note that I draw the branch labels on the left here. This is safe because all commits are permanently stuck to their branches, so once a changeset is on the line of its branch, it's always on the line of its branch. The only time there's a violation of the "changeset goes on the (single) line of its branch" rule is for a merge, when a changeset attaches to (exactly) two branches: it sits on its main branch, but draws in a connection to the other branch.
In git, on the other hand, a commit can be considered to be "on" zero or more branches (there's no "exactly 1 or 2" constraint), and the set of branches a commit is "on" is dynamic as branch names can be added or removed at any time. (Note also that the word "branch" has at least two meanings in git.)
Git's rebase works very similarly to Mercurial's: it actually copies the commits. But there's one important difference to start: the copies aren't specifically "on" any branch (and in fact the rebase process operates on no branch, using what git calls a "detached HEAD"). Then, there's an even more important difference at the end.
As before we can start with a graph drawing, but this time I'll draw it a bit differently:
f7 <- f8 <-- master
/
f1 <- f2 <- f3 <- f4
\
f5 <- f6 <-- feature/AAA-1
This time, the labels are on the right, with arrows. The name master
actually points directly to commit f8
, and it's f8
that points back to f7
and f7
points back to f4
and so on.
What this means is that, right now, commits f1
through f4
are "on" both branches. With git, it's better to say that these commits are "contained in" (the history of) both branches. There's nothing in any of those commits to say which branch they were originally "made on": they carry their parent pointers, source tree IDs, and author and committer names (and timestamps etc), but no "source branch name". (Newcomers to git from hg often find this quite frustrating, I think.)
If you now ask git to rebase f7
and f8
onto feature/AAA-1
, git will make copies of the two commits:
f7 <- f8
/
f1 <- f2 <- f3 <- f4
\
f5 <- f6 <- f7' <- f8'
(the '
marks, or f7prime and f8prime, mean these are copies of the originals—git cherry-pick
s, analogous to hg's grafts). But now we get to the key difference, the one that's tripping you up: git now "peels off" the original master
label and makes it point to the tip-most new commit instead. This means the final graph looks like this:
f7 <- f8 [abandoned -- was master]
/
f1 <- f2 <- f3 <- f4
\
f5 <- f6 <-- feature/AAA-1
\
f7' <- f8' <-- master
Mercurial can't do this: branch labels cannot be peeled off and shuffled around and re-pasted elsewhere. So it doesn't, and that's why its rebase works differently.
In git, what you want to do here is simply cherry-pick the two commits into the feature/AAA-1
branch, then remove them off the master
branch:
$ git checkout feature/AAA-1
$ git cherry-pick master~2..master # copy the commits
$ git checkout master
$ git reset --hard master~2 # back up over the originals
The idea here is that you're not rebasing master
at all, and you're not even really rebasing your feature branch either: instead, you're just copying two commits into your feature branch, then removing them from master.
1This is a bit of an overstatement, since transfers between repositories—git fetch
and git push
—use branch and tag labels as well. Also, you need some references to commits to keep them alive, otherwise git's garbage collector will eventually reap them as "unreachable".