All commits are read-only, always. They are also permanent, with some exceptions (having to do with whether anyone can find them). This means that git rebase
copies commits. That's it's job!
Let's take a look at your git log --graph --oneline ...
output, but let's start a bit simpler:
* b48659d (dev) ...
* | 46c1f40 (origin/master, origin/HEAD, master) ...
|/
* 5baae80 first commit
Note the shortened hash IDs, such as 5baae80
and b48659d
. These are the "true names" of each commit, shortened to just 7 characters since that's usually sufficient.1 Each commit records the ID of a parent commit, and Git uses these parent commit hashes to follow each commit backwards through history, starting from a more-recent branch tip commit. The tip commit of branch dev
is now 37c07a4...
:
5baae80--46c1f40 <-- master, origin/master
\
b48659d <-- dev
None of these commits can ever be changed!
You now git push
this to origin
. The git push
command calls up a second Git, over on the machine acting as origin
, and hands over the commits that you have that they don't, which everyone agrees to by their IDs. Then your Git asks their Git to set their dev
, which will be your origin/dev
, to the same value as your dev
: b48659d...
If they agree to this request, your Git remembers that they now have their dev
—your origin/dev
—pointing to b48659d
, just as we have drawn here, so now we have this:
5baae80--46c1f40 <-- master, origin/master
\
b48659d <-- dev, origin/dev
At this point, you run git rebase
, and this is where things start to go wrong.
1Git now dynamically chooses the shortened length, rather than just always using 7, but it starts with 7 by default. You can always use more: the full names are currently 40 characters long.
Rebase copies commits, then moves a branch label
The git rebase
command cannot change any existing commit, because nothing in Git can do that. Git is designed to make this impossible. So it does not even try. Instead, it copies commits.
When copying commits, Git needs two pieces of information:
- What commits should it copy?
- Where should it place the copies?
Git gets the first—the list of commits to copy—by listing commits starting from HEAD
and working backwards, stopping at some point you choose. (This list is backwards so it has to reverse it.) Git gets the second—the target "copy after" point—from the argument you give it, such as master
.
In this case, HEAD
names dev
(because you run git checkout dev
before you start the rebase), so the commits to copy end with the one to which the name dev
points:
5baae80--46c1f40 <-- master, origin/master
\
b48659d <-- dev (HEAD), origin/dev
We will therefore copy some series of commits stopping at b48659d
.
The place to copy them comes from your argument master
. The name master
identifies commit 46c1f40
, so the copies will go after 46c1f40
.
The tricky part is how Git computes which commits not to copy. It does this by starting from b48659d
and working backwards. You can imagine Git coloring each of these commits green temporarily. This takes Git to 5baae80
, which has no parent commit (it is the first commit in the repository, so it cannot have a parent). This stops the walk, so that these two commits are painted green. Then Git starts from the commit you specified by name—46c1f40
—and painting the commits red temporarily. The parent of 46c1f40
is 5baae80
, so Git paints this one red (don't copy), and tries to go on to its parent to paint those red as well. There is no parent, so we're done with the temporary painting, leaving one green commit, b48659d
.
This is the list of commits to copy. (It's backwards, but it's only one entry long anyway, so reversing it does nothing.)
Now Git begins the copying process. Each commit to be copied is copied as if by git cherry-pick
(if you use git rebase -i
, it's literally copied by cherry-pick). This makes new commits.
The new commit made from the existing b48659d
is 37c07a4
. Let's draw it in:
37c07a4
/
5baae80--46c1f40 <-- master, origin/master
\
b48659d <-- dev, origin/dev
Now that the entire list of commits has been copied, git rebase
does one last thing. It yanks the name dev
off the original string of commits, and pastes it onto the new copy. The result is:
37c07a4 <-- dev (HEAD)
/
5baae80--46c1f40 <-- master, origin/master
\
b48659d <-- origin/dev
You can see this in the git log --graph
output you quoted in your question.
Where did the last commit come from?
The one remaining question is, where did this come from:
* fbadb86 (HEAD -> dev) Merge branch 'dev' of *******
That commit exists because you ran git merge
.
You probably ran git merge
by running git pull
. What git pull
does, by default, is run git fetch
for you, and then run git merge
for you. (I advise beginners in Git never to use git pull
at all: what it does, and the way it does it, is excessively confusing. Split it up into git fetch
followed, sooner or later, by a second Git command, sometimes git rebase
, sometimes git merge
.)
What git merge
does, in general, is to try to combine two separate "lines of development". Given a graph like this:
37c07a4 <-- dev (HEAD)
/
5baae80--46c1f40
\
b48659d <-- origin/dev
Git thinks, more or less, that someone else wrote b48659d
(origin/dev
) while you wrote 37c07a4
. In order to combine them, Git finds their "merge base", which is where these two lines join up again. Following the lines by eye makes it clear: that's 5baae80
. So Git makes a new merge commit, which is a commit with two parents. Let's draw that in:
37c07a4--fbadb86 <-- dev (HEAD)
/ /
5baae80--46c1f40 /
\ __--------
b48659d <-- origin/dev
This is what you see now, in your git log --graph --oneline ...
output (with the master
and origin/master
arrows also added in—I left them out because they're just too hard to draw this way).
The fundamental error here was in running git rebase
on commits that someone else already had. In this case, the other Git over at origin
already had commit b48659d
. You cannot change that commit, so when you make your new copy, you stop using your b48659d
, but they still have theirs. Eventually you have to incorporate "theirs" back with "your copy", by merging, giving this rather messy picture.
(There is an exception to this rule: if you can convince everyone else who has b48659d
to give it up, and switch their dev
s to use your new copy instead, you can still rebase. But whether you can, and how, is another question.)