I understand that the difference between git rebase
and git merge
is that git rebase
moves the beginning of a branch to the tip of another branch, where as git merge
creates a new commit that represents (i.e. includes or integrates) branched commits from another branch. So for example:
Since git pull
is equivalent to git fetch
+ git merge
, and git pull --rebase
is equivalent to git fetch
+ git rebase
, let's say you have a local master branch and a remote master branch:
- o - o - o - H - A - B - C (master)
\
P - Q - R (origin/master)
HEAD is at C
If you git pull
you end up with this:
- o - o - o - H - A - B - C - X (master)
\ /
P - Q - R --- (origin/master)
HEAD will now be at X which is commit R of the remote repository's commit history because the merge commit (X) represents (i.e. includes or integrates) the remote branch's commit history that was merged
If on the other hand you did git pull --rebase
, you would end up with this:
- o - o - o - H - P - Q - R - A' - B' - C' (master)
|
(origin/master)
As you can see git pull
is not the same as git pull --rebase
because git pull --rebase
gives HEAD at C' which is commit C of the local master branch, where as when git pull
was used HEAD was at R. In other words, contents of the commit history via git pull
are the same as the contents of the commit history from git pull --rebase
, but the order of commits in the commit histories is different
But what happens when you are pulling from a remote branch when no work has been done on the local master branch:
local master branch and a remote master branch:
- o - o - o - H (master)
\
P - Q - R (origin/master)
HEAD is at H
If you git pull
you end up with this:
- o - o - o - H - X (master)
\ \
P - Q - R --- (origin/master)
HEAD will now be at X which is commit R of the repo because the merge commit represents the remote branch that was merged
If on the other hand you did git pull --rebase
, would you end up with this?:
- o - o - o - H - P - Q - R (master)
|
(origin/master)
Meaning that HEAD is at R which means that git pull --rebase
is the same as git pull
when no work is done on the local branch
When git pull
runs git merge
, it neither specifies --ff-only
nor --no-ff
, unless you specifically ask it to.
[given the commit graph]
...--o--o--o--H <-- master (HEAD) \ P--Q--R <-- origin/master
[you suggest that git merge, and therefore git pull, would produce]
...--o--o--o--H---------X <-- master (HEAD) \ / P--Q--R <-- origin/master
This is not the case—not by default, anyway. If you run git merge --no-ff origin/master
, you do get this result. If you run git merge --ff-only
, or allow the default action, you get instead:
...--o--o--o--H
\
P--Q--R <-- master (HEAD), origin/master
Git calls this a fast-forward. When it's specifically git merge
that does this,1 Git calls it a fast-forward merge, although no actual merge takes place: Git really just does a git checkout
of commit R
while moving the branch name master
so that it points to commit R
(and leaving HEAD
attached to branch name master
).
The --no-ff
option tells Git: Do not do a fast-forward even if you can. The result is a new merge commit (or an error, or whatever else might go wrong, of course). The --ff-only
option tells Git: If you can do a fast-forward, do it; if not, don't do anything, just error out.
When you have git pull
run git merge
, you get the same set of options. If a fast-forward is (a) possible and (b) permitted, the result of the git merge
is the same as that of a git rebase
: the name master
gets updated and nothing inside the commit graph changes in any way. If the fast-forward is not possible, or is inhibited via --no-ff
, the result of a git merge
differs from that of a git rebase
in that there is a new merge commit X
.
The contents—the saved snapshot—of your new merge commit X
match those of your commit R
. But since R
and X
are different commits, they have different hash IDs, and subsequent Git operations will behave differently.
1Both git push
and git fetch
can do this to references updated via refspecs. When git push
moves the server's branch in a fast-forward manner, you generally see nothing special. When git push
cannot move the branch this way, you normally get a rejection from the server. The git fetch
annotation, rather than ! rejected (non-fast-forward)
, is (forced update)
. (Well, that's one of three (!) git fetch
annotations.) See example immediately below.
What I like to emphasize here is that the fast-forwarding is a property of the label motion. A fast-forward is possible if the commit identified before the motion—in this case commit H
—is an ancestor of the commit to be identified afterward, i.e., commit R
.
Here's an example of a non-fast-forward fetch update. I have a local clone of the Git repository for Git. One of the branches in the upstream is named pu
, which stands for proposed updates. This branch is regularly rewound and rebuilt with new commits: someone proposes some new feature, writes a first stab at it, and those go in for testing. Bugs get found, the feature gets reworked, and the commits that went in are discarded in favor of new improved ones that go in to the pu
branch. So my origin/pu
will be force-updated:
$ git fetch
remote: Counting objects: 509, done.
remote: Compressing objects: 100% (214/214), done.
remote: Total 509 (delta 325), reused 400 (delta 295)
Receiving objects: 100% (509/509), 468.38 KiB | 1.52 MiB/s, done.
Resolving deltas: 100% (325/325), done.
From [url]
d8fdbe21b5..95628af9bb next -> origin/next
+ b6268ac8c6...465df7fb28 pu -> origin/pu (forced update)
8051bb0ca1..f0bab95d47 todo -> origin/todo
$
Note that my Git updated my origin/next
, my origin/pu
, and my origin/todo
. Two of these updates were fast-forward operations: commit d8fdbe21b5
(my old origin/next
) is an ancestor of 95628af9bb
(my updated origin/next
), for instance. But b6268ac8c6
, my old origin/pu
, is not an ancestor of 465df7fb28
. My Git updated my origin/pu
anyway, losing access to commit b6268ac8c6
(except via reflogs).
To announce this fact, git fetch
:
+
;(forced update)
at the end of the line.Because the changes to origin/next
and origin/todo
were fast-forward operations, my Git said nothing extra about them.